canvasframework 0.3.9 → 0.3.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,462 @@
1
+ import Component from '../core/Component.js';
2
+
3
+ /**
4
+ * Champ de saisie de mot de passe avec masquage
5
+ * @class
6
+ * @extends Component
7
+ * @property {string} placeholder - Texte d'indication
8
+ * @property {string} value - Valeur réelle
9
+ * @property {string} displayedValue - Valeur affichée (avec *)
10
+ * @property {number} fontSize - Taille de police
11
+ * @property {boolean} focused - Focus actif
12
+ * @property {string} platform - Plateforme
13
+ * @property {boolean} cursorVisible - Curseur visible
14
+ * @property {number} cursorPosition - Position du curseur
15
+ * @property {HTMLInputElement} hiddenInput - Input HTML caché
16
+ * @property {boolean} showPassword - Afficher le mot de passe en clair
17
+ * @property {string} maskChar - Caractère de masquage
18
+ */
19
+ class PasswordInput extends Component {
20
+ static activeInput = null;
21
+ static allInputs = new Set();
22
+ static globalClickHandler = null;
23
+
24
+ /**
25
+ * Crée une instance de PasswordInput
26
+ * @param {CanvasFramework} framework - Framework parent
27
+ * @param {Object} [options={}] - Options de configuration
28
+ * @param {string} [options.placeholder=''] - Texte d'indication
29
+ * @param {string} [options.value=''] - Valeur initiale
30
+ * @param {number} [options.fontSize=16] - Taille de police
31
+ * @param {Function} [options.onFocus] - Callback au focus
32
+ * @param {Function} [options.onBlur] - Callback au blur
33
+ * @param {string} [options.maskChar='*'] - Caractère de masquage
34
+ * @param {boolean} [options.showPassword=false] - Afficher le mot de passe initialement
35
+ */
36
+ constructor(framework, options = {}) {
37
+ super(framework, options);
38
+ this.placeholder = options.placeholder || 'Mot de passe';
39
+ this.value = options.value || '';
40
+ this.maskChar = options.maskChar || '•';
41
+ this.showPassword = options.showPassword || false;
42
+ this.fontSize = options.fontSize || 16;
43
+ this.focused = false;
44
+ this.platform = framework.platform;
45
+ this.cursorVisible = true;
46
+ this.cursorPosition = this.value.length;
47
+ this.displayedValue = this.getDisplayedValue();
48
+
49
+ // Bouton pour afficher/masquer le mot de passe
50
+ this.toggleButtonSize = 24;
51
+ this.toggleButtonPadding = 10;
52
+
53
+ // Gestion du focus
54
+ this.onFocus = this.onFocus.bind(this);
55
+ this.onBlur = this.onBlur.bind(this);
56
+ this.onTogglePassword = this.onTogglePassword.bind(this);
57
+
58
+ // Enregistrer cet input
59
+ PasswordInput.allInputs.add(this);
60
+
61
+ // Animation du curseur
62
+ this.cursorInterval = setInterval(() => {
63
+ if (this.focused) this.cursorVisible = !this.cursorVisible;
64
+ }, 500);
65
+
66
+ // Écouter les clics partout pour détecter quand on clique ailleurs
67
+ this.setupGlobalClickHandler();
68
+ }
69
+
70
+ /**
71
+ * Obtient la valeur affichée
72
+ * @returns {string} Valeur affichée
73
+ */
74
+ getDisplayedValue() {
75
+ if (this.showPassword) {
76
+ return this.value;
77
+ }
78
+ return this.maskChar.repeat(this.value.length);
79
+ }
80
+
81
+ /**
82
+ * Bascule l'affichage du mot de passe
83
+ */
84
+ onTogglePassword() {
85
+ this.showPassword = !this.showPassword;
86
+ this.displayedValue = this.getDisplayedValue();
87
+ }
88
+
89
+ /**
90
+ * Vérifie si un point est sur le bouton d'affichage
91
+ * @param {number} x - Coordonnée X
92
+ * @param {number} y - Coordonnée Y
93
+ * @returns {boolean} True si le point est sur le bouton
94
+ */
95
+ isPointOnToggleButton(x, y) {
96
+ const buttonX = this.x + this.width - this.toggleButtonSize - this.toggleButtonPadding;
97
+ const buttonY = this.y + (this.height - this.toggleButtonSize) / 2;
98
+
99
+ return x >= buttonX &&
100
+ x <= buttonX + this.toggleButtonSize &&
101
+ y >= buttonY &&
102
+ y <= buttonY + this.toggleButtonSize;
103
+ }
104
+
105
+ /**
106
+ * Écoute les clics globaux pour détecter les clics hors input
107
+ */
108
+ setupGlobalClickHandler() {
109
+ if (!PasswordInput.globalClickHandler) {
110
+ PasswordInput.globalClickHandler = (e) => {
111
+ let clickedOnInput = false;
112
+
113
+ for (let input of PasswordInput.allInputs) {
114
+ if (input.hiddenInput && e.target === input.hiddenInput) {
115
+ clickedOnInput = true;
116
+ break;
117
+ }
118
+ }
119
+
120
+ if (!clickedOnInput) {
121
+ PasswordInput.removeAllHiddenInputs();
122
+ }
123
+ };
124
+
125
+ document.addEventListener('click', PasswordInput.globalClickHandler, true);
126
+ document.addEventListener('touchstart', PasswordInput.globalClickHandler, true);
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Configure l'input HTML caché
132
+ * @private
133
+ */
134
+ setupHiddenInput() {
135
+ if (this.hiddenInput) return;
136
+
137
+ this.hiddenInput = document.createElement('input');
138
+ this.hiddenInput.style.position = 'fixed';
139
+ this.hiddenInput.type = this.showPassword ? 'text' : 'password';
140
+ this.hiddenInput.style.opacity = '0';
141
+ this.hiddenInput.style.pointerEvents = 'none';
142
+ this.hiddenInput.style.top = '-100px';
143
+ this.hiddenInput.style.zIndex = '9999';
144
+ document.body.appendChild(this.hiddenInput);
145
+
146
+ this.hiddenInput.addEventListener('input', (e) => {
147
+ if (this.focused) {
148
+ this.value = e.target.value;
149
+ this.cursorPosition = this.value.length;
150
+ this.displayedValue = this.getDisplayedValue();
151
+ }
152
+ });
153
+
154
+ this.hiddenInput.addEventListener('blur', () => {
155
+ this.focused = false;
156
+ this.cursorVisible = false;
157
+
158
+ setTimeout(() => {
159
+ this.destroyHiddenInput();
160
+ }, 100);
161
+ });
162
+ }
163
+
164
+ /**
165
+ * Gère le focus
166
+ */
167
+ onFocus() {
168
+ if (PasswordInput.activeInput === this) {
169
+ return;
170
+ }
171
+
172
+ PasswordInput.removeAllHiddenInputs();
173
+
174
+ for (let input of PasswordInput.allInputs) {
175
+ if (input !== this) {
176
+ input.focused = false;
177
+ input.cursorVisible = false;
178
+ }
179
+ }
180
+
181
+ this.focused = true;
182
+ this.cursorVisible = true;
183
+ PasswordInput.activeInput = this;
184
+
185
+ this.setupHiddenInput();
186
+
187
+ if (this.hiddenInput) {
188
+ this.hiddenInput.value = this.value;
189
+ this.hiddenInput.type = this.showPassword ? 'text' : 'password';
190
+
191
+ const adjustedY = this.y + this.framework.scrollOffset;
192
+ this.hiddenInput.style.top = `${adjustedY}px`;
193
+
194
+ setTimeout(() => {
195
+ if (this.hiddenInput && this.focused) {
196
+ this.hiddenInput.focus();
197
+ this.hiddenInput.setSelectionRange(this.value.length, this.value.length);
198
+ }
199
+ }, 50);
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Gère le blur
205
+ */
206
+ onBlur() {
207
+ this.focused = false;
208
+ this.cursorVisible = false;
209
+ }
210
+
211
+ /**
212
+ * Détruit l'input HTML
213
+ */
214
+ destroyHiddenInput() {
215
+ if (this.hiddenInput && this.hiddenInput.parentNode) {
216
+ this.hiddenInput.parentNode.removeChild(this.hiddenInput);
217
+ this.hiddenInput = null;
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Gère le clic
223
+ */
224
+ onClick() {
225
+ this.onFocus();
226
+ }
227
+
228
+ /**
229
+ * Gère le clic sur le bouton d'affichage
230
+ */
231
+ onToggleButtonClick() {
232
+ this.onTogglePassword();
233
+
234
+ // Mettre à jour le type de l'input HTML si existant
235
+ if (this.hiddenInput) {
236
+ this.hiddenInput.type = this.showPassword ? 'text' : 'password';
237
+ if (this.focused) {
238
+ setTimeout(() => {
239
+ if (this.hiddenInput) {
240
+ this.hiddenInput.focus();
241
+ }
242
+ }, 10);
243
+ }
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Méthode statique pour détruire tous les inputs HTML
249
+ */
250
+ static removeAllHiddenInputs() {
251
+ for (let input of PasswordInput.allInputs) {
252
+ input.focused = false;
253
+ input.cursorVisible = false;
254
+
255
+ if (input.hiddenInput && input.hiddenInput.parentNode) {
256
+ input.hiddenInput.parentNode.removeChild(input.hiddenInput);
257
+ input.hiddenInput = null;
258
+ }
259
+ }
260
+
261
+ PasswordInput.activeInput = null;
262
+ }
263
+
264
+ /**
265
+ * Vérifie si un point est dans les limites
266
+ * @param {number} x - Coordonnée X
267
+ * @param {number} y - Coordonnée Y
268
+ * @returns {boolean} True si le point est dans l'input
269
+ */
270
+ isPointInside(x, y) {
271
+ return x >= this.x &&
272
+ x <= this.x + this.width &&
273
+ y >= this.y &&
274
+ y <= this.y + this.height;
275
+ }
276
+
277
+ /**
278
+ * Dessine l'input
279
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
280
+ */
281
+ draw(ctx) {
282
+ ctx.save();
283
+
284
+ if (this.platform === 'material') {
285
+ ctx.strokeStyle = this.focused ? '#6200EE' : '#CCCCCC';
286
+ ctx.lineWidth = this.focused ? 2 : 1;
287
+ ctx.beginPath();
288
+ ctx.moveTo(this.x, this.y + this.height);
289
+ ctx.lineTo(this.x + this.width, this.y + this.height);
290
+ ctx.stroke();
291
+ } else {
292
+ ctx.strokeStyle = this.focused ? '#007AFF' : '#C7C7CC';
293
+ ctx.lineWidth = 1;
294
+ ctx.beginPath();
295
+ this.roundRect(ctx, this.x, this.y, this.width, this.height, 8);
296
+ ctx.stroke();
297
+ }
298
+
299
+ // Texte
300
+ ctx.fillStyle = this.value ? '#000000' : '#999999';
301
+ ctx.font = `${this.fontSize}px -apple-system, BlinkMacSystemFont, 'Roboto', sans-serif`;
302
+ ctx.textAlign = 'left';
303
+ ctx.textBaseline = 'middle';
304
+ const displayText = this.displayedValue || this.placeholder;
305
+
306
+ // Calculer la largeur disponible (en tenant compte du bouton)
307
+ const availableWidth = this.width - 20 - this.toggleButtonSize - this.toggleButtonPadding;
308
+
309
+ // Tronquer le texte si nécessaire
310
+ let finalDisplayText = displayText;
311
+ const textWidth = ctx.measureText(displayText).width;
312
+ if (textWidth > availableWidth) {
313
+ // Tronquer le texte avec "..."
314
+ for (let i = displayText.length; i > 0; i--) {
315
+ const truncated = displayText.substring(0, i) + '...';
316
+ if (ctx.measureText(truncated).width <= availableWidth) {
317
+ finalDisplayText = truncated;
318
+ break;
319
+ }
320
+ }
321
+ }
322
+
323
+ ctx.fillText(finalDisplayText, this.x + 10, this.y + this.height / 2);
324
+
325
+ // Curseur
326
+ if (this.focused && this.cursorVisible && this.displayedValue) {
327
+ const textWidth = ctx.measureText(finalDisplayText).width;
328
+ ctx.fillStyle = '#000000';
329
+ ctx.fillRect(this.x + 10 + textWidth, this.y + 10, 2, this.height - 20);
330
+ }
331
+
332
+ // Bouton d'affichage/masquage
333
+ this.drawToggleButton(ctx);
334
+
335
+ ctx.restore();
336
+ }
337
+
338
+ /**
339
+ * Dessine le bouton d'affichage/masquage
340
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
341
+ */
342
+ drawToggleButton(ctx) {
343
+ const buttonX = this.x + this.width - this.toggleButtonSize - this.toggleButtonPadding;
344
+ const buttonY = this.y + (this.height - this.toggleButtonSize) / 2;
345
+
346
+ // Cercle du bouton
347
+ ctx.fillStyle = '#F0F0F0';
348
+ ctx.beginPath();
349
+ ctx.arc(
350
+ buttonX + this.toggleButtonSize / 2,
351
+ buttonY + this.toggleButtonSize / 2,
352
+ this.toggleButtonSize / 2,
353
+ 0,
354
+ Math.PI * 2
355
+ );
356
+ ctx.fill();
357
+
358
+ // Icône de l'œil
359
+ ctx.strokeStyle = '#666666';
360
+ ctx.lineWidth = 2;
361
+ ctx.beginPath();
362
+
363
+ if (this.showPassword) {
364
+ // Œil barré (mot de passe visible)
365
+ // Contour de l'œil
366
+ ctx.arc(
367
+ buttonX + this.toggleButtonSize / 2,
368
+ buttonY + this.toggleButtonSize / 2,
369
+ this.toggleButtonSize / 4,
370
+ 0,
371
+ Math.PI * 2
372
+ );
373
+ ctx.stroke();
374
+
375
+ // Pupille
376
+ ctx.fillStyle = '#666666';
377
+ ctx.beginPath();
378
+ ctx.arc(
379
+ buttonX + this.toggleButtonSize / 2,
380
+ buttonY + this.toggleButtonSize / 2,
381
+ this.toggleButtonSize / 8,
382
+ 0,
383
+ Math.PI * 2
384
+ );
385
+ ctx.fill();
386
+
387
+ // Barre diagonale
388
+ ctx.beginPath();
389
+ ctx.moveTo(buttonX + 5, buttonY + 5);
390
+ ctx.lineTo(buttonX + this.toggleButtonSize - 5, buttonY + this.toggleButtonSize - 5);
391
+ ctx.stroke();
392
+ } else {
393
+ // Œil ouvert (mot de passe masqué)
394
+ // Contour de l'œil
395
+ ctx.arc(
396
+ buttonX + this.toggleButtonSize / 2,
397
+ buttonY + this.toggleButtonSize / 2,
398
+ this.toggleButtonSize / 4,
399
+ 0,
400
+ Math.PI * 2
401
+ );
402
+ ctx.stroke();
403
+
404
+ // Pupille
405
+ ctx.fillStyle = '#666666';
406
+ ctx.beginPath();
407
+ ctx.arc(
408
+ buttonX + this.toggleButtonSize / 2,
409
+ buttonY + this.toggleButtonSize / 2,
410
+ this.toggleButtonSize / 8,
411
+ 0,
412
+ Math.PI * 2
413
+ );
414
+ ctx.fill();
415
+ }
416
+ }
417
+
418
+ /**
419
+ * Dessine un rectangle avec coins arrondis
420
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
421
+ * @param {number} x - Position X
422
+ * @param {number} y - Position Y
423
+ * @param {number} width - Largeur
424
+ * @param {number} height - Hauteur
425
+ * @param {number} radius - Rayon des coins
426
+ * @private
427
+ */
428
+ roundRect(ctx, x, y, width, height, radius) {
429
+ ctx.moveTo(x + radius, y);
430
+ ctx.lineTo(x + width - radius, y);
431
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
432
+ ctx.lineTo(x + width, y + height - radius);
433
+ ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
434
+ ctx.lineTo(x + radius, y + height);
435
+ ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
436
+ ctx.lineTo(x, y + radius);
437
+ ctx.quadraticCurveTo(x, y, x + radius, y);
438
+ }
439
+
440
+ /**
441
+ * Nettoie les ressources
442
+ */
443
+ destroy() {
444
+ this.destroyHiddenInput();
445
+
446
+ if (this.cursorInterval) {
447
+ clearInterval(this.cursorInterval);
448
+ }
449
+
450
+ PasswordInput.allInputs.delete(this);
451
+
452
+ if (PasswordInput.allInputs.size === 0 && PasswordInput.globalClickHandler) {
453
+ document.removeEventListener('click', PasswordInput.globalClickHandler, true);
454
+ document.removeEventListener('touchstart', PasswordInput.globalClickHandler, true);
455
+ PasswordInput.globalClickHandler = null;
456
+ }
457
+
458
+ super.destroy && super.destroy();
459
+ }
460
+ }
461
+
462
+ export default PasswordInput;