@creative-web-solution/front-library 7.1.48 → 7.1.50

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.
@@ -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
  }
@@ -1,30 +1,37 @@
1
- import { addValidator } from './index';
1
+ import { addValidator } from "./index";
2
2
 
3
3
  /**
4
4
  * File size validation
5
5
  */
6
- addValidator( 'maxfilesize', '[data-max-file-size]', ( $input, value, isLiveValidation ) => {
7
- if (!($input as HTMLInputElement).files?.length) {
6
+ addValidator(
7
+ "maxfilesize",
8
+ "[data-max-file-size]",
9
+ ($input, value, isLiveValidation) => {
10
+ if (!($input as HTMLInputElement).files?.length) {
11
+ return Promise.resolve({
12
+ $input,
13
+ value,
14
+ isValid: true,
15
+ label: "maxfilesize",
16
+ isLiveValidation,
17
+ });
18
+ }
19
+
20
+ const size = ($input as HTMLInputElement).files![0].size;
21
+ const maxSize = Number(
22
+ $input.getAttribute("data-max-file-size") ?? "0",
23
+ );
24
+
8
25
  return Promise.resolve({
9
26
  $input,
10
27
  value,
11
- "isValid": true,
12
- "label": "maxfilesize",
13
- isLiveValidation
28
+ isValid:
29
+ ($input as HTMLInputElement).files?.length === 0 ||
30
+ size <= 0 ||
31
+ maxSize <= 0 ||
32
+ size <= maxSize,
33
+ label: "maxfilesize",
34
+ isLiveValidation,
14
35
  });
15
- }
16
-
17
- const size = ($input as HTMLInputElement).files![0].size;
18
- const maxSize = Number($input.getAttribute('data-max-file-size') ?? "0");
19
- console.log({size, maxSize})
20
- return Promise.resolve({
21
- $input,
22
- value,
23
- "isValid": ($input as HTMLInputElement).files?.length === 0 ||
24
- size <= 0 ||
25
- maxSize <= 0 ||
26
- size <= maxSize,
27
- "label": "maxfilesize",
28
- isLiveValidation
29
- });
30
- } );
36
+ },
37
+ );
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Front Library
2
2
 
3
- @version: 7.1.48
3
+ @version: 7.1.50
4
4
 
5
5
 
6
6
  ## Use
@@ -1,35 +1,41 @@
1
1
  declare namespace FLib {
2
2
  namespace Accordion {
3
-
4
- type AnimationFunction = ( $tab: HTMLElement, $panel: HTMLElement ) => Promise<void>;
5
- type Callback = ( $tab: HTMLElement, $panel: HTMLElement ) => void;
6
- type CloseCallback = ( $tab: HTMLElement, $panel: HTMLElement, autoclose: boolean ) => void;
3
+ type AnimationFunction = (
4
+ $tab: HTMLElement,
5
+ $panel: HTMLElement,
6
+ ) => Promise<void>;
7
+ type Callback = ($tab: HTMLElement, $panel: HTMLElement) => void;
8
+ type CloseCallback = (
9
+ $tab: HTMLElement,
10
+ $panel: HTMLElement,
11
+ autoclose: boolean,
12
+ ) => void;
7
13
 
8
14
  interface AnimationOptions {
9
- open: AnimationFunction;
10
- close: AnimationFunction;
11
- destroy: AnimationFunction;
15
+ open: AnimationFunction;
16
+ close: AnimationFunction;
17
+ destroy: AnimationFunction;
12
18
  }
13
19
 
14
20
  interface Options {
15
21
  /** @defaultValue 'button[aria-expanded]' */
16
- tabSelector: string;
22
+ tabSelector: string;
17
23
  /** @defaultValue false */
18
- allowMultipleTab: boolean;
24
+ allowMultipleTab: boolean;
19
25
  /** @defaultValue false */
20
- atLeastOneOpen: boolean;
21
- onOpenAtStart: Callback;
22
- onOpen: Callback;
23
- onClose: CloseCallback;
24
- animations: Partial<AnimationOptions>;
26
+ atLeastOneOpen: boolean;
27
+ onOpenAtStart: Callback;
28
+ onOpen: Callback;
29
+ onClose: CloseCallback;
30
+ animations: Partial<AnimationOptions>;
25
31
  }
26
32
 
27
33
  interface OptionsInit extends Partial<Options> {
28
- animations?: Partial<AnimationOptions>;
34
+ animations?: Partial<AnimationOptions>;
29
35
  }
30
36
 
31
37
  interface TabOptions extends Options {
32
- index: number;
38
+ index: number;
33
39
  }
34
40
  }
35
41
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@creative-web-solution/front-library",
3
3
  "title": "Frontend library",
4
4
  "description": "Frontend functions and modules",
5
- "version": "7.1.48",
5
+ "version": "7.1.50",
6
6
  "homepage": "https://github.com/creative-web-solution/front-library",
7
7
  "author": "Creative Web Solution <contact@cws-studio.com> (https://www.cws-studio.com)",
8
8
  "keywords": [],