canvasframework 0.3.9 → 0.3.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/AppBar.js +164 -70
- package/components/BottomNavigationBar.js +206 -69
- package/components/InputTags.js +586 -0
- package/components/PasswordInput.js +462 -0
- package/components/TimePicker.js +443 -0
- package/core/CanvasFramework.js +6 -4
- package/index.js +2 -0
- package/package.json +1 -1
|
@@ -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;
|