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,397 @@
|
|
|
1
|
+
import FAB from './FAB.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Speed Dial FAB - FAB qui ouvre un menu d'actions
|
|
5
|
+
* @class
|
|
6
|
+
* @extends FAB
|
|
7
|
+
* @property {Array} actions - Liste des actions du menu
|
|
8
|
+
* @property {boolean} isOpen - État ouvert/fermé
|
|
9
|
+
* @property {number} animProgress - Progression de l'animation (0-1)
|
|
10
|
+
*/
|
|
11
|
+
class SpeedDialFAB extends FAB {
|
|
12
|
+
/**
|
|
13
|
+
* Crée une instance de SpeedDialFAB
|
|
14
|
+
* @param {CanvasFramework} framework - Framework parent
|
|
15
|
+
* @param {Object} [options={}] - Options de configuration
|
|
16
|
+
* @param {Array} [options.actions=[]] - Actions du menu
|
|
17
|
+
* @example
|
|
18
|
+
* actions: [
|
|
19
|
+
* { icon: '✉', label: 'Email', bgColor: '#4CAF50', action: () => {...} },
|
|
20
|
+
* { icon: '📞', label: 'Call', bgColor: '#2196F3', action: () => {...} },
|
|
21
|
+
* { icon: '📍', label: Map', bgColor: '#FF9800', action: () => {...} }
|
|
22
|
+
* ]
|
|
23
|
+
*/
|
|
24
|
+
constructor(framework, options = {}) {
|
|
25
|
+
super(framework, {
|
|
26
|
+
...options,
|
|
27
|
+
icon: options.icon || '+'
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
this.actions = options.actions || [];
|
|
31
|
+
this.isOpen = false;
|
|
32
|
+
this.animProgress = 0;
|
|
33
|
+
this.actionSpacing = 72; // Espacement entre les mini FABs
|
|
34
|
+
|
|
35
|
+
// AJOUT: Flags pour gérer l'interaction
|
|
36
|
+
this.justClicked = false; // Pour éviter la fermeture immédiate
|
|
37
|
+
this.clickStartTime = 0;
|
|
38
|
+
this.clickStartY = 0;
|
|
39
|
+
this.isScrolling = false;
|
|
40
|
+
this.scrollThreshold = 5; // Seuil de mouvement pour détecter un scroll
|
|
41
|
+
|
|
42
|
+
// Initialiser les mini FABs
|
|
43
|
+
this.miniFabs = this.actions.map((action, index) => ({
|
|
44
|
+
...action,
|
|
45
|
+
size: 48,
|
|
46
|
+
x: this.x,
|
|
47
|
+
y: this.y,
|
|
48
|
+
targetY: this.y - (index + 1) * this.actionSpacing,
|
|
49
|
+
currentY: this.y,
|
|
50
|
+
alpha: 0,
|
|
51
|
+
pressed: false
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
// Overlay pour fermer le menu
|
|
55
|
+
this.showOverlay = false;
|
|
56
|
+
|
|
57
|
+
// Bind methods
|
|
58
|
+
this.onPress = this.handlePress.bind(this);
|
|
59
|
+
this.onMove = this.handleMove.bind(this);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Gère le début de la pression
|
|
64
|
+
* @param {number} x - Coordonnée X
|
|
65
|
+
* @param {number} y - Coordonnée Y
|
|
66
|
+
*/
|
|
67
|
+
handlePress(x, y) {
|
|
68
|
+
this.justClicked = true;
|
|
69
|
+
this.clickStartTime = Date.now();
|
|
70
|
+
this.clickStartY = y;
|
|
71
|
+
this.isScrolling = false;
|
|
72
|
+
|
|
73
|
+
const adjustedY = y - this.framework.scrollOffset;
|
|
74
|
+
|
|
75
|
+
// 1. Vérifier si c'est le FAB principal
|
|
76
|
+
const isMainFabClick = x >= this.x && x <= this.x + this.width &&
|
|
77
|
+
adjustedY >= this.y && adjustedY <= this.y + this.height;
|
|
78
|
+
|
|
79
|
+
if (isMainFabClick) {
|
|
80
|
+
// Le FAB principal peut toujours être cliqué (ouvrir/fermer)
|
|
81
|
+
this.toggle();
|
|
82
|
+
super.handlePress(x, y);
|
|
83
|
+
|
|
84
|
+
// Empêcher la fermeture immédiate
|
|
85
|
+
setTimeout(() => {
|
|
86
|
+
this.justClicked = false;
|
|
87
|
+
}, 300);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 2. Si le menu est ouvert, vérifier les mini FABs
|
|
92
|
+
if (this.isOpen) {
|
|
93
|
+
for (let i = 0; i < this.miniFabs.length; i++) {
|
|
94
|
+
const fab = this.miniFabs[i];
|
|
95
|
+
if (fab.alpha < 0.5) continue; // Ignorer les FABs pas encore visibles
|
|
96
|
+
|
|
97
|
+
const fabX = this.x + (this.width - fab.size) / 2;
|
|
98
|
+
const fabY = fab.currentY;
|
|
99
|
+
|
|
100
|
+
const distance = Math.sqrt(
|
|
101
|
+
Math.pow(x - (fabX + fab.size / 2), 2) +
|
|
102
|
+
Math.pow(adjustedY - (fabY + fab.size / 2), 2)
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
if (distance <= fab.size / 2) {
|
|
106
|
+
// Action cliquée
|
|
107
|
+
fab.pressed = true;
|
|
108
|
+
setTimeout(() => {
|
|
109
|
+
fab.pressed = false;
|
|
110
|
+
if (fab.action) fab.action();
|
|
111
|
+
this.close();
|
|
112
|
+
}, 150);
|
|
113
|
+
|
|
114
|
+
// Empêcher la fermeture par handleClickOutside
|
|
115
|
+
this.justClicked = true;
|
|
116
|
+
setTimeout(() => {
|
|
117
|
+
this.justClicked = false;
|
|
118
|
+
}, 300);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 3. Clic sur overlay (n'importe où ailleurs)
|
|
124
|
+
// Ne pas fermer immédiatement, attendre la fin du mouvement
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Pas de return ici, la fermeture sera gérée par handleClickOutside
|
|
128
|
+
// avec vérification du mouvement
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Gère le mouvement
|
|
133
|
+
* @param {number} x - Coordonnée X
|
|
134
|
+
* @param {number} y - Coordonnée Y
|
|
135
|
+
*/
|
|
136
|
+
handleMove(x, y) {
|
|
137
|
+
// Détecter si c'est un scroll
|
|
138
|
+
const deltaY = Math.abs(y - this.clickStartY);
|
|
139
|
+
if (deltaY > this.scrollThreshold) {
|
|
140
|
+
this.isScrolling = true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Ouvre le menu
|
|
146
|
+
*/
|
|
147
|
+
open() {
|
|
148
|
+
if (this.isOpen) return;
|
|
149
|
+
this.isOpen = true;
|
|
150
|
+
this.showOverlay = true;
|
|
151
|
+
this.animate();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Ferme le menu
|
|
156
|
+
*/
|
|
157
|
+
close() {
|
|
158
|
+
if (!this.isOpen) return;
|
|
159
|
+
this.isOpen = false;
|
|
160
|
+
this.animate();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Toggle l'état ouvert/fermé
|
|
165
|
+
*/
|
|
166
|
+
toggle() {
|
|
167
|
+
this.isOpen ? this.close() : this.open();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Anime l'ouverture/fermeture
|
|
172
|
+
* @private
|
|
173
|
+
*/
|
|
174
|
+
animate() {
|
|
175
|
+
const startTime = Date.now();
|
|
176
|
+
const duration = 300;
|
|
177
|
+
|
|
178
|
+
const step = () => {
|
|
179
|
+
const elapsed = Date.now() - startTime;
|
|
180
|
+
const progress = Math.min(elapsed / duration, 1);
|
|
181
|
+
|
|
182
|
+
// Easing out cubic
|
|
183
|
+
const eased = 1 - Math.pow(1 - progress, 3);
|
|
184
|
+
|
|
185
|
+
this.animProgress = this.isOpen ? eased : 1 - eased;
|
|
186
|
+
|
|
187
|
+
// Mettre à jour les positions des mini FABs
|
|
188
|
+
this.miniFabs.forEach((fab, index) => {
|
|
189
|
+
const delay = index * 0.05;
|
|
190
|
+
const fabProgress = Math.max(0, Math.min(1, (this.animProgress - delay) / (1 - delay)));
|
|
191
|
+
|
|
192
|
+
fab.currentY = this.y - (fabProgress * (index + 1) * this.actionSpacing);
|
|
193
|
+
fab.alpha = fabProgress;
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
if (progress < 1) {
|
|
197
|
+
requestAnimationFrame(step);
|
|
198
|
+
} else {
|
|
199
|
+
if (!this.isOpen) {
|
|
200
|
+
this.showOverlay = false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
step();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Dessine le Speed Dial FAB
|
|
210
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
211
|
+
*/
|
|
212
|
+
draw(ctx) {
|
|
213
|
+
// Overlay semi-transparent avec gestion du clic
|
|
214
|
+
if (this.showOverlay) {
|
|
215
|
+
ctx.save();
|
|
216
|
+
ctx.fillStyle = `rgba(0, 0, 0, ${this.animProgress * 0.5})`;
|
|
217
|
+
ctx.fillRect(0, 0, this.framework.width, this.framework.height);
|
|
218
|
+
ctx.restore();
|
|
219
|
+
|
|
220
|
+
// Dessiner une zone invisible pour détecter les clics sur l'overlay
|
|
221
|
+
// On enregistre cette zone pour la détection
|
|
222
|
+
this.overlayActive = true;
|
|
223
|
+
} else {
|
|
224
|
+
this.overlayActive = false;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Dessiner les mini FABs (de bas en haut)
|
|
228
|
+
if (this.animProgress > 0) {
|
|
229
|
+
for (let i = this.miniFabs.length - 1; i >= 0; i--) {
|
|
230
|
+
const fab = this.miniFabs[i];
|
|
231
|
+
if (fab.alpha > 0.01) {
|
|
232
|
+
this.drawMiniFab(ctx, fab);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// FAB principal
|
|
238
|
+
ctx.save();
|
|
239
|
+
|
|
240
|
+
// Rotation de l'icône + quand ouvert
|
|
241
|
+
if (this.icon === '+') {
|
|
242
|
+
ctx.save();
|
|
243
|
+
ctx.translate(this.x + this.width / 2, this.y + this.height / 2);
|
|
244
|
+
ctx.rotate((this.animProgress * 45) * Math.PI / 180);
|
|
245
|
+
ctx.translate(-(this.x + this.width / 2), -(this.y + this.height / 2));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
super.draw(ctx);
|
|
249
|
+
|
|
250
|
+
if (this.icon === '+') {
|
|
251
|
+
ctx.restore();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
ctx.restore();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Dessine un mini FAB
|
|
259
|
+
* @private
|
|
260
|
+
*/
|
|
261
|
+
drawMiniFab(ctx, fab) {
|
|
262
|
+
ctx.save();
|
|
263
|
+
ctx.globalAlpha = fab.alpha;
|
|
264
|
+
|
|
265
|
+
const fabX = this.x + (this.width - fab.size) / 2;
|
|
266
|
+
const fabY = fab.currentY;
|
|
267
|
+
|
|
268
|
+
// Ombre
|
|
269
|
+
if (!fab.pressed) {
|
|
270
|
+
ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
|
|
271
|
+
ctx.shadowBlur = 6;
|
|
272
|
+
ctx.shadowOffsetY = 3;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Background
|
|
276
|
+
const bgColor = fab.bgColor || this.bgColor;
|
|
277
|
+
ctx.fillStyle = fab.pressed ? this.darkenColor(bgColor) : bgColor;
|
|
278
|
+
ctx.beginPath();
|
|
279
|
+
ctx.arc(fabX + fab.size / 2, fabY + fab.size / 2, fab.size / 2, 0, Math.PI * 2);
|
|
280
|
+
ctx.fill();
|
|
281
|
+
|
|
282
|
+
ctx.shadowColor = 'transparent';
|
|
283
|
+
ctx.shadowBlur = 0;
|
|
284
|
+
ctx.shadowOffsetY = 0;
|
|
285
|
+
|
|
286
|
+
// Icône
|
|
287
|
+
ctx.fillStyle = fab.iconColor || '#FFFFFF';
|
|
288
|
+
ctx.font = 'bold 20px sans-serif';
|
|
289
|
+
ctx.textAlign = 'center';
|
|
290
|
+
ctx.textBaseline = 'middle';
|
|
291
|
+
ctx.fillText(fab.icon, fabX + fab.size / 2, fabY + fab.size / 2);
|
|
292
|
+
|
|
293
|
+
// Label à gauche
|
|
294
|
+
if (fab.label && fab.alpha > 0.5) {
|
|
295
|
+
ctx.fillStyle = '#FFFFFF';
|
|
296
|
+
ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
|
|
297
|
+
ctx.shadowBlur = 4;
|
|
298
|
+
|
|
299
|
+
const labelPadding = 12;
|
|
300
|
+
const labelHeight = 32;
|
|
301
|
+
ctx.font = '14px -apple-system, sans-serif';
|
|
302
|
+
const labelWidth = ctx.measureText(fab.label).width + labelPadding * 2;
|
|
303
|
+
|
|
304
|
+
// Fond du label
|
|
305
|
+
ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
|
|
306
|
+
this.roundRect(
|
|
307
|
+
ctx,
|
|
308
|
+
fabX - labelWidth - 8,
|
|
309
|
+
fabY + fab.size / 2 - labelHeight / 2,
|
|
310
|
+
labelWidth,
|
|
311
|
+
labelHeight,
|
|
312
|
+
4
|
|
313
|
+
);
|
|
314
|
+
ctx.fill();
|
|
315
|
+
|
|
316
|
+
ctx.shadowColor = 'transparent';
|
|
317
|
+
ctx.shadowBlur = 0;
|
|
318
|
+
|
|
319
|
+
// Texte du label
|
|
320
|
+
ctx.fillStyle = '#FFFFFF';
|
|
321
|
+
ctx.textAlign = 'right';
|
|
322
|
+
ctx.fillText(fab.label, fabX - 16, fabY + fab.size / 2);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
ctx.restore();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Dessine un rectangle arrondi
|
|
330
|
+
* @private
|
|
331
|
+
*/
|
|
332
|
+
roundRect(ctx, x, y, width, height, radius) {
|
|
333
|
+
ctx.beginPath();
|
|
334
|
+
ctx.moveTo(x + radius, y);
|
|
335
|
+
ctx.lineTo(x + width - radius, y);
|
|
336
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
337
|
+
ctx.lineTo(x + width, y + height - radius);
|
|
338
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
339
|
+
ctx.lineTo(x + radius, y + height);
|
|
340
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
341
|
+
ctx.lineTo(x, y + radius);
|
|
342
|
+
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Vérifie si un point est dans le Speed Dial
|
|
347
|
+
*/
|
|
348
|
+
/**
|
|
349
|
+
* Vérifie si un point est dans le Speed Dial
|
|
350
|
+
*/
|
|
351
|
+
isPointInside(x, y) {
|
|
352
|
+
const adjustedY = y - this.framework.scrollOffset;
|
|
353
|
+
|
|
354
|
+
// Vérifier le FAB principal
|
|
355
|
+
if (x >= this.x && x <= this.x + this.width &&
|
|
356
|
+
adjustedY >= this.y && adjustedY <= this.y + this.height) {
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Si ouvert, vérifier les mini FABs
|
|
361
|
+
if (this.isOpen) {
|
|
362
|
+
for (let fab of this.miniFabs) {
|
|
363
|
+
if (fab.alpha < 0.01) continue;
|
|
364
|
+
|
|
365
|
+
const fabX = this.x + (this.width - fab.size) / 2;
|
|
366
|
+
const fabY = fab.currentY;
|
|
367
|
+
|
|
368
|
+
const distance = Math.sqrt(
|
|
369
|
+
Math.pow(x - (fabX + fab.size / 2), 2) +
|
|
370
|
+
Math.pow(adjustedY - (fabY + fab.size / 2), 2)
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
if (distance <= fab.size / 2) {
|
|
374
|
+
return true;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// LA CLÉ : Quand le menu est ouvert, TOUT L'ÉCRAN compte comme "inside"
|
|
379
|
+
// Cela empêche le framework d'appeler handleClickOutside()
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Gère le clic en dehors (appelé par le framework)
|
|
388
|
+
*/
|
|
389
|
+
handleClickOutside() {
|
|
390
|
+
// Ne pas fermer automatiquement
|
|
391
|
+
// La fermeture se fera par handleClick() seulement
|
|
392
|
+
// si on a vraiment cliqué en dehors
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
}
|
|
397
|
+
export default SpeedDialFAB;
|