canvasframework 0.3.10 → 0.3.12

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,723 @@
1
+ import Component from '../core/Component.js';
2
+
3
+ /**
4
+ * Champ de saisie avec suggestions (datalist)
5
+ * @class
6
+ * @extends Component
7
+ * @property {string} placeholder - Texte d'indication
8
+ * @property {string} value - Valeur
9
+ * @property {Array} options - Liste des suggestions
10
+ * @property {Array} filteredOptions - Options filtrées
11
+ * @property {number} fontSize - Taille de police
12
+ * @property {boolean} focused - Focus actif
13
+ * @property {string} platform - Plateforme
14
+ * @property {boolean} cursorVisible - Curseur visible
15
+ * @property {number} cursorPosition - Position du curseur
16
+ * @property {HTMLInputElement} hiddenInput - Input HTML caché
17
+ * @property {number} selectedIndex - Index de l'option sélectionnée
18
+ * @property {boolean} showDropdown - Afficher la liste déroulante
19
+ * @property {number} maxDropdownItems - Nombre max d'éléments affichés
20
+ * @property {number} dropdownItemHeight - Hauteur d'un élément
21
+ */
22
+ class InputDatalist extends Component {
23
+ static activeInput = null;
24
+ static allInputs = new Set();
25
+ static globalClickHandler = null;
26
+
27
+ /**
28
+ * Crée une instance de InputDatalist
29
+ * @param {CanvasFramework} framework - Framework parent
30
+ * @param {Object} [options={}] - Options de configuration
31
+ * @param {string} [options.placeholder=''] - Texte d'indication
32
+ * @param {string} [options.value=''] - Valeur initiale
33
+ * @param {Array} [options.options=[]] - Liste des suggestions
34
+ * @param {number} [options.fontSize=16] - Taille de police
35
+ * @param {Function} [options.onFocus] - Callback au focus
36
+ * @param {Function} [options.onBlur] - Callback au blur
37
+ * @param {Function} [options.onSelect] - Callback quand une option est sélectionnée
38
+ * @param {Function} [options.onInput] - Callback quand la valeur change
39
+ * @param {number} [options.maxDropdownItems=5] - Nombre max d'éléments affichés
40
+ * @param {string} [options.dropdownBackground='#FFFFFF'] - Couleur de fond du dropdown
41
+ * @param {string} [options.hoverBackground='#F5F5F5'] - Couleur de fond au survol
42
+ * @param {string} [options.borderColor='#E0E0E0'] - Couleur de bordure du dropdown
43
+ */
44
+ constructor(framework, options = {}) {
45
+ super(framework, options);
46
+ this.placeholder = options.placeholder || '';
47
+ this.value = options.value || '';
48
+ this.options = Array.isArray(options.options) ? [...options.options] : [];
49
+ this.filteredOptions = [];
50
+ this.fontSize = options.fontSize || 16;
51
+ this.focused = false;
52
+ this.platform = framework.platform;
53
+ this.cursorVisible = true;
54
+ this.cursorPosition = this.value.length;
55
+ this.selectedIndex = -1;
56
+ this.showDropdown = false;
57
+ this.maxDropdownItems = options.maxDropdownItems || 5;
58
+ this.dropdownItemHeight = this.fontSize + 16;
59
+
60
+ // Options de style
61
+ this.dropdownBackground = options.dropdownBackground || '#FFFFFF';
62
+ this.hoverBackground = options.hoverBackground || '#F5F5F5';
63
+ this.borderColor = options.borderColor || '#E0E0E0';
64
+ this.selectedBackground = options.selectedBackground || '#E3F2FD';
65
+
66
+ // Callbacks
67
+ this.onSelect = options.onSelect || (() => {});
68
+ this.onInput = options.onInput || (() => {});
69
+
70
+ // Gestion du focus
71
+ this.onFocus = this.onFocus.bind(this);
72
+ this.onBlur = this.onBlur.bind(this);
73
+ this.filterOptions = this.filterOptions.bind(this);
74
+
75
+ // Enregistrer cet input
76
+ InputDatalist.allInputs.add(this);
77
+
78
+ // Animation du curseur
79
+ this.cursorInterval = setInterval(() => {
80
+ if (this.focused) this.cursorVisible = !this.cursorVisible;
81
+ }, 500);
82
+
83
+ // Écouter les clics globaux pour détecter les clics hors input
84
+ this.setupGlobalClickHandler();
85
+
86
+ // Filtrer les options initiales
87
+ if (this.value) {
88
+ this.filterOptions(this.value);
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Écoute les clics globaux pour détecter les clics hors input
94
+ */
95
+ setupGlobalClickHandler() {
96
+ if (!InputDatalist.globalClickHandler) {
97
+ InputDatalist.globalClickHandler = (e) => {
98
+ let clickedOnInput = false;
99
+
100
+ for (let input of InputDatalist.allInputs) {
101
+ if (input.hiddenInput && e.target === input.hiddenInput) {
102
+ clickedOnInput = true;
103
+ break;
104
+ }
105
+ }
106
+
107
+ if (!clickedOnInput) {
108
+ InputDatalist.removeAllHiddenInputs();
109
+ }
110
+ };
111
+
112
+ document.addEventListener('click', InputDatalist.globalClickHandler, true);
113
+ document.addEventListener('touchstart', InputDatalist.globalClickHandler, true);
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Configure l'input HTML caché
119
+ * @private
120
+ */
121
+ setupHiddenInput() {
122
+ if (this.hiddenInput) return;
123
+
124
+ this.hiddenInput = document.createElement('input');
125
+ this.hiddenInput.style.position = 'fixed';
126
+ this.hiddenInput.type = 'text'; // Important: type text pour la saisie normale
127
+ this.hiddenInput.style.opacity = '0';
128
+ this.hiddenInput.style.pointerEvents = 'none';
129
+ this.hiddenInput.style.top = '-100px';
130
+ this.hiddenInput.style.zIndex = '9999';
131
+ document.body.appendChild(this.hiddenInput);
132
+
133
+ this.hiddenInput.addEventListener('input', (e) => {
134
+ if (this.focused) {
135
+ this.value = e.target.value;
136
+ this.cursorPosition = this.value.length;
137
+ this.filterOptions(this.value);
138
+ this.onInput(this.value);
139
+ }
140
+ });
141
+
142
+ this.hiddenInput.addEventListener('keydown', (e) => {
143
+ switch (e.key) {
144
+ case 'ArrowDown':
145
+ e.preventDefault();
146
+ this.selectNextOption();
147
+ break;
148
+
149
+ case 'ArrowUp':
150
+ e.preventDefault();
151
+ this.selectPreviousOption();
152
+ break;
153
+
154
+ case 'Enter':
155
+ e.preventDefault();
156
+ this.selectCurrentOption();
157
+ break;
158
+
159
+ case 'Escape':
160
+ e.preventDefault();
161
+ this.hideDropdown();
162
+ // Garder le focus
163
+ if (this.hiddenInput) {
164
+ setTimeout(() => this.hiddenInput.focus(), 10);
165
+ }
166
+ break;
167
+
168
+ case 'Tab':
169
+ this.selectCurrentOption();
170
+ break;
171
+ }
172
+ });
173
+
174
+ this.hiddenInput.addEventListener('blur', () => {
175
+ // Temporisation plus longue pour permettre la sélection avec la souris
176
+ setTimeout(() => {
177
+ if (!this.isDropdownActive) {
178
+ this.focused = false;
179
+ this.cursorVisible = false;
180
+ this.hideDropdown();
181
+
182
+ setTimeout(() => {
183
+ this.destroyHiddenInput();
184
+ }, 100);
185
+ }
186
+ }, 300);
187
+ });
188
+ }
189
+
190
+ /**
191
+ * Vérifie si le dropdown est actif (souris dessus)
192
+ */
193
+ get isDropdownActive() {
194
+ // Cette propriété serait gérée via des événements mouseenter/mouseleave
195
+ // Pour l'instant, on retourne false par défaut
196
+ return false;
197
+ }
198
+
199
+ /**
200
+ * Filtre les options selon la recherche
201
+ * @param {string} search - Texte de recherche
202
+ */
203
+ filterOptions(search) {
204
+ const searchLower = search.toLowerCase();
205
+
206
+ if (search === '') {
207
+ this.filteredOptions = [...this.options];
208
+ } else {
209
+ this.filteredOptions = this.options.filter(option =>
210
+ option.toLowerCase().includes(searchLower)
211
+ );
212
+ }
213
+
214
+ this.selectedIndex = this.filteredOptions.length > 0 ? 0 : -1;
215
+ this.showDropdown = this.filteredOptions.length > 0 && this.focused;
216
+ }
217
+
218
+ /**
219
+ * Sélectionne l'option suivante
220
+ */
221
+ selectNextOption() {
222
+ if (this.filteredOptions.length === 0) return;
223
+
224
+ this.selectedIndex = (this.selectedIndex + 1) % this.filteredOptions.length;
225
+ this.ensureSelectedVisible();
226
+ }
227
+
228
+ /**
229
+ * Sélectionne l'option précédente
230
+ */
231
+ selectPreviousOption() {
232
+ if (this.filteredOptions.length === 0) return;
233
+
234
+ this.selectedIndex = this.selectedIndex <= 0
235
+ ? this.filteredOptions.length - 1
236
+ : this.selectedIndex - 1;
237
+ this.ensureSelectedVisible();
238
+ }
239
+
240
+ /**
241
+ * Assure que l'option sélectionnée est visible
242
+ */
243
+ ensureSelectedVisible() {
244
+ // Pour l'instant, on s'assure juste que l'index est valide
245
+ if (this.selectedIndex >= this.filteredOptions.length) {
246
+ this.selectedIndex = this.filteredOptions.length - 1;
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Sélectionne l'option actuelle
252
+ */
253
+ selectCurrentOption() {
254
+ if (this.selectedIndex >= 0 && this.selectedIndex < this.filteredOptions.length) {
255
+ const selectedOption = this.filteredOptions[this.selectedIndex];
256
+ this.value = selectedOption;
257
+
258
+ if (this.hiddenInput) {
259
+ this.hiddenInput.value = selectedOption;
260
+ }
261
+
262
+ this.filterOptions(selectedOption);
263
+ this.onSelect(selectedOption);
264
+ this.hideDropdown();
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Masque le dropdown
270
+ */
271
+ hideDropdown() {
272
+ this.showDropdown = false;
273
+ this.selectedIndex = -1;
274
+ }
275
+
276
+ /**
277
+ * Ajoute une option à la liste
278
+ * @param {string} option - Option à ajouter
279
+ */
280
+ addOption(option) {
281
+ if (!this.options.includes(option)) {
282
+ this.options.push(option);
283
+ this.filterOptions(this.value);
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Supprime une option de la liste
289
+ * @param {string} option - Option à supprimer
290
+ */
291
+ removeOption(option) {
292
+ const index = this.options.indexOf(option);
293
+ if (index > -1) {
294
+ this.options.splice(index, 1);
295
+ this.filterOptions(this.value);
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Remplace toutes les options
301
+ * @param {Array} newOptions - Nouvelles options
302
+ */
303
+ setOptions(newOptions) {
304
+ this.options = Array.isArray(newOptions) ? [...newOptions] : [];
305
+ this.filterOptions(this.value);
306
+ }
307
+
308
+ /**
309
+ * Gère le focus
310
+ */
311
+ onFocus() {
312
+ if (InputDatalist.activeInput === this) {
313
+ return;
314
+ }
315
+
316
+ InputDatalist.removeAllHiddenInputs();
317
+
318
+ for (let input of InputDatalist.allInputs) {
319
+ if (input !== this) {
320
+ input.focused = false;
321
+ input.cursorVisible = false;
322
+ input.hideDropdown();
323
+ }
324
+ }
325
+
326
+ this.focused = true;
327
+ this.cursorVisible = true;
328
+ InputDatalist.activeInput = this;
329
+
330
+ // Filtrer et montrer les options
331
+ this.filterOptions(this.value);
332
+ this.showDropdown = this.filteredOptions.length > 0;
333
+
334
+ this.setupHiddenInput();
335
+
336
+ if (this.hiddenInput) {
337
+ this.hiddenInput.value = this.value;
338
+
339
+ const adjustedY = this.y + this.framework.scrollOffset;
340
+ this.hiddenInput.style.top = `${adjustedY}px`;
341
+
342
+ setTimeout(() => {
343
+ if (this.hiddenInput && this.focused) {
344
+ this.hiddenInput.focus();
345
+ this.hiddenInput.setSelectionRange(this.value.length, this.value.length);
346
+ }
347
+ }, 50);
348
+ }
349
+ }
350
+
351
+ /**
352
+ * Gère le blur
353
+ */
354
+ onBlur() {
355
+ this.focused = false;
356
+ this.cursorVisible = false;
357
+ // Le dropdown sera caché par le blur handler de l'input HTML
358
+ }
359
+
360
+ /**
361
+ * Détruit l'input HTML
362
+ */
363
+ destroyHiddenInput() {
364
+ if (this.hiddenInput && this.hiddenInput.parentNode) {
365
+ this.hiddenInput.parentNode.removeChild(this.hiddenInput);
366
+ this.hiddenInput = null;
367
+ }
368
+ }
369
+
370
+ /**
371
+ * Vérifie si un point est dans le dropdown
372
+ * @param {number} x - Coordonnée X
373
+ * @param {number} y - Coordonnée Y
374
+ * @returns {number|null} Index de l'option ou null
375
+ */
376
+ getDropdownOptionAtPoint(x, y) {
377
+ if (!this.showDropdown || this.filteredOptions.length === 0) {
378
+ return null;
379
+ }
380
+
381
+ const dropdownY = this.y + this.height;
382
+ const itemsToShow = Math.min(this.filteredOptions.length, this.maxDropdownItems);
383
+ const dropdownHeight = itemsToShow * this.dropdownItemHeight;
384
+
385
+ // Vérifier si le point est dans la zone du dropdown
386
+ if (x >= this.x &&
387
+ x <= this.x + this.width &&
388
+ y >= dropdownY &&
389
+ y <= dropdownY + dropdownHeight) {
390
+
391
+ const relativeY = y - dropdownY;
392
+ const optionIndex = Math.floor(relativeY / this.dropdownItemHeight);
393
+
394
+ if (optionIndex >= 0 && optionIndex < itemsToShow) {
395
+ // Retourner l'index réel dans filteredOptions
396
+ return optionIndex;
397
+ }
398
+ }
399
+
400
+ return null;
401
+ }
402
+
403
+ /**
404
+ * Gère le clic
405
+ * @param {number} x - Coordonnée X du clic
406
+ * @param {number} y - Coordonnée Y du clic
407
+ * @returns {boolean} True si le clic a été géré
408
+ */
409
+ onClick(x, y) {
410
+ // Vérifier si on clique sur une option du dropdown
411
+ const optionIndex = this.getDropdownOptionAtPoint(x, y);
412
+ if (optionIndex !== null) {
413
+ this.selectedIndex = optionIndex;
414
+ this.selectCurrentOption();
415
+ return true;
416
+ }
417
+
418
+ // Vérifier si on clique dans la zone d'input
419
+ if (this.isPointInside(x, y)) {
420
+ this.onFocus();
421
+ return true;
422
+ }
423
+
424
+ // Clic hors de l'input et du dropdown
425
+ if (this.showDropdown) {
426
+ this.hideDropdown();
427
+ }
428
+
429
+ return false;
430
+ }
431
+
432
+ /**
433
+ * Méthode statique pour détruire tous les inputs HTML
434
+ */
435
+ static removeAllHiddenInputs() {
436
+ for (let input of InputDatalist.allInputs) {
437
+ input.focused = false;
438
+ input.cursorVisible = false;
439
+ input.hideDropdown();
440
+
441
+ if (input.hiddenInput && input.hiddenInput.parentNode) {
442
+ input.hiddenInput.parentNode.removeChild(input.hiddenInput);
443
+ input.hiddenInput = null;
444
+ }
445
+ }
446
+
447
+ InputDatalist.activeInput = null;
448
+ }
449
+
450
+ /**
451
+ * Vérifie si un point est dans les limites
452
+ * @param {number} x - Coordonnée X
453
+ * @param {number} y - Coordonnée Y
454
+ * @returns {boolean} True si le point est dans l'input
455
+ */
456
+ isPointInside(x, y) {
457
+ return x >= this.x &&
458
+ x <= this.x + this.width &&
459
+ y >= this.y &&
460
+ y <= this.y + this.height;
461
+ }
462
+
463
+ /**
464
+ * Dessine l'input
465
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
466
+ */
467
+ draw(ctx) {
468
+ ctx.save();
469
+
470
+ // Style de base de l'input
471
+ if (this.platform === 'material') {
472
+ ctx.strokeStyle = this.focused ? '#6200EE' : '#CCCCCC';
473
+ ctx.lineWidth = this.focused ? 2 : 1;
474
+ ctx.beginPath();
475
+ ctx.moveTo(this.x, this.y + this.height);
476
+ ctx.lineTo(this.x + this.width, this.y + this.height);
477
+ ctx.stroke();
478
+ } else {
479
+ ctx.strokeStyle = this.focused ? '#007AFF' : '#C7C7CC';
480
+ ctx.lineWidth = 1;
481
+ ctx.beginPath();
482
+ this.roundRect(ctx, this.x, this.y, this.width, this.height, 8);
483
+ ctx.stroke();
484
+ }
485
+
486
+ // Texte de l'input
487
+ ctx.fillStyle = this.value ? '#000000' : '#999999';
488
+ ctx.font = `${this.fontSize}px -apple-system, BlinkMacSystemFont, 'Roboto', sans-serif`;
489
+ ctx.textAlign = 'left';
490
+ ctx.textBaseline = 'middle';
491
+
492
+ const displayText = this.value || this.placeholder;
493
+ const textX = this.x + 10;
494
+ const textY = this.y + this.height / 2;
495
+
496
+ // Tronquer le texte si trop long
497
+ const maxTextWidth = this.width - 30; // 10px padding + 20px pour l'icône
498
+ let finalDisplayText = displayText;
499
+ const textWidth = ctx.measureText(displayText).width;
500
+
501
+ if (textWidth > maxTextWidth) {
502
+ // Tronquer avec "..."
503
+ for (let i = displayText.length; i > 0; i--) {
504
+ const truncated = displayText.substring(0, i) + '...';
505
+ if (ctx.measureText(truncated).width <= maxTextWidth) {
506
+ finalDisplayText = truncated;
507
+ break;
508
+ }
509
+ }
510
+ }
511
+
512
+ ctx.fillText(finalDisplayText, textX, textY);
513
+
514
+ // Curseur
515
+ if (this.focused && this.cursorVisible && this.value) {
516
+ const cursorTextWidth = ctx.measureText(finalDisplayText).width;
517
+ ctx.fillStyle = '#000000';
518
+ ctx.fillRect(textX + cursorTextWidth, this.y + 10, 2, this.height - 20);
519
+ }
520
+
521
+ // Icône de dropdown (flèche) seulement si des options existent
522
+ if (this.options.length > 0) {
523
+ this.drawDropdownIcon(ctx);
524
+ }
525
+
526
+ // Dropdown - TOUJOURS dessiner en dernier pour être au-dessus
527
+ if (this.showDropdown && this.filteredOptions.length > 0) {
528
+ this.drawDropdown(ctx);
529
+ }
530
+
531
+ ctx.restore();
532
+ }
533
+
534
+ /**
535
+ * Dessine l'icône de dropdown
536
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
537
+ */
538
+ drawDropdownIcon(ctx) {
539
+ const iconSize = 12;
540
+ const iconX = this.x + this.width - iconSize - 10;
541
+ const iconY = this.y + this.height / 2 - iconSize / 2;
542
+
543
+ ctx.strokeStyle = '#666666';
544
+ ctx.lineWidth = 2;
545
+ ctx.beginPath();
546
+ ctx.moveTo(iconX, iconY + iconSize / 3);
547
+ ctx.lineTo(iconX + iconSize / 2, iconY + 2 * iconSize / 3);
548
+ ctx.lineTo(iconX + iconSize, iconY + iconSize / 3);
549
+ ctx.stroke();
550
+ }
551
+
552
+ /**
553
+ * Dessine le dropdown
554
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
555
+ */
556
+ drawDropdown(ctx) {
557
+ const dropdownY = this.y + this.height;
558
+ const itemsToShow = Math.min(this.filteredOptions.length, this.maxDropdownItems);
559
+ const dropdownHeight = itemsToShow * this.dropdownItemHeight;
560
+
561
+ // Sauvegarder l'état du contexte
562
+ ctx.save();
563
+
564
+ // Ombre portée
565
+ ctx.shadowColor = 'rgba(0, 0, 0, 0.1)';
566
+ ctx.shadowBlur = 10;
567
+ ctx.shadowOffsetX = 0;
568
+ ctx.shadowOffsetY = 2;
569
+
570
+ // Fond du dropdown
571
+ ctx.fillStyle = this.dropdownBackground;
572
+ this.roundRect(ctx, this.x, dropdownY, this.width, dropdownHeight, 4);
573
+ ctx.fill();
574
+
575
+ // Désactiver l'ombre pour la bordure
576
+ ctx.shadowColor = 'transparent';
577
+ ctx.shadowBlur = 0;
578
+ ctx.shadowOffsetY = 0;
579
+
580
+ // Bordure
581
+ ctx.strokeStyle = this.borderColor;
582
+ ctx.lineWidth = 1;
583
+ this.roundRect(ctx, this.x, dropdownY, this.width, dropdownHeight, 4);
584
+ ctx.stroke();
585
+
586
+ // Options
587
+ ctx.font = `${this.fontSize}px -apple-system, BlinkMacSystemFont, 'Roboto', sans-serif`;
588
+ ctx.textAlign = 'left';
589
+ ctx.textBaseline = 'middle';
590
+
591
+ for (let i = 0; i < itemsToShow; i++) {
592
+ const optionY = dropdownY + i * this.dropdownItemHeight;
593
+ const optionHeight = this.dropdownItemHeight;
594
+
595
+ // Fond de l'option (si survolée/sélectionnée)
596
+ if (i === this.selectedIndex) {
597
+ ctx.fillStyle = this.selectedBackground;
598
+ ctx.fillRect(this.x, optionY, this.width, optionHeight);
599
+ }
600
+
601
+ // Séparateur (sauf pour le premier élément)
602
+ if (i > 0) {
603
+ ctx.strokeStyle = this.borderColor;
604
+ ctx.lineWidth = 0.5;
605
+ ctx.beginPath();
606
+ ctx.moveTo(this.x + 10, optionY);
607
+ ctx.lineTo(this.x + this.width - 10, optionY);
608
+ ctx.stroke();
609
+ }
610
+
611
+ // RÉINITIALISER la couleur du texte à chaque itération
612
+ ctx.fillStyle = '#000000';
613
+
614
+ // Dessiner le texte de l'option
615
+ const optionText = this.filteredOptions[i];
616
+ const textX = this.x + 10;
617
+ const textY = optionY + optionHeight / 2;
618
+
619
+ // Tronquer le texte si trop long
620
+ const maxOptionWidth = this.width - 20;
621
+ let displayOptionText = optionText;
622
+ const optionTextWidth = ctx.measureText(optionText).width;
623
+
624
+ if (optionTextWidth > maxOptionWidth) {
625
+ for (let j = optionText.length; j > 0; j--) {
626
+ const truncated = optionText.substring(0, j) + '...';
627
+ if (ctx.measureText(truncated).width <= maxOptionWidth) {
628
+ displayOptionText = truncated;
629
+ break;
630
+ }
631
+ }
632
+ }
633
+
634
+ // Mettre en évidence la partie correspondante
635
+ if (this.value && this.value.length > 0) {
636
+ const searchLower = this.value.toLowerCase();
637
+ const optionLower = optionText.toLowerCase();
638
+ const matchIndex = optionLower.indexOf(searchLower);
639
+
640
+ if (matchIndex >= 0 && matchIndex < displayOptionText.length) {
641
+ // Partie avant la correspondance
642
+ const beforeMatch = displayOptionText.substring(0, matchIndex);
643
+ const matchLength = Math.min(this.value.length, displayOptionText.length - matchIndex);
644
+ const matchPart = displayOptionText.substring(matchIndex, matchIndex + matchLength);
645
+ const afterMatch = displayOptionText.substring(matchIndex + matchLength);
646
+
647
+ // Dessiner partie avant
648
+ ctx.fillStyle = '#666666';
649
+ ctx.fillText(beforeMatch, textX, textY);
650
+
651
+ // Dessiner partie correspondante
652
+ const beforeWidth = ctx.measureText(beforeMatch).width;
653
+ ctx.fillStyle = '#000000';
654
+ ctx.font = `${this.fontSize}px -apple-system, BlinkMacSystemFont, 'Roboto', sans-serif`;
655
+ ctx.fillText(matchPart, textX + beforeWidth, textY);
656
+
657
+ // Dessiner partie après
658
+ const matchWidth = ctx.measureText(matchPart).width;
659
+ ctx.fillStyle = '#666666';
660
+ ctx.fillText(afterMatch, textX + beforeWidth + matchWidth, textY);
661
+ } else {
662
+ // Pas de correspondance
663
+ ctx.fillStyle = '#666666';
664
+ ctx.fillText(displayOptionText, textX, textY);
665
+ }
666
+ } else {
667
+ // Pas de valeur de recherche
668
+ ctx.fillStyle = '#666666';
669
+ ctx.fillText(displayOptionText, textX, textY);
670
+ }
671
+ }
672
+
673
+ // Restaurer l'état du contexte
674
+ ctx.restore();
675
+ }
676
+
677
+ /**
678
+ * Dessine un rectangle avec coins arrondis
679
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
680
+ * @param {number} x - Position X
681
+ * @param {number} y - Position Y
682
+ * @param {number} width - Largeur
683
+ * @param {number} height - Hauteur
684
+ * @param {number} radius - Rayon des coins
685
+ * @private
686
+ */
687
+ roundRect(ctx, x, y, width, height, radius) {
688
+ ctx.beginPath();
689
+ ctx.moveTo(x + radius, y);
690
+ ctx.lineTo(x + width - radius, y);
691
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
692
+ ctx.lineTo(x + width, y + height - radius);
693
+ ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
694
+ ctx.lineTo(x + radius, y + height);
695
+ ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
696
+ ctx.lineTo(x, y + radius);
697
+ ctx.quadraticCurveTo(x, y, x + radius, y);
698
+ ctx.closePath();
699
+ }
700
+
701
+ /**
702
+ * Nettoie les ressources
703
+ */
704
+ destroy() {
705
+ this.destroyHiddenInput();
706
+
707
+ if (this.cursorInterval) {
708
+ clearInterval(this.cursorInterval);
709
+ }
710
+
711
+ InputDatalist.allInputs.delete(this);
712
+
713
+ if (InputDatalist.allInputs.size === 0 && InputDatalist.globalClickHandler) {
714
+ document.removeEventListener('click', InputDatalist.globalClickHandler, true);
715
+ document.removeEventListener('touchstart', InputDatalist.globalClickHandler, true);
716
+ InputDatalist.globalClickHandler = null;
717
+ }
718
+
719
+ super.destroy && super.destroy();
720
+ }
721
+ }
722
+
723
+ export default InputDatalist;