@dile/ui 3.13.1 → 3.15.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.
- package/components/drop-file/drop-file.spec.js +79 -0
- package/components/drop-file/src/DileDropFile.js +18 -1
- package/components/input/input-search.spec.js +42 -0
- package/components/input/src/DileInputSearch.js +32 -13
- package/components/order-switch/order-switch.spec.js +4 -4
- package/components/select/__screenshots__/select-search.spec.js/dile-select-search-Value-API-uses-the-label-as-accessible-name-of-the-selected-field-1.png +0 -0
- package/components/select/index.js +3 -1
- package/components/select/select-overlay.spec.js +16 -0
- package/components/select/select-search.js +6 -0
- package/components/select/select-search.spec.js +318 -0
- package/components/select/src/DileSelectAjaxOverlay.js +18 -449
- package/components/select/src/DileSelectOverlay.js +3 -0
- package/components/select/src/DileSelectSearch.js +129 -0
- package/components/select/src/DileSelectSearchableBase.js +544 -0
- package/package.json +2 -2
|
@@ -1,236 +1,53 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { ifDefined } from 'lit/directives/if-defined.js';
|
|
3
|
-
import '../../spinner/spinner-horizontal.js';
|
|
4
|
-
import '../../spinner/spinner-icon.js';
|
|
5
|
-
import '../../icon/icon.js';
|
|
6
|
-
import '../select-overlay.js';
|
|
7
|
-
import '../../input/input-search.js';
|
|
8
|
-
import { searchIcon, clearIcon } from '@dile/icons/index.js';
|
|
9
|
-
import { DileEmmitChange } from '../../../mixins/form/index.js';
|
|
10
|
-
import { labelStyles } from '../../input/src/label-styles.js';
|
|
11
|
-
import { messageStyles } from '../../input/src/message-styles.js';
|
|
1
|
+
import { DileSelectSearchableBase } from './DileSelectSearchableBase.js';
|
|
12
2
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
export class DileSelectAjaxOverlay extends DileEmmitChange(LitElement) {
|
|
16
|
-
|
|
17
|
-
static get styles() {
|
|
18
|
-
return [
|
|
19
|
-
labelStyles,
|
|
20
|
-
messageStyles,
|
|
21
|
-
css`
|
|
22
|
-
* {
|
|
23
|
-
box-sizing: border-box;
|
|
24
|
-
}
|
|
25
|
-
:host {
|
|
26
|
-
display: block;
|
|
27
|
-
position: relative;
|
|
28
|
-
margin-bottom: 10px;
|
|
29
|
-
}
|
|
30
|
-
#result {
|
|
31
|
-
margin-bottom: 0.5rem;
|
|
32
|
-
}
|
|
33
|
-
.search-wrapper {
|
|
34
|
-
position: relative;
|
|
35
|
-
display: flex;
|
|
36
|
-
align-items: center;
|
|
37
|
-
width: var(--dile-input-width, 100%);
|
|
38
|
-
padding: var(--dile-input-padding, 5px);
|
|
39
|
-
border-radius: var(--dile-input-border-radius, 5px);
|
|
40
|
-
border: var(--dile-input-border-width, 1px) solid var(--dile-input-border-color, #888);
|
|
41
|
-
background-color: var(--dile-input-background-color, #fff);
|
|
42
|
-
}
|
|
43
|
-
.search-wrapper:focus-within {
|
|
44
|
-
border-color: var(--dile-input-focus-border-color, var(--dile-link-color, #6af));
|
|
45
|
-
}
|
|
46
|
-
.search-wrapper.errored {
|
|
47
|
-
border-color: var(--dile-input-error-border-color, #c00);
|
|
48
|
-
}
|
|
49
|
-
.search-wrapper.disabled {
|
|
50
|
-
opacity: 0.5;
|
|
51
|
-
}
|
|
52
|
-
input {
|
|
53
|
-
flex-grow: 1;
|
|
54
|
-
min-width: 0;
|
|
55
|
-
border: none;
|
|
56
|
-
outline: none;
|
|
57
|
-
color: var(--dile-input-color, #303030);
|
|
58
|
-
font-size: var(--dile-input-font-size, 1em);
|
|
59
|
-
line-height: var(--dile-input-line-height, 1.5em);
|
|
60
|
-
background-color: transparent;
|
|
61
|
-
font-family: inherit;
|
|
62
|
-
}
|
|
63
|
-
input::placeholder {
|
|
64
|
-
color: var(--dile-input-placeholder-color, #ccc);
|
|
65
|
-
}
|
|
66
|
-
dile-icon {
|
|
67
|
-
margin-left: 1px;
|
|
68
|
-
display: flex;
|
|
69
|
-
cursor: pointer;
|
|
70
|
-
}
|
|
71
|
-
dile-spinner-icon {
|
|
72
|
-
margin-left: 1px;
|
|
73
|
-
--dile-icon-size: 18px;
|
|
74
|
-
}
|
|
75
|
-
.status {
|
|
76
|
-
margin-top: 0.5rem;
|
|
77
|
-
position: absolute;
|
|
78
|
-
z-index: var(--dile-select-overlay-z-index, 999);
|
|
79
|
-
width: 100%;
|
|
80
|
-
box-sizing: border-box;
|
|
81
|
-
min-height: 2.5rem;
|
|
82
|
-
border-radius: var(--dile-select-overlay-border-radius, 5px);
|
|
83
|
-
padding: var(--dile-select-overlay-padding, 7px 10px);
|
|
84
|
-
background-color: var(--dile-select-overlay-background-color, #fff);
|
|
85
|
-
color: var(--dile-select-overlay-color, #303030);
|
|
86
|
-
box-shadow: var(--dile-select-overlay-box-shadow, 0 4px 10px rgba(0, 0, 0, 0.15));
|
|
87
|
-
display: flex;
|
|
88
|
-
align-items: center;
|
|
89
|
-
}
|
|
90
|
-
.status p {
|
|
91
|
-
margin: 0;
|
|
92
|
-
}
|
|
93
|
-
dile-select-overlay {
|
|
94
|
-
margin-bottom: 0;
|
|
95
|
-
/* Match the search field's own sizing (dile-input-search-like padding/font-size)
|
|
96
|
-
instead of dile-select-overlay's native-select-like defaults, so the visible
|
|
97
|
-
trigger shown in static mode looks consistent with the search field. */
|
|
98
|
-
--dile-input-padding: 5px;
|
|
99
|
-
--dile-select-font-size: 1em;
|
|
100
|
-
}
|
|
101
|
-
.visually-hidden {
|
|
102
|
-
position: absolute;
|
|
103
|
-
width: 1px;
|
|
104
|
-
height: 1px;
|
|
105
|
-
padding: 0;
|
|
106
|
-
margin: -1px;
|
|
107
|
-
overflow: hidden;
|
|
108
|
-
clip: rect(0, 0, 0, 0);
|
|
109
|
-
white-space: nowrap;
|
|
110
|
-
border: 0;
|
|
111
|
-
}
|
|
112
|
-
`];
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
static get formAssociated() {
|
|
116
|
-
return true;
|
|
117
|
-
}
|
|
3
|
+
export class DileSelectAjaxOverlay extends DileSelectSearchableBase {
|
|
118
4
|
|
|
119
5
|
static get properties() {
|
|
120
6
|
return {
|
|
121
7
|
endpoint: { type: String },
|
|
122
|
-
label: { type: String },
|
|
123
|
-
value: { type: String },
|
|
124
|
-
name: { type: String, reflect: true },
|
|
125
|
-
disabled: { type: Boolean },
|
|
126
|
-
errored: { type: Boolean },
|
|
127
|
-
data: { type: Array },
|
|
128
|
-
placeholder: { type: String },
|
|
129
|
-
emptyMessage: { type: String },
|
|
130
|
-
selectedText: { type: String },
|
|
131
8
|
ajaxErrorMessage: { type: String },
|
|
132
9
|
ajaxError: { type: Boolean },
|
|
133
10
|
queryStringVariable: { type: String },
|
|
134
11
|
/** Use resultDataProperty to declare witch property in the JSON object returned by the endpoint has the data to assign to the select options. Left it blank if the result JSON has directly the array with the data */
|
|
135
12
|
resultDataProperty: { type: String },
|
|
136
|
-
displayProperty: { type: String },
|
|
137
|
-
idProperty: { type: String },
|
|
138
|
-
/** Property in each data object holding a dile-iconlib "family.name" icon string, applied
|
|
139
|
-
* per option. Leave unset if you don't need per-option icons. */
|
|
140
|
-
iconProperty: { type: String },
|
|
141
|
-
/** dile-iconlib "family.name" icon string applied to every option that has no icon of its
|
|
142
|
-
* own via iconProperty. */
|
|
143
|
-
icon: { type: String },
|
|
144
|
-
delay: { type: Number },
|
|
145
|
-
keyword: {
|
|
146
|
-
type: String,
|
|
147
|
-
state: true,
|
|
148
|
-
},
|
|
149
|
-
loading: {
|
|
150
|
-
type: Boolean,
|
|
151
|
-
state: true,
|
|
152
|
-
},
|
|
153
|
-
isSelected: {
|
|
154
|
-
type: Boolean,
|
|
155
|
-
state: true,
|
|
156
|
-
},
|
|
157
13
|
/* When static is true there is no posibility to search, then all the options are displayed on the select on initialization */
|
|
158
14
|
static: {
|
|
159
15
|
type: Boolean,
|
|
160
16
|
},
|
|
161
|
-
/** Message Displayed */
|
|
162
|
-
message: { type: String },
|
|
163
|
-
/** Hide errors on input */
|
|
164
|
-
hideErrorOnInput: { type: Boolean },
|
|
165
17
|
/** Additional query string data */
|
|
166
18
|
additionalQueryString: {type: Object },
|
|
167
|
-
/** Text announced in a live region as the user navigates results with the keyboard */
|
|
168
|
-
_liveAnnouncement: { state: true },
|
|
169
19
|
};
|
|
170
20
|
}
|
|
171
21
|
|
|
172
|
-
updated(changedProperties) {
|
|
173
|
-
if(changedProperties.has("value")) {
|
|
174
|
-
this.emmitChange();
|
|
175
|
-
this.dispatchSelectedTextChanged();
|
|
176
|
-
this.internals.setFormValue(this.value);
|
|
177
|
-
}
|
|
178
|
-
// In search mode the nested dile-select-overlay's own trigger is hidden and collapsed to
|
|
179
|
-
// zero height, sitting right after this component's real, visible search field. Point its
|
|
180
|
-
// positioning at that real field instead, so a flip-to-top popup correctly leaves room for
|
|
181
|
-
// the field's actual height rather than covering it. Static mode keeps its own visible
|
|
182
|
-
// trigger, which is already the right thing to position against.
|
|
183
|
-
// Re-checked on every update (not just firstUpdated) because clearing a selection swaps
|
|
184
|
-
// back to a *new* dile-select-overlay instance (isSelected toggles the whole template
|
|
185
|
-
// branch), which needs the same wiring the original instance got.
|
|
186
|
-
if (!this.static && this.select && this._wiredTriggerFor !== this.select) {
|
|
187
|
-
const nestedSelect = this.select;
|
|
188
|
-
this._wiredTriggerFor = nestedSelect;
|
|
189
|
-
// The nested element's own firstUpdated() (which sets its default internal trigger
|
|
190
|
-
// reference) runs on its own update cycle, not synchronously with this one — wait for it
|
|
191
|
-
// to resolve first, otherwise it overwrites the external trigger we just set here.
|
|
192
|
-
nestedSelect.updateComplete.then(() => {
|
|
193
|
-
nestedSelect.setTriggerElement(this.shadowRoot.querySelector('.search-wrapper'));
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
22
|
constructor() {
|
|
199
23
|
super();
|
|
200
|
-
this.placeholder = "Search to choose...";
|
|
201
|
-
this.emptyMessage = "No results found";
|
|
202
24
|
this.ajaxErrorMessage = "Error loading data";
|
|
203
|
-
this.data = [];
|
|
204
|
-
this.keyword = '';
|
|
205
|
-
this.loading = false;
|
|
206
|
-
this.errored = false;
|
|
207
25
|
this.ajaxError = false;
|
|
208
|
-
this.isSelected = false;
|
|
209
26
|
this.static = false;
|
|
210
27
|
this.queryStringVariable = 'q';
|
|
211
|
-
this.delay = 300;
|
|
212
|
-
this.idProperty = 'id';
|
|
213
|
-
this._liveAnnouncement = '';
|
|
214
|
-
this._debounceTimeout = null;
|
|
215
|
-
this.internals = this.attachInternals();
|
|
216
|
-
this.selectId = `dile-select-ajax-overlay-${Math.random().toString(36).substr(2, 9)}`;
|
|
217
28
|
}
|
|
218
29
|
|
|
219
30
|
firstUpdated() {
|
|
220
|
-
|
|
221
|
-
this.isSelected = true;
|
|
222
|
-
this.searchValueInitial();
|
|
223
|
-
}
|
|
31
|
+
super.firstUpdated();
|
|
224
32
|
if(this.static) {
|
|
225
33
|
this.loadData();
|
|
226
34
|
}
|
|
227
35
|
}
|
|
228
36
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
37
|
+
get selectedTextChangedEventName() {
|
|
38
|
+
return 'dile-select-ajax-selected-text-changed';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get hasSearchField() {
|
|
42
|
+
return !this.static;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get statusText() {
|
|
46
|
+
return this.ajaxError ? this.ajaxErrorMessage : this.emptyMessage;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
resolveSelectedText() {
|
|
50
|
+
this.searchValueInitial();
|
|
234
51
|
}
|
|
235
52
|
|
|
236
53
|
searchValueInitial() {
|
|
@@ -245,176 +62,6 @@ export class DileSelectAjaxOverlay extends DileEmmitChange(LitElement) {
|
|
|
245
62
|
this.loading = false;
|
|
246
63
|
}
|
|
247
64
|
|
|
248
|
-
get search() {
|
|
249
|
-
return this.shadowRoot.getElementById('search');
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
get select() {
|
|
253
|
-
return this.shadowRoot.getElementById('elselect');
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
get opened() {
|
|
257
|
-
return this.select ? this.select.opened : false;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
render() {
|
|
261
|
-
return html`
|
|
262
|
-
<div>
|
|
263
|
-
${this.label
|
|
264
|
-
? html`<label for="${this.isSelected ? 'result' : 'search'}">${this.label}</label>`
|
|
265
|
-
: ""}
|
|
266
|
-
${this.isSelected
|
|
267
|
-
? this.selectedTemplate
|
|
268
|
-
: this.static ? this.dataTemplate : this.searchTemplate
|
|
269
|
-
}
|
|
270
|
-
</div>
|
|
271
|
-
${this.messageTemplate}
|
|
272
|
-
`;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
get selectedTemplate() {
|
|
276
|
-
return html`
|
|
277
|
-
<dile-input-search
|
|
278
|
-
id="result"
|
|
279
|
-
?errored=${this.errored}
|
|
280
|
-
?disabled=${this.disabled}
|
|
281
|
-
value="${this.selectedText}"
|
|
282
|
-
readOnly
|
|
283
|
-
@dile-input-search-cleared=${this.onClearSelected}
|
|
284
|
-
@dile-input-search=${this.hideErrorOnInteraction}
|
|
285
|
-
></dile-input-search>
|
|
286
|
-
`;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
get searchTemplate() {
|
|
290
|
-
return html`
|
|
291
|
-
<div
|
|
292
|
-
class="search-wrapper ${this.errored ? 'errored' : ''} ${this.disabled ? 'disabled' : ''}"
|
|
293
|
-
@click=${(e) => e.stopPropagation()}
|
|
294
|
-
>
|
|
295
|
-
<input
|
|
296
|
-
type="text"
|
|
297
|
-
id="search"
|
|
298
|
-
role="combobox"
|
|
299
|
-
aria-haspopup="listbox"
|
|
300
|
-
aria-expanded="${this.opened}"
|
|
301
|
-
aria-invalid="${this.errored}"
|
|
302
|
-
aria-describedby="${ifDefined(this.message ? `${this.selectId}-message` : undefined)}"
|
|
303
|
-
placeholder="${this.placeholder}"
|
|
304
|
-
.value="${this.keyword}"
|
|
305
|
-
?disabled=${this.disabled}
|
|
306
|
-
autocomplete="off"
|
|
307
|
-
@input=${this.onSearchInput}
|
|
308
|
-
@keydown=${this.onSearchKeydown}
|
|
309
|
-
@focus=${this.onSearchFocus}
|
|
310
|
-
@focusout=${this.onSearchFocusOut}
|
|
311
|
-
>
|
|
312
|
-
${this.loading
|
|
313
|
-
? html`<dile-spinner-icon active aria-hidden="true"></dile-spinner-icon>`
|
|
314
|
-
: html`
|
|
315
|
-
<dile-icon
|
|
316
|
-
@click=${this.onIconClick}
|
|
317
|
-
.icon="${this.keyword.length ? clearIcon : searchIcon}"
|
|
318
|
-
></dile-icon>
|
|
319
|
-
`
|
|
320
|
-
}
|
|
321
|
-
</div>
|
|
322
|
-
<div class="visually-hidden" role="status" aria-live="polite">${this._liveAnnouncement}</div>
|
|
323
|
-
${this.keyword.length > 0 && this.data.length === 0 ? this.statusTemplate : ''}
|
|
324
|
-
${this.dataTemplate}
|
|
325
|
-
`;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
get statusTemplate() {
|
|
329
|
-
return html`
|
|
330
|
-
<div class="status">
|
|
331
|
-
${this.loading
|
|
332
|
-
? html`<dile-spinner-horizontal active></dile-spinner-horizontal>`
|
|
333
|
-
: this.ajaxError
|
|
334
|
-
? html`<p>${this.ajaxErrorMessage}</p>`
|
|
335
|
-
: html`<p>${this.emptyMessage}</p>`
|
|
336
|
-
}
|
|
337
|
-
</div>
|
|
338
|
-
`;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
get dataTemplate() {
|
|
342
|
-
return html`
|
|
343
|
-
<dile-select-overlay
|
|
344
|
-
id="elselect"
|
|
345
|
-
?hideTrigger=${!this.static}
|
|
346
|
-
name="generated-select-field"
|
|
347
|
-
placeholder="${this.placeholder}"
|
|
348
|
-
?disabled=${this.disabled}
|
|
349
|
-
@element-changed=${this.doSelected}
|
|
350
|
-
>
|
|
351
|
-
<select slot="select" data-icon="${ifDefined(this.icon)}">
|
|
352
|
-
${this.data.map(item => html`
|
|
353
|
-
<option value="${item[this.idProperty]}" data-icon="${ifDefined(this.iconProperty ? item[this.iconProperty] : undefined)}">${item[this.displayProperty]}</option>
|
|
354
|
-
`)}
|
|
355
|
-
</select>
|
|
356
|
-
</dile-select-overlay>
|
|
357
|
-
`;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
get messageTemplate() {
|
|
361
|
-
return html`
|
|
362
|
-
${this.message
|
|
363
|
-
? html`<div class="message ${this.errored ? 'errored-msg' : ''}" id="${this.selectId}-message"><span>${this.message}</span></div>`
|
|
364
|
-
: ''
|
|
365
|
-
}
|
|
366
|
-
`
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
onSearchInput(e) {
|
|
370
|
-
this.keyword = e.target.value;
|
|
371
|
-
this.hideErrorOnInteraction();
|
|
372
|
-
if (this._debounceTimeout) {
|
|
373
|
-
clearTimeout(this._debounceTimeout);
|
|
374
|
-
}
|
|
375
|
-
// Mark as loading right away, not just once the debounced request actually starts,
|
|
376
|
-
// otherwise the empty-results message flashes for the length of the debounce delay
|
|
377
|
-
// before there's been any real request to come back empty.
|
|
378
|
-
if (this.keyword.length > 0) {
|
|
379
|
-
this.loading = true;
|
|
380
|
-
}
|
|
381
|
-
this._debounceTimeout = setTimeout(() => {
|
|
382
|
-
this.loadData();
|
|
383
|
-
}, this.delay);
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
onSearchKeydown(e) {
|
|
387
|
-
if (!NAVIGATION_KEYS.includes(e.key) || !this.select) return;
|
|
388
|
-
this.select.handleExternalKeydown(e);
|
|
389
|
-
this._liveAnnouncement = this.select.highlightedText;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
onSearchFocus() {
|
|
393
|
-
if (this.keyword.length > 0 && this.data.length > 0) {
|
|
394
|
-
this.select?.open();
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
onSearchFocusOut(e) {
|
|
399
|
-
if (!this.shadowRoot.contains(e.relatedTarget)) {
|
|
400
|
-
this.select?.close();
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
onIconClick() {
|
|
405
|
-
if (!this.keyword.length) return;
|
|
406
|
-
this.keyword = '';
|
|
407
|
-
this.data = [];
|
|
408
|
-
this.select?.close();
|
|
409
|
-
this.search.focus();
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
hideErrorOnInteraction() {
|
|
413
|
-
if (this.hideErrorOnInput && this.errored) {
|
|
414
|
-
this.clearError();
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
|
|
418
65
|
loadData() {
|
|
419
66
|
this.loading = true;
|
|
420
67
|
this.ajaxError = false;
|
|
@@ -446,13 +93,7 @@ export class DileSelectAjaxOverlay extends DileEmmitChange(LitElement) {
|
|
|
446
93
|
}
|
|
447
94
|
|
|
448
95
|
registerData(json) {
|
|
449
|
-
this.
|
|
450
|
-
this.updateComplete.then(() => {
|
|
451
|
-
this.loading = false;
|
|
452
|
-
if (this.keyword.length > 0 && this.data.length > 0) {
|
|
453
|
-
this.select?.open();
|
|
454
|
-
}
|
|
455
|
-
});
|
|
96
|
+
this.setResults(this.getResultData(json));
|
|
456
97
|
}
|
|
457
98
|
|
|
458
99
|
getResultData(json) {
|
|
@@ -462,76 +103,4 @@ export class DileSelectAjaxOverlay extends DileEmmitChange(LitElement) {
|
|
|
462
103
|
return json[this.resultDataProperty];
|
|
463
104
|
}
|
|
464
105
|
}
|
|
465
|
-
|
|
466
|
-
doSelected(e) {
|
|
467
|
-
e.stopPropagation();
|
|
468
|
-
let value = e.detail.value;
|
|
469
|
-
// The nested dile-select-overlay also fires element-changed with an empty value while
|
|
470
|
-
// syncing itself to its placeholder option on mount (before any real selection) since,
|
|
471
|
-
// unlike the original dile-select, it's kept mounted from the start so its MutationObserver
|
|
472
|
-
// is ready to pick up results as soon as they load. Ignore that non-interaction here.
|
|
473
|
-
if(e.detail.name === 'generated-select-field' && value !== '') {
|
|
474
|
-
this.selectedText = this.select.getOptionByValue(value).innerText;
|
|
475
|
-
this.value = value;
|
|
476
|
-
this.isSelected = true;
|
|
477
|
-
this.hideErrorOnInteraction();
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
clearError() {
|
|
482
|
-
this.errored = false;
|
|
483
|
-
this.message = '';
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
onClearSelected() {
|
|
487
|
-
this.value = undefined;
|
|
488
|
-
this.isSelected = false;
|
|
489
|
-
if(! this.static) {
|
|
490
|
-
this.keyword = '';
|
|
491
|
-
this.selectedText = '';
|
|
492
|
-
this.data = [];
|
|
493
|
-
this.updateComplete.then(() => {
|
|
494
|
-
this.search.focus();
|
|
495
|
-
});
|
|
496
|
-
}
|
|
497
|
-
this.hideErrorOnInteraction();
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
close() {
|
|
501
|
-
this.select?.close();
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
clear() {
|
|
505
|
-
this.isSelected = false;
|
|
506
|
-
this.value = undefined;
|
|
507
|
-
this.selectedText = '';
|
|
508
|
-
// isSelected flipping back to false remounts a brand new nested dile-select-overlay (the
|
|
509
|
-
// read-only and search branches are mutually exclusive templates). Its firstUpdated() syncs
|
|
510
|
-
// its value to the native <select>'s current value, and this component's <select> has no
|
|
511
|
-
// placeholder option — so leftover data/keyword from the previous search would leave a real
|
|
512
|
-
// option in place, get auto-picked as the new instance's value, and re-fire element-changed
|
|
513
|
-
// for the very same item that was just cleared (an infinite loop under addOnSelect).
|
|
514
|
-
if (!this.static) {
|
|
515
|
-
this.keyword = '';
|
|
516
|
-
this.data = [];
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
set(value) {
|
|
521
|
-
this.value = value;
|
|
522
|
-
if(value) {
|
|
523
|
-
this.isSelected = true;
|
|
524
|
-
this.searchValueInitial();
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
dispatchSelectedTextChanged() {
|
|
529
|
-
this.dispatchEvent(new CustomEvent('dile-select-ajax-selected-text-changed',
|
|
530
|
-
{
|
|
531
|
-
bubbles: true,
|
|
532
|
-
composed: true,
|
|
533
|
-
detail: { selectedText: this.selectedText }
|
|
534
|
-
}
|
|
535
|
-
));
|
|
536
|
-
}
|
|
537
106
|
}
|
|
@@ -121,6 +121,9 @@ export class DileSelectOverlay extends DileOverlay(DileCloseDocumentClick(DileEm
|
|
|
121
121
|
color: var(--dile-select-overlay-color, #303030);
|
|
122
122
|
background-color: var(--dile-select-overlay-background-color, #fff);
|
|
123
123
|
border-radius: var(--dile-select-overlay-border-radius, 5px);
|
|
124
|
+
/* The popup takes the trigger width (set in updatePosition()), but it can be wider
|
|
125
|
+
in compositions with a narrow trigger. Never wider than the viewport. */
|
|
126
|
+
min-width: min(var(--dile-select-overlay-min-width, 0px), calc(100vw - 25px));
|
|
124
127
|
max-height: var(--dile-select-overlay-max-height, 260px);
|
|
125
128
|
overflow-y: auto;
|
|
126
129
|
box-shadow: var(--dile-select-overlay-box-shadow, 0 4px 10px rgba(0, 0, 0, 0.15));
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { DileSelectSearchableBase } from './DileSelectSearchableBase.js';
|
|
2
|
+
|
|
3
|
+
export class DileSelectSearch extends DileSelectSearchableBase {
|
|
4
|
+
|
|
5
|
+
static get properties() {
|
|
6
|
+
return {
|
|
7
|
+
/** Show every option as soon as the search field gets focus, before typing anything */
|
|
8
|
+
showAllOnFocus: { type: Boolean },
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
constructor() {
|
|
13
|
+
super();
|
|
14
|
+
this.delay = 0;
|
|
15
|
+
this.showAllOnFocus = false;
|
|
16
|
+
this.idProperty = 'value';
|
|
17
|
+
this.displayProperty = 'text';
|
|
18
|
+
this.iconProperty = 'icon';
|
|
19
|
+
this.sourceOptions = [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get elselectSource() {
|
|
23
|
+
return this.querySelector('select');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
connectedCallback() {
|
|
27
|
+
super.connectedCallback();
|
|
28
|
+
this.connectSource();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Sets up `sourceOptions`. By default they are read from the light DOM select. */
|
|
32
|
+
connectSource() {
|
|
33
|
+
if (!this.elselectSource) {
|
|
34
|
+
throw new Error('Use dile-select-search with a select element in the slot "select"');
|
|
35
|
+
}
|
|
36
|
+
this.readSourceOptions();
|
|
37
|
+
// The light DOM <select> is never rendered, it is only the data source. Watch it so
|
|
38
|
+
// options added, removed or edited later are picked up by the search.
|
|
39
|
+
this._sourceObserver = new MutationObserver(() => {
|
|
40
|
+
this.readSourceOptions();
|
|
41
|
+
this.refreshFromSource();
|
|
42
|
+
});
|
|
43
|
+
this._sourceObserver.observe(this.elselectSource, { childList: true, subtree: true, characterData: true, attributes: true });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
disconnectedCallback() {
|
|
47
|
+
super.disconnectedCallback();
|
|
48
|
+
if (this._sourceObserver) {
|
|
49
|
+
this._sourceObserver.disconnect();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Updates the selected text and the current results after `sourceOptions` change. */
|
|
54
|
+
refreshFromSource() {
|
|
55
|
+
if (this.isSelected) {
|
|
56
|
+
this.resolveSelectedText();
|
|
57
|
+
} else if (this.keyword.length > 0 || this.data.length > 0) {
|
|
58
|
+
this.loadData();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
readSourceOptions() {
|
|
63
|
+
const select = this.elselectSource;
|
|
64
|
+
this.sourceOptions = Array.from(select.options).map(option => ({
|
|
65
|
+
value: option.value,
|
|
66
|
+
text: option.textContent.trim(),
|
|
67
|
+
icon: option.dataset.icon || select.dataset.icon,
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
get selectedTextChangedEventName() {
|
|
72
|
+
return 'dile-select-search-selected-text-changed';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get shouldOpenResults() {
|
|
76
|
+
return this.data.length > 0 && (this.keyword.length > 0 || this.showAllOnFocus);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
loadData() {
|
|
80
|
+
this.setResults(this.filterOptions(this.keyword));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
filterOptions(keyword) {
|
|
84
|
+
if (!keyword) {
|
|
85
|
+
return this.showAllOnFocus ? [...this.sourceOptions] : [];
|
|
86
|
+
}
|
|
87
|
+
return this.sourceOptions.filter(option => this.filterOption(option, keyword));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Template method: decide whether an option matches the keyword.
|
|
92
|
+
* By default it searches the keyword anywhere in the option text, ignoring case and accents.
|
|
93
|
+
* Override it in a subclass to customize the search (e.g. also match option.value).
|
|
94
|
+
* @param {{value: string, text: string, icon: string}} option
|
|
95
|
+
* @param {string} keyword
|
|
96
|
+
* @returns {boolean}
|
|
97
|
+
*/
|
|
98
|
+
filterOption(option, keyword) {
|
|
99
|
+
return this.normalizeSearchText(option.text).includes(this.normalizeSearchText(keyword));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Lowercases the text and removes accents, to compare texts in searches. */
|
|
103
|
+
normalizeSearchText(text) {
|
|
104
|
+
return String(text ?? '')
|
|
105
|
+
.normalize('NFD')
|
|
106
|
+
.replace(/\p{Diacritic}/gu, '')
|
|
107
|
+
.toLowerCase();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
resolveSelectedText() {
|
|
111
|
+
const option = this.sourceOptions.find(o => o.value === String(this.value));
|
|
112
|
+
this.selectedText = option ? option.text : this.value;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
onSearchFocus() {
|
|
116
|
+
if (this.showAllOnFocus && this.keyword.length === 0) {
|
|
117
|
+
this.loadData();
|
|
118
|
+
} else {
|
|
119
|
+
super.onSearchFocus();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
onIconClick() {
|
|
124
|
+
super.onIconClick();
|
|
125
|
+
if (this.showAllOnFocus) {
|
|
126
|
+
this.loadData();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|