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,586 @@
|
|
|
1
|
+
import Component from '../core/Component.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Champ de saisie de tags avec gestion de tags multiples
|
|
5
|
+
* @class
|
|
6
|
+
* @extends Component
|
|
7
|
+
* @property {string} placeholder - Texte d'indication
|
|
8
|
+
* @property {string} value - Valeur en cours de saisie
|
|
9
|
+
* @property {Array} tags - Liste des tags
|
|
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 {number} tagPadding - Padding interne des tags
|
|
17
|
+
* @property {number} tagSpacing - Espacement entre les tags
|
|
18
|
+
* @property {string} tagColor - Couleur des tags
|
|
19
|
+
* @property {string} tagTextColor - Couleur du texte des tags
|
|
20
|
+
* @property {string} deleteButtonColor - Couleur du bouton de suppression
|
|
21
|
+
*/
|
|
22
|
+
class InputTags extends Component {
|
|
23
|
+
static activeInput = null;
|
|
24
|
+
static allInputs = new Set();
|
|
25
|
+
static globalClickHandler = null;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Crée une instance de InputTags
|
|
29
|
+
* @param {CanvasFramework} framework - Framework parent
|
|
30
|
+
* @param {Object} [options={}] - Options de configuration
|
|
31
|
+
* @param {string} [options.placeholder=''] - Texte d'indication
|
|
32
|
+
* @param {Array} [options.tags=[]] - Tags initiaux
|
|
33
|
+
* @param {string} [options.value=''] - Valeur initiale
|
|
34
|
+
* @param {number} [options.fontSize=16] - Taille de police
|
|
35
|
+
* @param {Function} [options.onFocus] - Callback au focus
|
|
36
|
+
* @param {Function} [options.onBlur] - Callback au blur
|
|
37
|
+
* @param {Function} [options.onTagAdd] - Callback quand un tag est ajouté
|
|
38
|
+
* @param {Function} [options.onTagRemove] - Callback quand un tag est supprimé
|
|
39
|
+
* @param {number} [options.tagPadding=8] - Padding interne des tags
|
|
40
|
+
* @param {number} [options.tagSpacing=6] - Espacement entre les tags
|
|
41
|
+
* @param {string} [options.tagColor='#E0E0E0'] - Couleur des tags
|
|
42
|
+
* @param {string} [options.tagTextColor='#333333'] - Couleur du texte des tags
|
|
43
|
+
* @param {string} [options.deleteButtonColor='#666666'] - Couleur du bouton de suppression
|
|
44
|
+
*/
|
|
45
|
+
constructor(framework, options = {}) {
|
|
46
|
+
super(framework, options);
|
|
47
|
+
this.placeholder = options.placeholder || 'Ajouter des tags...';
|
|
48
|
+
this.value = options.value || '';
|
|
49
|
+
this.tags = Array.isArray(options.tags) ? [...options.tags] : [];
|
|
50
|
+
this.fontSize = options.fontSize || 16;
|
|
51
|
+
this.focused = false;
|
|
52
|
+
this.platform = framework.platform;
|
|
53
|
+
this.cursorVisible = true;
|
|
54
|
+
this.cursorPosition = this.value.length;
|
|
55
|
+
|
|
56
|
+
// Configuration des tags
|
|
57
|
+
this.tagPadding = options.tagPadding || 8;
|
|
58
|
+
this.tagSpacing = options.tagSpacing || 6;
|
|
59
|
+
this.tagColor = options.tagColor || '#E0E0E0';
|
|
60
|
+
this.tagTextColor = options.tagTextColor || '#333333';
|
|
61
|
+
this.deleteButtonColor = options.deleteButtonColor || '#666666';
|
|
62
|
+
|
|
63
|
+
// Callbacks
|
|
64
|
+
this.onTagAdd = options.onTagAdd || (() => {});
|
|
65
|
+
this.onTagRemove = options.onTagRemove || (() => {});
|
|
66
|
+
|
|
67
|
+
// Calculs de layout
|
|
68
|
+
this.tagHeight = this.fontSize + this.tagPadding * 2;
|
|
69
|
+
this.deleteButtonSize = this.fontSize * 0.8;
|
|
70
|
+
|
|
71
|
+
// Gestion du focus
|
|
72
|
+
this.onFocus = this.onFocus.bind(this);
|
|
73
|
+
this.onBlur = this.onBlur.bind(this);
|
|
74
|
+
|
|
75
|
+
// Enregistrer cet input
|
|
76
|
+
InputTags.allInputs.add(this);
|
|
77
|
+
|
|
78
|
+
// Animation du curseur
|
|
79
|
+
this.cursorInterval = setInterval(() => {
|
|
80
|
+
if (this.focused) this.cursorVisible = !this.cursorVisible;
|
|
81
|
+
}, 500);
|
|
82
|
+
|
|
83
|
+
// Écouter les clics partout pour détecter quand on clique ailleurs
|
|
84
|
+
this.setupGlobalClickHandler();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Écoute les clics globaux pour détecter les clics hors input
|
|
89
|
+
*/
|
|
90
|
+
setupGlobalClickHandler() {
|
|
91
|
+
if (!InputTags.globalClickHandler) {
|
|
92
|
+
InputTags.globalClickHandler = (e) => {
|
|
93
|
+
let clickedOnInput = false;
|
|
94
|
+
|
|
95
|
+
for (let input of InputTags.allInputs) {
|
|
96
|
+
if (input.hiddenInput && e.target === input.hiddenInput) {
|
|
97
|
+
clickedOnInput = true;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!clickedOnInput) {
|
|
103
|
+
InputTags.removeAllHiddenInputs();
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
document.addEventListener('click', InputTags.globalClickHandler, true);
|
|
108
|
+
document.addEventListener('touchstart', InputTags.globalClickHandler, true);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Configure l'input HTML caché
|
|
114
|
+
* @private
|
|
115
|
+
*/
|
|
116
|
+
setupHiddenInput() {
|
|
117
|
+
if (this.hiddenInput) return;
|
|
118
|
+
|
|
119
|
+
this.hiddenInput = document.createElement('input');
|
|
120
|
+
this.hiddenInput.style.position = 'fixed';
|
|
121
|
+
this.hiddenInput.style.opacity = '0';
|
|
122
|
+
this.hiddenInput.style.pointerEvents = 'none';
|
|
123
|
+
this.hiddenInput.style.top = '-100px';
|
|
124
|
+
this.hiddenInput.style.zIndex = '9999';
|
|
125
|
+
document.body.appendChild(this.hiddenInput);
|
|
126
|
+
|
|
127
|
+
this.hiddenInput.addEventListener('input', (e) => {
|
|
128
|
+
if (this.focused) {
|
|
129
|
+
this.value = e.target.value;
|
|
130
|
+
this.cursorPosition = this.value.length;
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
this.hiddenInput.addEventListener('keydown', (e) => {
|
|
135
|
+
if (e.key === 'Enter' || e.key === ',') {
|
|
136
|
+
e.preventDefault();
|
|
137
|
+
this.addCurrentTag();
|
|
138
|
+
} else if (e.key === 'Backspace' && this.value === '' && this.tags.length > 0) {
|
|
139
|
+
this.removeLastTag();
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
this.hiddenInput.addEventListener('blur', () => {
|
|
144
|
+
// Ajouter le tag en cours si non vide
|
|
145
|
+
if (this.value.trim() !== '') {
|
|
146
|
+
this.addCurrentTag();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
this.focused = false;
|
|
150
|
+
this.cursorVisible = false;
|
|
151
|
+
|
|
152
|
+
setTimeout(() => {
|
|
153
|
+
this.destroyHiddenInput();
|
|
154
|
+
}, 100);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Ajoute le tag en cours de saisie
|
|
160
|
+
*/
|
|
161
|
+
addCurrentTag() {
|
|
162
|
+
const tag = this.value.trim();
|
|
163
|
+
if (tag !== '' && !this.tags.includes(tag)) {
|
|
164
|
+
this.tags.push(tag);
|
|
165
|
+
this.value = '';
|
|
166
|
+
this.cursorPosition = 0;
|
|
167
|
+
|
|
168
|
+
if (this.hiddenInput) {
|
|
169
|
+
this.hiddenInput.value = '';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
this.onTagAdd(tag, this.tags);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Ajoute un tag spécifique
|
|
178
|
+
* @param {string} tag - Tag à ajouter
|
|
179
|
+
*/
|
|
180
|
+
addTag(tag) {
|
|
181
|
+
const trimmedTag = tag.trim();
|
|
182
|
+
if (trimmedTag !== '' && !this.tags.includes(trimmedTag)) {
|
|
183
|
+
this.tags.push(trimmedTag);
|
|
184
|
+
this.onTagAdd(trimmedTag, this.tags);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Supprime un tag par son index
|
|
190
|
+
* @param {number} index - Index du tag à supprimer
|
|
191
|
+
*/
|
|
192
|
+
removeTag(index) {
|
|
193
|
+
if (index >= 0 && index < this.tags.length) {
|
|
194
|
+
const removedTag = this.tags[index];
|
|
195
|
+
this.tags.splice(index, 1);
|
|
196
|
+
this.onTagRemove(removedTag, this.tags);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Supprime le dernier tag
|
|
202
|
+
*/
|
|
203
|
+
removeLastTag() {
|
|
204
|
+
if (this.tags.length > 0) {
|
|
205
|
+
this.removeTag(this.tags.length - 1);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Supprime tous les tags
|
|
211
|
+
*/
|
|
212
|
+
clearTags() {
|
|
213
|
+
const oldTags = [...this.tags];
|
|
214
|
+
this.tags = [];
|
|
215
|
+
oldTags.forEach(tag => this.onTagRemove(tag, this.tags));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Vérifie si un point est sur le bouton de suppression d'un tag
|
|
220
|
+
* @param {number} x - Coordonnée X
|
|
221
|
+
* @param {number} y - Coordonnée Y
|
|
222
|
+
* @returns {number|null} Index du tag ou null
|
|
223
|
+
*/
|
|
224
|
+
getTagIndexAtPoint(x, y) {
|
|
225
|
+
let currentX = this.x + 10;
|
|
226
|
+
const tagY = this.y + 10;
|
|
227
|
+
|
|
228
|
+
for (let i = 0; i < this.tags.length; i++) {
|
|
229
|
+
const tag = this.tags[i];
|
|
230
|
+
const tagWidth = this.measureTagWidth(tag);
|
|
231
|
+
|
|
232
|
+
// Vérifier si le point est dans le tag
|
|
233
|
+
if (x >= currentX && x <= currentX + tagWidth &&
|
|
234
|
+
y >= tagY && y <= tagY + this.tagHeight) {
|
|
235
|
+
return i;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
currentX += tagWidth + this.tagSpacing;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Vérifie si un point est sur le bouton de suppression d'un tag
|
|
246
|
+
* @param {number} x - Coordonnée X
|
|
247
|
+
* @param {number} y - Coordonnée Y
|
|
248
|
+
* @returns {number|null} Index du tag ou null
|
|
249
|
+
*/
|
|
250
|
+
getDeleteButtonIndexAtPoint(x, y) {
|
|
251
|
+
let currentX = this.x + 10;
|
|
252
|
+
const tagY = this.y + 10;
|
|
253
|
+
|
|
254
|
+
for (let i = 0; i < this.tags.length; i++) {
|
|
255
|
+
const tag = this.tags[i];
|
|
256
|
+
const tagWidth = this.measureTagWidth(tag);
|
|
257
|
+
const deleteButtonX = currentX + tagWidth - this.deleteButtonSize - this.tagPadding / 2;
|
|
258
|
+
const deleteButtonY = tagY + (this.tagHeight - this.deleteButtonSize) / 2;
|
|
259
|
+
|
|
260
|
+
// Vérifier si le point est sur le bouton de suppression
|
|
261
|
+
if (x >= deleteButtonX && x <= deleteButtonX + this.deleteButtonSize &&
|
|
262
|
+
y >= deleteButtonY && y <= deleteButtonY + this.deleteButtonSize) {
|
|
263
|
+
return i;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
currentX += tagWidth + this.tagSpacing;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Mesure la largeur d'un tag
|
|
274
|
+
* @param {string} tag - Tag à mesurer
|
|
275
|
+
* @returns {number} Largeur du tag
|
|
276
|
+
*/
|
|
277
|
+
measureTagWidth(tag) {
|
|
278
|
+
// Approximation de la largeur du texte
|
|
279
|
+
const textWidth = tag.length * (this.fontSize * 0.6);
|
|
280
|
+
return textWidth + this.tagPadding * 2 + this.deleteButtonSize + this.tagPadding / 2;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Gère le focus
|
|
285
|
+
*/
|
|
286
|
+
onFocus() {
|
|
287
|
+
if (InputTags.activeInput === this) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
InputTags.removeAllHiddenInputs();
|
|
292
|
+
|
|
293
|
+
for (let input of InputTags.allInputs) {
|
|
294
|
+
if (input !== this) {
|
|
295
|
+
input.focused = false;
|
|
296
|
+
input.cursorVisible = false;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
this.focused = true;
|
|
301
|
+
this.cursorVisible = true;
|
|
302
|
+
InputTags.activeInput = this;
|
|
303
|
+
|
|
304
|
+
this.setupHiddenInput();
|
|
305
|
+
|
|
306
|
+
if (this.hiddenInput) {
|
|
307
|
+
this.hiddenInput.value = this.value;
|
|
308
|
+
|
|
309
|
+
const adjustedY = this.y + this.framework.scrollOffset;
|
|
310
|
+
this.hiddenInput.style.top = `${adjustedY}px`;
|
|
311
|
+
|
|
312
|
+
setTimeout(() => {
|
|
313
|
+
if (this.hiddenInput && this.focused) {
|
|
314
|
+
this.hiddenInput.focus();
|
|
315
|
+
this.hiddenInput.setSelectionRange(this.value.length, this.value.length);
|
|
316
|
+
}
|
|
317
|
+
}, 50);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Gère le blur
|
|
323
|
+
*/
|
|
324
|
+
onBlur() {
|
|
325
|
+
this.focused = false;
|
|
326
|
+
this.cursorVisible = false;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Détruit l'input HTML
|
|
331
|
+
*/
|
|
332
|
+
destroyHiddenInput() {
|
|
333
|
+
if (this.hiddenInput && this.hiddenInput.parentNode) {
|
|
334
|
+
this.hiddenInput.parentNode.removeChild(this.hiddenInput);
|
|
335
|
+
this.hiddenInput = null;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Gère le clic
|
|
341
|
+
* @param {number} x - Coordonnée X du clic
|
|
342
|
+
* @param {number} y - Coordonnée Y du clic
|
|
343
|
+
* @returns {boolean} True si le clic a été géré
|
|
344
|
+
*/
|
|
345
|
+
onClick(x, y) {
|
|
346
|
+
// Vérifier si on clique sur un bouton de suppression
|
|
347
|
+
const deleteIndex = this.getDeleteButtonIndexAtPoint(x, y);
|
|
348
|
+
if (deleteIndex !== null) {
|
|
349
|
+
this.removeTag(deleteIndex);
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Vérifier si on clique sur un tag (pour focus l'input)
|
|
354
|
+
const tagIndex = this.getTagIndexAtPoint(x, y);
|
|
355
|
+
if (tagIndex !== null) {
|
|
356
|
+
this.onFocus();
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Vérifier si on clique dans la zone d'input
|
|
361
|
+
if (this.isPointInside(x, y)) {
|
|
362
|
+
this.onFocus();
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Méthode statique pour détruire tous les inputs HTML
|
|
371
|
+
*/
|
|
372
|
+
static removeAllHiddenInputs() {
|
|
373
|
+
for (let input of InputTags.allInputs) {
|
|
374
|
+
input.focused = false;
|
|
375
|
+
input.cursorVisible = false;
|
|
376
|
+
|
|
377
|
+
if (input.hiddenInput && input.hiddenInput.parentNode) {
|
|
378
|
+
input.hiddenInput.parentNode.removeChild(input.hiddenInput);
|
|
379
|
+
input.hiddenInput = null;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
InputTags.activeInput = null;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Vérifie si un point est dans les limites
|
|
388
|
+
* @param {number} x - Coordonnée X
|
|
389
|
+
* @param {number} y - Coordonnée Y
|
|
390
|
+
* @returns {boolean} True si le point est dans l'input
|
|
391
|
+
*/
|
|
392
|
+
isPointInside(x, y) {
|
|
393
|
+
return x >= this.x &&
|
|
394
|
+
x <= this.x + this.width &&
|
|
395
|
+
y >= this.y &&
|
|
396
|
+
y <= this.y + this.height;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Calcule la position X du curseur
|
|
401
|
+
* @returns {number} Position X du curseur
|
|
402
|
+
*/
|
|
403
|
+
getCursorXPosition() {
|
|
404
|
+
let cursorX = this.x + 10;
|
|
405
|
+
|
|
406
|
+
// Ajouter la largeur de tous les tags
|
|
407
|
+
for (let tag of this.tags) {
|
|
408
|
+
cursorX += this.measureTagWidth(tag) + this.tagSpacing;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Ajouter la largeur du texte en cours
|
|
412
|
+
const textWidth = this.value.length * (this.fontSize * 0.6);
|
|
413
|
+
cursorX += textWidth;
|
|
414
|
+
|
|
415
|
+
return cursorX;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Dessine l'input
|
|
420
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
421
|
+
*/
|
|
422
|
+
draw(ctx) {
|
|
423
|
+
ctx.save();
|
|
424
|
+
|
|
425
|
+
if (this.platform === 'material') {
|
|
426
|
+
ctx.strokeStyle = this.focused ? '#6200EE' : '#CCCCCC';
|
|
427
|
+
ctx.lineWidth = this.focused ? 2 : 1;
|
|
428
|
+
ctx.beginPath();
|
|
429
|
+
ctx.moveTo(this.x, this.y + this.height);
|
|
430
|
+
ctx.lineTo(this.x + this.width, this.y + this.height);
|
|
431
|
+
ctx.stroke();
|
|
432
|
+
} else {
|
|
433
|
+
ctx.strokeStyle = this.focused ? '#007AFF' : '#C7C7CC';
|
|
434
|
+
ctx.lineWidth = 1;
|
|
435
|
+
ctx.beginPath();
|
|
436
|
+
this.roundRect(ctx, this.x, this.y, this.width, this.height, 8);
|
|
437
|
+
ctx.stroke();
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Position de départ pour dessiner les tags
|
|
441
|
+
let currentX = this.x + 10;
|
|
442
|
+
const tagY = this.y + 10;
|
|
443
|
+
|
|
444
|
+
// Dessiner les tags
|
|
445
|
+
for (let i = 0; i < this.tags.length; i++) {
|
|
446
|
+
this.drawTag(ctx, this.tags[i], currentX, tagY);
|
|
447
|
+
const tagWidth = this.measureTagWidth(this.tags[i]);
|
|
448
|
+
currentX += tagWidth + this.tagSpacing;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Dessiner le texte en cours
|
|
452
|
+
if (this.value || (this.focused && this.tags.length === 0)) {
|
|
453
|
+
ctx.fillStyle = this.value ? '#000000' : '#999999';
|
|
454
|
+
ctx.font = `${this.fontSize}px -apple-system, BlinkMacSystemFont, 'Roboto', sans-serif`;
|
|
455
|
+
ctx.textAlign = 'left';
|
|
456
|
+
ctx.textBaseline = 'middle';
|
|
457
|
+
|
|
458
|
+
const displayText = this.value || this.placeholder;
|
|
459
|
+
const textY = this.y + this.height / 2;
|
|
460
|
+
|
|
461
|
+
ctx.fillText(displayText, currentX, textY);
|
|
462
|
+
|
|
463
|
+
// Curseur
|
|
464
|
+
if (this.focused && this.cursorVisible) {
|
|
465
|
+
const textWidth = ctx.measureText(this.value).width;
|
|
466
|
+
ctx.fillStyle = '#000000';
|
|
467
|
+
ctx.fillRect(currentX + textWidth, textY - this.fontSize / 2, 2, this.fontSize);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
ctx.restore();
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Dessine un tag
|
|
476
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
477
|
+
* @param {string} tag - Texte du tag
|
|
478
|
+
* @param {number} x - Position X
|
|
479
|
+
* @param {number} y - Position Y
|
|
480
|
+
*/
|
|
481
|
+
drawTag(ctx, tag, x, y) {
|
|
482
|
+
const tagWidth = this.measureTagWidth(tag);
|
|
483
|
+
|
|
484
|
+
// Fond du tag
|
|
485
|
+
ctx.fillStyle = this.tagColor;
|
|
486
|
+
this.roundRect(ctx, x, y, tagWidth, this.tagHeight, this.tagHeight / 2);
|
|
487
|
+
ctx.fill();
|
|
488
|
+
|
|
489
|
+
// Texte du tag
|
|
490
|
+
ctx.fillStyle = this.tagTextColor;
|
|
491
|
+
ctx.font = `${this.fontSize}px -apple-system, BlinkMacSystemFont, 'Roboto', sans-serif`;
|
|
492
|
+
ctx.textAlign = 'left';
|
|
493
|
+
ctx.textBaseline = 'middle';
|
|
494
|
+
|
|
495
|
+
const textX = x + this.tagPadding;
|
|
496
|
+
const textY = y + this.tagHeight / 2;
|
|
497
|
+
|
|
498
|
+
// Tronquer le texte si trop long
|
|
499
|
+
const maxTextWidth = tagWidth - this.tagPadding * 2 - this.deleteButtonSize - this.tagPadding / 2;
|
|
500
|
+
let displayTag = tag;
|
|
501
|
+
let textWidth = ctx.measureText(tag).width;
|
|
502
|
+
|
|
503
|
+
if (textWidth > maxTextWidth) {
|
|
504
|
+
// Tronquer le texte avec "..."
|
|
505
|
+
for (let i = tag.length; i > 0; i--) {
|
|
506
|
+
const truncated = tag.substring(0, i) + '...';
|
|
507
|
+
if (ctx.measureText(truncated).width <= maxTextWidth) {
|
|
508
|
+
displayTag = truncated;
|
|
509
|
+
break;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
ctx.fillText(displayTag, textX, textY);
|
|
515
|
+
|
|
516
|
+
// Bouton de suppression (×)
|
|
517
|
+
const deleteButtonX = x + tagWidth - this.deleteButtonSize - this.tagPadding / 2;
|
|
518
|
+
const deleteButtonY = y + (this.tagHeight - this.deleteButtonSize) / 2;
|
|
519
|
+
|
|
520
|
+
ctx.strokeStyle = this.deleteButtonColor;
|
|
521
|
+
ctx.lineWidth = 2;
|
|
522
|
+
ctx.beginPath();
|
|
523
|
+
|
|
524
|
+
// Croix
|
|
525
|
+
const centerX = deleteButtonX + this.deleteButtonSize / 2;
|
|
526
|
+
const centerY = deleteButtonY + this.deleteButtonSize / 2;
|
|
527
|
+
const crossSize = this.deleteButtonSize / 3;
|
|
528
|
+
|
|
529
|
+
ctx.moveTo(centerX - crossSize, centerY - crossSize);
|
|
530
|
+
ctx.lineTo(centerX + crossSize, centerY + crossSize);
|
|
531
|
+
ctx.moveTo(centerX + crossSize, centerY - crossSize);
|
|
532
|
+
ctx.lineTo(centerX - crossSize, centerY + crossSize);
|
|
533
|
+
ctx.stroke();
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Dessine un rectangle avec coins arrondis
|
|
538
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
539
|
+
* @param {number} x - Position X
|
|
540
|
+
* @param {number} y - Position Y
|
|
541
|
+
* @param {number} width - Largeur
|
|
542
|
+
* @param {number} height - Hauteur
|
|
543
|
+
* @param {number} radius - Rayon des coins
|
|
544
|
+
* @private
|
|
545
|
+
*/
|
|
546
|
+
roundRect(ctx, x, y, width, height, radius) {
|
|
547
|
+
if (ctx.roundRect) {
|
|
548
|
+
ctx.roundRect(x, y, width, height, radius);
|
|
549
|
+
} else {
|
|
550
|
+
ctx.beginPath();
|
|
551
|
+
ctx.moveTo(x + radius, y);
|
|
552
|
+
ctx.lineTo(x + width - radius, y);
|
|
553
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
554
|
+
ctx.lineTo(x + width, y + height - radius);
|
|
555
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
556
|
+
ctx.lineTo(x + radius, y + height);
|
|
557
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
558
|
+
ctx.lineTo(x, y + radius);
|
|
559
|
+
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
560
|
+
ctx.closePath();
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Nettoie les ressources
|
|
566
|
+
*/
|
|
567
|
+
destroy() {
|
|
568
|
+
this.destroyHiddenInput();
|
|
569
|
+
|
|
570
|
+
if (this.cursorInterval) {
|
|
571
|
+
clearInterval(this.cursorInterval);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
InputTags.allInputs.delete(this);
|
|
575
|
+
|
|
576
|
+
if (InputTags.allInputs.size === 0 && InputTags.globalClickHandler) {
|
|
577
|
+
document.removeEventListener('click', InputTags.globalClickHandler, true);
|
|
578
|
+
document.removeEventListener('touchstart', InputTags.globalClickHandler, true);
|
|
579
|
+
InputTags.globalClickHandler = null;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
super.destroy && super.destroy();
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
export default InputTags;
|