@itrocks/autocomplete 0.1.0 → 0.1.2
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.d.ts +9 -5
- package/autocomplete.js +132 -16
- package/package.json +1 -1
package/autocomplete.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ interface Item {
|
|
|
3
3
|
id: number;
|
|
4
4
|
}
|
|
5
5
|
export declare class AutoComplete {
|
|
6
|
+
fetching?: string;
|
|
6
7
|
idInput?: HTMLInputElement;
|
|
7
8
|
input: HTMLInputElement;
|
|
8
9
|
lastKey: string;
|
|
@@ -14,14 +15,15 @@ export declare class AutoComplete {
|
|
|
14
15
|
fetch(): void;
|
|
15
16
|
initIdInput(): HTMLInputElement | undefined;
|
|
16
17
|
initInput(input: HTMLInputElement): HTMLInputElement;
|
|
17
|
-
keyDown(event:
|
|
18
|
-
keyEnter(event:
|
|
19
|
-
keyEscape(event:
|
|
20
|
-
keyUp(event:
|
|
21
|
-
onBlur(_event:
|
|
18
|
+
keyDown(event: Event): void;
|
|
19
|
+
keyEnter(event: Event): void;
|
|
20
|
+
keyEscape(event: Event): void;
|
|
21
|
+
keyUp(event: Event): void;
|
|
22
|
+
onBlur(_event: Event): void;
|
|
22
23
|
onInput(event: Event): void;
|
|
23
24
|
onInputValueChange(): void;
|
|
24
25
|
onKeyDown(event: KeyboardEvent): void;
|
|
26
|
+
select(): void;
|
|
25
27
|
suggest(value?: string): void;
|
|
26
28
|
}
|
|
27
29
|
declare class Suggestions {
|
|
@@ -35,6 +37,7 @@ declare class Suggestions {
|
|
|
35
37
|
isFirstSelected(): boolean | null | undefined;
|
|
36
38
|
isLastSelected(): boolean | null | undefined;
|
|
37
39
|
isVisible(): boolean | undefined;
|
|
40
|
+
onPointerDown(event: MouseEvent): void;
|
|
38
41
|
removeList(): void;
|
|
39
42
|
selected(item?: HTMLLIElement | null): Item | null;
|
|
40
43
|
selectFirst(): void;
|
|
@@ -42,6 +45,7 @@ declare class Suggestions {
|
|
|
42
45
|
selectPrevious(): Item | null;
|
|
43
46
|
selectSibling(sibling: 'nextElementSibling' | 'previousElementSibling'): HTMLLIElement | null;
|
|
44
47
|
show(): HTMLUListElement;
|
|
48
|
+
unselect(item?: HTMLLIElement | null | undefined): void;
|
|
45
49
|
update(suggestions: Item[]): void;
|
|
46
50
|
}
|
|
47
51
|
export {};
|
package/autocomplete.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
const DEBUG = false;
|
|
1
2
|
export class AutoComplete {
|
|
3
|
+
fetching;
|
|
2
4
|
idInput;
|
|
3
5
|
input;
|
|
4
6
|
lastKey = '';
|
|
5
7
|
suggestions;
|
|
6
8
|
constructor(input) {
|
|
9
|
+
if (DEBUG)
|
|
10
|
+
console.log('new AutoComplete()', input);
|
|
7
11
|
this.input = this.initInput(input);
|
|
8
12
|
this.idInput = this.initIdInput();
|
|
9
13
|
this.suggestions = new Suggestions(this);
|
|
@@ -12,6 +16,8 @@ export class AutoComplete {
|
|
|
12
16
|
input.addEventListener('input', event => this.onInput(event));
|
|
13
17
|
}
|
|
14
18
|
autoComplete() {
|
|
19
|
+
if (DEBUG)
|
|
20
|
+
console.log('autoComplete()', this);
|
|
15
21
|
const input = this.input;
|
|
16
22
|
if (input.selectionStart !== input.value.length) {
|
|
17
23
|
return;
|
|
@@ -29,9 +35,13 @@ export class AutoComplete {
|
|
|
29
35
|
input.setSelectionRange(position, input.value.length);
|
|
30
36
|
}
|
|
31
37
|
autoEmptyClass() {
|
|
38
|
+
if (DEBUG)
|
|
39
|
+
console.log('autoEmptyClass()', this.input.value);
|
|
32
40
|
const input = this.input;
|
|
33
41
|
const classList = input.classList;
|
|
34
42
|
if (input.value === '') {
|
|
43
|
+
if (DEBUG)
|
|
44
|
+
console.log(' + empty');
|
|
35
45
|
classList.add('empty');
|
|
36
46
|
return;
|
|
37
47
|
}
|
|
@@ -41,6 +51,8 @@ export class AutoComplete {
|
|
|
41
51
|
}
|
|
42
52
|
}
|
|
43
53
|
autoIdInputValue() {
|
|
54
|
+
if (DEBUG)
|
|
55
|
+
console.log('autoIdInputValue', this.suggestions.selected());
|
|
44
56
|
const idInput = this.idInput;
|
|
45
57
|
if (!idInput)
|
|
46
58
|
return;
|
|
@@ -49,16 +61,31 @@ export class AutoComplete {
|
|
|
49
61
|
idInput.value = (suggestion && (toInsensitive(input.value) === toInsensitive(suggestion.caption)))
|
|
50
62
|
? '' + suggestion.id
|
|
51
63
|
: '';
|
|
64
|
+
if (DEBUG)
|
|
65
|
+
console.log(' idInput =', idInput.value);
|
|
52
66
|
}
|
|
53
67
|
fetch() {
|
|
54
68
|
const input = this.input;
|
|
69
|
+
const inputValue = ((input.selectionStart !== null) && (input.selectionEnd === input.value.length))
|
|
70
|
+
? input.value.slice(0, input.selectionStart)
|
|
71
|
+
: input.value;
|
|
72
|
+
if (this.fetching) {
|
|
73
|
+
if (inputValue !== this.fetching) {
|
|
74
|
+
setTimeout(() => this.fetch(), 50);
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
this.fetching = inputValue;
|
|
55
79
|
const dataFetch = input.dataset.fetch ?? input.closest('[data-fetch]')?.dataset.fetch;
|
|
56
80
|
const lastKey = this.lastKey;
|
|
57
81
|
const requestInit = { headers: { Accept: 'application/json' } };
|
|
58
|
-
const summaryRoute = dataFetch + (
|
|
82
|
+
const summaryRoute = dataFetch + (inputValue ? ('?startsWith=' + inputValue) : '');
|
|
83
|
+
if (DEBUG)
|
|
84
|
+
console.log('fetch()', 'startsWith=' + inputValue);
|
|
59
85
|
fetch(summaryRoute, requestInit).then(response => response.text()).then(json => {
|
|
86
|
+
this.fetching = undefined;
|
|
60
87
|
const summary = JSON.parse(json).map(([id, caption]) => ({ caption, id: +id }));
|
|
61
|
-
const startsWith = toInsensitive(
|
|
88
|
+
const startsWith = toInsensitive(inputValue);
|
|
62
89
|
const suggestions = startsWith.length
|
|
63
90
|
? summary.filter(item => toInsensitive(item.caption).startsWith(startsWith))
|
|
64
91
|
: summary;
|
|
@@ -68,6 +95,9 @@ export class AutoComplete {
|
|
|
68
95
|
}
|
|
69
96
|
this.onInputValueChange();
|
|
70
97
|
this.autoIdInputValue();
|
|
98
|
+
}).catch(() => {
|
|
99
|
+
this.fetching = undefined;
|
|
100
|
+
setTimeout(() => this.fetch(), 100);
|
|
71
101
|
});
|
|
72
102
|
}
|
|
73
103
|
initIdInput() {
|
|
@@ -83,6 +113,8 @@ export class AutoComplete {
|
|
|
83
113
|
return input;
|
|
84
114
|
}
|
|
85
115
|
keyDown(event) {
|
|
116
|
+
if (DEBUG)
|
|
117
|
+
console.log('keyDown()');
|
|
86
118
|
const suggestions = this.suggestions;
|
|
87
119
|
if (!suggestions.length) {
|
|
88
120
|
this.fetch();
|
|
@@ -101,21 +133,17 @@ export class AutoComplete {
|
|
|
101
133
|
this.suggest(suggestions.selectNext()?.caption);
|
|
102
134
|
}
|
|
103
135
|
keyEnter(event) {
|
|
136
|
+
if (DEBUG)
|
|
137
|
+
console.log('keyEnter()');
|
|
104
138
|
const suggestions = this.suggestions;
|
|
105
|
-
if (!suggestions.isVisible())
|
|
139
|
+
if (!suggestions.isVisible())
|
|
106
140
|
return;
|
|
107
|
-
}
|
|
108
141
|
event.preventDefault();
|
|
109
|
-
|
|
110
|
-
if (!suggestion) {
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
this.input.value = suggestion.caption;
|
|
114
|
-
this.onInputValueChange();
|
|
115
|
-
this.autoIdInputValue();
|
|
116
|
-
suggestions.hide();
|
|
142
|
+
this.select();
|
|
117
143
|
}
|
|
118
144
|
keyEscape(event) {
|
|
145
|
+
if (DEBUG)
|
|
146
|
+
console.log('keyEscape');
|
|
119
147
|
const suggestions = this.suggestions;
|
|
120
148
|
if ((this.input.value === '') && !suggestions.isVisible()) {
|
|
121
149
|
return;
|
|
@@ -125,11 +153,15 @@ export class AutoComplete {
|
|
|
125
153
|
suggestions.hide();
|
|
126
154
|
return;
|
|
127
155
|
}
|
|
156
|
+
if (DEBUG)
|
|
157
|
+
console.log('input.value =');
|
|
128
158
|
this.input.value = '';
|
|
129
159
|
this.onInputValueChange();
|
|
130
160
|
this.autoIdInputValue();
|
|
131
161
|
}
|
|
132
162
|
keyUp(event) {
|
|
163
|
+
if (DEBUG)
|
|
164
|
+
console.log('keyUp()');
|
|
133
165
|
const suggestions = this.suggestions;
|
|
134
166
|
if (!suggestions.isVisible()) {
|
|
135
167
|
return;
|
|
@@ -142,9 +174,13 @@ export class AutoComplete {
|
|
|
142
174
|
this.suggest(suggestions.selectPrevious()?.caption);
|
|
143
175
|
}
|
|
144
176
|
onBlur(_event) {
|
|
145
|
-
|
|
177
|
+
if (DEBUG)
|
|
178
|
+
console.log('onBlur()');
|
|
179
|
+
this.suggestions.removeList();
|
|
146
180
|
}
|
|
147
181
|
onInput(event) {
|
|
182
|
+
if (DEBUG)
|
|
183
|
+
console.log('onInput()');
|
|
148
184
|
if (document.activeElement !== event.target)
|
|
149
185
|
return;
|
|
150
186
|
if (this.input.dataset.lastValue === this.input.value)
|
|
@@ -152,6 +188,8 @@ export class AutoComplete {
|
|
|
152
188
|
this.fetch();
|
|
153
189
|
}
|
|
154
190
|
onInputValueChange() {
|
|
191
|
+
if (DEBUG)
|
|
192
|
+
console.log('onInputValueChange()');
|
|
155
193
|
this.input.dataset.lastValue = this.input.value;
|
|
156
194
|
this.input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
157
195
|
if (document.activeElement !== this.input) {
|
|
@@ -162,6 +200,8 @@ export class AutoComplete {
|
|
|
162
200
|
this.autoEmptyClass();
|
|
163
201
|
}
|
|
164
202
|
onKeyDown(event) {
|
|
203
|
+
if (DEBUG)
|
|
204
|
+
console.log('onKeyDown()', event.key);
|
|
165
205
|
this.lastKey = event.key;
|
|
166
206
|
switch (event.key) {
|
|
167
207
|
case 'ArrowDown':
|
|
@@ -176,12 +216,28 @@ export class AutoComplete {
|
|
|
176
216
|
return this.keyEnter(event);
|
|
177
217
|
}
|
|
178
218
|
}
|
|
219
|
+
select() {
|
|
220
|
+
const suggestions = this.suggestions;
|
|
221
|
+
const suggestion = suggestions.selected();
|
|
222
|
+
if (!suggestion)
|
|
223
|
+
return;
|
|
224
|
+
if (DEBUG)
|
|
225
|
+
console.log(' input =', suggestion.caption);
|
|
226
|
+
this.input.value = suggestion.caption;
|
|
227
|
+
this.onInputValueChange();
|
|
228
|
+
this.autoIdInputValue();
|
|
229
|
+
suggestions.hide();
|
|
230
|
+
}
|
|
179
231
|
suggest(value) {
|
|
232
|
+
if (DEBUG)
|
|
233
|
+
console.log('suggest()', value);
|
|
180
234
|
if (typeof value !== 'string') {
|
|
181
235
|
return;
|
|
182
236
|
}
|
|
183
237
|
const input = this.input;
|
|
184
238
|
const position = input.selectionStart;
|
|
239
|
+
if (DEBUG)
|
|
240
|
+
console.log(' input =', input.value);
|
|
185
241
|
input.value = value;
|
|
186
242
|
input.setSelectionRange(position, input.value.length);
|
|
187
243
|
this.autoComplete();
|
|
@@ -197,6 +253,8 @@ class Suggestions {
|
|
|
197
253
|
this.combo = combo;
|
|
198
254
|
}
|
|
199
255
|
createList() {
|
|
256
|
+
if (DEBUG)
|
|
257
|
+
console.log('Suggestions.createList()');
|
|
200
258
|
const list = this.list = document.createElement('ul');
|
|
201
259
|
list.classList.add('suggestions');
|
|
202
260
|
let input = this.combo.input;
|
|
@@ -205,13 +263,20 @@ class Suggestions {
|
|
|
205
263
|
input = idInput;
|
|
206
264
|
}
|
|
207
265
|
input.insertAdjacentElement('afterend', list);
|
|
266
|
+
if (DEBUG)
|
|
267
|
+
console.log('Suggestions.prepareClic');
|
|
268
|
+
list.addEventListener('pointerdown', event => this.onPointerDown(event));
|
|
208
269
|
return list;
|
|
209
270
|
}
|
|
210
271
|
first() {
|
|
272
|
+
if (DEBUG)
|
|
273
|
+
console.log('Suggestions.first()');
|
|
211
274
|
const item = this.list?.firstElementChild ?? null;
|
|
212
275
|
return item && { caption: item.innerText, id: +(item.dataset.id ?? 0) };
|
|
213
276
|
}
|
|
214
277
|
hide() {
|
|
278
|
+
if (DEBUG)
|
|
279
|
+
console.log('Suggestions.hide()');
|
|
215
280
|
const list = this.list;
|
|
216
281
|
if (!list)
|
|
217
282
|
return;
|
|
@@ -230,47 +295,98 @@ class Suggestions {
|
|
|
230
295
|
isVisible() {
|
|
231
296
|
return this.list && (this.list.style.display !== 'none');
|
|
232
297
|
}
|
|
298
|
+
onPointerDown(event) {
|
|
299
|
+
if (DEBUG)
|
|
300
|
+
console.log('Suggestions.onPointerDown()', event.button);
|
|
301
|
+
if (event.button !== 0)
|
|
302
|
+
return;
|
|
303
|
+
if (!(event.target instanceof Element))
|
|
304
|
+
return;
|
|
305
|
+
const item = event.target.closest('.suggestions > li');
|
|
306
|
+
const list = this.list;
|
|
307
|
+
if (DEBUG)
|
|
308
|
+
console.log(' item', item, 'list', list);
|
|
309
|
+
if (!item || !list)
|
|
310
|
+
return;
|
|
311
|
+
if (DEBUG)
|
|
312
|
+
console.log(' select', item);
|
|
313
|
+
this.unselect();
|
|
314
|
+
item.classList.add('selected');
|
|
315
|
+
this.combo.select();
|
|
316
|
+
event.preventDefault();
|
|
317
|
+
}
|
|
233
318
|
removeList() {
|
|
234
|
-
|
|
319
|
+
if (DEBUG)
|
|
320
|
+
console.log('Suggestions.removeList()');
|
|
321
|
+
if (!this.list)
|
|
322
|
+
return;
|
|
323
|
+
this.list.remove();
|
|
235
324
|
this.list = undefined;
|
|
236
325
|
}
|
|
237
326
|
selected(item = null) {
|
|
238
327
|
item ??= this.list?.querySelector('li.selected') ?? null;
|
|
328
|
+
if (DEBUG)
|
|
329
|
+
console.log('Suggestions.selected()', item && { caption: item.innerText, id: +(item.dataset.id ?? 0) });
|
|
239
330
|
return item && { caption: item.innerText, id: +(item.dataset.id ?? 0) };
|
|
240
331
|
}
|
|
241
332
|
selectFirst() {
|
|
333
|
+
if (DEBUG)
|
|
334
|
+
console.log('selectFirst()', this.list?.firstElementChild);
|
|
242
335
|
const list = this.list;
|
|
243
336
|
if (!list)
|
|
244
337
|
return;
|
|
245
|
-
|
|
338
|
+
this.unselect();
|
|
246
339
|
list.firstElementChild?.classList.add('selected');
|
|
247
340
|
}
|
|
248
341
|
selectNext() {
|
|
342
|
+
if (DEBUG)
|
|
343
|
+
console.log('selectNext()');
|
|
249
344
|
return this.selected(this.selectSibling('nextElementSibling'));
|
|
250
345
|
}
|
|
251
346
|
selectPrevious() {
|
|
347
|
+
if (DEBUG)
|
|
348
|
+
console.log('selectPrevious()');
|
|
252
349
|
return this.selected(this.selectSibling('previousElementSibling'));
|
|
253
350
|
}
|
|
254
351
|
selectSibling(sibling) {
|
|
352
|
+
if (DEBUG)
|
|
353
|
+
console.log('selectSibling()');
|
|
255
354
|
const list = this.list;
|
|
256
355
|
if (!list)
|
|
257
356
|
return null;
|
|
258
357
|
let item = list.querySelector('li.selected');
|
|
259
358
|
if (item && item[sibling]) {
|
|
260
|
-
|
|
359
|
+
this.unselect(item);
|
|
261
360
|
item = item[sibling];
|
|
262
361
|
item.classList.add('selected');
|
|
263
362
|
}
|
|
363
|
+
if (DEBUG)
|
|
364
|
+
console.log(' ', item);
|
|
264
365
|
return item;
|
|
265
366
|
}
|
|
266
367
|
show() {
|
|
368
|
+
if (DEBUG)
|
|
369
|
+
console.log('show()');
|
|
267
370
|
if (this.list) {
|
|
268
371
|
this.list.style.removeProperty('display');
|
|
269
372
|
return this.list;
|
|
270
373
|
}
|
|
271
374
|
return this.createList();
|
|
272
375
|
}
|
|
376
|
+
unselect(item = this.list?.querySelector('li.selected')) {
|
|
377
|
+
if (!item)
|
|
378
|
+
return;
|
|
379
|
+
const classList = item.classList;
|
|
380
|
+
if (!classList)
|
|
381
|
+
return;
|
|
382
|
+
classList.remove('selected');
|
|
383
|
+
if (!classList.length) {
|
|
384
|
+
item.removeAttribute('class');
|
|
385
|
+
}
|
|
386
|
+
}
|
|
273
387
|
update(suggestions) {
|
|
388
|
+
if (DEBUG)
|
|
389
|
+
console.log('update()');
|
|
274
390
|
let hasSelected = false;
|
|
275
391
|
const list = this.list ?? this.createList();
|
|
276
392
|
const selected = list.querySelector('li.selected')?.innerText;
|
package/package.json
CHANGED