@itrocks/autocomplete 0.1.6 → 0.1.9
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/autocomplete.css +3 -0
- package/autocomplete.d.ts +13 -2
- package/autocomplete.js +130 -31
- package/package.json +1 -1
package/autocomplete.css
CHANGED
package/autocomplete.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
export interface AutoCompleteOptions {
|
|
2
|
+
allowNew: boolean;
|
|
3
|
+
fetchAttribute: string;
|
|
4
|
+
}
|
|
1
5
|
interface Item {
|
|
2
6
|
caption: string;
|
|
3
7
|
id: number;
|
|
@@ -6,13 +10,18 @@ export declare class AutoComplete {
|
|
|
6
10
|
fetching?: string;
|
|
7
11
|
idInput?: HTMLInputElement;
|
|
8
12
|
input: HTMLInputElement;
|
|
13
|
+
lastItems: Item[];
|
|
9
14
|
lastKey: string;
|
|
15
|
+
lastStart: string;
|
|
16
|
+
options: AutoCompleteOptions;
|
|
10
17
|
suggestions: Suggestions;
|
|
11
|
-
constructor(input: HTMLInputElement);
|
|
18
|
+
constructor(input: HTMLInputElement, options?: Partial<AutoCompleteOptions>);
|
|
12
19
|
autoComplete(): void;
|
|
13
20
|
autoEmptyClass(): void;
|
|
14
21
|
autoIdInputValue(): void;
|
|
15
|
-
|
|
22
|
+
consumeSelectedChar(event: KeyboardEvent): boolean;
|
|
23
|
+
fetch(lastKey?: string): void;
|
|
24
|
+
fetchDone(items: Item[] | undefined, startsWith: string, lastKey: string): void;
|
|
16
25
|
initIdInput(): HTMLInputElement | undefined;
|
|
17
26
|
initInput(input: HTMLInputElement): HTMLInputElement;
|
|
18
27
|
initParent(): void;
|
|
@@ -28,11 +37,13 @@ export declare class AutoComplete {
|
|
|
28
37
|
openSuggestions(event: Event): boolean;
|
|
29
38
|
select(): void;
|
|
30
39
|
suggest(value?: string): void;
|
|
40
|
+
updateSuggestions(items: Item[], startsWith?: string): void;
|
|
31
41
|
}
|
|
32
42
|
declare class Suggestions {
|
|
33
43
|
autoComplete: AutoComplete;
|
|
34
44
|
length: number;
|
|
35
45
|
list?: HTMLUListElement;
|
|
46
|
+
partial: boolean;
|
|
36
47
|
pointerStart?: {
|
|
37
48
|
id: number;
|
|
38
49
|
item: HTMLLIElement;
|
package/autocomplete.js
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
const DEBUG = false;
|
|
2
|
+
const defaultOptions = {
|
|
3
|
+
allowNew: false,
|
|
4
|
+
fetchAttribute: 'data-fetch'
|
|
5
|
+
};
|
|
2
6
|
export class AutoComplete {
|
|
3
7
|
fetching;
|
|
4
8
|
idInput;
|
|
5
9
|
input;
|
|
10
|
+
lastItems = [];
|
|
6
11
|
lastKey = '';
|
|
12
|
+
lastStart = '';
|
|
13
|
+
options;
|
|
7
14
|
suggestions;
|
|
8
|
-
constructor(input) {
|
|
15
|
+
constructor(input, options = {}) {
|
|
9
16
|
if (DEBUG)
|
|
10
|
-
console.log('new AutoComplete()', input);
|
|
17
|
+
console.log('new AutoComplete()', input, options);
|
|
11
18
|
this.input = this.initInput(input);
|
|
12
19
|
this.idInput = this.initIdInput();
|
|
20
|
+
this.options = Object.assign(defaultOptions, options);
|
|
13
21
|
this.suggestions = new Suggestions(this);
|
|
14
22
|
this.initParent();
|
|
15
23
|
input.addEventListener('blur', event => this.onBlur(event));
|
|
@@ -47,6 +55,8 @@ export class AutoComplete {
|
|
|
47
55
|
classList.add('empty');
|
|
48
56
|
return;
|
|
49
57
|
}
|
|
58
|
+
if (DEBUG)
|
|
59
|
+
console.log(' - empty');
|
|
50
60
|
classList.remove('empty');
|
|
51
61
|
if (!classList.length) {
|
|
52
62
|
input.removeAttribute('class');
|
|
@@ -61,47 +71,80 @@ export class AutoComplete {
|
|
|
61
71
|
const input = this.input;
|
|
62
72
|
const suggestion = this.suggestions.selected();
|
|
63
73
|
idInput.value = (suggestion && (toInsensitive(input.value) === toInsensitive(suggestion.caption)))
|
|
64
|
-
? '' + suggestion.id
|
|
74
|
+
? (suggestion.id ? ('' + suggestion.id) : '')
|
|
65
75
|
: '';
|
|
66
76
|
if (DEBUG)
|
|
67
77
|
console.log(' idInput =', idInput.value);
|
|
68
78
|
}
|
|
69
|
-
|
|
79
|
+
consumeSelectedChar(event) {
|
|
80
|
+
if (event.key.length !== 1)
|
|
81
|
+
return false;
|
|
82
|
+
if (event.ctrlKey || event.altKey || event.metaKey)
|
|
83
|
+
return false;
|
|
84
|
+
if (!this.lastItems[this.lastItems.length - 1]?.id)
|
|
85
|
+
return false;
|
|
86
|
+
const input = this.input;
|
|
87
|
+
const start = input.selectionStart;
|
|
88
|
+
const end = input.selectionEnd;
|
|
89
|
+
if ((start === null) || (end === null) || (start === end))
|
|
90
|
+
return false;
|
|
91
|
+
const firstSelectedChar = input.value.slice(start, start + 1);
|
|
92
|
+
if (toInsensitive(event.key) !== toInsensitive(firstSelectedChar))
|
|
93
|
+
return false;
|
|
94
|
+
if (DEBUG)
|
|
95
|
+
console.log('consumeSelectedChar(' + event.key + ')');
|
|
96
|
+
event.preventDefault();
|
|
97
|
+
input.setSelectionRange(start + 1, input.value.length);
|
|
98
|
+
this.fetch(event.key);
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
fetch(lastKey = '') {
|
|
70
102
|
const input = this.input;
|
|
71
|
-
const
|
|
103
|
+
const startsWith = ((input.selectionStart !== null) && (input.selectionEnd === input.value.length))
|
|
72
104
|
? input.value.slice(0, input.selectionStart)
|
|
73
105
|
: input.value;
|
|
106
|
+
if (DEBUG)
|
|
107
|
+
console.log('fetch() with lastStart =', this.lastStart);
|
|
108
|
+
if (this.lastStart.length && toInsensitive(startsWith).startsWith(this.lastStart) && !this.suggestions.partial) {
|
|
109
|
+
this.fetchDone(undefined, startsWith, lastKey);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
this.lastStart = toInsensitive(startsWith);
|
|
74
113
|
if (this.fetching) {
|
|
75
|
-
if (
|
|
76
|
-
setTimeout(() => this.fetch(), 50);
|
|
114
|
+
if (startsWith !== this.fetching) {
|
|
115
|
+
setTimeout(() => this.fetch(lastKey), 50);
|
|
77
116
|
}
|
|
78
117
|
return;
|
|
79
118
|
}
|
|
80
|
-
this.fetching =
|
|
81
|
-
const
|
|
82
|
-
const
|
|
119
|
+
this.fetching = startsWith;
|
|
120
|
+
const fetchAttribute = this.options.fetchAttribute;
|
|
121
|
+
const dataFetch = input.closest('[' + fetchAttribute + ']')?.getAttribute(fetchAttribute);
|
|
83
122
|
const requestInit = { headers: { Accept: 'application/json' } };
|
|
84
|
-
const
|
|
123
|
+
const url = dataFetch + '?limit&startsWith=' + startsWith;
|
|
85
124
|
if (DEBUG)
|
|
86
|
-
console.log('fetch()', 'startsWith=' +
|
|
87
|
-
fetch(
|
|
88
|
-
this.
|
|
89
|
-
|
|
90
|
-
const startsWith = toInsensitive(inputValue);
|
|
91
|
-
const suggestions = startsWith.length
|
|
92
|
-
? summary.filter(item => toInsensitive(item.caption).startsWith(startsWith))
|
|
93
|
-
: summary;
|
|
94
|
-
this.suggestions.update(suggestions);
|
|
95
|
-
if (!['Backspace', 'Delete'].includes(lastKey)) {
|
|
96
|
-
this.autoComplete();
|
|
97
|
-
}
|
|
98
|
-
this.onInputValueChange();
|
|
99
|
-
this.autoIdInputValue();
|
|
100
|
-
}).catch(() => {
|
|
125
|
+
console.log('fetch()', 'limit&startsWith=' + startsWith);
|
|
126
|
+
fetch(url, requestInit).then(response => response.text())
|
|
127
|
+
.then(json => this.fetchDone(JSON.parse(json).map(([id, caption]) => ({ caption, id: +id })), startsWith, lastKey))
|
|
128
|
+
.catch(() => {
|
|
101
129
|
this.fetching = undefined;
|
|
102
|
-
setTimeout(() => this.fetch(), 100);
|
|
130
|
+
setTimeout(() => this.fetch(lastKey), 100);
|
|
103
131
|
});
|
|
104
132
|
}
|
|
133
|
+
fetchDone(items, startsWith, lastKey) {
|
|
134
|
+
if (items) {
|
|
135
|
+
this.lastItems = items;
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
items = this.lastItems;
|
|
139
|
+
}
|
|
140
|
+
this.fetching = undefined;
|
|
141
|
+
this.updateSuggestions(items, startsWith);
|
|
142
|
+
if (!['Backspace', 'Delete'].includes(lastKey)) {
|
|
143
|
+
this.autoComplete();
|
|
144
|
+
}
|
|
145
|
+
this.onInputValueChange();
|
|
146
|
+
this.autoIdInputValue();
|
|
147
|
+
}
|
|
105
148
|
initIdInput() {
|
|
106
149
|
const input = this.input;
|
|
107
150
|
const next = input.nextElementSibling;
|
|
@@ -154,6 +197,8 @@ export class AutoComplete {
|
|
|
154
197
|
if (DEBUG)
|
|
155
198
|
console.log('input.value =');
|
|
156
199
|
this.input.value = '';
|
|
200
|
+
this.lastStart = '';
|
|
201
|
+
this.suggestions.removeList();
|
|
157
202
|
this.onInputValueChange();
|
|
158
203
|
this.autoIdInputValue();
|
|
159
204
|
}
|
|
@@ -174,7 +219,20 @@ export class AutoComplete {
|
|
|
174
219
|
onBlur(_event) {
|
|
175
220
|
if (DEBUG)
|
|
176
221
|
console.log('onBlur()');
|
|
222
|
+
const suggestion = this.suggestions.selected();
|
|
177
223
|
this.suggestions.removeList();
|
|
224
|
+
if (suggestion
|
|
225
|
+
&& this.idInput?.value
|
|
226
|
+
&& (this.input.value !== suggestion.caption)
|
|
227
|
+
&& (toInsensitive(this.input.value) === toInsensitive(suggestion.caption))) {
|
|
228
|
+
this.input.value = suggestion.caption;
|
|
229
|
+
this.onInputValueChange();
|
|
230
|
+
}
|
|
231
|
+
else if (!this.options.allowNew && this.idInput && this.input.value && !this.idInput.value) {
|
|
232
|
+
this.input.value = '';
|
|
233
|
+
this.onInputValueChange();
|
|
234
|
+
this.autoIdInputValue();
|
|
235
|
+
}
|
|
178
236
|
}
|
|
179
237
|
onInput(event) {
|
|
180
238
|
if (DEBUG)
|
|
@@ -183,7 +241,7 @@ export class AutoComplete {
|
|
|
183
241
|
return;
|
|
184
242
|
if (this.input.dataset.lastValue === this.input.value)
|
|
185
243
|
return;
|
|
186
|
-
this.fetch();
|
|
244
|
+
this.fetch(this.lastKey);
|
|
187
245
|
}
|
|
188
246
|
onInputValueChange() {
|
|
189
247
|
if (DEBUG)
|
|
@@ -199,8 +257,10 @@ export class AutoComplete {
|
|
|
199
257
|
}
|
|
200
258
|
onKeyDown(event) {
|
|
201
259
|
if (DEBUG)
|
|
202
|
-
console.log('onKeyDown()', event.key);
|
|
260
|
+
console.log('########## onKeyDown()', event.key);
|
|
203
261
|
this.lastKey = event.key;
|
|
262
|
+
if (this.consumeSelectedChar(event))
|
|
263
|
+
return;
|
|
204
264
|
switch (event.key) {
|
|
205
265
|
case 'ArrowDown':
|
|
206
266
|
case 'Down':
|
|
@@ -218,14 +278,22 @@ export class AutoComplete {
|
|
|
218
278
|
this.openSuggestions(event);
|
|
219
279
|
}
|
|
220
280
|
openSuggestions(event) {
|
|
281
|
+
if (DEBUG)
|
|
282
|
+
console.log('openSuggestions()');
|
|
221
283
|
const suggestions = this.suggestions;
|
|
222
284
|
if (!suggestions.length) {
|
|
285
|
+
if (DEBUG)
|
|
286
|
+
console.log('OS: no suggestions => fetch');
|
|
223
287
|
this.fetch();
|
|
224
288
|
}
|
|
225
289
|
if (suggestions.isVisible()) {
|
|
290
|
+
if (DEBUG)
|
|
291
|
+
console.log('OS: isVisible is true => return');
|
|
226
292
|
return false;
|
|
227
293
|
}
|
|
228
294
|
if ((suggestions.length > 1) || (!this.input.value.length && suggestions.length)) {
|
|
295
|
+
if (DEBUG)
|
|
296
|
+
console.log('OS: has items => show');
|
|
229
297
|
event.preventDefault();
|
|
230
298
|
suggestions.show();
|
|
231
299
|
}
|
|
@@ -260,11 +328,19 @@ export class AutoComplete {
|
|
|
260
328
|
this.onInputValueChange();
|
|
261
329
|
this.autoIdInputValue();
|
|
262
330
|
}
|
|
331
|
+
updateSuggestions(items, startsWith = '') {
|
|
332
|
+
startsWith = toInsensitive(startsWith);
|
|
333
|
+
const suggestions = startsWith.length
|
|
334
|
+
? items.filter(item => (item.caption === '...') || toInsensitive(item.caption).startsWith(startsWith))
|
|
335
|
+
: items;
|
|
336
|
+
this.suggestions.update(suggestions);
|
|
337
|
+
}
|
|
263
338
|
}
|
|
264
339
|
class Suggestions {
|
|
265
340
|
autoComplete;
|
|
266
341
|
length = 0;
|
|
267
342
|
list;
|
|
343
|
+
partial = false;
|
|
268
344
|
pointerStart;
|
|
269
345
|
constructor(autoComplete) {
|
|
270
346
|
this.autoComplete = autoComplete;
|
|
@@ -331,6 +407,13 @@ class Suggestions {
|
|
|
331
407
|
item.classList.add('selected');
|
|
332
408
|
this.autoComplete.select();
|
|
333
409
|
this.pointerStart = undefined;
|
|
410
|
+
const input = this.autoComplete.input;
|
|
411
|
+
if (DEBUG)
|
|
412
|
+
console.log('down: focus+setSelectionRange', input.value);
|
|
413
|
+
setTimeout(() => {
|
|
414
|
+
input.focus();
|
|
415
|
+
input.setSelectionRange(0, input.value.length);
|
|
416
|
+
});
|
|
334
417
|
}
|
|
335
418
|
onPointerCancel(_event) {
|
|
336
419
|
this.pointerStart = undefined;
|
|
@@ -350,6 +433,13 @@ class Suggestions {
|
|
|
350
433
|
this.pointerStart.item.classList.add('selected');
|
|
351
434
|
this.autoComplete.select();
|
|
352
435
|
this.pointerStart = undefined;
|
|
436
|
+
const input = this.autoComplete.input;
|
|
437
|
+
if (DEBUG)
|
|
438
|
+
console.log('up: focus+setSelectionRange', input.value);
|
|
439
|
+
setTimeout(() => {
|
|
440
|
+
input.focus();
|
|
441
|
+
input.setSelectionRange(0, input.value.length);
|
|
442
|
+
});
|
|
353
443
|
}
|
|
354
444
|
removeList() {
|
|
355
445
|
if (DEBUG)
|
|
@@ -364,7 +454,7 @@ class Suggestions {
|
|
|
364
454
|
item ??= this.list?.querySelector('li.selected') ?? null;
|
|
365
455
|
if (DEBUG)
|
|
366
456
|
console.log('selected()', item && { caption: item.innerText, id: +(item.dataset.id ?? 0) });
|
|
367
|
-
return item && { caption: item.innerText, id: +(item.dataset.id ?? 0) };
|
|
457
|
+
return item && { caption: item.innerText === '...' ? '' : item.innerText, id: +(item.dataset.id ?? 0) };
|
|
368
458
|
}
|
|
369
459
|
selectFirst() {
|
|
370
460
|
if (DEBUG)
|
|
@@ -396,6 +486,7 @@ class Suggestions {
|
|
|
396
486
|
this.unselect(item);
|
|
397
487
|
item = item[sibling];
|
|
398
488
|
item.classList.add('selected');
|
|
489
|
+
item.scrollIntoView({ block: 'nearest' });
|
|
399
490
|
}
|
|
400
491
|
if (DEBUG)
|
|
401
492
|
console.log(' ', item);
|
|
@@ -428,12 +519,13 @@ class Suggestions {
|
|
|
428
519
|
}
|
|
429
520
|
update(suggestions) {
|
|
430
521
|
if (DEBUG)
|
|
431
|
-
console.log('update()');
|
|
522
|
+
console.log('update()', suggestions);
|
|
432
523
|
let hasSelected = false;
|
|
433
524
|
const list = this.list ?? this.createList();
|
|
434
525
|
const selected = list.querySelector('li.selected')?.innerText;
|
|
435
526
|
list.innerHTML = '';
|
|
436
527
|
this.length = suggestions.length;
|
|
528
|
+
this.partial = false;
|
|
437
529
|
for (const suggestion of suggestions) {
|
|
438
530
|
const item = document.createElement('li');
|
|
439
531
|
item.dataset.id = '' + suggestion.id;
|
|
@@ -443,7 +535,14 @@ class Suggestions {
|
|
|
443
535
|
item.classList.add('selected');
|
|
444
536
|
}
|
|
445
537
|
list.appendChild(item);
|
|
538
|
+
if (!suggestion.id && (suggestion.caption === '...')) {
|
|
539
|
+
this.partial = true;
|
|
540
|
+
}
|
|
446
541
|
}
|
|
542
|
+
if (DEBUG)
|
|
543
|
+
console.log('- length =', this.length);
|
|
544
|
+
if (DEBUG)
|
|
545
|
+
console.log('- partial =', this.partial ? 'true' : 'false');
|
|
447
546
|
if (!hasSelected) {
|
|
448
547
|
list.firstElementChild?.classList.add('selected');
|
|
449
548
|
}
|
package/package.json
CHANGED