canvasframework 0.3.15 → 0.3.17
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/Accordion.js +135 -122
- package/components/BottomSheet.js +104 -244
- package/components/Button.js +14 -0
- package/components/Checkbox.js +135 -149
- package/components/DatePicker.js +7 -0
- package/components/Dialog.js +252 -282
- package/components/IOSDatePickerWheel.js +257 -95
- package/components/ImageCarousel.js +100 -74
- package/components/InputTags.js +50 -12
- package/components/List.js +36 -35
- package/components/SegmentedControl.js +240 -85
- package/components/SwipeableListItem.js +249 -81
- package/components/Tabs.js +183 -53
- package/components/TextField.js +109 -289
- package/core/CanvasFramework.js +2 -7
- package/index.js +1 -1
- package/package.json +1 -1
|
@@ -4,13 +4,6 @@ import Component from '../core/Component.js';
|
|
|
4
4
|
* Élément de liste avec support swipe et actions
|
|
5
5
|
* @class
|
|
6
6
|
* @extends Component
|
|
7
|
-
* @property {string} title - Titre
|
|
8
|
-
* @property {string} subtitle - Sous-titre
|
|
9
|
-
* @property {Array<{icon?: string, text: string, color: string, onClick: Function}>} leftActions - Actions swipe gauche
|
|
10
|
-
* @property {Array<{icon?: string, text: string, color: string, onClick: Function}>} rightActions - Actions swipe droite
|
|
11
|
-
* @property {string} bgColor - Couleur de fond
|
|
12
|
-
* @property {string} platform - Plateforme ('material' ou 'cupertino')
|
|
13
|
-
* @property {number} height - Hauteur de l'item
|
|
14
7
|
*/
|
|
15
8
|
class SwipeableListItem extends Component {
|
|
16
9
|
constructor(framework, options = {}) {
|
|
@@ -29,9 +22,180 @@ class SwipeableListItem extends Component {
|
|
|
29
22
|
this.dragOffset = 0;
|
|
30
23
|
this.dragging = false;
|
|
31
24
|
this.startX = 0;
|
|
25
|
+
this.startY = 0;
|
|
26
|
+
this.hasMoved = false;
|
|
32
27
|
|
|
33
28
|
this.ripples = [];
|
|
34
|
-
this.
|
|
29
|
+
this.animationFrame = null;
|
|
30
|
+
this.lastAnimationTime = 0;
|
|
31
|
+
|
|
32
|
+
// 🔹 Binding des handlers
|
|
33
|
+
this.handleMouseDown = this.handleMouseDown.bind(this);
|
|
34
|
+
this.handleMouseMove = this.handleMouseMove.bind(this);
|
|
35
|
+
this.handleMouseUp = this.handleMouseUp.bind(this);
|
|
36
|
+
this.handleTouchStart = this.handleTouchStart.bind(this);
|
|
37
|
+
this.handleTouchMove = this.handleTouchMove.bind(this);
|
|
38
|
+
this.handleTouchEnd = this.handleTouchEnd.bind(this);
|
|
39
|
+
|
|
40
|
+
console.log('🏗️ SwipeableListItem créé', {
|
|
41
|
+
hasCanvas: !!this.framework.canvas,
|
|
42
|
+
x: this.x,
|
|
43
|
+
y: this.y,
|
|
44
|
+
width: this.width,
|
|
45
|
+
height: this.height
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// 🔹 Enregistrer les événements
|
|
49
|
+
this.setupEventListeners();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Configure les écouteurs d'événements
|
|
54
|
+
* @private
|
|
55
|
+
*/
|
|
56
|
+
setupEventListeners() {
|
|
57
|
+
if (!this.framework.canvas) {
|
|
58
|
+
console.error('❌ Pas de canvas trouvé dans le framework');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const canvas = this.framework.canvas;
|
|
63
|
+
console.log('📡 Configuration des événements sur le canvas');
|
|
64
|
+
|
|
65
|
+
// Événements souris
|
|
66
|
+
canvas.addEventListener('mousedown', this.handleMouseDown);
|
|
67
|
+
canvas.addEventListener('mousemove', this.handleMouseMove);
|
|
68
|
+
canvas.addEventListener('mouseup', this.handleMouseUp);
|
|
69
|
+
|
|
70
|
+
// Événements tactiles
|
|
71
|
+
canvas.addEventListener('touchstart', this.handleTouchStart, { passive: false });
|
|
72
|
+
canvas.addEventListener('touchmove', this.handleTouchMove, { passive: false });
|
|
73
|
+
canvas.addEventListener('touchend', this.handleTouchEnd);
|
|
74
|
+
|
|
75
|
+
console.log('✅ Événements configurés');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Nettoyer les événements lors de la destruction
|
|
80
|
+
*/
|
|
81
|
+
destroy() {
|
|
82
|
+
if (this.framework.canvas) {
|
|
83
|
+
const canvas = this.framework.canvas;
|
|
84
|
+
canvas.removeEventListener('mousedown', this.handleMouseDown);
|
|
85
|
+
canvas.removeEventListener('mousemove', this.handleMouseMove);
|
|
86
|
+
canvas.removeEventListener('mouseup', this.handleMouseUp);
|
|
87
|
+
canvas.removeEventListener('touchstart', this.handleTouchStart);
|
|
88
|
+
canvas.removeEventListener('touchmove', this.handleTouchMove);
|
|
89
|
+
canvas.removeEventListener('touchend', this.handleTouchEnd);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (this.animationFrame) {
|
|
93
|
+
cancelAnimationFrame(this.animationFrame);
|
|
94
|
+
this.animationFrame = null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (super.destroy) {
|
|
98
|
+
super.destroy();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Obtenir les coordonnées depuis un événement
|
|
104
|
+
* @private
|
|
105
|
+
*/
|
|
106
|
+
getCoordinates(event) {
|
|
107
|
+
const rect = this.framework.canvas.getBoundingClientRect();
|
|
108
|
+
if (event.touches && event.touches.length > 0) {
|
|
109
|
+
return {
|
|
110
|
+
x: event.touches[0].clientX - rect.left,
|
|
111
|
+
y: event.touches[0].clientY - rect.top
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
x: event.clientX - rect.left,
|
|
116
|
+
y: event.clientY - rect.top
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Handler mousedown
|
|
122
|
+
* @private
|
|
123
|
+
*/
|
|
124
|
+
handleMouseDown(event) {
|
|
125
|
+
const coords = this.getCoordinates(event);
|
|
126
|
+
console.log('🖱️ MouseDown', coords, 'isInside:', this.isPointInside(coords.x, coords.y));
|
|
127
|
+
this.onDragStart(coords.x, coords.y);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Handler mousemove
|
|
132
|
+
* @private
|
|
133
|
+
*/
|
|
134
|
+
handleMouseMove(event) {
|
|
135
|
+
if (!this.dragging) return;
|
|
136
|
+
const coords = this.getCoordinates(event);
|
|
137
|
+
console.log('🖱️ MouseMove', coords, 'dragging:', this.dragging);
|
|
138
|
+
this.onDragMove(coords.x, coords.y);
|
|
139
|
+
this.requestRender();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Handler mouseup
|
|
144
|
+
* @private
|
|
145
|
+
*/
|
|
146
|
+
handleMouseUp(event) {
|
|
147
|
+
if (!this.dragging) return;
|
|
148
|
+
console.log('🖱️ MouseUp');
|
|
149
|
+
this.onDragEnd();
|
|
150
|
+
this.requestRender();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Handler touchstart
|
|
155
|
+
* @private
|
|
156
|
+
*/
|
|
157
|
+
handleTouchStart(event) {
|
|
158
|
+
const coords = this.getCoordinates(event);
|
|
159
|
+
console.log('👆 TouchStart', coords, 'isInside:', this.isPointInside(coords.x, coords.y));
|
|
160
|
+
if (this.isPointInside(coords.x, coords.y)) {
|
|
161
|
+
event.preventDefault();
|
|
162
|
+
this.onDragStart(coords.x, coords.y);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Handler touchmove
|
|
168
|
+
* @private
|
|
169
|
+
*/
|
|
170
|
+
handleTouchMove(event) {
|
|
171
|
+
if (!this.dragging) return;
|
|
172
|
+
event.preventDefault();
|
|
173
|
+
const coords = this.getCoordinates(event);
|
|
174
|
+
console.log('👆 TouchMove', coords);
|
|
175
|
+
this.onDragMove(coords.x, coords.y);
|
|
176
|
+
this.requestRender();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Handler touchend
|
|
181
|
+
* @private
|
|
182
|
+
*/
|
|
183
|
+
handleTouchEnd(event) {
|
|
184
|
+
if (!this.dragging) return;
|
|
185
|
+
event.preventDefault();
|
|
186
|
+
console.log('👆 TouchEnd');
|
|
187
|
+
this.onDragEnd();
|
|
188
|
+
this.requestRender();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Demander un redessin
|
|
193
|
+
* @private
|
|
194
|
+
*/
|
|
195
|
+
requestRender() {
|
|
196
|
+
if (this.framework && this.framework.requestRender) {
|
|
197
|
+
this.framework.requestRender();
|
|
198
|
+
}
|
|
35
199
|
}
|
|
36
200
|
|
|
37
201
|
/**
|
|
@@ -40,21 +204,46 @@ class SwipeableListItem extends Component {
|
|
|
40
204
|
* @param {number} y
|
|
41
205
|
*/
|
|
42
206
|
onDragStart(x, y) {
|
|
43
|
-
|
|
207
|
+
const inside = this.isPointInside(x, y);
|
|
208
|
+
console.log('🟢 onDragStart', { x, y, inside, bounds: { x: this.x, y: this.y, w: this.width, h: this.height } });
|
|
209
|
+
|
|
210
|
+
if (inside) {
|
|
44
211
|
this.dragging = true;
|
|
45
212
|
this.startX = x;
|
|
213
|
+
this.startY = y;
|
|
214
|
+
this.hasMoved = false;
|
|
215
|
+
console.log('✅ Swipe démarré');
|
|
216
|
+
} else {
|
|
217
|
+
console.log('❌ Point en dehors des limites');
|
|
46
218
|
}
|
|
47
219
|
}
|
|
48
220
|
|
|
49
221
|
/**
|
|
50
222
|
* Déplacement pendant swipe
|
|
51
223
|
* @param {number} x
|
|
224
|
+
* @param {number} y
|
|
52
225
|
*/
|
|
53
|
-
onDragMove(x) {
|
|
226
|
+
onDragMove(x, y) {
|
|
54
227
|
if (this.dragging) {
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
228
|
+
const deltaX = x - this.startX;
|
|
229
|
+
const deltaY = Math.abs(y - this.startY);
|
|
230
|
+
|
|
231
|
+
console.log('🔄 onDragMove', { deltaX, deltaY, hasMoved: this.hasMoved });
|
|
232
|
+
|
|
233
|
+
// Swipe horizontal seulement si déplacement > 5px
|
|
234
|
+
if (Math.abs(deltaX) > 5 || this.hasMoved) {
|
|
235
|
+
this.hasMoved = true;
|
|
236
|
+
this.dragOffset = deltaX;
|
|
237
|
+
|
|
238
|
+
// Limiter le déplacement
|
|
239
|
+
const maxOffset = Math.max(
|
|
240
|
+
this.leftActions.length * 80,
|
|
241
|
+
this.rightActions.length * 80
|
|
242
|
+
);
|
|
243
|
+
this.dragOffset = Math.min(Math.max(this.dragOffset, -maxOffset), maxOffset);
|
|
244
|
+
|
|
245
|
+
console.log('↔️ Offset mis à jour:', this.dragOffset);
|
|
246
|
+
}
|
|
58
247
|
}
|
|
59
248
|
}
|
|
60
249
|
|
|
@@ -63,51 +252,24 @@ class SwipeableListItem extends Component {
|
|
|
63
252
|
*/
|
|
64
253
|
onDragEnd() {
|
|
65
254
|
if (this.dragging) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
this.
|
|
255
|
+
console.log('🔴 onDragEnd', { offset: this.dragOffset, hasMoved: this.hasMoved });
|
|
256
|
+
|
|
257
|
+
if (this.hasMoved) {
|
|
258
|
+
if (this.dragOffset > 80 && this.leftActions[0]) {
|
|
259
|
+
console.log('✅ Action gauche déclenchée');
|
|
260
|
+
this.leftActions[0].onClick?.();
|
|
261
|
+
} else if (this.dragOffset < -80 && this.rightActions[0]) {
|
|
262
|
+
console.log('✅ Action droite déclenchée');
|
|
263
|
+
this.rightActions[0].onClick?.();
|
|
264
|
+
}
|
|
70
265
|
}
|
|
266
|
+
|
|
71
267
|
this.dragOffset = 0;
|
|
72
268
|
this.dragging = false;
|
|
269
|
+
this.hasMoved = false;
|
|
73
270
|
}
|
|
74
271
|
}
|
|
75
272
|
|
|
76
|
-
/**
|
|
77
|
-
* Gestion du press pour ripple Material
|
|
78
|
-
* @param {number} x
|
|
79
|
-
* @param {number} y
|
|
80
|
-
*/
|
|
81
|
-
handlePress(x, y) {
|
|
82
|
-
if (this.platform === 'material') {
|
|
83
|
-
const adjustedY = y - this.framework.scrollOffset;
|
|
84
|
-
this.ripples.push({
|
|
85
|
-
x: x - this.x,
|
|
86
|
-
y: adjustedY - this.y,
|
|
87
|
-
radius: 0,
|
|
88
|
-
maxRadius: Math.max(this.width, this.height) * 1.5,
|
|
89
|
-
opacity: 1
|
|
90
|
-
});
|
|
91
|
-
this.animateRipple();
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
animateRipple() {
|
|
96
|
-
const animate = () => {
|
|
97
|
-
let hasActive = false;
|
|
98
|
-
for (let ripple of this.ripples) {
|
|
99
|
-
if (ripple.radius < ripple.maxRadius) {
|
|
100
|
-
ripple.radius += ripple.maxRadius / 15;
|
|
101
|
-
hasActive = true;
|
|
102
|
-
}
|
|
103
|
-
if (ripple.radius >= ripple.maxRadius * 0.5) ripple.opacity -= 0.05;
|
|
104
|
-
}
|
|
105
|
-
this.ripples = this.ripples.filter(r => r.opacity > 0);
|
|
106
|
-
if (hasActive) requestAnimationFrame(animate);
|
|
107
|
-
};
|
|
108
|
-
animate();
|
|
109
|
-
}
|
|
110
|
-
|
|
111
273
|
/**
|
|
112
274
|
* Dessine l'item
|
|
113
275
|
* @param {CanvasRenderingContext2D} ctx
|
|
@@ -115,12 +277,12 @@ class SwipeableListItem extends Component {
|
|
|
115
277
|
draw(ctx) {
|
|
116
278
|
ctx.save();
|
|
117
279
|
|
|
118
|
-
// Actions swipe
|
|
280
|
+
// Actions swipe (arrière-plan)
|
|
119
281
|
if (this.dragOffset > 0 && this.leftActions[0]) {
|
|
120
282
|
ctx.fillStyle = this.leftActions[0].color || '#388E3C';
|
|
121
283
|
ctx.fillRect(this.x, this.y, this.dragOffset, this.height);
|
|
122
284
|
ctx.fillStyle = '#FFF';
|
|
123
|
-
ctx.font = '14px sans-serif';
|
|
285
|
+
ctx.font = 'bold 14px sans-serif';
|
|
124
286
|
ctx.textAlign = 'center';
|
|
125
287
|
ctx.textBaseline = 'middle';
|
|
126
288
|
ctx.fillText(this.leftActions[0].text, this.x + this.dragOffset / 2, this.y + this.height / 2);
|
|
@@ -129,51 +291,57 @@ class SwipeableListItem extends Component {
|
|
|
129
291
|
ctx.fillStyle = this.rightActions[0].color || '#D32F2F';
|
|
130
292
|
ctx.fillRect(this.x + this.width - offset, this.y, offset, this.height);
|
|
131
293
|
ctx.fillStyle = '#FFF';
|
|
294
|
+
ctx.font = 'bold 14px sans-serif';
|
|
132
295
|
ctx.textAlign = 'center';
|
|
296
|
+
ctx.textBaseline = 'middle';
|
|
133
297
|
ctx.fillText(this.rightActions[0].text, this.x + this.width - offset / 2, this.y + this.height / 2);
|
|
134
298
|
}
|
|
135
299
|
|
|
300
|
+
// Déplacement du contenu principal
|
|
136
301
|
ctx.translate(this.dragOffset, 0);
|
|
137
302
|
|
|
138
303
|
// Background
|
|
139
304
|
ctx.fillStyle = this.bgColor;
|
|
140
305
|
ctx.fillRect(this.x, this.y, this.width, this.height);
|
|
141
306
|
|
|
142
|
-
//
|
|
143
|
-
if (this.platform === 'material') {
|
|
144
|
-
ctx.save();
|
|
145
|
-
ctx.beginPath();
|
|
146
|
-
ctx.rect(this.x, this.y, this.width, this.height);
|
|
147
|
-
ctx.clip();
|
|
148
|
-
for (let ripple of this.ripples) {
|
|
149
|
-
ctx.globalAlpha = ripple.opacity;
|
|
150
|
-
ctx.fillStyle = 'rgba(0,0,0,0.1)';
|
|
151
|
-
ctx.beginPath();
|
|
152
|
-
ctx.arc(this.x + ripple.x, this.y + ripple.y, ripple.radius, 0, Math.PI * 2);
|
|
153
|
-
ctx.fill();
|
|
154
|
-
}
|
|
155
|
-
ctx.restore();
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Texte
|
|
307
|
+
// Texte principal
|
|
159
308
|
ctx.fillStyle = '#000';
|
|
160
|
-
ctx.font = '16px sans-serif';
|
|
309
|
+
ctx.font = '16px -apple-system, Roboto, sans-serif';
|
|
161
310
|
ctx.textAlign = 'left';
|
|
162
|
-
ctx.textBaseline = 'middle';
|
|
163
|
-
|
|
311
|
+
ctx.textBaseline = this.subtitle ? 'top' : 'middle';
|
|
312
|
+
const titleY = this.subtitle ? this.y + 16 : this.y + this.height / 2;
|
|
313
|
+
ctx.fillText(this.title, this.x + 16, titleY);
|
|
314
|
+
|
|
315
|
+
// Sous-titre
|
|
164
316
|
if (this.subtitle) {
|
|
165
317
|
ctx.fillStyle = '#757575';
|
|
166
|
-
ctx.font = '14px sans-serif';
|
|
167
|
-
ctx.fillText(this.subtitle, this.x + 16, this.y +
|
|
318
|
+
ctx.font = '14px -apple-system, Roboto, sans-serif';
|
|
319
|
+
ctx.fillText(this.subtitle, this.x + 16, this.y + 38);
|
|
168
320
|
}
|
|
169
321
|
|
|
322
|
+
// Bordure inférieure
|
|
323
|
+
ctx.strokeStyle = '#E0E0E0';
|
|
324
|
+
ctx.lineWidth = 1;
|
|
325
|
+
ctx.beginPath();
|
|
326
|
+
ctx.moveTo(this.x, this.y + this.height);
|
|
327
|
+
ctx.lineTo(this.x + this.width, this.y + this.height);
|
|
328
|
+
ctx.stroke();
|
|
329
|
+
|
|
170
330
|
ctx.restore();
|
|
171
331
|
}
|
|
172
332
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
333
|
+
isPointInside(x, y) {
|
|
334
|
+
const inside = x >= this.x && x <= this.x + this.width &&
|
|
335
|
+
y >= this.y && y <= this.y + this.height;
|
|
336
|
+
|
|
337
|
+
console.log('🎯 isPointInside check', {
|
|
338
|
+
point: { x, y },
|
|
339
|
+
bounds: { x: this.x, y: this.y, width: this.width, height: this.height },
|
|
340
|
+
inside
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
return inside;
|
|
344
|
+
}
|
|
177
345
|
}
|
|
178
346
|
|
|
179
|
-
export default SwipeableListItem;
|
|
347
|
+
export default SwipeableListItem;
|
package/components/Tabs.js
CHANGED
|
@@ -1,32 +1,14 @@
|
|
|
1
1
|
import Component from '../core/Component.js';
|
|
2
|
+
|
|
2
3
|
/**
|
|
3
|
-
* Onglets de navigation
|
|
4
|
+
* Onglets de navigation avec support Material & Cupertino
|
|
4
5
|
* @class
|
|
5
6
|
* @extends Component
|
|
6
|
-
* @property {Array} tabs - Onglets [{label, icon}]
|
|
7
|
-
* @property {number} selectedIndex - Onglet sélectionné
|
|
8
|
-
* @property {Function} onChange - Callback au changement
|
|
9
|
-
* @property {string} platform - Plateforme
|
|
10
|
-
* @property {string} indicatorColor - Couleur de l'indicateur
|
|
11
|
-
* @property {string} textColor - Couleur du texte
|
|
12
|
-
* @property {string} selectedTextColor - Couleur du texte sélectionné
|
|
13
7
|
*/
|
|
14
8
|
class Tabs extends Component {
|
|
15
|
-
/**
|
|
16
|
-
* Crée une instance de Tabs
|
|
17
|
-
* @param {CanvasFramework} framework - Framework parent
|
|
18
|
-
* @param {Object} [options={}] - Options de configuration
|
|
19
|
-
* @param {Array} [options.tabs=[]] - Onglets
|
|
20
|
-
* @param {number} [options.selectedIndex=0] - Onglet sélectionné
|
|
21
|
-
* @param {Function} [options.onChange] - Callback au changement
|
|
22
|
-
* @param {number} [options.height=48] - Hauteur
|
|
23
|
-
* @param {string} [options.indicatorColor] - Couleur indicateur (auto selon platform)
|
|
24
|
-
* @param {string} [options.textColor='#000000'] - Couleur texte
|
|
25
|
-
* @param {string} [options.selectedTextColor] - Couleur texte sélectionné (auto selon indicatorColor)
|
|
26
|
-
*/
|
|
27
9
|
constructor(framework, options = {}) {
|
|
28
10
|
super(framework, options);
|
|
29
|
-
this.tabs = options.tabs || [];
|
|
11
|
+
this.tabs = options.tabs || [];
|
|
30
12
|
this.selectedIndex = options.selectedIndex || 0;
|
|
31
13
|
this.onChange = options.onChange;
|
|
32
14
|
this.platform = framework.platform;
|
|
@@ -35,13 +17,159 @@ class Tabs extends Component {
|
|
|
35
17
|
this.textColor = options.textColor || '#000000';
|
|
36
18
|
this.selectedTextColor = options.selectedTextColor || this.indicatorColor;
|
|
37
19
|
|
|
20
|
+
// 🔹 Système de ripples amélioré
|
|
21
|
+
this.ripples = [];
|
|
22
|
+
this.animationFrame = null;
|
|
23
|
+
this.lastAnimationTime = 0;
|
|
24
|
+
|
|
25
|
+
// 🔹 Pour détecter les doubles événements
|
|
26
|
+
this.lastEventTime = 0;
|
|
27
|
+
this.lastEventCoords = { x: -1, y: -1 };
|
|
28
|
+
|
|
38
29
|
this.onPress = this.handlePress.bind(this);
|
|
39
30
|
}
|
|
40
31
|
|
|
41
32
|
/**
|
|
42
|
-
*
|
|
43
|
-
* @
|
|
33
|
+
* Démarre l'animation des ripples
|
|
34
|
+
* @private
|
|
44
35
|
*/
|
|
36
|
+
startRippleAnimation() {
|
|
37
|
+
const animate = (timestamp) => {
|
|
38
|
+
if (!this.lastAnimationTime) this.lastAnimationTime = timestamp;
|
|
39
|
+
const deltaTime = timestamp - this.lastAnimationTime;
|
|
40
|
+
this.lastAnimationTime = timestamp;
|
|
41
|
+
|
|
42
|
+
let needsUpdate = false;
|
|
43
|
+
|
|
44
|
+
// Mettre à jour chaque ripple
|
|
45
|
+
for (let i = this.ripples.length - 1; i >= 0; i--) {
|
|
46
|
+
const ripple = this.ripples[i];
|
|
47
|
+
|
|
48
|
+
// Animer le rayon (expansion)
|
|
49
|
+
if (ripple.radius < ripple.maxRadius) {
|
|
50
|
+
ripple.radius += (ripple.maxRadius / 300) * deltaTime;
|
|
51
|
+
needsUpdate = true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Animer l'opacité (fade out)
|
|
55
|
+
if (ripple.radius >= ripple.maxRadius * 0.4) {
|
|
56
|
+
ripple.opacity -= (0.003 * deltaTime);
|
|
57
|
+
if (ripple.opacity < 0) ripple.opacity = 0;
|
|
58
|
+
needsUpdate = true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Supprimer les ripples terminés
|
|
62
|
+
if (ripple.opacity <= 0 && ripple.radius >= ripple.maxRadius * 0.95) {
|
|
63
|
+
this.ripples.splice(i, 1);
|
|
64
|
+
needsUpdate = true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Redessiner si nécessaire
|
|
69
|
+
if (needsUpdate) {
|
|
70
|
+
this.requestRender();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Continuer l'animation
|
|
74
|
+
if (this.ripples.length > 0) {
|
|
75
|
+
this.animationFrame = requestAnimationFrame(animate);
|
|
76
|
+
} else {
|
|
77
|
+
this.animationFrame = null;
|
|
78
|
+
this.lastAnimationTime = 0;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
if (this.ripples.length > 0 && !this.animationFrame) {
|
|
83
|
+
this.animationFrame = requestAnimationFrame(animate);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Demander un redessin
|
|
89
|
+
* @private
|
|
90
|
+
*/
|
|
91
|
+
requestRender() {
|
|
92
|
+
if (this.framework && this.framework.requestRender) {
|
|
93
|
+
this.framework.requestRender();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Nettoyer l'animation lors de la destruction
|
|
99
|
+
*/
|
|
100
|
+
destroy() {
|
|
101
|
+
if (this.animationFrame) {
|
|
102
|
+
cancelAnimationFrame(this.animationFrame);
|
|
103
|
+
this.animationFrame = null;
|
|
104
|
+
}
|
|
105
|
+
if (super.destroy) {
|
|
106
|
+
super.destroy();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
handlePress(x, y) {
|
|
111
|
+
const now = performance.now();
|
|
112
|
+
|
|
113
|
+
// 🔹 Détecter si c'est le même événement physique (mousedown + click)
|
|
114
|
+
const deltaTime = now - this.lastEventTime;
|
|
115
|
+
const deltaX = Math.abs(x - this.lastEventCoords.x);
|
|
116
|
+
const deltaY = Math.abs(y - this.lastEventCoords.y);
|
|
117
|
+
|
|
118
|
+
const isDoubleEvent = deltaX < 2 && deltaY < 2 && deltaTime < 50;
|
|
119
|
+
|
|
120
|
+
if (isDoubleEvent) {
|
|
121
|
+
return; // Ignorer le double événement
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 🔹 Enregistrer cet événement
|
|
125
|
+
this.lastEventTime = now;
|
|
126
|
+
this.lastEventCoords = { x, y };
|
|
127
|
+
|
|
128
|
+
const tabWidth = this.width / this.tabs.length;
|
|
129
|
+
const index = Math.floor((x - this.x) / tabWidth);
|
|
130
|
+
|
|
131
|
+
if (index >= 0 && index < this.tabs.length) {
|
|
132
|
+
// Ripple pour Material
|
|
133
|
+
if (this.platform === 'material') {
|
|
134
|
+
const rippleCenterX = this.x + index * tabWidth + tabWidth / 2;
|
|
135
|
+
|
|
136
|
+
// Calculer la taille maximale du ripple
|
|
137
|
+
const maxRippleRadius = Math.min(tabWidth * 0.6, this.height * 0.8);
|
|
138
|
+
|
|
139
|
+
this.ripples.push({
|
|
140
|
+
x: rippleCenterX,
|
|
141
|
+
y: this.y + this.height / 2,
|
|
142
|
+
radius: 0,
|
|
143
|
+
maxRadius: maxRippleRadius,
|
|
144
|
+
opacity: 1,
|
|
145
|
+
createdAt: now,
|
|
146
|
+
tabIndex: index
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Démarrer l'animation si elle n'est pas en cours
|
|
150
|
+
if (!this.animationFrame) {
|
|
151
|
+
this.startRippleAnimation();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Forcer un redessin
|
|
155
|
+
this.requestRender();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Changement d'onglet
|
|
159
|
+
if (index !== this.selectedIndex) {
|
|
160
|
+
this.selectedIndex = index;
|
|
161
|
+
if (this.onChange) this.onChange(index, this.tabs[index]);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
this.requestRender();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
isPointInside(x, y) {
|
|
169
|
+
return x >= this.x && x <= this.x + this.width &&
|
|
170
|
+
y >= this.y && y <= this.y + this.height;
|
|
171
|
+
}
|
|
172
|
+
|
|
45
173
|
draw(ctx) {
|
|
46
174
|
ctx.save();
|
|
47
175
|
|
|
@@ -56,16 +184,22 @@ class Tabs extends Component {
|
|
|
56
184
|
ctx.moveTo(this.x, this.y + this.height);
|
|
57
185
|
ctx.lineTo(this.x + this.width, this.y + this.height);
|
|
58
186
|
ctx.stroke();
|
|
59
|
-
|
|
187
|
+
|
|
60
188
|
const tabWidth = this.width / this.tabs.length;
|
|
61
|
-
|
|
189
|
+
|
|
190
|
+
// 🔹 Dessiner les ripples Material EN PREMIER
|
|
191
|
+
if (this.platform === 'material') {
|
|
192
|
+
this.drawRipples(ctx, tabWidth);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Dessiner les onglets
|
|
62
196
|
for (let i = 0; i < this.tabs.length; i++) {
|
|
63
197
|
const tab = this.tabs[i];
|
|
64
198
|
const tabX = this.x + i * tabWidth;
|
|
65
199
|
const isSelected = i === this.selectedIndex;
|
|
66
200
|
const color = isSelected ? this.selectedTextColor : this.textColor;
|
|
67
|
-
|
|
68
|
-
// Icône (
|
|
201
|
+
|
|
202
|
+
// Icône (facultative)
|
|
69
203
|
if (tab.icon) {
|
|
70
204
|
ctx.font = '20px sans-serif';
|
|
71
205
|
ctx.textAlign = 'center';
|
|
@@ -73,7 +207,7 @@ class Tabs extends Component {
|
|
|
73
207
|
ctx.fillStyle = color;
|
|
74
208
|
ctx.fillText(tab.icon, tabX + tabWidth / 2, this.y + 16);
|
|
75
209
|
}
|
|
76
|
-
|
|
210
|
+
|
|
77
211
|
// Label
|
|
78
212
|
ctx.font = `${isSelected ? 'bold ' : ''}14px -apple-system, Roboto, sans-serif`;
|
|
79
213
|
ctx.fillStyle = color;
|
|
@@ -81,44 +215,40 @@ class Tabs extends Component {
|
|
|
81
215
|
ctx.textBaseline = 'middle';
|
|
82
216
|
const labelY = tab.icon ? this.y + 36 : this.y + this.height / 2;
|
|
83
217
|
ctx.fillText(tab.label, tabX + tabWidth / 2, labelY);
|
|
84
|
-
|
|
85
|
-
// Indicateur (Material
|
|
218
|
+
|
|
219
|
+
// Indicateur (Material)
|
|
86
220
|
if (isSelected && this.platform === 'material') {
|
|
87
221
|
ctx.fillStyle = this.indicatorColor;
|
|
88
222
|
ctx.fillRect(tabX, this.y + this.height - 2, tabWidth, 2);
|
|
89
223
|
}
|
|
90
224
|
}
|
|
91
|
-
|
|
225
|
+
|
|
92
226
|
ctx.restore();
|
|
93
227
|
}
|
|
94
228
|
|
|
95
229
|
/**
|
|
96
|
-
*
|
|
97
|
-
* @param {number} x - Coordonnée X
|
|
98
|
-
* @param {number} y - Coordonnée Y
|
|
230
|
+
* Dessine les ripples (Material)
|
|
99
231
|
* @private
|
|
100
232
|
*/
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
233
|
+
drawRipples(ctx, tabWidth) {
|
|
234
|
+
// Sauvegarder le contexte
|
|
235
|
+
ctx.save();
|
|
104
236
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
237
|
+
// Créer un masque de clipping pour limiter les ripples aux onglets
|
|
238
|
+
ctx.beginPath();
|
|
239
|
+
ctx.rect(this.x, this.y, this.width, this.height);
|
|
240
|
+
ctx.clip();
|
|
241
|
+
|
|
242
|
+
for (let ripple of this.ripples) {
|
|
243
|
+
ctx.globalAlpha = ripple.opacity;
|
|
244
|
+
ctx.fillStyle = this.indicatorColor || '#6200EE';
|
|
245
|
+
ctx.beginPath();
|
|
246
|
+
ctx.arc(ripple.x, ripple.y, ripple.radius, 0, Math.PI * 2);
|
|
247
|
+
ctx.fill();
|
|
110
248
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
* Vérifie si un point est dans les limites
|
|
115
|
-
* @param {number} x - Coordonnée X
|
|
116
|
-
* @param {number} y - Coordonnée Y
|
|
117
|
-
* @returns {boolean} True si le point est dans les onglets
|
|
118
|
-
*/
|
|
119
|
-
isPointInside(x, y) {
|
|
120
|
-
return x >= this.x && x <= this.x + this.width &&
|
|
121
|
-
y >= this.y && y <= this.y + this.height;
|
|
249
|
+
|
|
250
|
+
// Restaurer le contexte
|
|
251
|
+
ctx.restore();
|
|
122
252
|
}
|
|
123
253
|
}
|
|
124
254
|
|