@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 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: KeyboardEvent): void;
18
- keyEnter(event: KeyboardEvent): void;
19
- keyEscape(event: KeyboardEvent): void;
20
- keyUp(event: KeyboardEvent): void;
21
- onBlur(_event: FocusEvent): void;
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 + (input.value ? ('?startsWith=' + input.value) : '');
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(input.value);
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
- setTimeout(() => this.suggestions.removeList());
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
- this.list?.remove();
235
- this.list = undefined;
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
- list.querySelector('li.selected')?.classList.remove('selected');
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
- item.classList.remove('selected');
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
@@ -40,5 +40,5 @@
40
40
  },
41
41
  "type": "module",
42
42
  "types": "./autocomplete.d.ts",
43
- "version": "0.1.0"
43
+ "version": "0.1.1"
44
44
  }