@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 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: 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;
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 + (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,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
- const suggestion = suggestions.selected();
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
- setTimeout(() => this.suggestions.removeList());
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
- this.list?.remove();
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
- list.querySelector('li.selected')?.classList.remove('selected');
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
- item.classList.remove('selected');
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
@@ -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.2"
44
44
  }