canvasframework 0.3.9 → 0.3.11
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/components/AppBar.js +164 -70
- package/components/BottomNavigationBar.js +206 -69
- package/components/InputDatalist.js +723 -0
- package/components/InputTags.js +586 -0
- package/components/PasswordInput.js +462 -0
- package/components/TimePicker.js +443 -0
- package/core/CanvasFramework.js +7 -4
- package/index.js +4 -1
- package/package.json +1 -1
|
@@ -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;
|