canvasframework 0.3.16 → 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.
@@ -93,6 +93,14 @@ class Button extends Component {
93
93
  this.rippleColor = this.hexToRgba(baseColor, 0.3);
94
94
  this.elevation = 0;
95
95
  break;
96
+
97
+ default :
98
+ this.bgColor = options.bgColor || '#FFFFFF';
99
+ this.textColor = options.textColor || baseColor;
100
+ this.borderWidth = 0;
101
+ this.rippleColor = this.hexToRgba(baseColor, 0.2);
102
+ this.elevation = options.elevation || 4;
103
+ break;
96
104
  }
97
105
  }
98
106
 
@@ -127,6 +135,12 @@ class Button extends Component {
127
135
  this.textColor = options.textColor || baseColor;
128
136
  this.borderWidth = 0;
129
137
  break;
138
+
139
+ default :
140
+ this.bgColor = 'transparent';
141
+ this.textColor = options.textColor || baseColor;
142
+ this.borderWidth = 0;
143
+ break;
130
144
  }
131
145
  }
132
146
 
@@ -3,22 +3,9 @@ import Component from '../core/Component.js';
3
3
  /**
4
4
  * Carousel / Slider d'images avec swipe horizontal et lazy load
5
5
  * Compatible Material et Cupertino
6
- * @class
7
- * @extends Component
6
+ * Tout le scroll est géré par le composant
8
7
  */
9
8
  class ImageCarousel extends Component {
10
- /**
11
- * @param {CanvasFramework} framework - Framework parent
12
- * @param {Object} [options={}]
13
- * @param {Array<string>} [options.images=[]] - URLs des images
14
- * @param {number} [options.height=200] - Hauteur du carousel
15
- * @param {number} [options.spacing=16] - Espacement entre images
16
- * @param {number} [options.borderRadius=8] - Coins arrondis
17
- * @param {number} [options.pageIndicatorSize=8] - Taille des dots
18
- * @param {string} [options.pageIndicatorColor='#6200EE'] - Couleur dot actif
19
- * @param {Function} [options.onSwipeEnd] - Callback quand la page change
20
- * @param {Function} [options.onImageClick] - Callback clic sur image
21
- */
22
9
  constructor(framework, options = {}) {
23
10
  super(framework, options);
24
11
 
@@ -33,8 +20,9 @@ class ImageCarousel extends Component {
33
20
  this.pageIndicatorColor = options.pageIndicatorColor || '#6200EE';
34
21
 
35
22
  this.platform = framework.platform;
36
- this.startX = 0;
23
+
37
24
  this.isDragging = false;
25
+ this.lastX = 0;
38
26
  this.velocity = 0;
39
27
 
40
28
  this.onSwipeEnd = options.onSwipeEnd || null;
@@ -42,98 +30,140 @@ class ImageCarousel extends Component {
42
30
 
43
31
  this.loadedImages = Array(this.images.length).fill(null);
44
32
 
45
- // Bind swipe
46
- this.framework.addEventListener('touchstart', this.onTouchStart.bind(this));
47
- this.framework.addEventListener('touchmove', this.onTouchMove.bind(this));
48
- this.framework.addEventListener('touchend', this.onTouchEnd.bind(this));
49
-
33
+ this._setupEventHandlers();
50
34
  this.animateScroll();
51
35
  }
52
36
 
53
- onTouchStart(e) {
54
- const touch = e.touches[0];
55
- this.startX = touch.clientX;
56
- this.isDragging = true;
57
- this.velocity = 0;
58
- this.lastTime = performance.now();
59
- this.lastX = touch.clientX;
60
- }
37
+ // --------------------------
38
+ // Event handlers
39
+ // --------------------------
40
+ _setupEventHandlers() {
41
+ const canvas = this.framework.canvas;
42
+
43
+ // TOUCH
44
+ canvas.addEventListener('touchstart', (e) => {
45
+ if (e.touches.length === 1 && this.isPointInsideTouch(e.touches[0])) {
46
+ this.isDragging = true;
47
+ this.lastX = e.touches[0].clientX;
48
+ this.velocity = 0;
49
+ e.preventDefault();
50
+ }
51
+ });
52
+
53
+ canvas.addEventListener('touchmove', (e) => {
54
+ if (this.isDragging && e.touches.length === 1) {
55
+ const delta = e.touches[0].clientX - this.lastX;
56
+ this.scrollX += delta;
57
+ this.velocity = delta;
58
+ this.lastX = e.touches[0].clientX;
59
+
60
+ this._clampScroll();
61
+ this._requestRedraw();
62
+ e.preventDefault();
63
+ }
64
+ });
61
65
 
62
- onTouchMove(e) {
63
- if (!this.isDragging) return;
64
- const touch = e.touches[0];
65
- const delta = touch.clientX - this.startX;
66
- this.scrollX = -this.currentIndex * (this.width + this.spacing) + delta;
67
-
68
- // calculer velocity
69
- const now = performance.now();
70
- const dt = now - this.lastTime;
71
- this.velocity = (touch.clientX - this.lastX) / dt * 16; // approximation
72
- this.lastTime = now;
73
- this.lastX = touch.clientX;
74
- }
66
+ canvas.addEventListener('touchend', () => this._endDrag());
67
+
68
+ // MOUSE
69
+ canvas.addEventListener('mousedown', (e) => {
70
+ if (this.isPointInside(e)) {
71
+ this.isDragging = true;
72
+ this.lastX = e.clientX;
73
+ this.velocity = 0;
74
+ e.preventDefault();
75
+ }
76
+ });
77
+
78
+ canvas.addEventListener('mousemove', (e) => {
79
+ if (this.isDragging) {
80
+ const delta = e.clientX - this.lastX;
81
+ this.scrollX += delta;
82
+ this.velocity = delta;
83
+ this.lastX = e.clientX;
84
+
85
+ this._clampScroll();
86
+ this._requestRedraw();
87
+ }
88
+ });
75
89
 
76
- onTouchEnd() {
77
- if (!this.isDragging) return;
90
+ canvas.addEventListener('mouseup', () => this._endDrag());
91
+ canvas.addEventListener('mouseleave', () => this._endDrag());
92
+ }
78
93
 
79
- // momentum scroll
80
- const momentumThreshold = this.width / 3;
81
- let targetIndex = this.currentIndex;
94
+ _endDrag() {
95
+ if (this.isDragging) {
96
+ this.isDragging = false;
97
+ // Snap à la page la plus proche
98
+ const targetIndex = Math.round(-this.scrollX / (this.width + this.spacing));
99
+ this.currentIndex = Math.min(Math.max(targetIndex, 0), this.images.length - 1);
100
+ this.scrollX = -this.currentIndex * (this.width + this.spacing);
82
101
 
83
- if (this.velocity < -0.5) targetIndex = Math.min(this.currentIndex + 1, this.images.length - 1);
84
- else if (this.velocity > 0.5) targetIndex = Math.max(this.currentIndex - 1, 0);
85
- else {
86
- const deltaIndex = Math.round(-this.scrollX / (this.width + this.spacing)) - this.currentIndex;
87
- if (deltaIndex > 0) targetIndex = Math.min(this.currentIndex + 1, this.images.length - 1);
88
- else if (deltaIndex < 0) targetIndex = Math.max(this.currentIndex - 1, 0);
102
+ if (this.onSwipeEnd) this.onSwipeEnd(this.currentIndex);
89
103
  }
104
+ }
90
105
 
91
- this.currentIndex = targetIndex;
92
- this.scrollX = -this.currentIndex * (this.width + this.spacing);
106
+ _clampScroll() {
107
+ const maxScroll = 0;
108
+ const minScroll = -(this.images.length - 1) * (this.width + this.spacing);
109
+ if (this.scrollX > maxScroll) this.scrollX = maxScroll;
110
+ if (this.scrollX < minScroll) this.scrollX = minScroll;
111
+ }
93
112
 
94
- this.isDragging = false;
95
- this.velocity = 0;
113
+ isPointInsideTouch(touch) {
114
+ const rect = this.framework.canvas.getBoundingClientRect();
115
+ const x = touch.clientX - rect.left;
116
+ const y = touch.clientY - rect.top;
117
+ return this.isPointInside(x, y);
118
+ }
119
+
120
+ isPointInside(x, y) {
121
+ return x >= this.x && x <= this.x + this.width &&
122
+ y >= this.y && y <= this.y + this.height;
123
+ }
96
124
 
97
- if (this.onSwipeEnd) this.onSwipeEnd(this.currentIndex);
125
+ _requestRedraw() {
126
+ if (this.framework.markComponentDirty) this.framework.markComponentDirty(this);
98
127
  }
99
128
 
129
+ // --------------------------
130
+ // Animation / Inertie
131
+ // --------------------------
100
132
  animateScroll() {
101
133
  const animate = () => {
102
134
  if (!this.isDragging) {
103
- // inertia effect
135
+ // inertia
104
136
  if (Math.abs(this.velocity) > 0.1) {
105
137
  this.scrollX += this.velocity;
106
- this.velocity *= 0.95; // friction
138
+ this.velocity *= 0.95;
107
139
 
108
- // clamp
109
- if (this.scrollX > 0) this.scrollX = 0;
110
- const maxScroll = -(this.images.length - 1) * (this.width + this.spacing);
111
- if (this.scrollX < maxScroll) this.scrollX = maxScroll;
140
+ this._clampScroll();
112
141
  } else {
113
- // snap to nearest
142
+ // snap doux vers la page
114
143
  const target = -this.currentIndex * (this.width + this.spacing);
115
144
  this.scrollX += (target - this.scrollX) * 0.2;
116
145
  }
117
146
  }
118
-
119
147
  requestAnimationFrame(animate);
120
148
  };
121
149
  animate();
122
150
  }
123
151
 
152
+ // --------------------------
153
+ // Draw
154
+ // --------------------------
124
155
  draw(ctx) {
125
156
  ctx.save();
126
-
127
157
  const startX = this.x + this.scrollX + this.spacing / 2;
128
158
 
129
159
  for (let i = 0; i < this.images.length; i++) {
130
160
  const imgX = startX + i * (this.width + this.spacing);
131
161
 
132
- // charger lazy image
162
+ // lazy load
133
163
  if (!this.loadedImages[i]) {
134
164
  const img = new Image();
135
165
  img.src = this.images[i];
136
- img.onload = () => { this.loadedImages[i] = img; };
166
+ img.onload = () => { this.loadedImages[i] = img; this._requestRedraw(); };
137
167
  }
138
168
 
139
169
  ctx.save();
@@ -152,10 +182,11 @@ class ImageCarousel extends Component {
152
182
  ctx.textBaseline = 'middle';
153
183
  ctx.fillText('🖼', imgX + this.width / 2, this.y + this.height / 2);
154
184
  }
185
+
155
186
  ctx.restore();
156
187
  }
157
188
 
158
- // Pagination Material
189
+ // pagination Material
159
190
  if (this.platform === 'material') {
160
191
  const dotY = this.y + this.height + 12;
161
192
  const totalWidth = this.images.length * this.pageIndicatorSize * 2;
@@ -183,11 +214,6 @@ class ImageCarousel extends Component {
183
214
  ctx.lineTo(x, y + radius);
184
215
  ctx.quadraticCurveTo(x, y, x + radius, y);
185
216
  }
186
-
187
- isPointInside(x, y) {
188
- return x >= this.x && x <= this.x + this.width &&
189
- y >= this.y && y <= this.y + this.height;
190
- }
191
217
  }
192
218
 
193
219
  export default ImageCarousel;
@@ -414,14 +414,44 @@ class InputTags extends Component {
414
414
 
415
415
  return cursorX;
416
416
  }
417
+
418
+ updateHeight() {
419
+ const maxX = this.x + this.width - 10; // marge droite
420
+ let currentX = this.x + 10;
421
+ let currentY = this.y + 10;
422
+ let lines = 1;
423
+
424
+ for (let i = 0; i < this.tags.length; i++) {
425
+ const tagWidth = this.measureTagWidth(this.tags[i]);
426
+
427
+ if (currentX + tagWidth > maxX) {
428
+ currentX = this.x + 10;
429
+ currentY += this.tagHeight + this.tagSpacing;
430
+ lines++;
431
+ }
432
+
433
+ currentX += tagWidth + this.tagSpacing;
434
+ }
435
+
436
+ // Ajouter la largeur du texte en cours
437
+ const textWidth = this.value.length * (this.fontSize * 0.6);
438
+ if (currentX + textWidth > maxX) {
439
+ lines++;
440
+ }
441
+
442
+ // Mettre à jour la hauteur dynamique
443
+ this.height = Math.max(lines * (this.tagHeight + this.tagSpacing) + 10, 40); // 40 = minHeight
444
+ }
417
445
 
418
446
  /**
419
447
  * Dessine l'input
420
448
  * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
421
449
  */
422
450
  draw(ctx) {
451
+ this.updateHeight();
423
452
  ctx.save();
424
-
453
+
454
+ // Dessin du contour
425
455
  if (this.platform === 'material') {
426
456
  ctx.strokeStyle = this.focused ? '#6200EE' : '#CCCCCC';
427
457
  ctx.lineWidth = this.focused ? 2 : 1;
@@ -436,30 +466,38 @@ class InputTags extends Component {
436
466
  this.roundRect(ctx, this.x, this.y, this.width, this.height, 8);
437
467
  ctx.stroke();
438
468
  }
439
-
469
+
440
470
  // Position de départ pour dessiner les tags
441
471
  let currentX = this.x + 10;
442
- const tagY = this.y + 10;
443
-
472
+ let currentY = this.y + 10;
473
+ const maxX = this.x + this.width - 10;
474
+
444
475
  // Dessiner les tags
445
476
  for (let i = 0; i < this.tags.length; i++) {
446
- this.drawTag(ctx, this.tags[i], currentX, tagY);
447
477
  const tagWidth = this.measureTagWidth(this.tags[i]);
478
+
479
+ // Retour à la ligne si dépassement
480
+ if (currentX + tagWidth > maxX) {
481
+ currentX = this.x + 10;
482
+ currentY += this.tagHeight + this.tagSpacing;
483
+ }
484
+
485
+ this.drawTag(ctx, this.tags[i], currentX, currentY);
448
486
  currentX += tagWidth + this.tagSpacing;
449
487
  }
450
-
451
- // Dessiner le texte en cours
488
+
489
+ // Dessiner le texte en cours ou placeholder
452
490
  if (this.value || (this.focused && this.tags.length === 0)) {
453
491
  ctx.fillStyle = this.value ? '#000000' : '#999999';
454
492
  ctx.font = `${this.fontSize}px -apple-system, BlinkMacSystemFont, 'Roboto', sans-serif`;
455
493
  ctx.textAlign = 'left';
456
494
  ctx.textBaseline = 'middle';
457
-
495
+
458
496
  const displayText = this.value || this.placeholder;
459
- const textY = this.y + this.height / 2;
460
-
497
+ const textY = currentY + this.tagHeight / 2;
498
+
461
499
  ctx.fillText(displayText, currentX, textY);
462
-
500
+
463
501
  // Curseur
464
502
  if (this.focused && this.cursorVisible) {
465
503
  const textWidth = ctx.measureText(this.value).width;
@@ -467,7 +505,7 @@ class InputTags extends Component {
467
505
  ctx.fillRect(currentX + textWidth, textY - this.fontSize / 2, 2, this.fontSize);
468
506
  }
469
507
  }
470
-
508
+
471
509
  ctx.restore();
472
510
  }
473
511
 
@@ -1,24 +1,12 @@
1
1
  import Component from '../core/Component.js';
2
2
  import ListItem from '../components/ListItem.js';
3
+
3
4
  /**
4
5
  * Conteneur pour les éléments de liste (ListItems) avec défilement automatique
5
6
  * @class
6
7
  * @extends Component
7
- * @param {Framework} framework - Instance du framework
8
- * @param {Object} [options={}] - Options de configuration
9
- * @param {number} [options.itemHeight=56] - Hauteur de chaque item en pixels
10
- * @param {Function} [options.onItemClick] - Callback appelé lors du clic sur un item
11
- * @param {number} [options.y=0] - Position Y de départ
12
- * @example
13
- * const list = new List(framework, {
14
- * itemHeight: 64,
15
- * onItemClick: (index, itemOptions) => console.log('Item clicked:', index)
16
- * });
17
8
  */
18
9
  class List extends Component {
19
- /**
20
- * @constructs List
21
- */
22
10
  constructor(framework, options = {}) {
23
11
  super(framework, options);
24
12
  /** @type {ListItem[]} */
@@ -28,37 +16,50 @@ class List extends Component {
28
16
  /** @type {Function|undefined} */
29
17
  this.onItemClick = options.onItemClick;
30
18
  /** @type {number} */
31
- this.y = options.y || 0; // Position Y de départ
19
+ this.y = options.y || 0;
32
20
  }
33
21
 
34
22
  /**
35
23
  * Ajoute un item à la liste
36
- * @param {Object} itemOptions - Options pour l'item
37
- * @param {string} itemOptions.text - Texte à afficher
38
- * @param {Function} [itemOptions.onClick] - Callback spécifique à l'item
39
- * @param {Object} [itemOptions.style] - Style optionnel pour l'item
40
- * @returns {ListItem} L'item créé
24
+ * @param {Object|Component} itemOptions - Options pour l'item OU instance de Component (ex: SwipeableListItem)
25
+ * @returns {ListItem|Component} L'item créé ou passé
41
26
  */
42
27
  addItem(itemOptions) {
43
- const item = new ListItem(this.framework, {
44
- ...itemOptions,
45
- x: this.x,
46
- y: this.y + (this.items.length * this.itemHeight),
47
- width: this.width,
48
- height: this.itemHeight, // IMPORTANT: définir la hauteur
49
- onClick: () => {
50
- if (this.onItemClick) {
51
- this.onItemClick(this.items.length, itemOptions);
52
- }
53
- if (itemOptions.onClick) {
54
- itemOptions.onClick();
28
+ let item;
29
+
30
+ // 🔹 Vérifier si c'est déjà un composant instancié (SwipeableListItem)
31
+ if (itemOptions instanceof Component) {
32
+ item = itemOptions;
33
+
34
+ // 🔹 CRITIQUE : Définir la position et taille de l'item
35
+ item.x = this.x;
36
+ item.y = this.y + (this.items.length * this.itemHeight);
37
+ item.width = this.width;
38
+ item.height = this.itemHeight;
39
+
40
+ console.log(`📦 SwipeableListItem positionné à y=${item.y}`);
41
+ } else {
42
+ // Créer un ListItem standard
43
+ item = new ListItem(this.framework, {
44
+ ...itemOptions,
45
+ x: this.x,
46
+ y: this.y + (this.items.length * this.itemHeight),
47
+ width: this.width,
48
+ height: this.itemHeight,
49
+ onClick: () => {
50
+ if (this.onItemClick) {
51
+ this.onItemClick(this.items.length, itemOptions);
52
+ }
53
+ if (itemOptions.onClick) {
54
+ itemOptions.onClick();
55
+ }
55
56
  }
56
- }
57
- });
57
+ });
58
+ }
58
59
 
59
60
  this.items.push(item);
60
- this.framework.add(item); // Ajouter chaque item au framework
61
- this.height = this.items.length * this.itemHeight; // Mettre à jour la hauteur totale
61
+ this.framework.add(item);
62
+ this.height = this.items.length * this.itemHeight;
62
63
 
63
64
  return item;
64
65
  }
@@ -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.onPress = this.handlePress.bind(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
- if (this.isPointInside(x, y)) {
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
- this.dragOffset = x - this.startX;
56
- const maxOffset = Math.max(this.leftActions.length, this.rightActions.length) * 80;
57
- this.dragOffset = Math.min(Math.max(this.dragOffset, -maxOffset), maxOffset);
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
- if (this.dragOffset > 80 && this.leftActions[0]) {
67
- this.leftActions[0].onClick?.();
68
- } else if (this.dragOffset < -80 && this.rightActions[0]) {
69
- this.rightActions[0].onClick?.();
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
- // Ripple
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
- ctx.fillText(this.title, this.x + 16, this.y + this.height / 2);
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 + this.height / 2 + 12);
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
- isPointInside(x, y) {
174
- return x >= this.x && x <= this.x + this.width &&
175
- y >= this.y && y <= this.y + this.height;
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;
@@ -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 || []; // [{label: 'Tab 1', icon: '📱'}, ...]
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
- * Dessine les onglets
43
- * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
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 (si présente)
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 uniquement)
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
- * Gère la pression (clic)
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
- handlePress(x, y) {
102
- const tabWidth = this.width / this.tabs.length;
103
- const index = Math.floor((x - this.x) / tabWidth);
233
+ drawRipples(ctx, tabWidth) {
234
+ // Sauvegarder le contexte
235
+ ctx.save();
104
236
 
105
- if (index >= 0 && index < this.tabs.length && index !== this.selectedIndex) {
106
- this.selectedIndex = index;
107
- if (this.onChange) {
108
- this.onChange(index, this.tabs[index]);
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvasframework",
3
- "version": "0.3.16",
3
+ "version": "0.3.17",
4
4
  "description": "Canvas-based cross-platform UI framework (Material & Cupertino)",
5
5
  "type": "module",
6
6
  "main": "./index.js",