@itrocks/autocomplete 0.1.0 → 0.1.1
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 +8 -5
- package/autocomplete.js +125 -7
- 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,11 +15,11 @@ 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;
|
|
@@ -35,6 +36,7 @@ declare class Suggestions {
|
|
|
35
36
|
isFirstSelected(): boolean | null | undefined;
|
|
36
37
|
isLastSelected(): boolean | null | undefined;
|
|
37
38
|
isVisible(): boolean | undefined;
|
|
39
|
+
onClick(event: MouseEvent): void;
|
|
38
40
|
removeList(): void;
|
|
39
41
|
selected(item?: HTMLLIElement | null): Item | null;
|
|
40
42
|
selectFirst(): void;
|
|
@@ -42,6 +44,7 @@ declare class Suggestions {
|
|
|
42
44
|
selectPrevious(): Item | null;
|
|
43
45
|
selectSibling(sibling: 'nextElementSibling' | 'previousElementSibling'): HTMLLIElement | null;
|
|
44
46
|
show(): HTMLUListElement;
|
|
47
|
+
unselect(item?: HTMLLIElement | null | undefined): void;
|
|
45
48
|
update(suggestions: Item[]): void;
|
|
46
49
|
}
|
|
47
50
|
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,6 +133,8 @@ 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
139
|
if (!suggestions.isVisible()) {
|
|
106
140
|
return;
|
|
@@ -110,12 +144,16 @@ export class AutoComplete {
|
|
|
110
144
|
if (!suggestion) {
|
|
111
145
|
return;
|
|
112
146
|
}
|
|
147
|
+
if (DEBUG)
|
|
148
|
+
console.log(' input =', suggestion.caption);
|
|
113
149
|
this.input.value = suggestion.caption;
|
|
114
150
|
this.onInputValueChange();
|
|
115
151
|
this.autoIdInputValue();
|
|
116
152
|
suggestions.hide();
|
|
117
153
|
}
|
|
118
154
|
keyEscape(event) {
|
|
155
|
+
if (DEBUG)
|
|
156
|
+
console.log('keyEscape');
|
|
119
157
|
const suggestions = this.suggestions;
|
|
120
158
|
if ((this.input.value === '') && !suggestions.isVisible()) {
|
|
121
159
|
return;
|
|
@@ -125,11 +163,15 @@ export class AutoComplete {
|
|
|
125
163
|
suggestions.hide();
|
|
126
164
|
return;
|
|
127
165
|
}
|
|
166
|
+
if (DEBUG)
|
|
167
|
+
console.log('input.value =');
|
|
128
168
|
this.input.value = '';
|
|
129
169
|
this.onInputValueChange();
|
|
130
170
|
this.autoIdInputValue();
|
|
131
171
|
}
|
|
132
172
|
keyUp(event) {
|
|
173
|
+
if (DEBUG)
|
|
174
|
+
console.log('keyUp()');
|
|
133
175
|
const suggestions = this.suggestions;
|
|
134
176
|
if (!suggestions.isVisible()) {
|
|
135
177
|
return;
|
|
@@ -142,9 +184,13 @@ export class AutoComplete {
|
|
|
142
184
|
this.suggest(suggestions.selectPrevious()?.caption);
|
|
143
185
|
}
|
|
144
186
|
onBlur(_event) {
|
|
145
|
-
|
|
187
|
+
if (DEBUG)
|
|
188
|
+
console.log('onBlur()');
|
|
189
|
+
this.suggestions.removeList();
|
|
146
190
|
}
|
|
147
191
|
onInput(event) {
|
|
192
|
+
if (DEBUG)
|
|
193
|
+
console.log('onInput()');
|
|
148
194
|
if (document.activeElement !== event.target)
|
|
149
195
|
return;
|
|
150
196
|
if (this.input.dataset.lastValue === this.input.value)
|
|
@@ -152,6 +198,8 @@ export class AutoComplete {
|
|
|
152
198
|
this.fetch();
|
|
153
199
|
}
|
|
154
200
|
onInputValueChange() {
|
|
201
|
+
if (DEBUG)
|
|
202
|
+
console.log('onInputValueChange()');
|
|
155
203
|
this.input.dataset.lastValue = this.input.value;
|
|
156
204
|
this.input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
157
205
|
if (document.activeElement !== this.input) {
|
|
@@ -162,6 +210,8 @@ export class AutoComplete {
|
|
|
162
210
|
this.autoEmptyClass();
|
|
163
211
|
}
|
|
164
212
|
onKeyDown(event) {
|
|
213
|
+
if (DEBUG)
|
|
214
|
+
console.log('onKeyDown()', event.key);
|
|
165
215
|
this.lastKey = event.key;
|
|
166
216
|
switch (event.key) {
|
|
167
217
|
case 'ArrowDown':
|
|
@@ -177,11 +227,15 @@ export class AutoComplete {
|
|
|
177
227
|
}
|
|
178
228
|
}
|
|
179
229
|
suggest(value) {
|
|
230
|
+
if (DEBUG)
|
|
231
|
+
console.log('suggest()', value);
|
|
180
232
|
if (typeof value !== 'string') {
|
|
181
233
|
return;
|
|
182
234
|
}
|
|
183
235
|
const input = this.input;
|
|
184
236
|
const position = input.selectionStart;
|
|
237
|
+
if (DEBUG)
|
|
238
|
+
console.log(' input =', input.value);
|
|
185
239
|
input.value = value;
|
|
186
240
|
input.setSelectionRange(position, input.value.length);
|
|
187
241
|
this.autoComplete();
|
|
@@ -197,6 +251,8 @@ class Suggestions {
|
|
|
197
251
|
this.combo = combo;
|
|
198
252
|
}
|
|
199
253
|
createList() {
|
|
254
|
+
if (DEBUG)
|
|
255
|
+
console.log('Suggestions.createList()');
|
|
200
256
|
const list = this.list = document.createElement('ul');
|
|
201
257
|
list.classList.add('suggestions');
|
|
202
258
|
let input = this.combo.input;
|
|
@@ -205,13 +261,20 @@ class Suggestions {
|
|
|
205
261
|
input = idInput;
|
|
206
262
|
}
|
|
207
263
|
input.insertAdjacentElement('afterend', list);
|
|
264
|
+
if (DEBUG)
|
|
265
|
+
console.log('Suggestions.prepareClic');
|
|
266
|
+
list.addEventListener('click', event => this.onClick(event));
|
|
208
267
|
return list;
|
|
209
268
|
}
|
|
210
269
|
first() {
|
|
270
|
+
if (DEBUG)
|
|
271
|
+
console.log('Suggestions.first()');
|
|
211
272
|
const item = this.list?.firstElementChild ?? null;
|
|
212
273
|
return item && { caption: item.innerText, id: +(item.dataset.id ?? 0) };
|
|
213
274
|
}
|
|
214
275
|
hide() {
|
|
276
|
+
if (DEBUG)
|
|
277
|
+
console.log('Suggestions.hide()');
|
|
215
278
|
const list = this.list;
|
|
216
279
|
if (!list)
|
|
217
280
|
return;
|
|
@@ -230,47 +293,102 @@ class Suggestions {
|
|
|
230
293
|
isVisible() {
|
|
231
294
|
return this.list && (this.list.style.display !== 'none');
|
|
232
295
|
}
|
|
296
|
+
onClick(event) {
|
|
297
|
+
if (DEBUG)
|
|
298
|
+
console.log('Suggestions.onClick()', event.button);
|
|
299
|
+
if (event.button !== 0)
|
|
300
|
+
return;
|
|
301
|
+
if (!(event.target instanceof Element))
|
|
302
|
+
return;
|
|
303
|
+
const item = event.target.closest('.suggestions > li');
|
|
304
|
+
const list = this.list;
|
|
305
|
+
if (DEBUG)
|
|
306
|
+
console.log(' item', item, 'list', list);
|
|
307
|
+
if (!item || !list)
|
|
308
|
+
return;
|
|
309
|
+
this.unselect();
|
|
310
|
+
item.classList.add('selected');
|
|
311
|
+
if (DEBUG)
|
|
312
|
+
console.log(' selected', item);
|
|
313
|
+
this.combo.keyEnter(event);
|
|
314
|
+
}
|
|
233
315
|
removeList() {
|
|
234
|
-
|
|
235
|
-
|
|
316
|
+
if (DEBUG)
|
|
317
|
+
console.log('Suggestions.removeList()');
|
|
318
|
+
setTimeout(() => this.hide(), 100);
|
|
319
|
+
setTimeout(() => {
|
|
320
|
+
if (DEBUG)
|
|
321
|
+
console.log(' list.remove()');
|
|
322
|
+
if (!this.list || (this.list.style.display !== 'none'))
|
|
323
|
+
return;
|
|
324
|
+
this.list.remove();
|
|
325
|
+
this.list = undefined;
|
|
326
|
+
}, 200);
|
|
236
327
|
}
|
|
237
328
|
selected(item = null) {
|
|
238
329
|
item ??= this.list?.querySelector('li.selected') ?? null;
|
|
330
|
+
if (DEBUG)
|
|
331
|
+
console.log('Suggestions.selected()', item && { caption: item.innerText, id: +(item.dataset.id ?? 0) });
|
|
239
332
|
return item && { caption: item.innerText, id: +(item.dataset.id ?? 0) };
|
|
240
333
|
}
|
|
241
334
|
selectFirst() {
|
|
335
|
+
if (DEBUG)
|
|
336
|
+
console.log('selectFirst()', this.list?.firstElementChild);
|
|
242
337
|
const list = this.list;
|
|
243
338
|
if (!list)
|
|
244
339
|
return;
|
|
245
|
-
|
|
340
|
+
this.unselect();
|
|
246
341
|
list.firstElementChild?.classList.add('selected');
|
|
247
342
|
}
|
|
248
343
|
selectNext() {
|
|
344
|
+
if (DEBUG)
|
|
345
|
+
console.log('selectNext()');
|
|
249
346
|
return this.selected(this.selectSibling('nextElementSibling'));
|
|
250
347
|
}
|
|
251
348
|
selectPrevious() {
|
|
349
|
+
if (DEBUG)
|
|
350
|
+
console.log('selectPrevious()');
|
|
252
351
|
return this.selected(this.selectSibling('previousElementSibling'));
|
|
253
352
|
}
|
|
254
353
|
selectSibling(sibling) {
|
|
354
|
+
if (DEBUG)
|
|
355
|
+
console.log('selectSibling()');
|
|
255
356
|
const list = this.list;
|
|
256
357
|
if (!list)
|
|
257
358
|
return null;
|
|
258
359
|
let item = list.querySelector('li.selected');
|
|
259
360
|
if (item && item[sibling]) {
|
|
260
|
-
|
|
361
|
+
this.unselect(item);
|
|
261
362
|
item = item[sibling];
|
|
262
363
|
item.classList.add('selected');
|
|
263
364
|
}
|
|
365
|
+
if (DEBUG)
|
|
366
|
+
console.log(' ', item);
|
|
264
367
|
return item;
|
|
265
368
|
}
|
|
266
369
|
show() {
|
|
370
|
+
if (DEBUG)
|
|
371
|
+
console.log('show()');
|
|
267
372
|
if (this.list) {
|
|
268
373
|
this.list.style.removeProperty('display');
|
|
269
374
|
return this.list;
|
|
270
375
|
}
|
|
271
376
|
return this.createList();
|
|
272
377
|
}
|
|
378
|
+
unselect(item = this.list?.querySelector('li.selected')) {
|
|
379
|
+
if (!item)
|
|
380
|
+
return;
|
|
381
|
+
const classList = item.classList;
|
|
382
|
+
if (!classList)
|
|
383
|
+
return;
|
|
384
|
+
classList.remove('selected');
|
|
385
|
+
if (!classList.length) {
|
|
386
|
+
item.removeAttribute('class');
|
|
387
|
+
}
|
|
388
|
+
}
|
|
273
389
|
update(suggestions) {
|
|
390
|
+
if (DEBUG)
|
|
391
|
+
console.log('update()');
|
|
274
392
|
let hasSelected = false;
|
|
275
393
|
const list = this.list ?? this.createList();
|
|
276
394
|
const selected = list.querySelector('li.selected')?.innerText;
|
package/package.json
CHANGED