@itrocks/autocomplete 0.1.6 → 0.1.7
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 +120 -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;
|
|
@@ -175,6 +218,11 @@ export class AutoComplete {
|
|
|
175
218
|
if (DEBUG)
|
|
176
219
|
console.log('onBlur()');
|
|
177
220
|
this.suggestions.removeList();
|
|
221
|
+
if (!this.options.allowNew && this.idInput && this.input.value && !this.idInput.value) {
|
|
222
|
+
this.input.value = '';
|
|
223
|
+
this.onInputValueChange();
|
|
224
|
+
this.autoIdInputValue();
|
|
225
|
+
}
|
|
178
226
|
}
|
|
179
227
|
onInput(event) {
|
|
180
228
|
if (DEBUG)
|
|
@@ -183,7 +231,7 @@ export class AutoComplete {
|
|
|
183
231
|
return;
|
|
184
232
|
if (this.input.dataset.lastValue === this.input.value)
|
|
185
233
|
return;
|
|
186
|
-
this.fetch();
|
|
234
|
+
this.fetch(this.lastKey);
|
|
187
235
|
}
|
|
188
236
|
onInputValueChange() {
|
|
189
237
|
if (DEBUG)
|
|
@@ -199,8 +247,10 @@ export class AutoComplete {
|
|
|
199
247
|
}
|
|
200
248
|
onKeyDown(event) {
|
|
201
249
|
if (DEBUG)
|
|
202
|
-
console.log('onKeyDown()', event.key);
|
|
250
|
+
console.log('########## onKeyDown()', event.key);
|
|
203
251
|
this.lastKey = event.key;
|
|
252
|
+
if (this.consumeSelectedChar(event))
|
|
253
|
+
return;
|
|
204
254
|
switch (event.key) {
|
|
205
255
|
case 'ArrowDown':
|
|
206
256
|
case 'Down':
|
|
@@ -218,14 +268,22 @@ export class AutoComplete {
|
|
|
218
268
|
this.openSuggestions(event);
|
|
219
269
|
}
|
|
220
270
|
openSuggestions(event) {
|
|
271
|
+
if (DEBUG)
|
|
272
|
+
console.log('openSuggestions()');
|
|
221
273
|
const suggestions = this.suggestions;
|
|
222
274
|
if (!suggestions.length) {
|
|
275
|
+
if (DEBUG)
|
|
276
|
+
console.log('OS: no suggestions => fetch');
|
|
223
277
|
this.fetch();
|
|
224
278
|
}
|
|
225
279
|
if (suggestions.isVisible()) {
|
|
280
|
+
if (DEBUG)
|
|
281
|
+
console.log('OS: isVisible is true => return');
|
|
226
282
|
return false;
|
|
227
283
|
}
|
|
228
284
|
if ((suggestions.length > 1) || (!this.input.value.length && suggestions.length)) {
|
|
285
|
+
if (DEBUG)
|
|
286
|
+
console.log('OS: has items => show');
|
|
229
287
|
event.preventDefault();
|
|
230
288
|
suggestions.show();
|
|
231
289
|
}
|
|
@@ -260,11 +318,19 @@ export class AutoComplete {
|
|
|
260
318
|
this.onInputValueChange();
|
|
261
319
|
this.autoIdInputValue();
|
|
262
320
|
}
|
|
321
|
+
updateSuggestions(items, startsWith = '') {
|
|
322
|
+
startsWith = toInsensitive(startsWith);
|
|
323
|
+
const suggestions = startsWith.length
|
|
324
|
+
? items.filter(item => (item.caption === '...') || toInsensitive(item.caption).startsWith(startsWith))
|
|
325
|
+
: items;
|
|
326
|
+
this.suggestions.update(suggestions);
|
|
327
|
+
}
|
|
263
328
|
}
|
|
264
329
|
class Suggestions {
|
|
265
330
|
autoComplete;
|
|
266
331
|
length = 0;
|
|
267
332
|
list;
|
|
333
|
+
partial = false;
|
|
268
334
|
pointerStart;
|
|
269
335
|
constructor(autoComplete) {
|
|
270
336
|
this.autoComplete = autoComplete;
|
|
@@ -331,6 +397,13 @@ class Suggestions {
|
|
|
331
397
|
item.classList.add('selected');
|
|
332
398
|
this.autoComplete.select();
|
|
333
399
|
this.pointerStart = undefined;
|
|
400
|
+
const input = this.autoComplete.input;
|
|
401
|
+
if (DEBUG)
|
|
402
|
+
console.log('down: focus+setSelectionRange', input.value);
|
|
403
|
+
setTimeout(() => {
|
|
404
|
+
input.focus();
|
|
405
|
+
input.setSelectionRange(0, input.value.length);
|
|
406
|
+
});
|
|
334
407
|
}
|
|
335
408
|
onPointerCancel(_event) {
|
|
336
409
|
this.pointerStart = undefined;
|
|
@@ -350,6 +423,13 @@ class Suggestions {
|
|
|
350
423
|
this.pointerStart.item.classList.add('selected');
|
|
351
424
|
this.autoComplete.select();
|
|
352
425
|
this.pointerStart = undefined;
|
|
426
|
+
const input = this.autoComplete.input;
|
|
427
|
+
if (DEBUG)
|
|
428
|
+
console.log('up: focus+setSelectionRange', input.value);
|
|
429
|
+
setTimeout(() => {
|
|
430
|
+
input.focus();
|
|
431
|
+
input.setSelectionRange(0, input.value.length);
|
|
432
|
+
});
|
|
353
433
|
}
|
|
354
434
|
removeList() {
|
|
355
435
|
if (DEBUG)
|
|
@@ -364,7 +444,7 @@ class Suggestions {
|
|
|
364
444
|
item ??= this.list?.querySelector('li.selected') ?? null;
|
|
365
445
|
if (DEBUG)
|
|
366
446
|
console.log('selected()', item && { caption: item.innerText, id: +(item.dataset.id ?? 0) });
|
|
367
|
-
return item && { caption: item.innerText, id: +(item.dataset.id ?? 0) };
|
|
447
|
+
return item && { caption: item.innerText === '...' ? '' : item.innerText, id: +(item.dataset.id ?? 0) };
|
|
368
448
|
}
|
|
369
449
|
selectFirst() {
|
|
370
450
|
if (DEBUG)
|
|
@@ -396,6 +476,7 @@ class Suggestions {
|
|
|
396
476
|
this.unselect(item);
|
|
397
477
|
item = item[sibling];
|
|
398
478
|
item.classList.add('selected');
|
|
479
|
+
item.scrollIntoView({ block: 'nearest' });
|
|
399
480
|
}
|
|
400
481
|
if (DEBUG)
|
|
401
482
|
console.log(' ', item);
|
|
@@ -428,12 +509,13 @@ class Suggestions {
|
|
|
428
509
|
}
|
|
429
510
|
update(suggestions) {
|
|
430
511
|
if (DEBUG)
|
|
431
|
-
console.log('update()');
|
|
512
|
+
console.log('update()', suggestions);
|
|
432
513
|
let hasSelected = false;
|
|
433
514
|
const list = this.list ?? this.createList();
|
|
434
515
|
const selected = list.querySelector('li.selected')?.innerText;
|
|
435
516
|
list.innerHTML = '';
|
|
436
517
|
this.length = suggestions.length;
|
|
518
|
+
this.partial = false;
|
|
437
519
|
for (const suggestion of suggestions) {
|
|
438
520
|
const item = document.createElement('li');
|
|
439
521
|
item.dataset.id = '' + suggestion.id;
|
|
@@ -443,7 +525,14 @@ class Suggestions {
|
|
|
443
525
|
item.classList.add('selected');
|
|
444
526
|
}
|
|
445
527
|
list.appendChild(item);
|
|
528
|
+
if (!suggestion.id && (suggestion.caption === '...')) {
|
|
529
|
+
this.partial = true;
|
|
530
|
+
}
|
|
446
531
|
}
|
|
532
|
+
if (DEBUG)
|
|
533
|
+
console.log('- length =', this.length);
|
|
534
|
+
if (DEBUG)
|
|
535
|
+
console.log('- partial =', this.partial ? 'true' : 'false');
|
|
447
536
|
if (!hasSelected) {
|
|
448
537
|
list.firstElementChild?.classList.add('selected');
|
|
449
538
|
}
|
package/package.json
CHANGED