@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.
@@ -1,742 +0,0 @@
1
- import { gesture, gestureOff } from '../Events/Gesture';
2
- import { extend } from '../Helpers/Extend';
3
- import { append, remove } from '../DOM/Manipulation';
4
- import { height } from '../DOM/Size';
5
- import { offset } from '../DOM/Offset';
6
- import { outerHeight, outerWidth } from '../DOM/OuterSize';
7
- import { position } from '../DOM/Position';
8
-
9
-
10
- const defaultOptions = {
11
- "$panelWrapper": document.body,
12
- "maxHeight": 200,
13
- "useCache": false,
14
- "minchar": 3,
15
- "updateOnSelect": true,
16
- "layerPosition": "top",
17
- "cssPositionning": false,
18
- "queryParams": query => {
19
- return { search: query };
20
- },
21
- "normalize": data => {
22
- return data;
23
- },
24
- "renderFieldValue": ( { item } ) => {
25
- return item.name;
26
- },
27
- "render": ( { resultItem, index, cssClass } ) => {
28
- return `<li role="option" class="${ cssClass.item }"><a class="${ cssClass.link }" data-idx="${ index }">${ resultItem.markedName }</a></li>`;
29
- },
30
- "renderList": ( { resultList, cssClass } ) => {
31
- return `<ul role="listbox" class="${ cssClass.list }">${ resultList.join('') }</ul>`;
32
- },
33
- "renderError": ( { errorMsg, cssClass } ) => {
34
- return `<li class="${ cssClass.error }">${ errorMsg }</li>`;
35
- },
36
- "renderMark": ( { resultItem, reQuery, cssClass } ) => {
37
- if ( !reQuery ) {
38
- resultItem.markedName = resultItem.name;
39
- return;
40
- }
41
-
42
- resultItem.name && ( resultItem.markedName = resultItem.name.replace(
43
- reQuery,
44
- `<mark class="${ cssClass.mark }">$1</mark>`
45
- ) );
46
- },
47
- "l10n": {
48
- "noResult": "No result",
49
- "error": "Server error"
50
- },
51
- "className": {
52
- "layer": "ac-layer",
53
- "list": "ac-list",
54
- "item": "acl-itm",
55
- "link": "acl-lnk",
56
- "mark": "acl-mrk",
57
- "error": "acl-error",
58
- "hover": "hover",
59
- "disable": "disable"
60
- }
61
- };
62
-
63
- const PREVENT_DEFAULT_KEY_LIST = [ "Up", "ArrowUp", "Down", "ArrowDown", "Enter", "Esc", "Escape" ];
64
-
65
-
66
- /**
67
- * Autocomplete
68
- *
69
- * @see extra/modules/autocomplete.md for details.
70
- */
71
- export default class Autocomplete {
72
- #options: FLib.Autocomplete.Options;
73
- #cacheQuery: Record<string, FLib.Autocomplete.Item[]>;
74
- #timeoutId;
75
- #$layer: HTMLElement;
76
- #$list;
77
- #currentResults;
78
- #tplSuggestion: string;
79
- #selectionLocked;
80
- #hasResults;
81
- #selectedIndex;
82
- #nbResults;
83
- #url;
84
- #hideTimeoutId;
85
- #$field;
86
- #className: FLib.Autocomplete.Classname;
87
- #$panelWrapper;
88
- #l10n: FLib.Autocomplete.L10N;
89
- #currentQuery;
90
- #requestAbortController;
91
- #isDisabled;
92
-
93
-
94
- get isDisable(): boolean {
95
- return this.#isDisabled || this.#$field.disabled
96
- }
97
-
98
-
99
- constructor( userOptions: FLib.Autocomplete.OptionsInit ) {
100
- if ( !( "AbortController" in window ) ) {
101
- throw 'This plugin uses fecth and AbortController. You may need to add a polyfill for this browser.';
102
- }
103
-
104
- this.#options = extend( defaultOptions, userOptions );
105
-
106
- this.#cacheQuery = {};
107
- this.#selectionLocked = true;
108
- this.#selectedIndex = -1;
109
- this.#nbResults = 0;
110
- this.#hasResults = false;
111
- this.#url = this.#options.url;
112
- this.#$field = this.#options.$searchField;
113
- this.#$panelWrapper = this.#options.$panelWrapper;
114
- this.#className = this.#options.className;
115
- this.#l10n = this.#options.l10n;
116
-
117
- this.#tplSuggestion = `<ul role="listbox" class="${ this.#className.list }">{LIST}</ul>`;
118
-
119
- this.#$layer = document.createElement( 'DIV' );
120
- this.#$layer.classList.add( this.#className.layer as string );
121
-
122
- append( this.#$layer, this.#$panelWrapper );
123
-
124
- this.#$field.setAttribute( 'autocomplete', 'off' );
125
- this.#isDisabled = this.#$field.disabled || this.#$field.classList.contains( this.#className.disable );
126
-
127
-
128
- // Bind
129
-
130
- gesture( this.#$layer, '__AutocompleteTapLayer', {
131
- "selector": `.${ this.#className.link }`,
132
- "tap": this.#onTapLinks
133
- } );
134
-
135
- gesture( this.#$field, '__AutocompleteTapField', {
136
- "tap": this.#onTapField
137
- } );
138
-
139
- this.#$field.addEventListener( 'keyup', this.#onKeyup );
140
- this.#$field.addEventListener( 'keydown', this.#onKeydown );
141
- this.#$field.addEventListener( 'focus', this.#onFocus );
142
- }
143
-
144
-
145
-
146
- /**
147
- * Update the current options
148
- *
149
- * @param newOption - Same options as constructor
150
- */
151
- setOptions( _newOption: FLib.Autocomplete.Options ): this {
152
- this.#options = extend( this.#options, _newOption );
153
-
154
- return this;
155
- }
156
-
157
-
158
- /**
159
- * Display the result list
160
- */
161
- #show = (): void => {
162
- let topVal, parentFieldOffset, fieldHeight, wrapperHeight;
163
-
164
- const wrapperStyle = this.#$layer.style;
165
-
166
- wrapperStyle.display = 'block';
167
-
168
- if ( !this.#options.cssPositionning ) {
169
- parentFieldOffset = offset( this.#$field.parentNode );
170
- wrapperHeight = height( this.#$layer );
171
- fieldHeight = outerHeight( this.#$field );
172
-
173
- topVal =
174
- this.#options.layerPosition === "top"
175
- ? parentFieldOffset.top - wrapperHeight + 1
176
- : parentFieldOffset.top + fieldHeight + 1;
177
-
178
-
179
- wrapperStyle.top = `${ topVal }px`;
180
- wrapperStyle.left = `${ parentFieldOffset.left }px`;
181
- wrapperStyle.width = `${ outerWidth( this.#$field ) }px`;
182
- }
183
-
184
- this.#$layer.scrollTop = 0;
185
-
186
- setTimeout( () => {
187
- gesture( document.body, '__AutocompleteTapOutside', {
188
- "tap": this.#clickOutsideHandler
189
- } )
190
- }, 0 )
191
- }
192
-
193
-
194
- /**
195
- * Hide the result list
196
- */
197
- #hide = (): void => {
198
- clearTimeout( this.#hideTimeoutId );
199
- gestureOff( document.body, '__AutocompleteTapOutside' );
200
-
201
- this.#$layer.style.display = 'none';
202
- this.#selectionLocked = true;
203
- // $layer.innerHTML = '';
204
- // $list = null;
205
- }
206
-
207
-
208
- #clickOutsideHandler = ( e: Event, $target: HTMLElement ): void => {
209
- const $parent = $target.closest( this.#className.layer as string );
210
-
211
- if ( !$parent && $target !== this.#$field ) {
212
- this.#hideTimeoutId = setTimeout( () => {
213
- this.#hide();
214
- }, 500 );
215
- }
216
- }
217
-
218
-
219
- /**
220
- * Create the results list
221
- *
222
- * @param _list - Array of result
223
- */
224
- #set = ( _list: FLib.Autocomplete.Item[], _query: string ): this => {
225
- let html;
226
-
227
- this.#currentResults = _list;
228
- this.#currentQuery = _query;
229
-
230
- html = [];
231
- this.#hasResults = true;
232
-
233
- const reQuery = new RegExp(`(${ _query
234
- .replace( /a/gi, '[aàâä]' )
235
- .replace( /c/gi, '[cç]' )
236
- .replace( /e/gi, '[eéèêë]' )
237
- .replace( /i/gi, '[iìïî]' )
238
- .replace( /o/gi, '[oòôö]' )
239
- .replace( /u/gi, '[uùûü]' )
240
- .replace( /y/gi, '[y]')})`, 'gi' );
241
-
242
- html = _list.map( ( item, i ) => {
243
- this.#options.renderMark?.(
244
- {
245
- "resultItem": item,
246
- reQuery,
247
- "query": _query,
248
- "index": i,
249
- "resultList": _list,
250
- "cssClass": this.#className
251
- }
252
- );
253
- return this.#options.render( {
254
- "resultItem": item,
255
- "index": i,
256
- "query": _query,
257
- "itemsList": _list,
258
- "cssClass": this.#className
259
- } );
260
- } );
261
-
262
- this.#$layer.innerHTML = this.#options.renderList( {
263
- "resultList": html,
264
- "query": _query,
265
- "cssClass": this.#className
266
- } );
267
-
268
- this.#$list = this.#$layer.querySelector( this.#className.list );
269
-
270
- this.#selectionLocked = false;
271
- this.#nbResults = _list.length;
272
- this.#selectedIndex = -1;
273
-
274
- return this;
275
- }
276
-
277
-
278
- /**
279
- * Create the results list
280
- *
281
- * @param _list - Array of result
282
- */
283
- #setAll = ( _list: FLib.Autocomplete.Item[] ): this => {
284
- let html;
285
-
286
- this.#currentResults = _list;
287
- this.#currentQuery = '';
288
-
289
- html = [];
290
- this.#hasResults = true;
291
-
292
- html = _list.map( ( item, i ) => {
293
- this.#options.renderMark(
294
- {
295
- "resultItem": item,
296
- "index": i,
297
- "query": "",
298
- "resultList": _list,
299
- "cssClass": this.#className
300
- }
301
- );
302
- return this.#options.render( {
303
- "resultItem": item,
304
- "index": i,
305
- "query": "",
306
- "itemsList": _list,
307
- "cssClass": this.#className
308
- } );
309
- } );
310
-
311
- this.#$layer.innerHTML = this.#options.renderList( {
312
- "resultList": html,
313
- "cssClass": this.#className,
314
- "query": ""
315
- } );
316
-
317
- this.#$list = this.#$layer.querySelector( this.#className.list );
318
-
319
- this.#selectionLocked = false;
320
- this.#nbResults = _list.length;
321
- this.#selectedIndex = -1;
322
-
323
- return this;
324
- }
325
-
326
-
327
- /**
328
- * Display error message (like "No results")
329
- */
330
- #setError = ( errorMsg?: string ): void => {
331
- const html: string[] = [];
332
-
333
- html.push( this.#options.renderError( {
334
- "errorMsg": errorMsg || this.#l10n.noResult,
335
- "query": this.#currentQuery,
336
- "cssClass": this.#className
337
- } ) );
338
-
339
- this.#$layer.innerHTML = this.#tplSuggestion.replace( '{LIST}', html.join('') );
340
-
341
- this.#selectionLocked = true;
342
- this.#selectedIndex = 0;
343
- this.#nbResults = 0;
344
- }
345
-
346
-
347
- /**
348
- * Select the choose element and fill the field with its value
349
- *
350
- * @param selectedIndex - Index of the selected item
351
- */
352
- #select = ( _selectedIndex?: number ): void => {
353
- if ( this.#selectionLocked ) {
354
- return;
355
- }
356
-
357
- this.#selectedIndex = typeof _selectedIndex === 'undefined' ? this.#selectedIndex : _selectedIndex;
358
-
359
- if ( this.#options.updateOnSelect && this.#options.renderFieldValue ) {
360
- this.#$field.value = this.#options.renderFieldValue.call(
361
- this,
362
- {
363
- "item": this.#currentResults[ this.#selectedIndex ],
364
- "query": this.#currentQuery,
365
- "resultList": this.#currentResults
366
- }
367
- );
368
- }
369
-
370
- this.#hide();
371
-
372
- if ( this.#options.onSelect ) {
373
- this.#options.onSelect.call(
374
- this,
375
- {
376
- "resultsList": this.#currentResults,
377
- "query": this.#currentQuery,
378
- "item": this.#currentResults[ this.#selectedIndex ]
379
- }
380
- );
381
- }
382
- }
383
-
384
-
385
- /**
386
- * Highlight the choosen item
387
- *
388
- * @param _index - Index of the item to hover
389
- */
390
- #hover = ( _index: number ): void => {
391
- let $item, itemPos, top, itemHeight;
392
-
393
- if ( this.#selectionLocked ) {
394
- return;
395
- }
396
-
397
- this.#selectedIndex = _index;
398
- $item = this.#$layer.querySelector( `.${ this.#className.hover }` );
399
-
400
- if ( $item ) {
401
- $item.classList.remove( this.#className.hover );
402
- }
403
-
404
- $item = this.#$layer.querySelectorAll( 'li' )[ _index ];
405
-
406
- if ( $item ) {
407
- $item.classList.add( this.#className.hover );
408
- }
409
-
410
- // $field.value = query;
411
-
412
- if ( this.#$list ) {
413
- itemPos = position( $item );
414
- itemHeight = outerHeight( $item );
415
- top = this.#$list.scrollTop;
416
-
417
- if ( itemPos.top + itemHeight > this.#options.maxHeight + top) {
418
- this.#$list.scrollTop = itemPos.top - this.#options.maxHeight + itemHeight;
419
- }
420
- else if ( top > 0 && itemPos.top < top ) {
421
- this.#$list.scrollTop = itemPos.top;
422
- }
423
- }
424
- }
425
-
426
-
427
- /**
428
- * Highlight the next item
429
- */
430
- #hoverNext = (): void => {
431
- this.#hover( this.#selectedIndex + 1 < this.#nbResults ? this.#selectedIndex + 1 : 0 );
432
- }
433
-
434
-
435
- /**
436
- * Highlight the previous item
437
- */
438
- #hoverPrevious = (): void => {
439
- this.#hover( this.#selectedIndex - 1 >= 0 ? this.#selectedIndex - 1 : this.#nbResults - 1 );
440
- }
441
-
442
-
443
- /**
444
- * Load the results list
445
- */
446
- #load = ( _query: string ): void => {
447
- this.#currentResults = [];
448
-
449
- this.#hasResults = false;
450
-
451
- if (!!this.#cacheQuery[ _query ] && this.#options.useCache ) {
452
- this.#set( this.#cacheQuery[ _query ], _query );
453
- this.#show();
454
- }
455
- else if ( this.#options.source ) {
456
- this.#options.source( _query, results => {
457
- this.#cacheQuery[ _query ] = results;
458
-
459
- if ( results.length ) {
460
- this.#set( results, _query );
461
- }
462
- else {
463
- this.#setError( this.#l10n.noResult );
464
- }
465
-
466
- this.#show();
467
- } );
468
-
469
- return;
470
- }
471
- else {
472
- let params;
473
-
474
- if ( this.#requestAbortController ) {
475
- this.#requestAbortController.abort();
476
- }
477
-
478
- this.#requestAbortController = new AbortController();
479
-
480
- params = this.#options.queryParams( _query );
481
-
482
- params = Object.keys( params ).reduce( ( result, key, index ) => {
483
- return (index > 0 ? result + '&' : '') + key + '=' + params[ key ];
484
- }, '' );
485
-
486
- const newUrl = [ this.#url, this.#url.indexOf( '?' ) > -1 ? '&' : '?', params ].join('');
487
-
488
- const myHeaders = new Headers();
489
- myHeaders.append( 'X-Requested-With', 'XMLHttpRequest' );
490
-
491
- fetch( newUrl, {
492
- "signal": this.#requestAbortController.signal,
493
- "headers": myHeaders
494
- } )
495
- .then( response => {
496
- if ( response.status >= 200 && response.status < 300 ) {
497
- return response;
498
- }
499
- else {
500
- throw new Error( response.statusText );
501
- }
502
- })
503
- .then( function( response ) {
504
- return response.json();
505
- } )
506
- .then( _data => {
507
- const normalizedData = this.#options.normalize( _data );
508
-
509
- if (
510
- normalizedData.success &&
511
- normalizedData.results.length > 0
512
- ) {
513
- this.#set( normalizedData.results, _query);
514
- this.#cacheQuery[ _query ] = normalizedData.results;
515
- }
516
- else if ( !normalizedData.results.length ) {
517
- this.#setError( this.#l10n.noResult );
518
- }
519
- else {
520
- this.#setError( this.#l10n.error ) ;
521
- }
522
- } )
523
- .catch( () => {
524
- this.#setError();
525
- } )
526
- .finally(() => {
527
- this.#show();
528
- this.#requestAbortController = null;
529
- } );
530
- }
531
- }
532
-
533
-
534
- #onKeydown = ( e: KeyboardEvent ): void => {
535
- if ( PREVENT_DEFAULT_KEY_LIST.includes( e.key ) ) {
536
- e.preventDefault();
537
- }
538
- }
539
-
540
-
541
- #onKeyup = ( e: KeyboardEvent ): void => {
542
-
543
- if ( this.isDisable ) {
544
- return;
545
- }
546
-
547
- const query = ( e.target as HTMLInputElement ).value;
548
-
549
- switch ( e.key ) {
550
- case "Up":
551
- case "ArrowUp":
552
- this.#hoverPrevious();
553
- break;
554
-
555
- case "Down":
556
- case "ArrowDown":
557
- this.#hoverNext();
558
- break;
559
-
560
- case "Enter":
561
- this.#select();
562
- break;
563
-
564
- case "Esc":
565
- case "Escape":
566
- this.#hide();
567
- // if ( this.#options.results ) {
568
- // this.#options.results.hide();
569
- // }
570
- break;
571
-
572
- case "Left":
573
- case "ArrowLeft":
574
- case "Right":
575
- case "ArrowRight":
576
- case "Shift":
577
- case "Control":
578
- case "Alt":
579
- case "CapsLock":
580
- case "End":
581
- case "Home":
582
- case "PageDown":
583
- case "PageUp":
584
- case "Meta":
585
- case "OS":
586
- break;
587
-
588
- default:
589
- clearTimeout( this.#timeoutId );
590
-
591
- if ( query.trim().length >= this.#options.minchar ) {
592
- this.#timeoutId = setTimeout( () => {
593
- this.#load( query );
594
- }, 800 );
595
- }
596
- else {
597
- this.#hide();
598
- if ( this.#requestAbortController ) {
599
- this.#requestAbortController.abort();
600
- }
601
- }
602
- break;
603
- }
604
- }
605
-
606
-
607
- #onFocus = (): void => {
608
- clearTimeout( this.#hideTimeoutId );
609
- }
610
-
611
-
612
- #onTapField = (): void => {
613
- if ( this.isDisable ) {
614
- return;
615
- }
616
-
617
- if ( this.#hasResults ) {
618
- this.#selectionLocked = false;
619
- this.#show();
620
- }
621
- }
622
-
623
-
624
- #onTapLinks = ( e: Event, $target: HTMLElement ): void => {
625
- if ( this.isDisable ) {
626
- return;
627
- }
628
-
629
- const idx = $target.getAttribute( 'data-idx' );
630
- this.#select( Number( idx ) );
631
- }
632
-
633
-
634
-
635
- /**
636
- * When 'source' is used, display the list with all items
637
- */
638
- showAll(): this {
639
- if ( !this.#options.source || this.isDisable) {
640
- return this;
641
- }
642
-
643
- this.#options.source( null, ( results: FLib.Autocomplete.Item[] ) => {
644
- if ( results.length ) {
645
- this.#setAll( results );
646
- }
647
- else {
648
- this.#setError( this.#l10n.noResult );
649
- }
650
-
651
- this.#show();
652
- } );
653
-
654
- return this;
655
- }
656
-
657
-
658
- /**
659
- * Disable the autocomplete
660
- */
661
- disable(): this {
662
- this.#isDisabled = true;
663
- this.#$field.disabled = true;
664
- this.#$field.classList.add( this.#className.disable );
665
-
666
- return this;
667
- }
668
-
669
-
670
- /**
671
- * Enable the autocomplete
672
- */
673
- enable(): this {
674
- this.#isDisabled = false;
675
- this.#$field.disabled = false;
676
- this.#$field.classList.remove( this.#className.disable );
677
-
678
- return this;
679
- }
680
-
681
-
682
- /**
683
- * Destroy the Autocomplete
684
- */
685
- clean(): this {
686
- this.#$field.removeEventListener( 'keyup', this.#onKeyup );
687
- this.#$field.removeEventListener( 'keydown', this.#onKeydown );
688
- this.#$field.removeEventListener( 'focus', this.#onFocus );
689
-
690
- gestureOff( document.body, '__AutocompleteTapOutside' );
691
-
692
- gestureOff( this.#$field, '__AutocompleteTapField' );
693
-
694
- gestureOff( this.#$layer, '__AutocompleteTapLayer' );
695
-
696
- clearTimeout( this.#hideTimeoutId );
697
-
698
- if ( this.#requestAbortController ) {
699
- this.#requestAbortController.abort();
700
- this.#requestAbortController = null;
701
- }
702
-
703
- remove( this.#$layer );
704
-
705
- return this;
706
- }
707
-
708
-
709
- /**
710
- * Reset the input field
711
- */
712
- resetField(): this {
713
- this.#$field.value = '';
714
-
715
- return this;
716
- }
717
-
718
-
719
- /**
720
- * Reset the results
721
- */
722
- resetResults(): this {
723
- this.#currentQuery = '';
724
- this.#currentResults = [];
725
- this.#selectedIndex = -1;
726
- this.#nbResults = 0;
727
- this.#hasResults = false;
728
-
729
- return this;
730
- }
731
-
732
-
733
- /**
734
- * Reset the input field and the results
735
- */
736
- reset(): this {
737
- this.resetField();
738
- this.resetResults();
739
-
740
- return this;
741
- }
742
- }