canvasframework 0.4.1 → 0.4.2
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/Input.js +585 -63
- package/package.json +1 -1
package/components/Input.js
CHANGED
|
@@ -1,17 +1,31 @@
|
|
|
1
1
|
import Component from '../core/Component.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Champ de saisie texte
|
|
4
|
+
* Champ de saisie texte avec styles Material 3 (Filled/Outlined) et Cupertino
|
|
5
5
|
* @class
|
|
6
6
|
* @extends Component
|
|
7
7
|
* @property {string} placeholder - Texte d'indication
|
|
8
8
|
* @property {string} value - Valeur
|
|
9
9
|
* @property {number} fontSize - Taille de police
|
|
10
10
|
* @property {boolean} focused - Focus actif
|
|
11
|
-
* @property {string} platform - Plateforme
|
|
11
|
+
* @property {string} platform - Plateforme ('material' ou 'cupertino')
|
|
12
|
+
* @property {string} variant - Variante Material ('filled' ou 'outlined')
|
|
12
13
|
* @property {boolean} cursorVisible - Curseur visible
|
|
13
14
|
* @property {number} cursorPosition - Position du curseur
|
|
14
15
|
* @property {HTMLInputElement} hiddenInput - Input HTML caché
|
|
16
|
+
* @property {string} label - Label flottant (Material)
|
|
17
|
+
* @property {boolean} hasLabel - Si un label est défini
|
|
18
|
+
* @property {boolean} error - État d'erreur
|
|
19
|
+
* @property {string} errorText - Texte d'erreur
|
|
20
|
+
* @property {boolean} disabled - État désactivé
|
|
21
|
+
* @property {string} helperText - Texte d'aide
|
|
22
|
+
* @property {boolean} leadingIcon - Afficher une icône à gauche
|
|
23
|
+
* @property {boolean} trailingIcon - Afficher une icône à droite
|
|
24
|
+
* @property {string} backgroundColor - Couleur de fond personnalisée
|
|
25
|
+
* @property {string} borderColor - Couleur de bordure personnalisée
|
|
26
|
+
* @property {string} focusColor - Couleur au focus personnalisée
|
|
27
|
+
* @property {string} textColor - Couleur du texte personnalisée
|
|
28
|
+
* @property {string} placeholderColor - Couleur du placeholder personnalisée
|
|
15
29
|
*/
|
|
16
30
|
class Input extends Component {
|
|
17
31
|
static activeInput = null;
|
|
@@ -24,30 +38,97 @@ class Input extends Component {
|
|
|
24
38
|
* @param {Object} [options={}] - Options de configuration
|
|
25
39
|
* @param {string} [options.placeholder=''] - Texte d'indication
|
|
26
40
|
* @param {string} [options.value=''] - Valeur initiale
|
|
41
|
+
* @param {string} [options.label=''] - Label flottant (Material)
|
|
27
42
|
* @param {number} [options.fontSize=16] - Taille de police
|
|
43
|
+
* @param {string} [options.variant='filled'] - Variante Material ('filled' ou 'outlined')
|
|
44
|
+
* @param {boolean} [options.error=false] - État d'erreur
|
|
45
|
+
* @param {string} [options.errorText=''] - Texte d'erreur
|
|
46
|
+
* @param {boolean} [options.disabled=false] - État désactivé
|
|
47
|
+
* @param {string} [options.helperText=''] - Texte d'aide
|
|
48
|
+
* @param {boolean} [options.leadingIcon=false] - Afficher une icône à gauche
|
|
49
|
+
* @param {boolean} [options.trailingIcon=false] - Afficher une icône à droite
|
|
50
|
+
* @param {string} [options.backgroundColor] - Couleur de fond personnalisée
|
|
51
|
+
* @param {string} [options.borderColor] - Couleur de bordure personnalisée
|
|
52
|
+
* @param {string} [options.focusColor] - Couleur au focus personnalisée
|
|
53
|
+
* @param {string} [options.textColor] - Couleur du texte personnalisée
|
|
54
|
+
* @param {string} [options.placeholderColor] - Couleur du placeholder personnalisée
|
|
55
|
+
* @param {string} [options.labelColor] - Couleur du label personnalisée
|
|
28
56
|
* @param {Function} [options.onFocus] - Callback au focus
|
|
29
57
|
* @param {Function} [options.onBlur] - Callback au blur
|
|
58
|
+
* @param {Function} [options.onChange] - Callback au changement
|
|
30
59
|
*/
|
|
31
60
|
constructor(framework, options = {}) {
|
|
32
61
|
super(framework, options);
|
|
33
62
|
this.placeholder = options.placeholder || '';
|
|
34
63
|
this.value = options.value || '';
|
|
64
|
+
this.label = options.label || '';
|
|
35
65
|
this.fontSize = options.fontSize || 16;
|
|
36
66
|
this.focused = false;
|
|
37
67
|
this.platform = framework.platform;
|
|
68
|
+
this.variant = options.variant || 'filled'; // 'filled' ou 'outlined'
|
|
38
69
|
this.cursorVisible = true;
|
|
39
70
|
this.cursorPosition = this.value.length;
|
|
71
|
+
this.error = options.error || false;
|
|
72
|
+
this.errorText = options.errorText || '';
|
|
73
|
+
this.disabled = options.disabled || false;
|
|
74
|
+
this.helperText = options.helperText || '';
|
|
75
|
+
this.hasLabel = !!this.label;
|
|
76
|
+
this.leadingIcon = options.leadingIcon || false;
|
|
77
|
+
this.trailingIcon = options.trailingIcon || false;
|
|
78
|
+
|
|
79
|
+
// Couleurs personnalisables
|
|
80
|
+
this.backgroundColor = options.backgroundColor || null;
|
|
81
|
+
this.borderColor = options.borderColor || null;
|
|
82
|
+
this.focusColor = options.focusColor || null;
|
|
83
|
+
this.textColor = options.textColor || null;
|
|
84
|
+
this.placeholderColor = options.placeholderColor || null;
|
|
85
|
+
this.labelColor = options.labelColor || null;
|
|
86
|
+
|
|
87
|
+
// Dimensions pour le padding
|
|
88
|
+
this.paddingLeft = this.leadingIcon ? 56 : 16;
|
|
89
|
+
this.paddingRight = this.trailingIcon ? 56 : 16;
|
|
90
|
+
|
|
91
|
+
// Couleurs Material Design 3 par défaut
|
|
92
|
+
this.m3Colors = {
|
|
93
|
+
primary: '#6750A4',
|
|
94
|
+
onSurface: '#1D1B20',
|
|
95
|
+
onSurfaceVariant: '#49454F',
|
|
96
|
+
surfaceVariant: '#E7E0EC',
|
|
97
|
+
surface: '#FEF7FF',
|
|
98
|
+
error: '#BA1A1A',
|
|
99
|
+
onError: '#FFFFFF',
|
|
100
|
+
outline: '#79747E',
|
|
101
|
+
outlineVariant: '#CAC4D0',
|
|
102
|
+
disabled: '#1D1B20' + '61', // 38% opacity
|
|
103
|
+
disabledContainer: '#E7E0EC' + '80',
|
|
104
|
+
// Valeurs par défaut si pas personnalisé
|
|
105
|
+
defaultBackground: '#F5F5F5', // Gris clair pour filled
|
|
106
|
+
defaultBorder: '#CCCCCC', // Gris pour bordure non focus
|
|
107
|
+
defaultFocus: '#1976D2' // Bleu Material par défaut
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Couleurs iOS Cupertino par défaut
|
|
111
|
+
this.cupertinoColors = {
|
|
112
|
+
blue: '#007AFF',
|
|
113
|
+
label: '#000000',
|
|
114
|
+
placeholder: '#999999',
|
|
115
|
+
background: '#FFFFFF',
|
|
116
|
+
border: '#C7C7CC',
|
|
117
|
+
error: '#FF3B30',
|
|
118
|
+
disabled: '#000000' + '4D' // 30% opacity
|
|
119
|
+
};
|
|
40
120
|
|
|
41
121
|
// Gestion du focus
|
|
42
122
|
this.onFocus = this.onFocus.bind(this);
|
|
43
123
|
this.onBlur = this.onBlur.bind(this);
|
|
124
|
+
this.onChange = options.onChange || (() => {});
|
|
44
125
|
|
|
45
126
|
// Enregistrer cet input
|
|
46
127
|
Input.allInputs.add(this);
|
|
47
128
|
|
|
48
129
|
// Animation du curseur
|
|
49
130
|
this.cursorInterval = setInterval(() => {
|
|
50
|
-
if (this.focused) this.cursorVisible = !this.cursorVisible;
|
|
131
|
+
if (this.focused && !this.disabled) this.cursorVisible = !this.cursorVisible;
|
|
51
132
|
}, 500);
|
|
52
133
|
|
|
53
134
|
// Écouter les clics partout pour détecter quand on clique ailleurs
|
|
@@ -58,10 +139,8 @@ class Input extends Component {
|
|
|
58
139
|
* Écoute les clics globaux pour détecter les clics hors input
|
|
59
140
|
*/
|
|
60
141
|
setupGlobalClickHandler() {
|
|
61
|
-
// On crée un gestionnaire unique pour tous les inputs
|
|
62
142
|
if (!Input.globalClickHandler) {
|
|
63
143
|
Input.globalClickHandler = (e) => {
|
|
64
|
-
// Vérifier si on a cliqué en dehors de TOUS les inputs
|
|
65
144
|
let clickedOnInput = false;
|
|
66
145
|
|
|
67
146
|
for (let input of Input.allInputs) {
|
|
@@ -71,13 +150,11 @@ class Input extends Component {
|
|
|
71
150
|
}
|
|
72
151
|
}
|
|
73
152
|
|
|
74
|
-
// Si on n'a pas cliqué sur un input, détruire tous les inputs HTML
|
|
75
153
|
if (!clickedOnInput) {
|
|
76
154
|
Input.removeAllHiddenInputs();
|
|
77
155
|
}
|
|
78
156
|
};
|
|
79
157
|
|
|
80
|
-
// Attacher l'écouteur avec capture pour qu'il se déclenche tôt
|
|
81
158
|
document.addEventListener('click', Input.globalClickHandler, true);
|
|
82
159
|
document.addEventListener('touchstart', Input.globalClickHandler, true);
|
|
83
160
|
}
|
|
@@ -90,7 +167,6 @@ class Input extends Component {
|
|
|
90
167
|
setupHiddenInput() {
|
|
91
168
|
if (this.hiddenInput) return;
|
|
92
169
|
|
|
93
|
-
// Créer un input HTML caché unique pour cette instance
|
|
94
170
|
this.hiddenInput = document.createElement('input');
|
|
95
171
|
this.hiddenInput.style.position = 'fixed';
|
|
96
172
|
this.hiddenInput.style.opacity = '0';
|
|
@@ -100,9 +176,13 @@ class Input extends Component {
|
|
|
100
176
|
document.body.appendChild(this.hiddenInput);
|
|
101
177
|
|
|
102
178
|
this.hiddenInput.addEventListener('input', (e) => {
|
|
103
|
-
if (this.focused) {
|
|
179
|
+
if (this.focused && !this.disabled) {
|
|
180
|
+
const oldValue = this.value;
|
|
104
181
|
this.value = e.target.value;
|
|
105
182
|
this.cursorPosition = this.value.length;
|
|
183
|
+
if (oldValue !== this.value) {
|
|
184
|
+
this.onChange(this.value);
|
|
185
|
+
}
|
|
106
186
|
}
|
|
107
187
|
});
|
|
108
188
|
|
|
@@ -110,26 +190,30 @@ class Input extends Component {
|
|
|
110
190
|
this.focused = false;
|
|
111
191
|
this.cursorVisible = false;
|
|
112
192
|
|
|
113
|
-
// Détruire l'input HTML après un court délai
|
|
114
193
|
setTimeout(() => {
|
|
115
194
|
this.destroyHiddenInput();
|
|
116
195
|
}, 100);
|
|
117
196
|
});
|
|
197
|
+
|
|
198
|
+
// Gérer les touches spéciales
|
|
199
|
+
this.hiddenInput.addEventListener('keydown', (e) => {
|
|
200
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
201
|
+
e.preventDefault();
|
|
202
|
+
this.focused = false;
|
|
203
|
+
this.cursorVisible = false;
|
|
204
|
+
this.destroyHiddenInput();
|
|
205
|
+
}
|
|
206
|
+
});
|
|
118
207
|
}
|
|
119
208
|
|
|
120
209
|
/**
|
|
121
210
|
* Gère le focus
|
|
122
211
|
*/
|
|
123
212
|
onFocus() {
|
|
124
|
-
|
|
125
|
-
if (Input.activeInput === this) {
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
213
|
+
if (this.disabled || Input.activeInput === this) return;
|
|
128
214
|
|
|
129
|
-
// D'abord, détruire TOUS les autres inputs HTML
|
|
130
215
|
Input.removeAllHiddenInputs();
|
|
131
216
|
|
|
132
|
-
// Désactiver tous les autres inputs visuellement
|
|
133
217
|
for (let input of Input.allInputs) {
|
|
134
218
|
if (input !== this) {
|
|
135
219
|
input.focused = false;
|
|
@@ -137,25 +221,21 @@ class Input extends Component {
|
|
|
137
221
|
}
|
|
138
222
|
}
|
|
139
223
|
|
|
140
|
-
// Activer celui-ci
|
|
141
224
|
this.focused = true;
|
|
142
225
|
this.cursorVisible = true;
|
|
143
226
|
Input.activeInput = this;
|
|
144
227
|
|
|
145
|
-
// Créer l'input HTML si nécessaire
|
|
146
228
|
this.setupHiddenInput();
|
|
147
229
|
|
|
148
230
|
if (this.hiddenInput) {
|
|
149
231
|
this.hiddenInput.value = this.value;
|
|
150
|
-
|
|
232
|
+
this.hiddenInput.disabled = this.disabled;
|
|
151
233
|
const adjustedY = this.y + this.framework.scrollOffset;
|
|
152
234
|
this.hiddenInput.style.top = `${adjustedY}px`;
|
|
153
235
|
|
|
154
|
-
// Focus avec un petit délai
|
|
155
236
|
setTimeout(() => {
|
|
156
237
|
if (this.hiddenInput && this.focused) {
|
|
157
238
|
this.hiddenInput.focus();
|
|
158
|
-
// Positionner le curseur à la fin
|
|
159
239
|
this.hiddenInput.setSelectionRange(this.value.length, this.value.length);
|
|
160
240
|
}
|
|
161
241
|
}, 50);
|
|
@@ -184,19 +264,19 @@ class Input extends Component {
|
|
|
184
264
|
* Gère le clic
|
|
185
265
|
*/
|
|
186
266
|
onClick() {
|
|
187
|
-
this.
|
|
267
|
+
if (!this.disabled) {
|
|
268
|
+
this.onFocus();
|
|
269
|
+
}
|
|
188
270
|
}
|
|
189
271
|
|
|
190
272
|
/**
|
|
191
273
|
* Méthode statique pour détruire tous les inputs HTML
|
|
192
274
|
*/
|
|
193
275
|
static removeAllHiddenInputs() {
|
|
194
|
-
// Désactiver tous les inputs visuels
|
|
195
276
|
for (let input of Input.allInputs) {
|
|
196
277
|
input.focused = false;
|
|
197
278
|
input.cursorVisible = false;
|
|
198
279
|
|
|
199
|
-
// Détruire l'input HTML
|
|
200
280
|
if (input.hiddenInput && input.hiddenInput.parentNode) {
|
|
201
281
|
input.hiddenInput.parentNode.removeChild(input.hiddenInput);
|
|
202
282
|
input.hiddenInput = null;
|
|
@@ -208,9 +288,6 @@ class Input extends Component {
|
|
|
208
288
|
|
|
209
289
|
/**
|
|
210
290
|
* Vérifie si un point est dans les limites
|
|
211
|
-
* @param {number} x - Coordonnée X
|
|
212
|
-
* @param {number} y - Coordonnée Y
|
|
213
|
-
* @returns {boolean} True si le point est dans l'input
|
|
214
291
|
*/
|
|
215
292
|
isPointInside(x, y) {
|
|
216
293
|
return x >= this.x &&
|
|
@@ -220,42 +297,416 @@ class Input extends Component {
|
|
|
220
297
|
}
|
|
221
298
|
|
|
222
299
|
/**
|
|
223
|
-
*
|
|
300
|
+
* Obtient la couleur avec fallback
|
|
301
|
+
* @param {string} customColor - Couleur personnalisée
|
|
302
|
+
* @param {string} defaultColor - Couleur par défaut
|
|
303
|
+
* @returns {string} La couleur à utiliser
|
|
304
|
+
* @private
|
|
305
|
+
*/
|
|
306
|
+
getColor(customColor, defaultColor) {
|
|
307
|
+
return this.disabled ? this.m3Colors.disabled :
|
|
308
|
+
(customColor || defaultColor);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Obtient la couleur de focus
|
|
313
|
+
* @returns {string} La couleur au focus
|
|
314
|
+
* @private
|
|
315
|
+
*/
|
|
316
|
+
getFocusColor() {
|
|
317
|
+
if (this.disabled) return this.m3Colors.outlineVariant;
|
|
318
|
+
if (this.error && this.errorText) return this.m3Colors.error;
|
|
319
|
+
return this.focusColor || this.m3Colors.defaultFocus;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Obtient la couleur de bordure
|
|
324
|
+
* @returns {string} La couleur de bordure
|
|
325
|
+
* @private
|
|
326
|
+
*/
|
|
327
|
+
getBorderColor() {
|
|
328
|
+
if (this.disabled) return this.m3Colors.outlineVariant;
|
|
329
|
+
if (this.error && this.errorText) return this.m3Colors.error;
|
|
330
|
+
if (this.focused) return this.getFocusColor();
|
|
331
|
+
return this.borderColor || this.m3Colors.defaultBorder;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Obtient la couleur de fond
|
|
336
|
+
* @returns {string} La couleur de fond
|
|
337
|
+
* @private
|
|
338
|
+
*/
|
|
339
|
+
getBackgroundColor() {
|
|
340
|
+
if (this.disabled) return this.m3Colors.disabledContainer;
|
|
341
|
+
return this.backgroundColor || this.m3Colors.defaultBackground;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Obtient la couleur du texte
|
|
346
|
+
* @returns {string} La couleur du texte
|
|
347
|
+
* @private
|
|
348
|
+
*/
|
|
349
|
+
getTextColor() {
|
|
350
|
+
if (this.disabled) return this.m3Colors.disabled;
|
|
351
|
+
return this.textColor || this.m3Colors.onSurface;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Obtient la couleur du placeholder
|
|
356
|
+
* @returns {string} La couleur du placeholder
|
|
357
|
+
* @private
|
|
358
|
+
*/
|
|
359
|
+
getPlaceholderColor() {
|
|
360
|
+
if (this.disabled) return this.m3Colors.disabled;
|
|
361
|
+
return this.placeholderColor || this.m3Colors.onSurfaceVariant;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Obtient la couleur du label
|
|
366
|
+
* @returns {string} La couleur du label
|
|
367
|
+
* @private
|
|
368
|
+
*/
|
|
369
|
+
getLabelColor() {
|
|
370
|
+
if (this.disabled) return this.m3Colors.disabled;
|
|
371
|
+
if (this.error && this.errorText) return this.m3Colors.error;
|
|
372
|
+
if (this.focused) return this.getFocusColor();
|
|
373
|
+
return this.labelColor || this.m3Colors.onSurfaceVariant;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Dessine l'icône Material
|
|
224
378
|
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
379
|
+
* @param {string} type - Type d'icône ('leading' ou 'trailing')
|
|
380
|
+
* @private
|
|
225
381
|
*/
|
|
226
|
-
|
|
382
|
+
drawMaterialIcon(ctx, type) {
|
|
383
|
+
const isError = this.error && this.errorText;
|
|
384
|
+
|
|
227
385
|
ctx.save();
|
|
386
|
+
ctx.strokeStyle = this.disabled ? this.m3Colors.disabled :
|
|
387
|
+
isError ? this.m3Colors.error :
|
|
388
|
+
this.focused ? this.getFocusColor() : this.m3Colors.onSurfaceVariant;
|
|
389
|
+
ctx.lineWidth = 2;
|
|
390
|
+
ctx.lineCap = 'round';
|
|
228
391
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
392
|
+
// Position de l'icône
|
|
393
|
+
const iconSize = 24;
|
|
394
|
+
const iconX = type === 'leading' ?
|
|
395
|
+
this.x + 16 : this.x + this.width - 40;
|
|
396
|
+
const iconY = this.y + this.height / 2 - iconSize / 2;
|
|
397
|
+
|
|
398
|
+
// Dessiner une icône simple
|
|
399
|
+
ctx.beginPath();
|
|
400
|
+
if (type === 'leading') {
|
|
401
|
+
// Icône de recherche (loupe)
|
|
402
|
+
ctx.arc(iconX + 12, iconY + 12, 8, 0, Math.PI * 2);
|
|
403
|
+
ctx.moveTo(iconX + 18, iconY + 18);
|
|
404
|
+
ctx.lineTo(iconX + 22, iconY + 22);
|
|
405
|
+
} else {
|
|
406
|
+
// Icône d'erreur ou clear
|
|
407
|
+
if (this.error) {
|
|
408
|
+
// Point d'exclamation dans un cercle
|
|
409
|
+
ctx.arc(iconX + 12, iconY + 12, 10, 0, Math.PI * 2);
|
|
410
|
+
ctx.stroke();
|
|
411
|
+
ctx.fillStyle = this.m3Colors.error;
|
|
412
|
+
ctx.fill();
|
|
413
|
+
ctx.fillStyle = this.m3Colors.onError;
|
|
414
|
+
ctx.fillRect(iconX + 11, iconY + 6, 2, 8);
|
|
415
|
+
ctx.fillRect(iconX + 11, iconY + 16, 2, 2);
|
|
416
|
+
ctx.restore();
|
|
417
|
+
return;
|
|
418
|
+
} else {
|
|
419
|
+
// Croix simple (clear)
|
|
420
|
+
ctx.moveTo(iconX + 8, iconY + 8);
|
|
421
|
+
ctx.lineTo(iconX + 16, iconY + 16);
|
|
422
|
+
ctx.moveTo(iconX + 16, iconY + 8);
|
|
423
|
+
ctx.lineTo(iconX + 8, iconY + 16);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
ctx.stroke();
|
|
428
|
+
ctx.restore();
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Dessine l'input Material Design 3 - Filled avec bordure en bas
|
|
433
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
434
|
+
* @private
|
|
435
|
+
*/
|
|
436
|
+
drawMaterialFilledInput(ctx) {
|
|
437
|
+
const isActive = this.focused || this.value.length > 0;
|
|
438
|
+
const showError = this.error && this.errorText;
|
|
439
|
+
|
|
440
|
+
// Container avec fond personnalisable
|
|
441
|
+
ctx.fillStyle = this.getBackgroundColor();
|
|
442
|
+
ctx.beginPath();
|
|
443
|
+
// Coins arrondis seulement en haut
|
|
444
|
+
ctx.moveTo(this.x + 12, this.y);
|
|
445
|
+
ctx.lineTo(this.x + this.width - 12, this.y);
|
|
446
|
+
ctx.quadraticCurveTo(this.x + this.width, this.y, this.x + this.width, this.y + 12);
|
|
447
|
+
ctx.lineTo(this.x + this.width, this.y + this.height);
|
|
448
|
+
ctx.lineTo(this.x, this.y + this.height);
|
|
449
|
+
ctx.lineTo(this.x, this.y + 12);
|
|
450
|
+
ctx.quadraticCurveTo(this.x, this.y, this.x + 12, this.y);
|
|
451
|
+
ctx.closePath();
|
|
452
|
+
ctx.fill();
|
|
453
|
+
|
|
454
|
+
// Bordure inférieure - toujours visible
|
|
455
|
+
const borderHeight = 1;
|
|
456
|
+
const focusBorderHeight = 2;
|
|
457
|
+
|
|
458
|
+
// Bordure normale (gris par défaut)
|
|
459
|
+
ctx.fillStyle = this.getBorderColor();
|
|
460
|
+
|
|
461
|
+
if (this.focused || showError) {
|
|
462
|
+
// Bordure épaisse au focus/erreur
|
|
463
|
+
ctx.fillRect(this.x, this.y + this.height - focusBorderHeight,
|
|
464
|
+
this.width, focusBorderHeight);
|
|
237
465
|
} else {
|
|
238
|
-
//
|
|
239
|
-
ctx.
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
466
|
+
// Bordure fine normale
|
|
467
|
+
ctx.fillRect(this.x, this.y + this.height - borderHeight,
|
|
468
|
+
this.width, borderHeight);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Icône leading
|
|
472
|
+
if (this.leadingIcon) {
|
|
473
|
+
this.drawMaterialIcon(ctx, 'leading');
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Icône trailing
|
|
477
|
+
if (this.trailingIcon) {
|
|
478
|
+
this.drawMaterialIcon(ctx, 'trailing');
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Label flottant
|
|
482
|
+
if (this.hasLabel) {
|
|
483
|
+
ctx.fillStyle = this.getLabelColor();
|
|
484
|
+
ctx.font = `${isActive ? 12 : this.fontSize}px 'Roboto', -apple-system, BlinkMacSystemFont, sans-serif`;
|
|
485
|
+
|
|
486
|
+
const labelX = this.x + this.paddingLeft;
|
|
487
|
+
const labelY = this.focused || this.value.length > 0 ?
|
|
488
|
+
this.y + 12 : this.y + this.height / 2;
|
|
489
|
+
|
|
490
|
+
ctx.fillText(this.label, labelX, labelY);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Texte de saisie
|
|
494
|
+
if (this.value || (!this.hasLabel && !this.focused)) {
|
|
495
|
+
const displayText = this.value || this.placeholder;
|
|
496
|
+
ctx.fillStyle = this.value ? this.getTextColor() : this.getPlaceholderColor();
|
|
497
|
+
ctx.font = `${this.fontSize}px 'Roboto', -apple-system, BlinkMacSystemFont, sans-serif`;
|
|
498
|
+
ctx.textAlign = 'left';
|
|
499
|
+
ctx.textBaseline = 'middle';
|
|
500
|
+
|
|
501
|
+
const textX = this.x + this.paddingLeft;
|
|
502
|
+
const textY = this.hasLabel && (this.focused || this.value.length > 0) ?
|
|
503
|
+
this.y + this.height / 2 + 8 : this.y + this.height / 2;
|
|
504
|
+
|
|
505
|
+
// Troncature du texte
|
|
506
|
+
const maxWidth = this.width - this.paddingLeft - this.paddingRight;
|
|
507
|
+
let displayTextAdjusted = displayText;
|
|
508
|
+
let textWidth = ctx.measureText(displayText).width;
|
|
509
|
+
|
|
510
|
+
if (textWidth > maxWidth) {
|
|
511
|
+
while (textWidth > maxWidth && displayTextAdjusted.length > 0) {
|
|
512
|
+
displayTextAdjusted = displayTextAdjusted.slice(0, -1);
|
|
513
|
+
textWidth = ctx.measureText(displayTextAdjusted + '...').width;
|
|
514
|
+
}
|
|
515
|
+
displayTextAdjusted += '...';
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
ctx.fillText(displayTextAdjusted, textX, textY);
|
|
519
|
+
|
|
520
|
+
// Curseur
|
|
521
|
+
if (this.focused && this.cursorVisible && this.value) {
|
|
522
|
+
ctx.fillStyle = this.getFocusColor();
|
|
523
|
+
ctx.fillRect(textX + textWidth, textY - 12, 2, 24);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Texte d'aide ou d'erreur
|
|
528
|
+
if (showError || this.helperText) {
|
|
529
|
+
const helpText = showError ? this.errorText : this.helperText;
|
|
530
|
+
ctx.fillStyle = showError ? this.m3Colors.error : this.m3Colors.onSurfaceVariant;
|
|
531
|
+
ctx.font = `12px 'Roboto', -apple-system, BlinkMacSystemFont, sans-serif`;
|
|
532
|
+
ctx.textAlign = 'left';
|
|
533
|
+
ctx.textBaseline = 'top';
|
|
534
|
+
ctx.fillText(helpText, this.x + this.paddingLeft, this.y + this.height + 4);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Dessine l'input Material Design 3 - Outlined
|
|
540
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
541
|
+
* @private
|
|
542
|
+
*/
|
|
543
|
+
drawMaterialOutlinedInput(ctx) {
|
|
544
|
+
const isActive = this.focused || this.value.length > 0;
|
|
545
|
+
const showError = this.error && this.errorText;
|
|
546
|
+
|
|
547
|
+
// Background personnalisable
|
|
548
|
+
ctx.fillStyle = this.getBackgroundColor();
|
|
549
|
+
this.roundRect(ctx, this.x, this.y, this.width, this.height, 4);
|
|
550
|
+
ctx.fill();
|
|
551
|
+
|
|
552
|
+
// Bordure avec épaisseur variable
|
|
553
|
+
ctx.strokeStyle = this.getBorderColor();
|
|
554
|
+
ctx.lineWidth = this.focused ? 2 : 1;
|
|
555
|
+
this.roundRect(ctx, this.x, this.y, this.width, this.height, 4);
|
|
556
|
+
ctx.stroke();
|
|
557
|
+
|
|
558
|
+
// Fond pour le label (pour qu'il apparaisse au-dessus de la bordure)
|
|
559
|
+
if (this.hasLabel && (this.focused || this.value.length > 0)) {
|
|
560
|
+
ctx.fillStyle = this.getBackgroundColor();
|
|
561
|
+
ctx.fillRect(this.x + 12, this.y - 6, ctx.measureText(this.label).width + 8, 12);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Icône leading
|
|
565
|
+
if (this.leadingIcon) {
|
|
566
|
+
this.drawMaterialIcon(ctx, 'leading');
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Icône trailing
|
|
570
|
+
if (this.trailingIcon) {
|
|
571
|
+
this.drawMaterialIcon(ctx, 'trailing');
|
|
244
572
|
}
|
|
245
573
|
|
|
574
|
+
// Label flottant
|
|
575
|
+
if (this.hasLabel) {
|
|
576
|
+
ctx.fillStyle = this.getLabelColor();
|
|
577
|
+
ctx.font = `${isActive ? 12 : this.fontSize}px 'Roboto', -apple-system, BlinkMacSystemFont, sans-serif`;
|
|
578
|
+
|
|
579
|
+
const labelX = this.x + this.paddingLeft;
|
|
580
|
+
const labelY = this.focused || this.value.length > 0 ?
|
|
581
|
+
this.y - 2 : this.y + this.height / 2;
|
|
582
|
+
|
|
583
|
+
ctx.fillText(this.label, labelX, labelY);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Texte de saisie
|
|
587
|
+
if (this.value || (!this.hasLabel && !this.focused)) {
|
|
588
|
+
const displayText = this.value || this.placeholder;
|
|
589
|
+
ctx.fillStyle = this.value ? this.getTextColor() : this.getPlaceholderColor();
|
|
590
|
+
ctx.font = `${this.fontSize}px 'Roboto', -apple-system, BlinkMacSystemFont, sans-serif`;
|
|
591
|
+
ctx.textAlign = 'left';
|
|
592
|
+
ctx.textBaseline = 'middle';
|
|
593
|
+
|
|
594
|
+
const textX = this.x + this.paddingLeft;
|
|
595
|
+
const textY = this.y + this.height / 2;
|
|
596
|
+
|
|
597
|
+
// Troncature du texte
|
|
598
|
+
const maxWidth = this.width - this.paddingLeft - this.paddingRight;
|
|
599
|
+
let displayTextAdjusted = displayText;
|
|
600
|
+
let textWidth = ctx.measureText(displayText).width;
|
|
601
|
+
|
|
602
|
+
if (textWidth > maxWidth) {
|
|
603
|
+
while (textWidth > maxWidth && displayTextAdjusted.length > 0) {
|
|
604
|
+
displayTextAdjusted = displayTextAdjusted.slice(0, -1);
|
|
605
|
+
textWidth = ctx.measureText(displayTextAdjusted + '...').width;
|
|
606
|
+
}
|
|
607
|
+
displayTextAdjusted += '...';
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
ctx.fillText(displayTextAdjusted, textX, textY);
|
|
611
|
+
|
|
612
|
+
// Curseur
|
|
613
|
+
if (this.focused && this.cursorVisible && this.value) {
|
|
614
|
+
ctx.fillStyle = this.getFocusColor();
|
|
615
|
+
ctx.fillRect(textX + textWidth, textY - 12, 2, 24);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Texte d'aide ou d'erreur
|
|
620
|
+
if (showError || this.helperText) {
|
|
621
|
+
const helpText = showError ? this.errorText : this.helperText;
|
|
622
|
+
ctx.fillStyle = showError ? this.m3Colors.error : this.m3Colors.onSurfaceVariant;
|
|
623
|
+
ctx.font = `12px 'Roboto', -apple-system, BlinkMacSystemFont, sans-serif`;
|
|
624
|
+
ctx.textAlign = 'left';
|
|
625
|
+
ctx.textBaseline = 'top';
|
|
626
|
+
ctx.fillText(helpText, this.x + this.paddingLeft, this.y + this.height + 4);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Dessine l'input iOS Cupertino
|
|
632
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
633
|
+
* @private
|
|
634
|
+
*/
|
|
635
|
+
drawCupertinoInput(ctx) {
|
|
636
|
+
const showError = this.error && this.errorText;
|
|
637
|
+
|
|
638
|
+
// Background personnalisable
|
|
639
|
+
ctx.fillStyle = this.backgroundColor || this.cupertinoColors.background;
|
|
640
|
+
this.roundRect(ctx, this.x, this.y, this.width, this.height, 10);
|
|
641
|
+
ctx.fill();
|
|
642
|
+
|
|
643
|
+
// Bordure
|
|
644
|
+
ctx.strokeStyle = this.disabled ? this.cupertinoColors.border + '80' :
|
|
645
|
+
showError ? this.cupertinoColors.error :
|
|
646
|
+
this.focused ? (this.focusColor || this.cupertinoColors.blue) :
|
|
647
|
+
(this.borderColor || this.cupertinoColors.border);
|
|
648
|
+
ctx.lineWidth = this.focused ? 2 : 1;
|
|
649
|
+
this.roundRect(ctx, this.x, this.y, this.width, this.height, 10);
|
|
650
|
+
ctx.stroke();
|
|
651
|
+
|
|
246
652
|
// Texte
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
653
|
+
if (this.value || this.placeholder) {
|
|
654
|
+
const displayText = this.value || this.placeholder;
|
|
655
|
+
ctx.fillStyle = this.disabled ? this.cupertinoColors.disabled :
|
|
656
|
+
this.value ? (this.textColor || this.cupertinoColors.label) :
|
|
657
|
+
(this.placeholderColor || this.cupertinoColors.placeholder);
|
|
658
|
+
ctx.font = `${this.fontSize}px -apple-system, BlinkMacSystemFont, sans-serif`;
|
|
659
|
+
ctx.textAlign = 'left';
|
|
660
|
+
ctx.textBaseline = 'middle';
|
|
661
|
+
|
|
662
|
+
// Calculer la largeur maximale disponible
|
|
663
|
+
const maxWidth = this.width - 32;
|
|
664
|
+
let displayTextAdjusted = displayText;
|
|
665
|
+
let textWidth = ctx.measureText(displayText).width;
|
|
666
|
+
|
|
667
|
+
// Tronquer le texte si nécessaire
|
|
668
|
+
if (textWidth > maxWidth) {
|
|
669
|
+
while (textWidth > maxWidth && displayTextAdjusted.length > 0) {
|
|
670
|
+
displayTextAdjusted = displayTextAdjusted.slice(0, -1);
|
|
671
|
+
textWidth = ctx.measureText(displayTextAdjusted + '...').width;
|
|
672
|
+
}
|
|
673
|
+
displayTextAdjusted += '...';
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
ctx.fillText(displayTextAdjusted, this.x + 16, this.y + this.height / 2);
|
|
677
|
+
|
|
678
|
+
// Curseur
|
|
679
|
+
if (this.focused && this.cursorVisible && this.value) {
|
|
680
|
+
ctx.fillStyle = this.focusColor || this.cupertinoColors.blue;
|
|
681
|
+
ctx.fillRect(this.x + 16 + textWidth, this.y + 10, 2, this.height - 20);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Texte d'erreur
|
|
686
|
+
if (showError) {
|
|
687
|
+
ctx.fillStyle = this.cupertinoColors.error;
|
|
688
|
+
ctx.font = `13px -apple-system, BlinkMacSystemFont, sans-serif`;
|
|
689
|
+
ctx.textAlign = 'left';
|
|
690
|
+
ctx.textBaseline = 'top';
|
|
691
|
+
ctx.fillText(this.errorText, this.x + 16, this.y + this.height + 4);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Dessine l'input
|
|
697
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
698
|
+
*/
|
|
699
|
+
draw(ctx) {
|
|
700
|
+
ctx.save();
|
|
701
|
+
|
|
702
|
+
if (this.platform === 'material') {
|
|
703
|
+
if (this.variant === 'outlined') {
|
|
704
|
+
this.drawMaterialOutlinedInput(ctx);
|
|
705
|
+
} else {
|
|
706
|
+
this.drawMaterialFilledInput(ctx);
|
|
707
|
+
}
|
|
708
|
+
} else {
|
|
709
|
+
this.drawCupertinoInput(ctx);
|
|
259
710
|
}
|
|
260
711
|
|
|
261
712
|
ctx.restore();
|
|
@@ -263,15 +714,9 @@ class Input extends Component {
|
|
|
263
714
|
|
|
264
715
|
/**
|
|
265
716
|
* Dessine un rectangle avec coins arrondis
|
|
266
|
-
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
267
|
-
* @param {number} x - Position X
|
|
268
|
-
* @param {number} y - Position Y
|
|
269
|
-
* @param {number} width - Largeur
|
|
270
|
-
* @param {number} height - Hauteur
|
|
271
|
-
* @param {number} radius - Rayon des coins
|
|
272
|
-
* @private
|
|
273
717
|
*/
|
|
274
718
|
roundRect(ctx, x, y, width, height, radius) {
|
|
719
|
+
ctx.beginPath();
|
|
275
720
|
ctx.moveTo(x + radius, y);
|
|
276
721
|
ctx.lineTo(x + width - radius, y);
|
|
277
722
|
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
@@ -281,6 +726,84 @@ class Input extends Component {
|
|
|
281
726
|
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
282
727
|
ctx.lineTo(x, y + radius);
|
|
283
728
|
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
729
|
+
ctx.closePath();
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Met à jour la valeur
|
|
734
|
+
* @param {string} newValue - Nouvelle valeur
|
|
735
|
+
*/
|
|
736
|
+
setValue(newValue) {
|
|
737
|
+
const oldValue = this.value;
|
|
738
|
+
this.value = newValue;
|
|
739
|
+
this.cursorPosition = this.value.length;
|
|
740
|
+
|
|
741
|
+
if (oldValue !== newValue) {
|
|
742
|
+
this.onChange(newValue);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Change la variante Material
|
|
748
|
+
* @param {string} variant - 'filled' ou 'outlined'
|
|
749
|
+
*/
|
|
750
|
+
setVariant(variant) {
|
|
751
|
+
if (variant === 'filled' || variant === 'outlined') {
|
|
752
|
+
this.variant = variant;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Définit les couleurs personnalisées
|
|
758
|
+
* @param {Object} colors - Objet contenant les couleurs
|
|
759
|
+
* @param {string} [colors.backgroundColor] - Couleur de fond
|
|
760
|
+
* @param {string} [colors.borderColor] - Couleur de bordure
|
|
761
|
+
* @param {string} [colors.focusColor] - Couleur au focus
|
|
762
|
+
* @param {string} [colors.textColor] - Couleur du texte
|
|
763
|
+
* @param {string} [colors.placeholderColor] - Couleur du placeholder
|
|
764
|
+
* @param {string} [colors.labelColor] - Couleur du label
|
|
765
|
+
*/
|
|
766
|
+
setColors(colors) {
|
|
767
|
+
if (colors.backgroundColor !== undefined) this.backgroundColor = colors.backgroundColor;
|
|
768
|
+
if (colors.borderColor !== undefined) this.borderColor = colors.borderColor;
|
|
769
|
+
if (colors.focusColor !== undefined) this.focusColor = colors.focusColor;
|
|
770
|
+
if (colors.textColor !== undefined) this.textColor = colors.textColor;
|
|
771
|
+
if (colors.placeholderColor !== undefined) this.placeholderColor = colors.placeholderColor;
|
|
772
|
+
if (colors.labelColor !== undefined) this.labelColor = colors.labelColor;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Active/désactive l'état d'erreur
|
|
777
|
+
* @param {boolean} error - État d'erreur
|
|
778
|
+
* @param {string} errorText - Texte d'erreur
|
|
779
|
+
*/
|
|
780
|
+
setError(error, errorText = '') {
|
|
781
|
+
this.error = error;
|
|
782
|
+
this.errorText = errorText;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Active/désactive l'état désactivé
|
|
787
|
+
* @param {boolean} disabled - État désactivé
|
|
788
|
+
*/
|
|
789
|
+
setDisabled(disabled) {
|
|
790
|
+
this.disabled = disabled;
|
|
791
|
+
if (disabled && this.focused) {
|
|
792
|
+
this.onBlur();
|
|
793
|
+
this.destroyHiddenInput();
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* Configure les icônes
|
|
799
|
+
* @param {boolean} leading - Afficher l'icône à gauche
|
|
800
|
+
* @param {boolean} trailing - Afficher l'icône à droite
|
|
801
|
+
*/
|
|
802
|
+
setIcons(leading, trailing) {
|
|
803
|
+
this.leadingIcon = leading;
|
|
804
|
+
this.trailingIcon = trailing;
|
|
805
|
+
this.paddingLeft = leading ? 56 : 16;
|
|
806
|
+
this.paddingRight = trailing ? 56 : 16;
|
|
284
807
|
}
|
|
285
808
|
|
|
286
809
|
/**
|
|
@@ -295,7 +818,6 @@ class Input extends Component {
|
|
|
295
818
|
|
|
296
819
|
Input.allInputs.delete(this);
|
|
297
820
|
|
|
298
|
-
// Si c'était le dernier input, retirer le gestionnaire global
|
|
299
821
|
if (Input.allInputs.size === 0 && Input.globalClickHandler) {
|
|
300
822
|
document.removeEventListener('click', Input.globalClickHandler, true);
|
|
301
823
|
document.removeEventListener('touchstart', Input.globalClickHandler, true);
|