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.
- package/components/AppBar.js +107 -249
- package/components/BottomNavigationBar.js +143 -378
- package/components/Cards.js +232 -0
- package/components/FileUpload.js +225 -248
- package/core/CanvasFramework.js +4 -3
- package/core/UIBuilder.js +2 -2
- package/index.js +1 -1
- package/package.json +1 -1
- package/components/Card.js +0 -534
|
@@ -1,433 +1,198 @@
|
|
|
1
1
|
import Component from '../core/Component.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* FileUpload Material 3 & Cupertino avec ripple
|
|
5
5
|
* @class
|
|
6
6
|
* @extends Component
|
|
7
7
|
*/
|
|
8
|
-
class
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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 || '#
|
|
40
|
-
this.
|
|
41
|
-
this.
|
|
42
|
-
this.
|
|
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
|
-
|
|
45
|
-
this.
|
|
46
|
-
this.
|
|
47
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
40
|
+
// Input HTML caché
|
|
41
|
+
this.createFileInput();
|
|
42
|
+
}
|
|
77
43
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
52
|
+
this.fileInput.addEventListener('change', (e) => {
|
|
53
|
+
this.handleFiles(Array.from(e.target.files));
|
|
54
|
+
});
|
|
55
|
+
}
|
|
106
56
|
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
117
|
-
|
|
60
|
+
for (let file of fileList) {
|
|
61
|
+
validFiles.push(file);
|
|
118
62
|
}
|
|
119
|
-
}
|
|
120
63
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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.
|
|
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.
|
|
210
|
-
ctx.
|
|
211
|
-
|
|
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.
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
ctx.
|
|
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.
|
|
269
|
-
ctx.
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
ctx.
|
|
360
|
-
ctx.lineTo(x +
|
|
361
|
-
ctx.quadraticCurveTo(x +
|
|
362
|
-
ctx.lineTo(x
|
|
363
|
-
ctx.quadraticCurveTo(x
|
|
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
|
|
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
|
-
|
|
379
|
-
|
|
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
|
|
198
|
+
export default FileUpload;
|