@creative-web-solution/front-library 7.1.49 → 7.1.51

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/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 7.1.51
4
+
5
+ - [Validator]: Update error message selection
6
+
7
+ ## 7.1.50
8
+
9
+ - [Autocomplete]: Rework module
10
+ - [DOM]: Update Index function
11
+ - [DOM]: Add type to strToDOM
12
+ - [QuickTemplate]: Add params for opening and closing tag
13
+
3
14
  ## 7.1.49
4
15
 
5
16
  - [Accordion]: Fix bug when a tab is opened at start
package/DOM/Index.ts CHANGED
@@ -6,22 +6,10 @@
6
6
  *
7
7
  * @returns - Position (starting at 0) of the element in the DOM list is belong to. -1 if there is no parentNode.
8
8
  */
9
- export function index( $element: Element ): number {
10
- let $child: ChildNode | null, idx: number;
11
-
12
- if ( !$element.parentNode ) {
9
+ export function index($element: Element): number {
10
+ if (!$element.parentElement) {
13
11
  return -1;
14
12
  }
15
13
 
16
- idx = 0;
17
- $child = $element.parentNode.firstChild;
18
-
19
- while ( $child && $element !== $child ) {
20
- if ( $child.nodeType === 1 ) {
21
- idx++;
22
- }
23
- $child = $child.nextSibling;
24
- }
25
-
26
- return idx;
14
+ return Array.from($element.parentElement.children).indexOf($element);
27
15
  }
package/DOM/StrToDOM.ts CHANGED
@@ -12,24 +12,25 @@
12
12
  *
13
13
  * @returns The created DOM element
14
14
  */
15
- export function strToDOM( html: string, tag = 'DIV' ): Node | DocumentFragment {
15
+ export function strToDOM<T extends HTMLElement>(html: string, tag = "DIV"): T {
16
16
  let $child;
17
17
 
18
18
  const $frag: DocumentFragment = document.createDocumentFragment();
19
- const $elem: HTMLElement = document.createElement( tag );
19
+ const $elem: HTMLElement = document.createElement(tag);
20
20
 
21
21
  $elem.innerHTML = html;
22
22
 
23
- while ( $elem.childNodes.length ) {
24
- $child = $elem.childNodes[ 0 ];
23
+ while ($elem.childNodes.length) {
24
+ $child = $elem.childNodes[0];
25
25
 
26
- if ( $child && $child.nodeType === 1 ) {
27
- $frag.appendChild( $child );
28
- }
29
- else {
30
- $elem.removeChild( $child );
26
+ if ($child && $child.nodeType === 1) {
27
+ $frag.appendChild($child);
28
+ } else {
29
+ $elem.removeChild($child);
31
30
  }
32
31
  }
33
32
 
34
- return $frag.childNodes.length === 1 ? $frag.childNodes[ 0 ] : $frag;
33
+ const $result = $frag.childNodes.length === 1 ? $frag.childNodes[0] : $frag;
34
+
35
+ return $result as T;
35
36
  }
@@ -0,0 +1,24 @@
1
+ export default abstract class AbstractAutocompleteAdapter<ItemDataType> {
2
+ abstract layerRender(): string;
3
+ abstract listRender(): string;
4
+ abstract itemRender(options: { item: ItemDataType; index: number }): string;
5
+ abstract errorRender(errorMessage: string, query: string): string;
6
+ abstract noResultRender(query: string): string;
7
+ abstract queryParamAdapter(
8
+ query: string,
9
+ $input: HTMLElement,
10
+ ): Record<string, any>;
11
+ abstract markValue(options: {
12
+ item: ItemDataType;
13
+ index: number;
14
+ query: string;
15
+ }): ItemDataType;
16
+ abstract updateInputValueFromItem?(options: {
17
+ item: ItemDataType;
18
+ index: number;
19
+ query: string;
20
+ results: ItemDataType[];
21
+ }): string;
22
+ abstract source(query: string): Promise<ItemDataType[]>;
23
+ abstract abortSource?(): void;
24
+ }
@@ -0,0 +1,534 @@
1
+ import { off, on } from "../../Events/EventsManager";
2
+ import { debounce } from "../../Helpers/Debounce";
3
+ import AbstractAutocompleteAdapter from "./AbstractAutocompleteAdapter";
4
+ import { extend } from "../../Helpers/Extend";
5
+ import { strToDOM } from "../../DOM/StrToDOM";
6
+ import { offset } from "DOM/Offset";
7
+ import { height } from "../../DOM/Size";
8
+ import { position } from "../../DOM/Position";
9
+ import { outerHeight, outerWidth } from "../../DOM/OuterSize";
10
+ import { remove } from "../../DOM/Manipulation";
11
+ import { index } from "../../DOM/Index";
12
+
13
+ type AutocompleteOptions<ItemDataType> = {
14
+ adapter: AbstractAutocompleteAdapter<ItemDataType>;
15
+ cssPositionning?: boolean;
16
+ debounceDelay?: number;
17
+ hideLayerDelay?: number;
18
+ layerPosition?: "top" | "bottom";
19
+ $layerWrapper: HTMLElement;
20
+ minChar?: number;
21
+ onSelect?: (options: {
22
+ item: ItemDataType;
23
+ index: number;
24
+ query: string;
25
+ results: ItemDataType[];
26
+ }) => void;
27
+ $searchField: HTMLInputElement;
28
+ shouldMarkValue?: boolean;
29
+ };
30
+
31
+ const PREVENT_DEFAULT_KEY_LIST = [
32
+ "Up",
33
+ "ArrowUp",
34
+ "Down",
35
+ "ArrowDown",
36
+ "Esc",
37
+ "Escape",
38
+ ];
39
+
40
+ const ARIA_SELECTED_ATTRIBUTE = "aria-selected";
41
+ const HOVERED_ATTRIBUTE = "data-hovered";
42
+ const ITEM_ATTRIBUTE = "data-ac-item";
43
+ const ITEM_SELECTOR = `[${ITEM_ATTRIBUTE}]`;
44
+
45
+ const DEFAULT_OPTIONS = {
46
+ cssPositionning: true,
47
+ debounceDelay: 800,
48
+ hideLayerDelay: 300,
49
+ layerPosition: "top",
50
+ $layerWrapper: document.body,
51
+ minChar: 3,
52
+ shouldMarkValue: false,
53
+ };
54
+
55
+ export default class Autocomplete<ItemDataType> {
56
+ #adapter: AbstractAutocompleteAdapter<ItemDataType>;
57
+ #debouncedSourceCall!: (query: string) => Promise<void>;
58
+ #currentQuery: string = "";
59
+ #currentResults: { $item: HTMLElement; item: ItemDataType }[] = [];
60
+ #hideTimeoutId;
61
+ #hoveredItemIndex: number = -1;
62
+ #isDisabled: boolean = false;
63
+ #$layerWrapper: HTMLElement;
64
+ #$layer!: HTMLElement;
65
+ #$list!: HTMLElement;
66
+ #options: AutocompleteOptions<ItemDataType>;
67
+ #$searchField: HTMLInputElement;
68
+ #selectedItemIndex: number = -1;
69
+ #selectionLocked: boolean = false;
70
+
71
+ get isDisable(): boolean {
72
+ return this.#isDisabled || this.#$searchField.disabled;
73
+ }
74
+
75
+ constructor(options: AutocompleteOptions<ItemDataType>) {
76
+ this.#options = extend(DEFAULT_OPTIONS, options);
77
+
78
+ this.#adapter = this.#options.adapter;
79
+ this.#$layerWrapper = this.#options.$layerWrapper;
80
+ this.#$layerWrapper = this.#options.$layerWrapper;
81
+ this.#$searchField = this.#options.$searchField;
82
+
83
+ this.#initElements();
84
+ this.#init();
85
+ }
86
+
87
+ #initElements(): void {
88
+ this.#$layer = strToDOM(this.#adapter.layerRender()) as HTMLElement;
89
+ this.#$list = strToDOM(this.#adapter.listRender()) as HTMLElement;
90
+ this.#$layer.appendChild(this.#$list);
91
+ this.#$layerWrapper.appendChild(this.#$layer);
92
+
93
+ this.#$layer.inert = true;
94
+ this.#$searchField.setAttribute("autocomplete", "off");
95
+ }
96
+
97
+ #init(): void {
98
+ this.#debouncedSourceCall = debounce(
99
+ this.#sourceCall,
100
+ this.#options.debounceDelay,
101
+ ) as (query: string) => Promise<void>;
102
+
103
+ on(this.#$searchField, {
104
+ eventsName: "keyup",
105
+ callback: this.#onKeyup,
106
+ });
107
+
108
+ on(this.#$searchField, {
109
+ eventsName: "keydown",
110
+ callback: this.#onKeydown,
111
+ });
112
+
113
+ on(this.#$searchField, {
114
+ eventsName: "focus",
115
+ callback: this.#onFocus,
116
+ });
117
+
118
+ on(this.#$list, {
119
+ selector: ITEM_SELECTOR,
120
+ eventsName: "click",
121
+ callback: this.#onItemClick,
122
+ });
123
+
124
+ on(this.#$list, {
125
+ selector: ITEM_SELECTOR,
126
+ eventsName: "mouseover",
127
+ callback: this.#onItemOver,
128
+ });
129
+ }
130
+
131
+ #onItemClick = (e: Event, $item: HTMLElement): void => {
132
+ if (this.isDisable) {
133
+ return;
134
+ }
135
+
136
+ this.#select(index($item));
137
+ };
138
+
139
+ #onKeydown = (e: KeyboardEvent): void => {
140
+ if (
141
+ PREVENT_DEFAULT_KEY_LIST.includes(e.key) ||
142
+ (e.key === "Enter" && this.#hoveredItemIndex !== -1)
143
+ ) {
144
+ e.preventDefault();
145
+ }
146
+ };
147
+
148
+ #onKeyup = (e: KeyboardEvent): void => {
149
+ if (this.isDisable) {
150
+ return;
151
+ }
152
+
153
+ const query = this.#$searchField.value;
154
+
155
+ switch (e.key) {
156
+ case "Up":
157
+ case "ArrowUp":
158
+ this.#hoverPrevious();
159
+ break;
160
+
161
+ case "Down":
162
+ case "ArrowDown":
163
+ this.#hoverNext();
164
+ break;
165
+
166
+ case "Enter":
167
+ this.#select();
168
+ break;
169
+
170
+ case "Esc":
171
+ case "Escape":
172
+ this.hideLayer();
173
+ break;
174
+
175
+ case "Left":
176
+ case "ArrowLeft":
177
+ case "Right":
178
+ case "ArrowRight":
179
+ case "Shift":
180
+ case "Control":
181
+ case "Alt":
182
+ case "CapsLock":
183
+ case "End":
184
+ case "Home":
185
+ case "PageDown":
186
+ case "PageUp":
187
+ case "Meta":
188
+ case "OS":
189
+ break;
190
+
191
+ default:
192
+ if (query.trim().length >= this.#options.minChar!) {
193
+ this.#debouncedSourceCall(query.trim());
194
+ } else {
195
+ this.hideLayer();
196
+ this.#adapter.abortSource?.();
197
+ }
198
+ break;
199
+ }
200
+ };
201
+
202
+ async #sourceCall(query: string): Promise<void> {
203
+ this.#currentQuery = query;
204
+
205
+ try {
206
+ const results = await this.#adapter.source(query);
207
+ this.#parseResults(results);
208
+ } catch (e: any) {
209
+ this.#createErrorDisplay(e.message);
210
+ }
211
+ }
212
+
213
+ #parseResults(results: ItemDataType[]): void {
214
+ if (!results.length) {
215
+ this.#createEmptyResultDisplay();
216
+ return;
217
+ }
218
+ this.#createResults(results);
219
+ }
220
+
221
+ #createResults(results: ItemDataType[]): void {
222
+ this.resetResults();
223
+
224
+ this.#currentResults = results.map((item, index) => {
225
+ const markedItem = this.#options.shouldMarkValue
226
+ ? this.#adapter.markValue({
227
+ item,
228
+ index,
229
+ query: this.#currentQuery,
230
+ })
231
+ : item;
232
+ const $item = strToDOM(
233
+ this.#adapter.itemRender({
234
+ item: markedItem,
235
+ index,
236
+ }),
237
+ ) as HTMLElement;
238
+
239
+ $item.setAttribute(ARIA_SELECTED_ATTRIBUTE, "false");
240
+ $item.setAttribute(ITEM_ATTRIBUTE, String(index));
241
+
242
+ this.#$list.appendChild($item);
243
+
244
+ return {
245
+ $item,
246
+ item: markedItem,
247
+ };
248
+ });
249
+
250
+ this.#show();
251
+ }
252
+
253
+ #createEmptyResultDisplay(): void {
254
+ this.resetResults();
255
+ this.#$list.appendChild(
256
+ strToDOM(this.#adapter.noResultRender(this.#currentQuery)),
257
+ );
258
+ this.#show();
259
+ }
260
+
261
+ #createErrorDisplay(errorMessage: string): void {
262
+ this.resetResults();
263
+ this.#$list.appendChild(
264
+ strToDOM(
265
+ this.#adapter.errorRender(errorMessage, this.#currentQuery),
266
+ ),
267
+ );
268
+ this.#show();
269
+ }
270
+
271
+ #select = (_selectedIndex?: number): void => {
272
+ if (this.#selectionLocked) {
273
+ return;
274
+ }
275
+
276
+ const newSelectedItemIndex =
277
+ typeof _selectedIndex === "undefined"
278
+ ? this.#hoveredItemIndex
279
+ : _selectedIndex;
280
+
281
+ if (
282
+ newSelectedItemIndex < 0 ||
283
+ newSelectedItemIndex > this.#currentResults.length - 1
284
+ ) {
285
+ return;
286
+ }
287
+
288
+ const newSelectedResult = this.#currentResults[newSelectedItemIndex];
289
+
290
+ if (!newSelectedResult) {
291
+ return;
292
+ }
293
+ const currentlySelectedResult =
294
+ this.#currentResults[this.#selectedItemIndex];
295
+
296
+ if (currentlySelectedResult) {
297
+ currentlySelectedResult.$item.setAttribute(
298
+ ARIA_SELECTED_ATTRIBUTE,
299
+ "false",
300
+ );
301
+ }
302
+
303
+ newSelectedResult.$item.setAttribute(ARIA_SELECTED_ATTRIBUTE, "true");
304
+
305
+ this.#selectedItemIndex = newSelectedItemIndex;
306
+
307
+ const data = {
308
+ index: this.#selectedItemIndex,
309
+ item: newSelectedResult.item,
310
+ query: this.#currentQuery,
311
+ results: this.#currentResults.map((result) => result.item),
312
+ };
313
+
314
+ if (this.#adapter.updateInputValueFromItem) {
315
+ this.#$searchField.value =
316
+ this.#adapter.updateInputValueFromItem(data);
317
+ }
318
+
319
+ this.hideLayer();
320
+
321
+ this.#options.onSelect?.(data);
322
+ };
323
+
324
+ #show = (): void => {
325
+ const wrapperStyle = this.#$layer.style;
326
+
327
+ wrapperStyle.display = "block";
328
+
329
+ if (!this.#options.cssPositionning) {
330
+ const parentFieldOffset = offset(this.#$searchField.parentElement!);
331
+ const wrapperHeight = height(this.#$layer);
332
+ const fieldHeight = outerHeight(this.#$searchField);
333
+
334
+ const topVal =
335
+ this.#options.layerPosition === "top"
336
+ ? parentFieldOffset.top - wrapperHeight + 1
337
+ : parentFieldOffset.top + fieldHeight + 1;
338
+
339
+ wrapperStyle.top = `${topVal}px`;
340
+ wrapperStyle.left = `${parentFieldOffset.left}px`;
341
+ wrapperStyle.width = `${outerWidth(this.#$searchField)}px`;
342
+ }
343
+
344
+ this.#$layer.scrollTop = 0;
345
+ this.#$layer.inert = false;
346
+ this.#selectionLocked = false;
347
+
348
+ setTimeout(() => {
349
+ on(document.body, {
350
+ eventsName: "click",
351
+ callback: this.#clickOutsideHandler,
352
+ });
353
+ }, 0);
354
+ };
355
+
356
+ hideLayer = (): void => {
357
+ clearTimeout(this.#hideTimeoutId);
358
+ off(document.body, {
359
+ eventsName: "click",
360
+ callback: this.#clickOutsideHandler,
361
+ });
362
+
363
+ this.#$layer.style.display = "none";
364
+ this.#$layer.inert = true;
365
+ this.#selectionLocked = true;
366
+ this.#hoveredItemIndex = -1;
367
+ this.#selectedItemIndex = -1;
368
+ };
369
+
370
+ #clickOutsideHandler = (e: Event): void => {
371
+ const $target = e.target as HTMLElement;
372
+
373
+ if (
374
+ this.#$layer !== $target &&
375
+ !this.#$layer.contains($target) &&
376
+ $target !== this.#$searchField
377
+ ) {
378
+ this.#hideTimeoutId = setTimeout(() => {
379
+ this.hideLayer();
380
+ }, this.#options.hideLayerDelay!);
381
+ }
382
+ };
383
+
384
+ #onFocus = (): void => {
385
+ clearTimeout(this.#hideTimeoutId);
386
+
387
+ if (this.isDisable) {
388
+ return;
389
+ }
390
+
391
+ if (this.#currentResults.length) {
392
+ this.#show();
393
+ }
394
+ };
395
+
396
+ #onItemOver = (e: Event, $item: HTMLElement): void => {
397
+ if (this.isDisable) {
398
+ return;
399
+ }
400
+
401
+ this.#hover(index($item));
402
+ };
403
+
404
+ #hover = (_index: number): void => {
405
+ if (
406
+ this.#selectionLocked ||
407
+ _index < 0 ||
408
+ _index >= this.#currentResults.length
409
+ ) {
410
+ return;
411
+ }
412
+
413
+ const currentlyHoveredItem =
414
+ this.#currentResults[this.#hoveredItemIndex];
415
+
416
+ const nextHoveredItem = this.#currentResults[_index];
417
+ this.#hoveredItemIndex = _index;
418
+
419
+ if (currentlyHoveredItem) {
420
+ currentlyHoveredItem.$item.removeAttribute(HOVERED_ATTRIBUTE);
421
+ }
422
+
423
+ if (nextHoveredItem) {
424
+ nextHoveredItem.$item.setAttribute(HOVERED_ATTRIBUTE, "true");
425
+ }
426
+
427
+ if (this.#$list) {
428
+ const listOuterHeight = outerHeight(this.#$list);
429
+ const itemPos = position(nextHoveredItem.$item);
430
+ const itemHeight = outerHeight(nextHoveredItem.$item);
431
+ const top = this.#$list.scrollTop;
432
+
433
+ if (itemPos.top + itemHeight > listOuterHeight + top) {
434
+ this.#$list.scrollTop =
435
+ itemPos.top - listOuterHeight + itemHeight;
436
+ } else if (top > 0 && itemPos.top < top) {
437
+ this.#$list.scrollTop = itemPos.top;
438
+ }
439
+ }
440
+ };
441
+
442
+ #hoverNext = (): void => {
443
+ this.#hover(
444
+ this.#hoveredItemIndex + 1 < this.#currentResults.length
445
+ ? this.#hoveredItemIndex + 1
446
+ : 0,
447
+ );
448
+ };
449
+
450
+ #hoverPrevious = (): void => {
451
+ this.#hover(
452
+ this.#hoveredItemIndex - 1 >= 0
453
+ ? this.#hoveredItemIndex - 1
454
+ : this.#currentResults.length - 1,
455
+ );
456
+ };
457
+
458
+ resetField(): this {
459
+ this.#currentQuery = "";
460
+ this.#$searchField.value = "";
461
+
462
+ return this;
463
+ }
464
+
465
+ resetResults(): this {
466
+ this.#currentResults.length;
467
+ this.#selectedItemIndex = -1;
468
+ this.#hoveredItemIndex = -1;
469
+ this.#$list.innerHTML = "";
470
+
471
+ return this;
472
+ }
473
+
474
+ reset(): this {
475
+ this.resetField();
476
+ this.resetResults();
477
+
478
+ return this;
479
+ }
480
+
481
+ disable(): this {
482
+ this.#isDisabled = true;
483
+ this.#$searchField.disabled = true;
484
+
485
+ return this;
486
+ }
487
+
488
+ enable(): this {
489
+ this.#isDisabled = false;
490
+ this.#$searchField.disabled = false;
491
+
492
+ return this;
493
+ }
494
+
495
+ dispose(): this {
496
+ off(this.#$searchField, {
497
+ eventsName: "keyup",
498
+ callback: this.#onKeyup,
499
+ });
500
+
501
+ off(this.#$searchField, {
502
+ eventsName: "keydown",
503
+ callback: this.#onKeydown,
504
+ });
505
+
506
+ off(this.#$searchField, {
507
+ eventsName: "focus",
508
+ callback: this.#onFocus,
509
+ });
510
+
511
+ off(this.#$list, {
512
+ eventsName: "click",
513
+ callback: this.#onItemClick,
514
+ });
515
+
516
+ off(document.body, {
517
+ eventsName: "click",
518
+ callback: this.#clickOutsideHandler,
519
+ });
520
+
521
+ off(this.#$list, {
522
+ eventsName: "mouseover",
523
+ callback: this.#onItemOver,
524
+ });
525
+
526
+ clearTimeout(this.#hideTimeoutId);
527
+
528
+ this.#adapter.abortSource?.();
529
+
530
+ remove(this.#$layer);
531
+
532
+ return this;
533
+ }
534
+ }
@@ -4,12 +4,29 @@
4
4
  * @example
5
5
  * ```ts
6
6
  * let HTML = quickTemplate( 'My name is {{ lastname }}. {{ firstname }} {{ lastname }}.', { "firstname": "James", "lastname": "Bond" } );
7
+ * let HTML2 = quickTemplate( 'My name is < lastname >. < firstname > < lastname >.',
8
+ * { "firstname": "James", "lastname": "Bond" },
9
+ * {
10
+ * "open": "<",
11
+ * "close": ">"
12
+ * }
13
+ * );
7
14
  * ```
8
15
  */
9
- export default function quickTemplate( template: string, data: { [ key: string ]: string } ): string {
10
- Object.keys( data ).forEach( key => {
11
- template = template.replace( new RegExp( `{{\\s*${ key }\\s*}}`, 'g' ), data[ key ] );
12
- } );
16
+ export default function quickTemplate(
17
+ template: string,
18
+ data: { [key: string]: string },
19
+ options?: { open: string; close: string },
20
+ ): string {
21
+ Object.keys(data).forEach((key) => {
22
+ template = template.replace(
23
+ new RegExp(
24
+ `${options?.open ?? "{{"}\\s*${key}\\s*${options?.close ?? "}}"}`,
25
+ "g",
26
+ ),
27
+ data[key],
28
+ );
29
+ });
13
30
 
14
31
  return template;
15
32
  }