@itrocks/autocomplete 0.1.5 → 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 +4 -0
- package/autocomplete.d.ts +25 -5
- package/autocomplete.js +155 -39
- 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,19 +37,30 @@ 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;
|
|
36
|
-
|
|
46
|
+
partial: boolean;
|
|
47
|
+
pointerStart?: {
|
|
48
|
+
id: number;
|
|
49
|
+
item: HTMLLIElement;
|
|
50
|
+
x: number;
|
|
51
|
+
y: number;
|
|
52
|
+
};
|
|
53
|
+
constructor(autoComplete: AutoComplete);
|
|
37
54
|
createList(): HTMLUListElement;
|
|
38
55
|
first(): Item | null;
|
|
39
56
|
hide(): void;
|
|
40
57
|
isFirstSelected(): boolean | null | undefined;
|
|
41
58
|
isLastSelected(): boolean | null | undefined;
|
|
42
59
|
isVisible(): boolean | undefined;
|
|
43
|
-
onPointerDown(event:
|
|
60
|
+
onPointerDown(event: PointerEvent): void;
|
|
61
|
+
onPointerCancel(_event: PointerEvent): void;
|
|
62
|
+
onPointerMove(event: PointerEvent): void;
|
|
63
|
+
onPointerUp(event: PointerEvent): void;
|
|
44
64
|
removeList(): void;
|
|
45
65
|
selected(item?: HTMLLIElement | null): Item | null;
|
|
46
66
|
selectFirst(): void;
|
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;
|
|
70
86
|
const input = this.input;
|
|
71
|
-
const
|
|
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 = '') {
|
|
102
|
+
const input = this.input;
|
|
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(
|
|
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(() => {
|
|
88
129
|
this.fetching = undefined;
|
|
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(() => {
|
|
101
|
-
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;
|
|
@@ -137,6 +180,7 @@ export class AutoComplete {
|
|
|
137
180
|
return;
|
|
138
181
|
event.preventDefault();
|
|
139
182
|
this.select();
|
|
183
|
+
suggestions.hide();
|
|
140
184
|
}
|
|
141
185
|
keyEscape(event) {
|
|
142
186
|
if (DEBUG)
|
|
@@ -174,6 +218,11 @@ export class AutoComplete {
|
|
|
174
218
|
if (DEBUG)
|
|
175
219
|
console.log('onBlur()');
|
|
176
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
|
+
}
|
|
177
226
|
}
|
|
178
227
|
onInput(event) {
|
|
179
228
|
if (DEBUG)
|
|
@@ -182,7 +231,7 @@ export class AutoComplete {
|
|
|
182
231
|
return;
|
|
183
232
|
if (this.input.dataset.lastValue === this.input.value)
|
|
184
233
|
return;
|
|
185
|
-
this.fetch();
|
|
234
|
+
this.fetch(this.lastKey);
|
|
186
235
|
}
|
|
187
236
|
onInputValueChange() {
|
|
188
237
|
if (DEBUG)
|
|
@@ -198,8 +247,10 @@ export class AutoComplete {
|
|
|
198
247
|
}
|
|
199
248
|
onKeyDown(event) {
|
|
200
249
|
if (DEBUG)
|
|
201
|
-
console.log('onKeyDown()', event.key);
|
|
250
|
+
console.log('########## onKeyDown()', event.key);
|
|
202
251
|
this.lastKey = event.key;
|
|
252
|
+
if (this.consumeSelectedChar(event))
|
|
253
|
+
return;
|
|
203
254
|
switch (event.key) {
|
|
204
255
|
case 'ArrowDown':
|
|
205
256
|
case 'Down':
|
|
@@ -217,14 +268,22 @@ export class AutoComplete {
|
|
|
217
268
|
this.openSuggestions(event);
|
|
218
269
|
}
|
|
219
270
|
openSuggestions(event) {
|
|
271
|
+
if (DEBUG)
|
|
272
|
+
console.log('openSuggestions()');
|
|
220
273
|
const suggestions = this.suggestions;
|
|
221
274
|
if (!suggestions.length) {
|
|
275
|
+
if (DEBUG)
|
|
276
|
+
console.log('OS: no suggestions => fetch');
|
|
222
277
|
this.fetch();
|
|
223
278
|
}
|
|
224
279
|
if (suggestions.isVisible()) {
|
|
280
|
+
if (DEBUG)
|
|
281
|
+
console.log('OS: isVisible is true => return');
|
|
225
282
|
return false;
|
|
226
283
|
}
|
|
227
284
|
if ((suggestions.length > 1) || (!this.input.value.length && suggestions.length)) {
|
|
285
|
+
if (DEBUG)
|
|
286
|
+
console.log('OS: has items => show');
|
|
228
287
|
event.preventDefault();
|
|
229
288
|
suggestions.show();
|
|
230
289
|
}
|
|
@@ -242,7 +301,6 @@ export class AutoComplete {
|
|
|
242
301
|
this.input.value = suggestion.caption;
|
|
243
302
|
this.onInputValueChange();
|
|
244
303
|
this.autoIdInputValue();
|
|
245
|
-
suggestions.hide();
|
|
246
304
|
}
|
|
247
305
|
suggest(value) {
|
|
248
306
|
if (DEBUG)
|
|
@@ -260,21 +318,33 @@ 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;
|
|
268
|
-
|
|
269
|
-
|
|
333
|
+
partial = false;
|
|
334
|
+
pointerStart;
|
|
335
|
+
constructor(autoComplete) {
|
|
336
|
+
this.autoComplete = autoComplete;
|
|
270
337
|
}
|
|
271
338
|
createList() {
|
|
272
339
|
if (DEBUG)
|
|
273
340
|
console.log('createList()');
|
|
274
341
|
const list = this.list = document.createElement('ul');
|
|
275
342
|
list.classList.add('suggestions');
|
|
276
|
-
this.
|
|
343
|
+
this.autoComplete.input.insertAdjacentElement('afterend', list);
|
|
277
344
|
list.addEventListener('pointerdown', event => this.onPointerDown(event));
|
|
345
|
+
list.addEventListener('pointercancel', event => this.onPointerCancel(event));
|
|
346
|
+
list.addEventListener('pointermove', event => this.onPointerMove(event));
|
|
347
|
+
list.addEventListener('pointerup', event => this.onPointerUp(event));
|
|
278
348
|
return list;
|
|
279
349
|
}
|
|
280
350
|
first() {
|
|
@@ -307,7 +377,7 @@ class Suggestions {
|
|
|
307
377
|
onPointerDown(event) {
|
|
308
378
|
if (DEBUG)
|
|
309
379
|
console.log('onPointerDown()', event.button);
|
|
310
|
-
if (event.button !== 0)
|
|
380
|
+
if ((event.pointerType === 'mouse') && (event.button !== 0))
|
|
311
381
|
return;
|
|
312
382
|
if (!(event.target instanceof Element))
|
|
313
383
|
return;
|
|
@@ -319,10 +389,47 @@ class Suggestions {
|
|
|
319
389
|
return;
|
|
320
390
|
if (DEBUG)
|
|
321
391
|
console.log(' select', item);
|
|
392
|
+
if (event.pointerType !== 'mouse') {
|
|
393
|
+
this.pointerStart = { id: event.pointerId, item, x: event.clientX, y: event.clientY };
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
322
396
|
this.unselect();
|
|
323
397
|
item.classList.add('selected');
|
|
324
|
-
this.
|
|
325
|
-
|
|
398
|
+
this.autoComplete.select();
|
|
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
|
+
});
|
|
407
|
+
}
|
|
408
|
+
onPointerCancel(_event) {
|
|
409
|
+
this.pointerStart = undefined;
|
|
410
|
+
}
|
|
411
|
+
onPointerMove(event) {
|
|
412
|
+
if (!this.pointerStart || (event.pointerId !== this.pointerStart.id))
|
|
413
|
+
return;
|
|
414
|
+
const distance = Math.abs(event.clientX - this.pointerStart.x) + Math.abs(event.clientY - this.pointerStart.y);
|
|
415
|
+
if (distance < 8)
|
|
416
|
+
return;
|
|
417
|
+
this.pointerStart = undefined;
|
|
418
|
+
}
|
|
419
|
+
onPointerUp(event) {
|
|
420
|
+
if (!this.pointerStart || (event.pointerId !== this.pointerStart.id))
|
|
421
|
+
return;
|
|
422
|
+
this.unselect();
|
|
423
|
+
this.pointerStart.item.classList.add('selected');
|
|
424
|
+
this.autoComplete.select();
|
|
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
|
+
});
|
|
326
433
|
}
|
|
327
434
|
removeList() {
|
|
328
435
|
if (DEBUG)
|
|
@@ -337,7 +444,7 @@ class Suggestions {
|
|
|
337
444
|
item ??= this.list?.querySelector('li.selected') ?? null;
|
|
338
445
|
if (DEBUG)
|
|
339
446
|
console.log('selected()', item && { caption: item.innerText, id: +(item.dataset.id ?? 0) });
|
|
340
|
-
return item && { caption: item.innerText, id: +(item.dataset.id ?? 0) };
|
|
447
|
+
return item && { caption: item.innerText === '...' ? '' : item.innerText, id: +(item.dataset.id ?? 0) };
|
|
341
448
|
}
|
|
342
449
|
selectFirst() {
|
|
343
450
|
if (DEBUG)
|
|
@@ -369,6 +476,7 @@ class Suggestions {
|
|
|
369
476
|
this.unselect(item);
|
|
370
477
|
item = item[sibling];
|
|
371
478
|
item.classList.add('selected');
|
|
479
|
+
item.scrollIntoView({ block: 'nearest' });
|
|
372
480
|
}
|
|
373
481
|
if (DEBUG)
|
|
374
482
|
console.log(' ', item);
|
|
@@ -401,12 +509,13 @@ class Suggestions {
|
|
|
401
509
|
}
|
|
402
510
|
update(suggestions) {
|
|
403
511
|
if (DEBUG)
|
|
404
|
-
console.log('update()');
|
|
512
|
+
console.log('update()', suggestions);
|
|
405
513
|
let hasSelected = false;
|
|
406
514
|
const list = this.list ?? this.createList();
|
|
407
515
|
const selected = list.querySelector('li.selected')?.innerText;
|
|
408
516
|
list.innerHTML = '';
|
|
409
517
|
this.length = suggestions.length;
|
|
518
|
+
this.partial = false;
|
|
410
519
|
for (const suggestion of suggestions) {
|
|
411
520
|
const item = document.createElement('li');
|
|
412
521
|
item.dataset.id = '' + suggestion.id;
|
|
@@ -416,7 +525,14 @@ class Suggestions {
|
|
|
416
525
|
item.classList.add('selected');
|
|
417
526
|
}
|
|
418
527
|
list.appendChild(item);
|
|
528
|
+
if (!suggestion.id && (suggestion.caption === '...')) {
|
|
529
|
+
this.partial = true;
|
|
530
|
+
}
|
|
419
531
|
}
|
|
532
|
+
if (DEBUG)
|
|
533
|
+
console.log('- length =', this.length);
|
|
534
|
+
if (DEBUG)
|
|
535
|
+
console.log('- partial =', this.partial ? 'true' : 'false');
|
|
420
536
|
if (!hasSelected) {
|
|
421
537
|
list.firstElementChild?.classList.add('selected');
|
|
422
538
|
}
|
package/package.json
CHANGED