canvasframework 0.3.8 → 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/Button.js +182 -62
- package/components/InputTags.js +586 -0
- package/components/MorphingFAB.js +428 -0
- package/components/PasswordInput.js +462 -0
- package/components/SpeedDialFAB.js +397 -0
- package/components/TimePicker.js +443 -0
- package/core/CanvasFramework.js +8 -4
- package/index.js +5 -1
- package/package.json +1 -1
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import FAB from './FAB.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Morphing FAB - FAB qui se transforme en barre d'actions
|
|
5
|
+
* @class
|
|
6
|
+
* @extends FAB
|
|
7
|
+
* @property {boolean} isMorphed - État transformé
|
|
8
|
+
* @property {Array} actions - Actions disponibles dans la barre
|
|
9
|
+
* @property {number} morphProgress - Progression de l'animation (0-1)
|
|
10
|
+
* @property {number} targetWidth - Largeur cible en mode morphé
|
|
11
|
+
*/
|
|
12
|
+
class MorphingFAB extends FAB {
|
|
13
|
+
/**
|
|
14
|
+
* Crée une instance de MorphingFAB
|
|
15
|
+
* @param {CanvasFramework} framework - Framework parent
|
|
16
|
+
* @param {Object} [options={}] - Options de configuration
|
|
17
|
+
* @param {Array} [options.actions=[]] - Actions de la barre
|
|
18
|
+
* @param {string} [options.morphType='toolbar'] - Type: 'toolbar', 'searchbar'
|
|
19
|
+
* @example
|
|
20
|
+
* // Toolbar
|
|
21
|
+
* actions: [
|
|
22
|
+
* { icon: '🏠', label: 'Home', action: () => {...} },
|
|
23
|
+
* { icon: '⭐', label: 'Favorites', action: () => {...} },
|
|
24
|
+
* { icon: '⚙', label: 'Settings', action: () => {...} }
|
|
25
|
+
* ]
|
|
26
|
+
*
|
|
27
|
+
* // Searchbar
|
|
28
|
+
* morphType: 'searchbar'
|
|
29
|
+
*/
|
|
30
|
+
constructor(framework, options = {}) {
|
|
31
|
+
super(framework, {
|
|
32
|
+
...options,
|
|
33
|
+
icon: options.icon || '+'
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
this.actions = options.actions || [];
|
|
37
|
+
this.morphType = options.morphType || 'toolbar';
|
|
38
|
+
this.isMorphed = false;
|
|
39
|
+
this.morphProgress = 0;
|
|
40
|
+
|
|
41
|
+
// Dimensions
|
|
42
|
+
this.originalWidth = this.width;
|
|
43
|
+
this.originalHeight = this.height;
|
|
44
|
+
this.originalX = this.x;
|
|
45
|
+
this.originalY = this.y;
|
|
46
|
+
|
|
47
|
+
// Calculer la largeur cible selon le type
|
|
48
|
+
if (this.morphType === 'searchbar') {
|
|
49
|
+
this.targetWidth = Math.min(framework.width - 32, 400);
|
|
50
|
+
this.targetHeight = 56;
|
|
51
|
+
this.targetX = (framework.width - this.targetWidth) / 2;
|
|
52
|
+
this.targetY = 16;
|
|
53
|
+
} else {
|
|
54
|
+
// toolbar
|
|
55
|
+
this.targetWidth = Math.min(framework.width - 32, this.actions.length * 80 + 32);
|
|
56
|
+
this.targetHeight = 56;
|
|
57
|
+
this.targetX = (framework.width - this.targetWidth) / 2;
|
|
58
|
+
this.targetY = framework.height - 80;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// État des boutons d'actions
|
|
62
|
+
this.actionButtons = this.actions.map((action, index) => ({
|
|
63
|
+
...action,
|
|
64
|
+
x: 0,
|
|
65
|
+
y: 0,
|
|
66
|
+
width: 60,
|
|
67
|
+
height: 48,
|
|
68
|
+
alpha: 0,
|
|
69
|
+
pressed: false
|
|
70
|
+
}));
|
|
71
|
+
|
|
72
|
+
// État de la searchbar
|
|
73
|
+
this.searchText = '';
|
|
74
|
+
this.searchFocused = false;
|
|
75
|
+
|
|
76
|
+
// Input caché pour la searchbar
|
|
77
|
+
if (this.morphType === 'searchbar') {
|
|
78
|
+
this.setupHiddenInput();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Bind
|
|
82
|
+
this.onPress = this.handlePress.bind(this);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Configure l'input HTML caché pour la searchbar
|
|
87
|
+
* @private
|
|
88
|
+
*/
|
|
89
|
+
setupHiddenInput() {
|
|
90
|
+
this.hiddenInput = document.createElement('input');
|
|
91
|
+
this.hiddenInput.style.position = 'fixed';
|
|
92
|
+
this.hiddenInput.style.opacity = '0';
|
|
93
|
+
this.hiddenInput.style.pointerEvents = 'none';
|
|
94
|
+
this.hiddenInput.style.top = '-100px';
|
|
95
|
+
document.body.appendChild(this.hiddenInput);
|
|
96
|
+
|
|
97
|
+
this.hiddenInput.addEventListener('input', (e) => {
|
|
98
|
+
if (this.searchFocused) {
|
|
99
|
+
this.searchText = e.target.value;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
this.hiddenInput.addEventListener('blur', () => {
|
|
104
|
+
this.searchFocused = false;
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Gère la pression
|
|
110
|
+
*/
|
|
111
|
+
handlePress(x, y) {
|
|
112
|
+
const adjustedY = y - this.framework.scrollOffset;
|
|
113
|
+
|
|
114
|
+
if (this.isMorphed) {
|
|
115
|
+
// Mode morphé
|
|
116
|
+
if (this.morphType === 'searchbar') {
|
|
117
|
+
// Zone de l'input searchbar
|
|
118
|
+
const searchInputX = this.x + 48;
|
|
119
|
+
const searchInputWidth = this.width - 96;
|
|
120
|
+
|
|
121
|
+
if (x >= searchInputX && x <= searchInputX + searchInputWidth &&
|
|
122
|
+
adjustedY >= this.y && adjustedY <= this.y + this.height) {
|
|
123
|
+
// Clic sur l'input - activer le focus
|
|
124
|
+
this.searchFocused = true;
|
|
125
|
+
if (this.hiddenInput) {
|
|
126
|
+
this.hiddenInput.value = this.searchText;
|
|
127
|
+
this.hiddenInput.style.top = `${this.y}px`;
|
|
128
|
+
this.hiddenInput.focus();
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Bouton fermer (X)
|
|
134
|
+
const closeX = this.x + this.width - 40;
|
|
135
|
+
if (x >= closeX && x <= closeX + 40 &&
|
|
136
|
+
adjustedY >= this.y && adjustedY <= this.y + this.height) {
|
|
137
|
+
this.searchText = '';
|
|
138
|
+
this.toggle();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
// Toolbar - vérifier les actions
|
|
143
|
+
for (let btn of this.actionButtons) {
|
|
144
|
+
if (x >= btn.x && x <= btn.x + btn.width &&
|
|
145
|
+
adjustedY >= btn.y && adjustedY <= btn.y + btn.height) {
|
|
146
|
+
btn.pressed = true;
|
|
147
|
+
setTimeout(() => {
|
|
148
|
+
btn.pressed = false;
|
|
149
|
+
if (btn.action) btn.action();
|
|
150
|
+
}, 150);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Clic en dehors -> fermer
|
|
157
|
+
if (!this.isPointInside(x, y)) {
|
|
158
|
+
this.toggle();
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
// Mode normal - ouvrir
|
|
162
|
+
this.toggle();
|
|
163
|
+
super.handlePress(x, y);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Toggle entre FAB et barre
|
|
169
|
+
*/
|
|
170
|
+
toggle() {
|
|
171
|
+
this.isMorphed = !this.isMorphed;
|
|
172
|
+
|
|
173
|
+
// Si on ferme et que c'est une searchbar, nettoyer l'input
|
|
174
|
+
if (!this.isMorphed && this.morphType === 'searchbar') {
|
|
175
|
+
this.searchText = '';
|
|
176
|
+
this.searchFocused = false;
|
|
177
|
+
if (this.hiddenInput) {
|
|
178
|
+
this.hiddenInput.blur();
|
|
179
|
+
this.hiddenInput.remove();
|
|
180
|
+
this.hiddenInput = null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Si on ouvre une searchbar, recréer l'input
|
|
185
|
+
if (this.isMorphed && this.morphType === 'searchbar' && !this.hiddenInput) {
|
|
186
|
+
this.setupHiddenInput();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
this.animate();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Anime la transformation
|
|
194
|
+
* @private
|
|
195
|
+
*/
|
|
196
|
+
animate() {
|
|
197
|
+
const startTime = Date.now();
|
|
198
|
+
const duration = 400;
|
|
199
|
+
|
|
200
|
+
const step = () => {
|
|
201
|
+
const elapsed = Date.now() - startTime;
|
|
202
|
+
const progress = Math.min(elapsed / duration, 1);
|
|
203
|
+
|
|
204
|
+
// Easing out cubic
|
|
205
|
+
const eased = 1 - Math.pow(1 - progress, 3);
|
|
206
|
+
|
|
207
|
+
this.morphProgress = this.isMorphed ? eased : 1 - eased;
|
|
208
|
+
|
|
209
|
+
// Interpoler les dimensions
|
|
210
|
+
this.width = this.lerp(this.originalWidth, this.targetWidth, this.morphProgress);
|
|
211
|
+
this.height = this.lerp(this.originalHeight, this.targetHeight, this.morphProgress);
|
|
212
|
+
this.x = this.lerp(this.originalX, this.targetX, this.morphProgress);
|
|
213
|
+
this.y = this.lerp(this.originalY, this.targetY, this.morphProgress);
|
|
214
|
+
|
|
215
|
+
// Mettre à jour les positions des actions
|
|
216
|
+
if (this.morphType === 'toolbar') {
|
|
217
|
+
this.updateActionPositions();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (progress < 1) {
|
|
221
|
+
requestAnimationFrame(step);
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
step();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Met à jour les positions des boutons d'action
|
|
230
|
+
* @private
|
|
231
|
+
*/
|
|
232
|
+
updateActionPositions() {
|
|
233
|
+
const spacing = this.targetWidth / (this.actionButtons.length + 1);
|
|
234
|
+
|
|
235
|
+
this.actionButtons.forEach((btn, index) => {
|
|
236
|
+
btn.x = this.x + spacing * (index + 1) - btn.width / 2;
|
|
237
|
+
btn.y = this.y + (this.height - btn.height) / 2;
|
|
238
|
+
btn.alpha = this.morphProgress;
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Interpolation linéaire
|
|
244
|
+
* @private
|
|
245
|
+
*/
|
|
246
|
+
lerp(start, end, t) {
|
|
247
|
+
return start + (end - start) * t;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Dessine le Morphing FAB
|
|
252
|
+
*/
|
|
253
|
+
draw(ctx) {
|
|
254
|
+
ctx.save();
|
|
255
|
+
|
|
256
|
+
if (this.isMorphed || this.morphProgress > 0) {
|
|
257
|
+
this.drawMorphed(ctx);
|
|
258
|
+
} else {
|
|
259
|
+
super.draw(ctx);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
ctx.restore();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Dessine l'état morphé
|
|
267
|
+
* @private
|
|
268
|
+
*/
|
|
269
|
+
drawMorphed(ctx) {
|
|
270
|
+
// Ombre
|
|
271
|
+
ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
|
|
272
|
+
ctx.shadowBlur = 12;
|
|
273
|
+
ctx.shadowOffsetY = 4;
|
|
274
|
+
|
|
275
|
+
// Background de la barre
|
|
276
|
+
ctx.fillStyle = this.bgColor;
|
|
277
|
+
ctx.beginPath();
|
|
278
|
+
const radius = this.lerp(this.borderRadius, 28, this.morphProgress);
|
|
279
|
+
this.roundRect(ctx, this.x, this.y, this.width, this.height, radius);
|
|
280
|
+
ctx.fill();
|
|
281
|
+
|
|
282
|
+
ctx.shadowColor = 'transparent';
|
|
283
|
+
ctx.shadowBlur = 0;
|
|
284
|
+
ctx.shadowOffsetY = 0;
|
|
285
|
+
|
|
286
|
+
if (this.morphType === 'searchbar') {
|
|
287
|
+
this.drawSearchBar(ctx);
|
|
288
|
+
} else {
|
|
289
|
+
this.drawToolbar(ctx);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Dessine la toolbar
|
|
295
|
+
* @private
|
|
296
|
+
*/
|
|
297
|
+
drawToolbar(ctx) {
|
|
298
|
+
// Dessiner les actions
|
|
299
|
+
for (let btn of this.actionButtons) {
|
|
300
|
+
if (btn.alpha > 0.01) {
|
|
301
|
+
ctx.save();
|
|
302
|
+
ctx.globalAlpha = btn.alpha;
|
|
303
|
+
|
|
304
|
+
// Highlight si pressed
|
|
305
|
+
if (btn.pressed) {
|
|
306
|
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.2)';
|
|
307
|
+
ctx.beginPath();
|
|
308
|
+
ctx.arc(btn.x + btn.width / 2, btn.y + btn.height / 2, 24, 0, Math.PI * 2);
|
|
309
|
+
ctx.fill();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Icône
|
|
313
|
+
ctx.fillStyle = this.iconColor;
|
|
314
|
+
ctx.font = 'bold 20px sans-serif';
|
|
315
|
+
ctx.textAlign = 'center';
|
|
316
|
+
ctx.textBaseline = 'middle';
|
|
317
|
+
ctx.fillText(btn.icon, btn.x + btn.width / 2, btn.y + btn.height / 2 - 8);
|
|
318
|
+
|
|
319
|
+
// Label
|
|
320
|
+
if (btn.label) {
|
|
321
|
+
ctx.font = '11px -apple-system, sans-serif';
|
|
322
|
+
ctx.fillText(btn.label, btn.x + btn.width / 2, btn.y + btn.height / 2 + 12);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
ctx.restore();
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Dessine la search bar
|
|
332
|
+
* @private
|
|
333
|
+
*/
|
|
334
|
+
drawSearchBar(ctx) {
|
|
335
|
+
ctx.save();
|
|
336
|
+
ctx.globalAlpha = this.morphProgress;
|
|
337
|
+
|
|
338
|
+
// Icône de recherche
|
|
339
|
+
ctx.fillStyle = this.iconColor;
|
|
340
|
+
ctx.font = 'bold 20px sans-serif';
|
|
341
|
+
ctx.textAlign = 'left';
|
|
342
|
+
ctx.textBaseline = 'middle';
|
|
343
|
+
ctx.fillText('🔍', this.x + 16, this.y + this.height / 2);
|
|
344
|
+
|
|
345
|
+
// Curseur clignotant si focus
|
|
346
|
+
let showCursor = false;
|
|
347
|
+
if (this.searchFocused) {
|
|
348
|
+
showCursor = Math.floor(Date.now() / 500) % 2 === 0;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Placeholder ou texte
|
|
352
|
+
ctx.font = '16px -apple-system, sans-serif';
|
|
353
|
+
ctx.fillStyle = this.searchText ? this.iconColor : 'rgba(255, 255, 255, 0.6)';
|
|
354
|
+
const displayText = this.searchText || 'Search...';
|
|
355
|
+
ctx.fillText(displayText, this.x + 48, this.y + this.height / 2);
|
|
356
|
+
|
|
357
|
+
// Curseur
|
|
358
|
+
if (showCursor && this.searchText) {
|
|
359
|
+
const textWidth = ctx.measureText(this.searchText).width;
|
|
360
|
+
ctx.fillStyle = this.iconColor;
|
|
361
|
+
ctx.fillRect(this.x + 48 + textWidth + 2, this.y + this.height / 2 - 10, 2, 20);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Bouton fermer
|
|
365
|
+
if (this.searchText || this.isMorphed) {
|
|
366
|
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
|
|
367
|
+
ctx.font = 'bold 18px sans-serif';
|
|
368
|
+
ctx.textAlign = 'right';
|
|
369
|
+
ctx.fillText('✕', this.x + this.width - 16, this.y + this.height / 2);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
ctx.restore();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Dessine un rectangle arrondi
|
|
377
|
+
* @private
|
|
378
|
+
*/
|
|
379
|
+
roundRect(ctx, x, y, width, height, radius) {
|
|
380
|
+
ctx.beginPath();
|
|
381
|
+
ctx.moveTo(x + radius, y);
|
|
382
|
+
ctx.lineTo(x + width - radius, y);
|
|
383
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
384
|
+
ctx.lineTo(x + width, y + height - radius);
|
|
385
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
386
|
+
ctx.lineTo(x + radius, y + height);
|
|
387
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
388
|
+
ctx.lineTo(x, y + radius);
|
|
389
|
+
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Vérifie si un point est dans le composant
|
|
394
|
+
*/
|
|
395
|
+
isPointInside(x, y) {
|
|
396
|
+
const adjustedY = y - this.framework.scrollOffset;
|
|
397
|
+
return x >= this.x && x <= this.x + this.width &&
|
|
398
|
+
adjustedY >= this.y && adjustedY <= this.y + this.height;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Gère l'input texte (pour searchbar)
|
|
403
|
+
* @param {string} char - Caractère à ajouter
|
|
404
|
+
*/
|
|
405
|
+
addChar(char) {
|
|
406
|
+
if (this.morphType === 'searchbar' && this.isMorphed) {
|
|
407
|
+
this.searchText += char;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Efface un caractère
|
|
413
|
+
*/
|
|
414
|
+
backspace() {
|
|
415
|
+
if (this.morphType === 'searchbar' && this.isMorphed) {
|
|
416
|
+
this.searchText = this.searchText.slice(0, -1);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Réinitialise la recherche
|
|
422
|
+
*/
|
|
423
|
+
clearSearch() {
|
|
424
|
+
this.searchText = '';
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export default MorphingFAB;
|