canvasframework 0.5.18 → 0.5.19

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 (112) hide show
  1. package/components/Accordion.js +265 -0
  2. package/components/AndroidDatePickerDialog.js +406 -0
  3. package/components/AppBar.js +398 -0
  4. package/components/AudioPlayer.js +611 -0
  5. package/components/Avatar.js +202 -0
  6. package/components/Banner.js +342 -0
  7. package/components/BottomNavigationBar.js +433 -0
  8. package/components/BottomSheet.js +234 -0
  9. package/components/Button.js +358 -0
  10. package/components/Camera.js +644 -0
  11. package/components/Card.js +193 -0
  12. package/components/Chart.js +700 -0
  13. package/components/Checkbox.js +166 -0
  14. package/components/Chip.js +212 -0
  15. package/components/CircularProgress.js +327 -0
  16. package/components/ContextMenu.js +116 -0
  17. package/components/DatePicker.js +298 -0
  18. package/components/Dialog.js +337 -0
  19. package/components/Divider.js +125 -0
  20. package/components/Drawer.js +276 -0
  21. package/components/FAB.js +270 -0
  22. package/components/FileUpload.js +315 -0
  23. package/components/FloatedCamera.js +644 -0
  24. package/components/IOSDatePickerWheel.js +430 -0
  25. package/components/ImageCarousel.js +219 -0
  26. package/components/ImageComponent.js +223 -0
  27. package/components/Input.js +831 -0
  28. package/components/InputDatalist.js +723 -0
  29. package/components/InputTags.js +624 -0
  30. package/components/List.js +95 -0
  31. package/components/ListItem.js +269 -0
  32. package/components/Modal.js +364 -0
  33. package/components/MorphingFAB.js +428 -0
  34. package/components/MultiSelectDialog.js +206 -0
  35. package/components/NumberInput.js +271 -0
  36. package/components/PasswordInput.js +462 -0
  37. package/components/ProgressBar.js +88 -0
  38. package/components/QRCodeReader.js +539 -0
  39. package/components/RadioButton.js +151 -0
  40. package/components/SearchInput.js +315 -0
  41. package/components/SegmentedControl.js +357 -0
  42. package/components/Select.js +199 -0
  43. package/components/SelectDialog.js +255 -0
  44. package/components/Slider.js +113 -0
  45. package/components/SliverAppBar.js +139 -0
  46. package/components/Snackbar.js +243 -0
  47. package/components/SpeedDialFAB.js +397 -0
  48. package/components/Stepper.js +281 -0
  49. package/components/SwipeableListItem.js +327 -0
  50. package/components/Switch.js +147 -0
  51. package/components/Table.js +492 -0
  52. package/components/Tabs.js +423 -0
  53. package/components/Text.js +141 -0
  54. package/components/TextField.js +151 -0
  55. package/components/TimePicker.js +934 -0
  56. package/components/Toast.js +236 -0
  57. package/components/TreeView.js +420 -0
  58. package/components/Video.js +397 -0
  59. package/components/View.js +140 -0
  60. package/components/VirtualList.js +120 -0
  61. package/core/CanvasFramework.js +3045 -0
  62. package/core/Component.js +243 -0
  63. package/core/ThemeManager.js +358 -0
  64. package/core/UIBuilder.js +267 -0
  65. package/core/WebGLCanvasAdapter.js +782 -0
  66. package/features/Column.js +43 -0
  67. package/features/Grid.js +47 -0
  68. package/features/LayoutComponent.js +43 -0
  69. package/features/OpenStreetMap.js +310 -0
  70. package/features/Positioned.js +33 -0
  71. package/features/PullToRefresh.js +328 -0
  72. package/features/Row.js +40 -0
  73. package/features/SignaturePad.js +257 -0
  74. package/features/Skeleton.js +193 -0
  75. package/features/Stack.js +21 -0
  76. package/index.js +119 -0
  77. package/manager/AccessibilityManager.js +107 -0
  78. package/manager/ErrorHandler.js +59 -0
  79. package/manager/FeatureFlags.js +60 -0
  80. package/manager/MemoryManager.js +107 -0
  81. package/manager/PerformanceMonitor.js +84 -0
  82. package/manager/SecurityManager.js +54 -0
  83. package/package.json +22 -16
  84. package/utils/AnimationEngine.js +734 -0
  85. package/utils/CryptoManager.js +303 -0
  86. package/utils/DataStore.js +403 -0
  87. package/utils/DevTools.js +1618 -0
  88. package/utils/DevToolsConsole.js +201 -0
  89. package/utils/EventBus.js +407 -0
  90. package/utils/FetchClient.js +74 -0
  91. package/utils/FirebaseAuth.js +653 -0
  92. package/utils/FirebaseCore.js +246 -0
  93. package/utils/FirebaseFirestore.js +581 -0
  94. package/utils/FirebaseFunctions.js +97 -0
  95. package/utils/FirebaseRealtimeDB.js +498 -0
  96. package/utils/FirebaseStorage.js +612 -0
  97. package/utils/FormValidator.js +355 -0
  98. package/utils/GeoLocationService.js +62 -0
  99. package/utils/I18n.js +207 -0
  100. package/utils/IndexedDBManager.js +273 -0
  101. package/utils/InspectionOverlay.js +308 -0
  102. package/utils/NotificationManager.js +60 -0
  103. package/utils/OfflineSyncManager.js +342 -0
  104. package/utils/PayPalPayment.js +678 -0
  105. package/utils/QueryBuilder.js +478 -0
  106. package/utils/SafeArea.js +64 -0
  107. package/utils/SecureStorage.js +289 -0
  108. package/utils/StateManager.js +207 -0
  109. package/utils/StripePayment.js +552 -0
  110. package/utils/WebSocketClient.js +66 -0
  111. package/dist/canvasframework.js +0 -2
  112. package/dist/canvasframework.js.LICENSE.txt +0 -1
@@ -0,0 +1,831 @@
1
+ import Component from '../core/Component.js';
2
+
3
+ /**
4
+ * Champ de saisie texte avec styles Material 3 (Filled/Outlined) et Cupertino
5
+ * @class
6
+ * @extends Component
7
+ * @property {string} placeholder - Texte d'indication
8
+ * @property {string} value - Valeur
9
+ * @property {number} fontSize - Taille de police
10
+ * @property {boolean} focused - Focus actif
11
+ * @property {string} platform - Plateforme ('material' ou 'cupertino')
12
+ * @property {string} variant - Variante Material ('filled' ou 'outlined')
13
+ * @property {boolean} cursorVisible - Curseur visible
14
+ * @property {number} cursorPosition - Position du curseur
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
29
+ */
30
+ class Input extends Component {
31
+ static activeInput = null;
32
+ static allInputs = new Set();
33
+ static globalClickHandler = null;
34
+
35
+ /**
36
+ * Crée une instance de Input
37
+ * @param {CanvasFramework} framework - Framework parent
38
+ * @param {Object} [options={}] - Options de configuration
39
+ * @param {string} [options.placeholder=''] - Texte d'indication
40
+ * @param {string} [options.value=''] - Valeur initiale
41
+ * @param {string} [options.label=''] - Label flottant (Material)
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
56
+ * @param {Function} [options.onFocus] - Callback au focus
57
+ * @param {Function} [options.onBlur] - Callback au blur
58
+ * @param {Function} [options.onChange] - Callback au changement
59
+ */
60
+ constructor(framework, options = {}) {
61
+ super(framework, options);
62
+ this.placeholder = options.placeholder || '';
63
+ this.value = options.value || '';
64
+ this.label = options.label || '';
65
+ this.fontSize = options.fontSize || 16;
66
+ this.focused = false;
67
+ this.platform = framework.platform;
68
+ this.variant = options.variant || 'filled'; // 'filled' ou 'outlined'
69
+ this.cursorVisible = true;
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
+ };
120
+
121
+ // Gestion du focus
122
+ this.onFocus = this.onFocus.bind(this);
123
+ this.onBlur = this.onBlur.bind(this);
124
+ this.onChange = options.onChange || (() => {});
125
+
126
+ // Enregistrer cet input
127
+ Input.allInputs.add(this);
128
+
129
+ // Animation du curseur
130
+ this.cursorInterval = setInterval(() => {
131
+ if (this.focused && !this.disabled) this.cursorVisible = !this.cursorVisible;
132
+ }, 500);
133
+
134
+ // Écouter les clics partout pour détecter quand on clique ailleurs
135
+ this.setupGlobalClickHandler();
136
+ }
137
+
138
+ /**
139
+ * Écoute les clics globaux pour détecter les clics hors input
140
+ */
141
+ setupGlobalClickHandler() {
142
+ if (!Input.globalClickHandler) {
143
+ Input.globalClickHandler = (e) => {
144
+ let clickedOnInput = false;
145
+
146
+ for (let input of Input.allInputs) {
147
+ if (input.hiddenInput && e.target === input.hiddenInput) {
148
+ clickedOnInput = true;
149
+ break;
150
+ }
151
+ }
152
+
153
+ if (!clickedOnInput) {
154
+ Input.removeAllHiddenInputs();
155
+ }
156
+ };
157
+
158
+ document.addEventListener('click', Input.globalClickHandler, true);
159
+ document.addEventListener('touchstart', Input.globalClickHandler, true);
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Configure l'input HTML caché
165
+ * @private
166
+ */
167
+ setupHiddenInput() {
168
+ if (this.hiddenInput) return;
169
+
170
+ this.hiddenInput = document.createElement('input');
171
+ this.hiddenInput.style.position = 'fixed';
172
+ this.hiddenInput.style.opacity = '0';
173
+ this.hiddenInput.style.pointerEvents = 'none';
174
+ this.hiddenInput.style.top = '-100px';
175
+ this.hiddenInput.style.zIndex = '9999';
176
+ document.body.appendChild(this.hiddenInput);
177
+
178
+ this.hiddenInput.addEventListener('input', (e) => {
179
+ if (this.focused && !this.disabled) {
180
+ const oldValue = this.value;
181
+ this.value = e.target.value;
182
+ this.cursorPosition = this.value.length;
183
+ if (oldValue !== this.value) {
184
+ this.onChange(this.value);
185
+ }
186
+ }
187
+ });
188
+
189
+ this.hiddenInput.addEventListener('blur', () => {
190
+ this.focused = false;
191
+ this.cursorVisible = false;
192
+
193
+ setTimeout(() => {
194
+ this.destroyHiddenInput();
195
+ }, 100);
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
+ });
207
+ }
208
+
209
+ /**
210
+ * Gère le focus
211
+ */
212
+ onFocus() {
213
+ if (this.disabled || Input.activeInput === this) return;
214
+
215
+ Input.removeAllHiddenInputs();
216
+
217
+ for (let input of Input.allInputs) {
218
+ if (input !== this) {
219
+ input.focused = false;
220
+ input.cursorVisible = false;
221
+ }
222
+ }
223
+
224
+ this.focused = true;
225
+ this.cursorVisible = true;
226
+ Input.activeInput = this;
227
+
228
+ this.setupHiddenInput();
229
+
230
+ if (this.hiddenInput) {
231
+ this.hiddenInput.value = this.value;
232
+ this.hiddenInput.disabled = this.disabled;
233
+ const adjustedY = this.y + this.framework.scrollOffset;
234
+ this.hiddenInput.style.top = `${adjustedY}px`;
235
+
236
+ setTimeout(() => {
237
+ if (this.hiddenInput && this.focused) {
238
+ this.hiddenInput.focus();
239
+ this.hiddenInput.setSelectionRange(this.value.length, this.value.length);
240
+ }
241
+ }, 50);
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Gère le blur
247
+ */
248
+ onBlur() {
249
+ this.focused = false;
250
+ this.cursorVisible = false;
251
+ }
252
+
253
+ /**
254
+ * Détruit l'input HTML
255
+ */
256
+ destroyHiddenInput() {
257
+ if (this.hiddenInput && this.hiddenInput.parentNode) {
258
+ this.hiddenInput.parentNode.removeChild(this.hiddenInput);
259
+ this.hiddenInput = null;
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Gère le clic
265
+ */
266
+ onClick() {
267
+ if (!this.disabled) {
268
+ this.onFocus();
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Méthode statique pour détruire tous les inputs HTML
274
+ */
275
+ static removeAllHiddenInputs() {
276
+ for (let input of Input.allInputs) {
277
+ input.focused = false;
278
+ input.cursorVisible = false;
279
+
280
+ if (input.hiddenInput && input.hiddenInput.parentNode) {
281
+ input.hiddenInput.parentNode.removeChild(input.hiddenInput);
282
+ input.hiddenInput = null;
283
+ }
284
+ }
285
+
286
+ Input.activeInput = null;
287
+ }
288
+
289
+ /**
290
+ * Vérifie si un point est dans les limites
291
+ */
292
+ isPointInside(x, y) {
293
+ return x >= this.x &&
294
+ x <= this.x + this.width &&
295
+ y >= this.y &&
296
+ y <= this.y + this.height;
297
+ }
298
+
299
+ /**
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
378
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
379
+ * @param {string} type - Type d'icône ('leading' ou 'trailing')
380
+ * @private
381
+ */
382
+ drawMaterialIcon(ctx, type) {
383
+ const isError = this.error && this.errorText;
384
+
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';
391
+
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);
465
+ } else {
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');
572
+ }
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
+
652
+ // Texte
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);
710
+ }
711
+
712
+ ctx.restore();
713
+ }
714
+
715
+ /**
716
+ * Dessine un rectangle avec coins arrondis
717
+ */
718
+ roundRect(ctx, x, y, width, height, radius) {
719
+ ctx.beginPath();
720
+ ctx.moveTo(x + radius, y);
721
+ ctx.lineTo(x + width - radius, y);
722
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
723
+ ctx.lineTo(x + width, y + height - radius);
724
+ ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
725
+ ctx.lineTo(x + radius, y + height);
726
+ ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
727
+ ctx.lineTo(x, y + radius);
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;
807
+ }
808
+
809
+ /**
810
+ * Nettoie les ressources
811
+ */
812
+ destroy() {
813
+ this.destroyHiddenInput();
814
+
815
+ if (this.cursorInterval) {
816
+ clearInterval(this.cursorInterval);
817
+ }
818
+
819
+ Input.allInputs.delete(this);
820
+
821
+ if (Input.allInputs.size === 0 && Input.globalClickHandler) {
822
+ document.removeEventListener('click', Input.globalClickHandler, true);
823
+ document.removeEventListener('touchstart', Input.globalClickHandler, true);
824
+ Input.globalClickHandler = null;
825
+ }
826
+
827
+ super.destroy && super.destroy();
828
+ }
829
+ }
830
+
831
+ export default Input;