canvasframework 0.6.3 → 0.7.1

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,156 +1,185 @@
1
1
  import Component from '../core/Component.js';
2
+ import { roundRect, roundRectTop } from '../core/CanvasUtils.js';
2
3
 
3
4
  /**
4
- * BottomSheet avec styles Material et Cupertino
5
- * @class
6
- * @extends Component
5
+ * BottomSheet Material et Cupertino.
6
+ *
7
+ * Corrections :
8
+ * - roundRect / roundRectTop viennent de CanvasUtils
9
+ * - Guard _destroyed dans les boucles RAF
10
+ * - destroy() nettoie les ressources
7
11
  */
8
12
  class BottomSheet extends Component {
13
+ /**
14
+ * @param {CanvasFramework} framework
15
+ * @param {Object} [options={}]
16
+ * @param {number} [options.height] - Hauteur de la feuille (défaut : 60% de l'écran)
17
+ * @param {boolean} [options.dragHandle=true]
18
+ * @param {boolean} [options.closeOnOverlayClick=true]
19
+ * @param {string} [options.bgColor]
20
+ * @param {number} [options.borderRadius]
21
+ */
9
22
  constructor(framework, options = {}) {
10
23
  super(framework, {
11
24
  x: 0,
12
25
  y: framework.height,
13
26
  width: framework.width,
14
27
  height: options.height || framework.height * 0.6,
15
- visible: false
28
+ visible: false,
16
29
  });
17
30
 
18
- this.platform = framework.platform; // material / cupertino
19
-
20
- this.children = [];
21
- this.dragHandle = options.dragHandle !== false;
31
+ this.platform = framework.platform;
32
+ this.children = [];
33
+ this.dragHandle = options.dragHandle !== false;
22
34
  this.closeOnOverlayClick = options.closeOnOverlayClick !== false;
23
35
 
24
- // Styles plateforme
36
+ // Styles selon la plateforme
25
37
  if (this.platform === 'material') {
26
- this.bgColor = options.bgColor || '#FFFFFF';
27
- this.overlayColor = 'rgba(0,0,0,0.5)';
28
- this.shadowBlur = 20;
38
+ this.bgColor = options.bgColor || '#FFFFFF';
39
+ this.overlayColor = 'rgba(0,0,0,0.5)';
40
+ this.shadowBlur = 20;
29
41
  this.shadowOffsetY = -5;
30
- this.borderRadius = 11;
31
- } else { // cupertino
32
- this.bgColor = options.bgColor || 'rgba(255,255,255,0.95)';
33
- this.overlayColor = 'rgba(0,0,0,0.2)';
34
- this.shadowBlur = 0;
42
+ this.borderRadius = 11;
43
+ } else {
44
+ this.bgColor = options.bgColor || 'rgba(255,255,255,0.95)';
45
+ this.overlayColor = 'rgba(0,0,0,0.2)';
46
+ this.shadowBlur = 0;
35
47
  this.shadowOffsetY = 0;
36
- this.borderRadius = options.borderRadius || 20;
48
+ this.borderRadius = options.borderRadius || 20;
37
49
  }
38
50
 
39
- this.targetY = framework.height;
40
- this.isOpen = false;
41
- this.animating = false;
42
- this.dragging = false;
43
- this.dragStartY = 0;
44
- this.dragOffset = 0;
51
+ this.targetY = framework.height;
52
+ this.isOpen = false;
53
+ this._animating = false;
54
+ this._rafId = null;
55
+ this.dragging = false;
56
+ this.dragStartY = 0;
57
+ this.dragOffset = 0;
45
58
  this.lastClickTime = 0;
46
59
 
47
- this.onPress = this.handlePress.bind(this);
48
- this.onMove = this.handleMove.bind(this);
49
- this.onRelease = this.handleRelease.bind(this);
60
+ this.onPress = this._handlePress.bind(this);
61
+ this.onMove = this._handleMove.bind(this);
62
+ this.onRelease = this._handleRelease.bind(this);
50
63
  }
51
64
 
65
+ // ─────────────────────────────────────────
66
+ // ENFANTS
67
+ // ─────────────────────────────────────────
68
+
52
69
  add(child) {
53
70
  this.children.push(child);
54
71
  return child;
55
72
  }
56
73
 
74
+ // ─────────────────────────────────────────
75
+ // OPEN / CLOSE
76
+ // ─────────────────────────────────────────
77
+
57
78
  open() {
58
79
  this.visible = true;
59
- this.isOpen = true;
80
+ this.isOpen = true;
60
81
  this.targetY = this.framework.height - this.height;
61
- this.animate();
82
+ this._animate();
62
83
  }
63
84
 
64
85
  close() {
65
- this.isOpen = false;
86
+ this.isOpen = false;
66
87
  this.targetY = this.framework.height;
67
- this.animate(() => {
68
- this.visible = false;
69
- });
88
+ this._animate(() => { this.visible = false; });
70
89
  }
71
90
 
72
- animate(callback) {
73
- if (this.animating) return;
74
- this.animating = true;
91
+ /** @private */
92
+ _animate(callback) {
93
+ if (this._animating) return;
94
+ this._animating = true;
95
+
96
+ if (this._rafId) cancelAnimationFrame(this._rafId);
75
97
 
76
98
  const step = () => {
77
- let diff = this.targetY - this.y;
99
+ if (this._destroyed) { this._rafId = null; this._animating = false; return; }
100
+
101
+ const diff = this.targetY - this.y;
78
102
 
79
103
  if (Math.abs(diff) < 1) {
80
- this.y = this.targetY;
81
- this.animating = false;
82
- if (callback) callback();
104
+ this.y = this.targetY;
105
+ this._animating = false;
106
+ this._rafId = null;
107
+ callback?.();
83
108
  return;
84
109
  }
85
110
 
86
- // Animation type spring pour iOS, easing pour Material
87
- if (this.platform === 'cupertino') {
88
- diff *= 0.15; // spring
89
- } else {
90
- diff *= 0.2; // easing
91
- }
92
-
93
- this.y += diff;
94
-
95
- requestAnimationFrame(step);
111
+ // Spring iOS, easing Material
112
+ this.y += diff * (this.platform === 'cupertino' ? 0.15 : 0.2);
113
+ this._rafId = requestAnimationFrame(step);
96
114
  };
97
115
 
98
- step();
116
+ this._rafId = requestAnimationFrame(step);
99
117
  }
100
118
 
101
- handlePress(x, y) {
119
+ // ─────────────────────────────────────────
120
+ // INTERACTIONS
121
+ // ─────────────────────────────────────────
122
+
123
+ /** @private */
124
+ _handlePress(x, y) {
102
125
  const now = Date.now();
103
126
  if (now - this.lastClickTime < 300) return;
104
127
  this.lastClickTime = now;
105
128
 
129
+ // Clic sur l'overlay
106
130
  if (y < this.y && this.closeOnOverlayClick) {
107
131
  this.close();
108
132
  return;
109
133
  }
110
134
 
111
- // Clic sur la poignée
112
- if (this.dragHandle && y >= this.y && y <= this.y + 40) {
113
- // ✅ Fermer le bottom sheet immédiatement
114
- this.close();
115
- return;
116
- }
135
+ // Clic sur la poignée → fermeture
136
+ if (this.dragHandle && y >= this.y && y <= this.y + 40) {
137
+ this.close();
138
+ return;
139
+ }
117
140
 
118
- // Gestion clic enfants
141
+ // Déléguer aux enfants
119
142
  const contentY = this.y + (this.dragHandle ? 40 : 16);
120
143
  for (let i = this.children.length - 1; i >= 0; i--) {
121
144
  const child = this.children[i];
122
145
  if (!child.visible) continue;
123
146
 
124
- const childAbsX = this.x + 16 + child.x;
125
- const childAbsY = contentY + child.y;
147
+ const absX = this.x + 16 + child.x;
148
+ const absY = contentY + child.y;
126
149
 
127
- if (x >= childAbsX && x <= childAbsX + child.width &&
128
- y >= childAbsY && y <= childAbsY + child.height) {
129
- if (child.onClick) child.onClick();
150
+ if (x >= absX && x <= absX + child.width && y >= absY && y <= absY + child.height) {
151
+ child.onClick?.();
130
152
  return;
131
153
  }
132
154
  }
133
155
  }
134
156
 
135
- handleMove(x, y) {
157
+ /** @private */
158
+ _handleMove(x, y) {
136
159
  if (!this.dragging) return;
137
160
  this.dragOffset = y - this.dragStartY;
138
- let newY = (this.framework.height - this.height) + this.dragOffset;
161
+ const newY = (this.framework.height - this.height) + this.dragOffset;
139
162
  if (newY >= this.framework.height - this.height) this.y = newY;
140
163
  }
141
164
 
142
- handleRelease() {
165
+ /** @private */
166
+ _handleRelease() {
143
167
  if (!this.dragging) return;
144
- this.dragging = false;
168
+ this.dragging = false;
145
169
  this.framework.activeComponent = null;
146
170
 
147
- if (this.dragOffset > this.height * 0.3) this.close();
148
- else {
171
+ if (this.dragOffset > this.height * 0.3) {
172
+ this.close();
173
+ } else {
149
174
  this.targetY = this.framework.height - this.height;
150
- this.animate();
175
+ this._animate();
151
176
  }
152
177
  }
153
178
 
179
+ // ─────────────────────────────────────────
180
+ // DESSIN
181
+ // ─────────────────────────────────────────
182
+
154
183
  draw(ctx) {
155
184
  if (!this.visible) return;
156
185
  ctx.save();
@@ -159,27 +188,27 @@ class BottomSheet extends Component {
159
188
  ctx.fillStyle = this.overlayColor;
160
189
  ctx.fillRect(0, 0, this.framework.width, this.framework.height);
161
190
 
162
- // Sheet
163
- ctx.fillStyle = this.bgColor;
164
- ctx.shadowColor = this.platform === 'material' ? 'rgba(0,0,0,0.3)' : 'transparent';
165
- ctx.shadowBlur = this.shadowBlur;
166
- ctx.shadowOffsetY = this.shadowOffsetY;
191
+ // Feuille
192
+ ctx.fillStyle = this.bgColor;
193
+ ctx.shadowColor = this.platform === 'material' ? 'rgba(0,0,0,0.3)' : 'transparent';
194
+ ctx.shadowBlur = this.shadowBlur;
195
+ ctx.shadowOffsetY = this.shadowOffsetY;
167
196
 
168
197
  ctx.beginPath();
169
- this.roundRectTop(ctx, this.x, this.y, this.width, this.height, this.borderRadius);
198
+ roundRectTop(ctx, this.x, this.y, this.width, this.height, this.borderRadius);
170
199
  ctx.fill();
171
200
  ctx.shadowColor = 'transparent';
172
201
 
173
- // Drag handle
202
+ // Poignée
174
203
  if (this.dragHandle) {
175
204
  ctx.fillStyle = this.platform === 'material' ? '#CCCCCC' : '#E0E0E0';
176
205
  ctx.beginPath();
177
- this.roundRect(ctx, this.width / 2 - 20, this.y + 12, 40, 4, 2);
206
+ roundRect(ctx, this.width / 2 - 20, this.y + 12, 40, 4, 2);
178
207
  ctx.fill();
179
208
  }
180
209
 
181
- // Enfants
182
- const contentY = this.y + (this.dragHandle ? 40 : 16);
210
+ // Clip pour les enfants
211
+ const contentY = this.y + (this.dragHandle ? 40 : 16);
183
212
  const contentHeight = this.height - (this.dragHandle ? 40 : 16);
184
213
 
185
214
  ctx.save();
@@ -187,7 +216,7 @@ class BottomSheet extends Component {
187
216
  ctx.rect(this.x, contentY, this.width, contentHeight);
188
217
  ctx.clip();
189
218
 
190
- for (let child of this.children) {
219
+ for (const child of this.children) {
191
220
  if (!child.visible) continue;
192
221
  const origX = child.x;
193
222
  const origY = child.y;
@@ -202,33 +231,22 @@ class BottomSheet extends Component {
202
231
  ctx.restore();
203
232
  }
204
233
 
205
- roundRectTop(ctx, x, y, width, height, radius) {
206
- ctx.moveTo(x + radius, y);
207
- ctx.lineTo(x + width - radius, y);
208
- ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
209
- ctx.lineTo(x + width, y + height);
210
- ctx.lineTo(x, y + height);
211
- ctx.lineTo(x, y + radius);
212
- ctx.quadraticCurveTo(x, y, x + radius, y);
234
+ isPointInside(x, y) {
235
+ return this.visible;
213
236
  }
214
237
 
215
- roundRect(ctx, x, y, width, height, radius) {
216
- if (radius === 0) return ctx.rect(x, y, width, height);
217
- ctx.moveTo(x + radius, y);
218
- ctx.lineTo(x + width - radius, y);
219
- ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
220
- ctx.lineTo(x + width, y + height - radius);
221
- ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
222
- ctx.lineTo(x + radius, y + height);
223
- ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
224
- ctx.lineTo(x, y + radius);
225
- ctx.quadraticCurveTo(x, y, x + radius, y);
226
- ctx.closePath();
227
- }
238
+ // ─────────────────────────────────────────
239
+ // DESTROY
240
+ // ─────────────────────────────────────────
228
241
 
229
- isPointInside(x, y) {
230
- return this.visible;
242
+ destroy() {
243
+ if (this._rafId) {
244
+ cancelAnimationFrame(this._rafId);
245
+ this._rafId = null;
246
+ }
247
+ this.children = [];
248
+ super.destroy();
231
249
  }
232
250
  }
233
251
 
234
- export default BottomSheet;
252
+ export default BottomSheet;