canvasframework 0.5.40 → 0.5.42

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.
@@ -1,433 +1,198 @@
1
1
  import Component from '../core/Component.js';
2
2
 
3
3
  /**
4
- * Barre de navigation inférieure (Material & Cupertino)
4
+ * FileUpload Material 3 & Cupertino avec ripple
5
5
  * @class
6
6
  * @extends Component
7
7
  */
8
- class BottomNavigationBar extends Component {
9
- /**
10
- * Crée une instance de BottomNavigationBar
11
- * @param {CanvasFramework} framework - Framework parent
12
- * @param {Object} [options={}] - Options de configuration
13
- * @param {Array} [options.items=[]] - Items [{icon, label}]
14
- * @param {number} [options.selectedIndex=0] - Index sélectionné
15
- * @param {Function} [options.onChange] - Callback au changement
16
- * @param {number} [options.height] - Hauteur
17
- * @param {string} [options.bgColor] - Couleur de fond
18
- * @param {string} [options.selectedColor] - Couleur sélectionnée
19
- * @param {string} [options.unselectedColor] - Couleur non sélectionnée
20
- */
8
+ class FileUpload extends Component {
21
9
  constructor(framework, options = {}) {
22
- const height = options.height || (framework.platform === 'material' ? 56 : 50);
23
-
24
- super(framework, {
25
- x: 0,
26
- y: framework.height - height,
27
- width: framework.width,
28
- height: height,
29
- ...options
30
- });
31
-
32
- this.items = options.items || [];
33
- this.selectedIndex = options.selectedIndex || 0;
34
- this.onChange = options.onChange;
10
+ super(framework, options);
11
+
12
+ this.label = options.label || 'Select files';
13
+ this.accept = options.accept || '*';
14
+ this.multiple = options.multiple !== false;
15
+ this.files = [];
16
+ this.isPressed = false;
17
+
35
18
  this.platform = framework.platform;
36
-
37
- // Couleurs selon la plateforme
19
+ this.width = options.width || 300;
20
+ this.height = options.height || (this.platform === 'material' ? 80 : 90);
21
+
22
+ // Couleurs selon plateforme
38
23
  if (this.platform === 'material') {
39
- this.bgColor = options.bgColor || '#FFFFFF';
40
- this.selectedColor = options.selectedColor || '#6200EE';
41
- this.unselectedColor = options.unselectedColor || '#757575';
42
- this.rippleColor = 'rgba(98, 0, 238, 0.2)';
24
+ this.bgColor = options.bgColor || '#F3E8FF'; // M3 surface variant
25
+ this.borderColor = options.borderColor || '#BB86FC';
26
+ this.iconColor = options.iconColor || '#6200EE';
27
+ this.borderRadius = 12;
28
+ this.ripples = [];
29
+ this.animationFrame = null;
43
30
  } else {
44
- // iOS : background transparent avec blur
45
- this.bgColor = options.bgColor || 'rgba(248, 248, 248, 0.95)';
46
- this.selectedColor = options.selectedColor || '#007AFF';
47
- this.unselectedColor = options.unselectedColor || '#8E8E93';
31
+ this.bgColor = options.bgColor || 'rgba(248,248,248,0.95)';
32
+ this.borderColor = options.borderColor || '#007AFF';
33
+ this.iconColor = options.iconColor || '#007AFF';
34
+ this.borderRadius = 16;
48
35
  }
49
-
50
- // Ripple effect (Material)
51
- this.ripples = [];
52
- this.animationFrame = null;
53
- this.lastAnimationTime = 0;
54
-
55
- // Animation de l'indicateur (iOS)
56
- this.indicatorX = 0;
57
- this.targetIndicatorX = 0;
58
- this.animatingIndicator = false;
59
-
60
- this.onPress = this.handlePress.bind(this);
61
-
62
- // Initialiser la position de l'indicateur
63
- this.updateIndicatorPosition();
64
- }
65
36
 
66
- /**
67
- * Démarrer l'animation des ripples
68
- * @private
69
- */
70
- startRippleAnimation() {
71
- const animate = (timestamp) => {
72
- if (!this.lastAnimationTime) this.lastAnimationTime = timestamp;
73
- const deltaTime = timestamp - this.lastAnimationTime;
74
- this.lastAnimationTime = timestamp;
37
+ this.onFilesSelected = options.onFilesSelected || null;
38
+ this.onError = options.onError || null;
75
39
 
76
- let needsUpdate = false;
40
+ // Input HTML caché
41
+ this.createFileInput();
42
+ }
77
43
 
78
- // Mettre à jour chaque ripple
79
- for (let i = this.ripples.length - 1; i >= 0; i--) {
80
- const ripple = this.ripples[i];
81
-
82
- // Animer le rayon (expansion)
83
- if (ripple.radius < ripple.maxRadius) {
84
- ripple.radius += (ripple.maxRadius / 300) * deltaTime;
85
- needsUpdate = true;
86
- }
87
-
88
- // Animer l'opacité (fade out)
89
- if (ripple.radius >= ripple.maxRadius * 0.4) {
90
- ripple.opacity -= (0.003 * deltaTime);
91
- if (ripple.opacity < 0) ripple.opacity = 0;
92
- needsUpdate = true;
93
- }
94
-
95
- // Supprimer les ripples terminés
96
- if (ripple.opacity <= 0 && ripple.radius >= ripple.maxRadius * 0.95) {
97
- this.ripples.splice(i, 1);
98
- needsUpdate = true;
99
- }
100
- }
44
+ createFileInput() {
45
+ this.fileInput = document.createElement('input');
46
+ this.fileInput.type = 'file';
47
+ this.fileInput.accept = this.accept;
48
+ this.fileInput.multiple = this.multiple;
49
+ this.fileInput.style.display = 'none';
50
+ document.body.appendChild(this.fileInput);
101
51
 
102
- // Redessiner si nécessaire
103
- if (needsUpdate) {
104
- this.requestRender();
105
- }
52
+ this.fileInput.addEventListener('change', (e) => {
53
+ this.handleFiles(Array.from(e.target.files));
54
+ });
55
+ }
106
56
 
107
- // Continuer l'animation
108
- if (this.ripples.length > 0) {
109
- this.animationFrame = requestAnimationFrame(animate);
110
- } else {
111
- this.animationFrame = null;
112
- this.lastAnimationTime = 0;
113
- }
114
- };
57
+ handleFiles(fileList) {
58
+ const validFiles = [];
115
59
 
116
- if (this.ripples.length > 0 && !this.animationFrame) {
117
- this.animationFrame = requestAnimationFrame(animate);
60
+ for (let file of fileList) {
61
+ validFiles.push(file);
118
62
  }
119
- }
120
63
 
121
- /**
122
- * Demander un redessin
123
- * @private
124
- */
125
- requestRender() {
126
- if (this.framework && this.framework.requestRender) {
127
- this.framework.requestRender();
64
+ if (validFiles.length > 0) {
65
+ this.files = validFiles;
66
+ if (this.onFilesSelected) this.onFilesSelected(validFiles);
128
67
  }
129
- }
130
68
 
131
- /**
132
- * Nettoyer l'animation lors de la destruction
133
- */
134
- destroy() {
135
- if (this.animationFrame) {
136
- cancelAnimationFrame(this.animationFrame);
137
- this.animationFrame = null;
138
- }
139
- super.destroy();
69
+ this.fileInput.value = '';
70
+ this.requestRender();
140
71
  }
141
72
 
142
- /**
143
- * Met à jour la position de l'indicateur iOS
144
- * @private
145
- */
146
- updateIndicatorPosition() {
147
- const itemWidth = this.width / this.items.length;
148
- this.targetIndicatorX = this.selectedIndex * itemWidth;
149
-
150
- if (!this.animatingIndicator) {
151
- this.indicatorX = this.targetIndicatorX;
152
- }
153
- }
73
+ // Ripple effect similaire au BottomNavigationBar
74
+ startRipple(x, y) {
75
+ if (this.platform !== 'material') return;
76
+
77
+ const maxRadius = Math.max(this.width, this.height) * 0.8;
78
+ this.ripples.push({ x, y, radius: 0, maxRadius, opacity: 0.3 });
154
79
 
155
- /**
156
- * Anime l'indicateur iOS
157
- * @private
158
- */
159
- animateIndicator() {
160
- this.animatingIndicator = true;
161
- const startTime = performance.now();
162
- const duration = 300; // 300ms d'animation
163
- const startX = this.indicatorX;
164
- const endX = this.targetIndicatorX;
165
-
166
- const animate = (currentTime) => {
167
- const elapsed = currentTime - startTime;
168
- const progress = Math.min(elapsed / duration, 1);
169
-
170
- // Easing function (easeOutCubic)
171
- const easeProgress = 1 - Math.pow(1 - progress, 3);
172
- this.indicatorX = startX + (endX - startX) * easeProgress;
173
-
174
- if (progress < 1) {
175
- requestAnimationFrame(animate);
176
- this.requestRender();
177
- } else {
178
- this.indicatorX = endX;
179
- this.animatingIndicator = false;
180
- this.requestRender();
80
+ const animate = (timestamp) => {
81
+ let needsUpdate = false;
82
+ for (let i = this.ripples.length - 1; i >= 0; i--) {
83
+ const ripple = this.ripples[i];
84
+ ripple.radius += maxRadius / 15;
85
+ ripple.opacity -= 0.02;
86
+ if (ripple.opacity <= 0) this.ripples.splice(i, 1);
87
+ else needsUpdate = true;
181
88
  }
89
+
90
+ this.requestRender();
91
+
92
+ if (needsUpdate) requestAnimationFrame(animate);
182
93
  };
183
94
 
184
95
  requestAnimationFrame(animate);
185
96
  }
186
97
 
187
- /**
188
- * Dessine la barre de navigation
189
- */
98
+ onClick(globalX, globalY) {
99
+ const localX = globalX - this.x;
100
+ const localY = globalY - this.y;
101
+
102
+ if (this.isPointInside(globalX, globalY)) {
103
+ if (this.platform === 'material') this.startRipple(localX, localY);
104
+ this.fileInput.click();
105
+ }
106
+ }
107
+
190
108
  draw(ctx) {
191
109
  ctx.save();
192
-
110
+
193
111
  // Background
194
112
  ctx.fillStyle = this.bgColor;
195
- ctx.fillRect(this.x, this.y, this.width, this.height);
196
-
197
- // Bordure/Ombre supérieure
113
+ this.roundRect(ctx, this.x, this.y, this.width, this.height, this.borderRadius);
114
+ ctx.fill();
115
+
116
+ // Border
117
+ ctx.strokeStyle = this.borderColor;
118
+ ctx.lineWidth = 2;
119
+ this.roundRect(ctx, this.x, this.y, this.width, this.height, this.borderRadius);
120
+ ctx.stroke();
121
+
122
+ // Ripple (Material)
198
123
  if (this.platform === 'material') {
199
- ctx.shadowColor = 'rgba(0, 0, 0, 0.1)';
200
- ctx.shadowBlur = 8;
201
- ctx.shadowOffsetY = -2;
202
- ctx.fillRect(this.x, this.y, this.width, 1);
203
- ctx.shadowColor = 'transparent';
204
- } else {
205
- // iOS : fine ligne de séparation
206
- ctx.strokeStyle = 'rgba(0, 0, 0, 0.1)';
207
- ctx.lineWidth = 0.5;
124
+ ctx.save();
208
125
  ctx.beginPath();
209
- ctx.moveTo(this.x, this.y);
210
- ctx.lineTo(this.x + this.width, this.y);
211
- ctx.stroke();
212
- }
213
-
214
- // Items
215
- const itemWidth = this.width / this.items.length;
216
-
217
- for (let i = 0; i < this.items.length; i++) {
218
- const item = this.items[i];
219
- const itemX = this.x + i * itemWidth;
220
- const isSelected = i === this.selectedIndex;
221
- const color = isSelected ? this.selectedColor : this.unselectedColor;
222
-
223
- // iOS : Indicateur de sélection (fond arrondi)
224
- if (this.platform === 'cupertino' && isSelected) {
225
- ctx.fillStyle = `${this.selectedColor}15`;
226
- const indicatorWidth = 60;
227
- const indicatorHeight = 32;
228
- const indicatorX = itemX + itemWidth / 2 - indicatorWidth / 2;
229
- const indicatorY = this.y + 6;
230
-
126
+ ctx.rect(this.x, this.y, this.width, this.height);
127
+ ctx.clip();
128
+ for (let ripple of this.ripples) {
129
+ ctx.globalAlpha = ripple.opacity;
130
+ ctx.fillStyle = '#6200EE';
231
131
  ctx.beginPath();
232
- this.roundRect(ctx, indicatorX, indicatorY, indicatorWidth, indicatorHeight, 16);
132
+ ctx.arc(this.x + ripple.x, this.y + ripple.y, ripple.radius, 0, Math.PI * 2);
233
133
  ctx.fill();
234
134
  }
235
-
236
- // Icône
237
- const iconY = this.platform === 'material' ? this.y + 12 : this.y + 8;
238
- this.drawIcon(ctx, item.icon, itemX + itemWidth / 2, iconY, color, isSelected);
239
-
240
- // Label
241
- ctx.fillStyle = color;
242
- const fontSize = this.platform === 'material' ? 12 : 10;
243
- ctx.font = `${isSelected && this.platform === 'material' ? 'bold ' : ''}${fontSize}px -apple-system, Roboto, sans-serif`;
244
- ctx.textAlign = 'center';
245
- ctx.textBaseline = 'top';
246
- const labelY = this.platform === 'material' ? this.y + 34 : this.y + 30;
247
- ctx.fillText(item.label, itemX + itemWidth / 2, labelY);
248
- }
249
-
250
- // Ripples (Material) - DESSINER APRÈS LES ÉLÉMENTS
251
- if (this.platform === 'material') {
252
- this.drawRipples(ctx);
135
+ ctx.restore();
253
136
  }
254
-
255
- ctx.restore();
256
- }
257
137
 
258
- /**
259
- * Dessine les ripples (Material)
260
- * @private
261
- */
262
- drawRipples(ctx) {
263
- // Sauvegarder le contexte
264
- ctx.save();
265
-
266
- // Créer un masque de clipping pour limiter les ripples à la barre
138
+ // Icon simple
139
+ const iconSize = 32;
140
+ const iconX = this.x + this.width / 2 - iconSize / 2;
141
+ const iconY = this.y + 10;
142
+
143
+ ctx.strokeStyle = this.iconColor;
144
+ ctx.lineWidth = 3;
267
145
  ctx.beginPath();
268
- ctx.rect(this.x, this.y, this.width, this.height);
269
- ctx.clip();
270
-
271
- for (let ripple of this.ripples) {
272
- ctx.globalAlpha = ripple.opacity;
273
- ctx.fillStyle = this.rippleColor;
274
- ctx.beginPath();
275
- ctx.arc(ripple.x, ripple.y, ripple.radius, 0, Math.PI * 2);
276
- ctx.fill();
146
+ ctx.moveTo(iconX + iconSize / 2, iconY);
147
+ ctx.lineTo(iconX + iconSize / 2, iconY + iconSize);
148
+ ctx.moveTo(iconX, iconY + iconSize / 3);
149
+ ctx.lineTo(iconX + iconSize, iconY + iconSize / 3);
150
+ ctx.stroke();
151
+
152
+ // Label
153
+ ctx.fillStyle = '#000';
154
+ ctx.font = '16px -apple-system, Roboto, sans-serif';
155
+ ctx.textAlign = 'center';
156
+ ctx.textBaseline = 'top';
157
+ ctx.fillText(this.label, this.x + this.width / 2, iconY + iconSize + 10);
158
+
159
+ // Fichiers sélectionnés
160
+ if (this.files.length > 0) {
161
+ ctx.fillStyle = this.borderColor;
162
+ ctx.font = '12px -apple-system, Roboto, sans-serif';
163
+ const fileText = this.files.length === 1 ? this.files[0].name : `${this.files.length} files selected`;
164
+ ctx.fillText(fileText, this.x + this.width / 2, this.y + this.height - 20);
277
165
  }
278
-
279
- // Restaurer le contexte
280
- ctx.restore();
281
- }
282
166
 
283
- /**
284
- * Dessine une icône
285
- * @private
286
- */
287
- drawIcon(ctx, icon, x, y, color, isSelected) {
288
- ctx.strokeStyle = color;
289
- ctx.fillStyle = color;
290
- ctx.lineWidth = isSelected ? 2.5 : 2;
291
- ctx.lineCap = 'round';
292
- ctx.lineJoin = 'round';
293
-
294
- switch(icon) {
295
- case 'home':
296
- ctx.beginPath();
297
- ctx.moveTo(x, y + 2);
298
- ctx.lineTo(x - 10, y + 10);
299
- ctx.lineTo(x - 10, y + 18);
300
- ctx.lineTo(x + 10, y + 18);
301
- ctx.lineTo(x + 10, y + 10);
302
- ctx.closePath();
303
- if (isSelected) ctx.fill();
304
- else ctx.stroke();
305
- break;
306
-
307
- case 'search':
308
- ctx.beginPath();
309
- ctx.arc(x - 2, y + 6, 7, 0, Math.PI * 2);
310
- ctx.stroke();
311
- ctx.beginPath();
312
- ctx.moveTo(x + 4, y + 11);
313
- ctx.lineTo(x + 9, y + 16);
314
- ctx.stroke();
315
- break;
316
-
317
- case 'favorite':
318
- ctx.beginPath();
319
- ctx.moveTo(x, y + 3);
320
- for (let i = 0; i < 5; i++) {
321
- const angle = (i * 4 * Math.PI / 5) - Math.PI / 2;
322
- const radius = i % 2 === 0 ? 9 : 4;
323
- ctx.lineTo(x + Math.cos(angle) * radius, y + 10 + Math.sin(angle) * radius);
324
- }
325
- ctx.closePath();
326
- if (isSelected) ctx.fill();
327
- else ctx.stroke();
328
- break;
329
-
330
- case 'person':
331
- ctx.beginPath();
332
- ctx.arc(x, y + 6, 5, 0, Math.PI * 2);
333
- ctx.stroke();
334
- ctx.beginPath();
335
- ctx.arc(x, y + 20, 9, Math.PI, 0, true);
336
- ctx.stroke();
337
- break;
338
-
339
- case 'settings':
340
- ctx.beginPath();
341
- ctx.arc(x, y + 10, 5, 0, Math.PI * 2);
342
- ctx.stroke();
343
- for (let i = 0; i < 4; i++) {
344
- const angle = (i * Math.PI / 2) - Math.PI / 4;
345
- ctx.beginPath();
346
- ctx.moveTo(x + Math.cos(angle) * 7, y + 10 + Math.sin(angle) * 7);
347
- ctx.lineTo(x + Math.cos(angle) * 11, y + 10 + Math.sin(angle) * 11);
348
- ctx.stroke();
349
- }
350
- break;
351
- }
167
+ ctx.restore();
352
168
  }
353
169
 
354
- /**
355
- * Dessine un rectangle arrondi
356
- * @private
357
- */
358
- roundRect(ctx, x, y, width, height, radius) {
359
- ctx.moveTo(x + radius, y);
360
- ctx.lineTo(x + width - radius, y);
361
- ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
362
- ctx.lineTo(x + width, y + height - radius);
363
- ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
364
- ctx.lineTo(x + radius, y + height);
365
- ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
366
- ctx.lineTo(x, y + radius);
367
- ctx.quadraticCurveTo(x, y, x + radius, y);
170
+ roundRect(ctx, x, y, w, h, r) {
171
+ ctx.moveTo(x + r, y);
172
+ ctx.lineTo(x + w - r, y);
173
+ ctx.quadraticCurveTo(x + w, y, x + w, y + r);
174
+ ctx.lineTo(x + w, y + h - r);
175
+ ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
176
+ ctx.lineTo(x + r, y + h);
177
+ ctx.quadraticCurveTo(x, y + h, x, y + h - r);
178
+ ctx.lineTo(x, y + r);
179
+ ctx.quadraticCurveTo(x, y, x + r, y);
368
180
  }
369
181
 
370
- /**
371
- * Vérifie si un point est dans les limites
372
- */
373
182
  isPointInside(x, y) {
374
- return y >= this.y && y <= this.y + this.height;
183
+ return x >= this.x && x <= this.x + this.width &&
184
+ y >= this.y && y <= this.y + this.height;
375
185
  }
376
186
 
377
- /**
378
- * Gère la pression (clic)
379
- * @private
380
- */
381
- handlePress(x, y) {
382
- // Convertir les coordonnées absolues en coordonnées relatives à la barre
383
- const relativeX = x - this.x;
384
- const relativeY = y - this.y;
385
-
386
- // Vérifier si on est dans la barre
387
- if (relativeY >= 0 && relativeY <= this.height) {
388
- const itemWidth = this.width / this.items.length;
389
- const index = Math.floor(relativeX / itemWidth);
390
-
391
- if (index >= 0 && index < this.items.length && index !== this.selectedIndex) {
392
- // Ripple effect (Material)
393
- if (this.platform === 'material') {
394
- // Calculer la taille maximale du ripple (ne pas dépasser la hauteur de la barre)
395
- const maxRippleRadius = Math.min(itemWidth * 0.6, this.height * 0.8);
396
-
397
- this.ripples.push({
398
- x: this.x + (index + 0.5) * itemWidth, // Coordonnée absolue
399
- y: this.y + this.height / 2, // Coordonnée absolue
400
- radius: 0,
401
- maxRadius: maxRippleRadius,
402
- opacity: 1,
403
- createdAt: performance.now()
404
- });
405
-
406
- // Démarrer l'animation si elle n'est pas en cours
407
- if (!this.animationFrame) {
408
- this.startRippleAnimation();
409
- }
410
-
411
- // Forcer un redessin
412
- this.requestRender();
413
- }
414
-
415
- this.selectedIndex = index;
416
- this.updateIndicatorPosition();
417
-
418
- // Animer l'indicateur (iOS)
419
- if (this.platform === 'cupertino') {
420
- this.animateIndicator();
421
- }
422
-
423
- if (this.onChange) {
424
- this.onChange(index, this.items[index]);
425
- }
426
-
427
- this.requestRender();
428
- }
187
+ destroy() {
188
+ if (this.fileInput && this.fileInput.parentNode) {
189
+ this.fileInput.parentNode.removeChild(this.fileInput);
429
190
  }
430
191
  }
192
+
193
+ requestRender() {
194
+ if (this.framework && this.framework.requestRender) this.framework.requestRender();
195
+ }
431
196
  }
432
197
 
433
- export default BottomNavigationBar;
198
+ export default FileUpload;