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.
Files changed (2) hide show
  1. package/components/Input.js +585 -63
  2. package/package.json +1 -1
@@ -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
- // Si c'est déjà l'input actif, ne rien faire
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
- // Positionner l'input au bon endroit pour le scroll du clavier
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.onFocus();
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
- * Dessine l'input
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
- draw(ctx) {
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
- if (this.platform === 'material') {
230
- // Material Design Input
231
- ctx.strokeStyle = this.focused ? '#6200EE' : '#CCCCCC';
232
- ctx.lineWidth = this.focused ? 2 : 1;
233
- ctx.beginPath();
234
- ctx.moveTo(this.x, this.y + this.height);
235
- ctx.lineTo(this.x + this.width, this.y + this.height);
236
- ctx.stroke();
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
- // Cupertino Input
239
- ctx.strokeStyle = this.focused ? '#007AFF' : '#C7C7CC';
240
- ctx.lineWidth = 1;
241
- ctx.beginPath();
242
- this.roundRect(ctx, this.x, this.y, this.width, this.height, 8);
243
- ctx.stroke();
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
- ctx.fillStyle = this.value ? '#000000' : '#999999';
248
- ctx.font = `${this.fontSize}px -apple-system, BlinkMacSystemFont, 'Roboto', sans-serif`;
249
- ctx.textAlign = 'left';
250
- ctx.textBaseline = 'middle';
251
- const displayText = this.value || this.placeholder;
252
- ctx.fillText(displayText, this.x + 10, this.y + this.height / 2);
253
-
254
- // Curseur
255
- if (this.focused && this.cursorVisible && this.value) {
256
- const textWidth = ctx.measureText(this.value).width;
257
- ctx.fillStyle = '#000000';
258
- ctx.fillRect(this.x + 10 + textWidth, this.y + 10, 2, this.height - 20);
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvasframework",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Canvas-based cross-platform UI framework (Material & Cupertino)",
5
5
  "type": "module",
6
6
  "main": "./index.js",