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.
package/core/Component.js CHANGED
@@ -37,25 +37,53 @@ class Component {
37
37
  this.hovered = false;
38
38
  this.onClick = options.onClick;
39
39
  this.onPress = options.onPress;
40
-
41
- // Système dirty simple (optionnel)
40
+
41
+ // Système dirty simple
42
42
  this._dirty = true;
43
-
43
+
44
+ // Flag de destruction — stoppe les RAF/timers des sous-classes
45
+ this._destroyed = false;
46
+
44
47
  // Lifecycle
45
48
  this._mounted = false;
46
49
 
47
- // Pour détecter les updates
48
- this._prevProps = { ...options };
49
-
50
- // Système de listeners
50
+ // Deep copy pour éviter que _prevProps référence les mêmes objets
51
+ // qu'options (important si options contient des tableaux ou objets imbriqués).
52
+ this._prevProps = Component._deepClone(options);
53
+
54
+ // Système de listeners
51
55
  this._listeners = new Map();
52
56
  }
53
57
 
58
+ // ─────────────────────────────────────────
59
+ // UTILITAIRE INTERNE
60
+ // ─────────────────────────────────────────
61
+
54
62
  /**
55
- * Ajoute un listener pour un événement
56
- * @param {string} event - Nom de l'événement
57
- * @param {Function} handler - Fonction callback
58
- * @returns {Component} - Pour le chaînage
63
+ * Clone profond léger (JSON-safe).
64
+ * Pour des options contenant des fonctions (onClick…), on préfère
65
+ * une copie structurée qui ignore les fonctions non-sérialisables.
66
+ * @private
67
+ */
68
+ static _deepClone(obj) {
69
+ try {
70
+ return JSON.parse(JSON.stringify(obj, (key, val) =>
71
+ typeof val === 'function' ? undefined : val
72
+ ));
73
+ } catch {
74
+ return { ...obj };
75
+ }
76
+ }
77
+
78
+ // ─────────────────────────────────────────
79
+ // SYSTÈME DE LISTENERS
80
+ // ─────────────────────────────────────────
81
+
82
+ /**
83
+ * Ajoute un listener pour un événement.
84
+ * @param {string} event
85
+ * @param {Function} handler
86
+ * @returns {Component}
59
87
  */
60
88
  on(event, handler) {
61
89
  if (!this._listeners.has(event)) {
@@ -66,26 +94,23 @@ class Component {
66
94
  }
67
95
 
68
96
  /**
69
- * Retire un listener
70
- * @param {string} event - Nom de l'événement
71
- * @param {Function} handler - Fonction à retirer
97
+ * Retire un listener.
98
+ * @param {string} event
99
+ * @param {Function} handler
72
100
  * @returns {Component}
73
101
  */
74
102
  off(event, handler) {
75
103
  if (!this._listeners.has(event)) return this;
76
-
77
104
  const handlers = this._listeners.get(event);
78
105
  const index = handlers.indexOf(handler);
79
- if (index > -1) {
80
- handlers.splice(index, 1);
81
- }
106
+ if (index > -1) handlers.splice(index, 1);
82
107
  return this;
83
108
  }
84
109
 
85
110
  /**
86
- * Ajoute un listener qui s'exécute une seule fois
87
- * @param {string} event - Nom de l'événement
88
- * @param {Function} handler - Fonction callback
111
+ * Ajoute un listener qui s'exécute une seule fois.
112
+ * @param {string} event
113
+ * @param {Function} handler
89
114
  * @returns {Component}
90
115
  */
91
116
  once(event, handler) {
@@ -97,28 +122,26 @@ class Component {
97
122
  }
98
123
 
99
124
  /**
100
- * Émet un événement
101
- * @param {string} event - Nom de l'événement
102
- * @param {...any} args - Arguments à passer aux handlers
125
+ * Émet un événement.
126
+ * @param {string} event
127
+ * @param {...any} args
103
128
  * @returns {Component}
104
129
  */
105
130
  emit(event, ...args) {
106
131
  if (!this._listeners.has(event)) return this;
107
-
108
- const handlers = this._listeners.get(event);
109
- for (let handler of handlers) {
132
+ for (const handler of this._listeners.get(event)) {
110
133
  try {
111
134
  handler(...args);
112
135
  } catch (error) {
113
- console.error(`Error in ${event} handler:`, error);
136
+ console.error(`Error in "${event}" handler:`, error);
114
137
  }
115
138
  }
116
139
  return this;
117
140
  }
118
141
 
119
142
  /**
120
- * Retire tous les listeners d'un événement (ou tous)
121
- * @param {string} [event] - Nom de l'événement (optionnel)
143
+ * Retire tous les listeners (ou ceux d'un événement précis).
144
+ * @param {string} [event]
122
145
  * @returns {Component}
123
146
  */
124
147
  removeAllListeners(event) {
@@ -131,24 +154,23 @@ class Component {
131
154
  }
132
155
 
133
156
  /**
134
- * Retourne le nombre de listeners pour un événement
135
- * @param {string} event - Nom de l'événement
157
+ * Retourne le nombre de listeners pour un événement.
158
+ * @param {string} event
136
159
  * @returns {number}
137
160
  */
138
161
  listenerCount(event) {
139
162
  return this._listeners.has(event) ? this._listeners.get(event).length : 0;
140
- }
141
- /* =======================
142
- LIFECYCLE HOOKS
143
- ======================= */
163
+ }
164
+
165
+ // ─────────────────────────────────────────
166
+ // LIFECYCLE HOOKS (à surcharger)
167
+ // ─────────────────────────────────────────
144
168
 
145
169
  onMount() {}
146
170
  onUnmount() {}
147
171
  onUpdate(prevProps) {}
148
172
  onResize(width, height) {}
149
173
 
150
- /* ======================= */
151
-
152
174
  _mount() {
153
175
  if (!this._mounted) {
154
176
  this._mounted = true;
@@ -165,7 +187,7 @@ class Component {
165
187
 
166
188
  _update(newProps) {
167
189
  this.onUpdate(this._prevProps);
168
- this._prevProps = { ...newProps };
190
+ this._prevProps = Component._deepClone(newProps);
169
191
  this.markDirty();
170
192
  }
171
193
 
@@ -176,25 +198,23 @@ class Component {
176
198
 
177
199
  setProps(newProps = {}) {
178
200
  const changed = Object.keys(newProps).some(
179
- key => this[key] !== newProps[key]
201
+ (key) => this[key] !== newProps[key]
180
202
  );
181
-
182
203
  if (!changed) return;
183
-
184
204
  Object.assign(this, newProps);
185
205
  this._update(newProps);
186
206
  }
187
207
 
188
208
  measure(constraints) {
189
- return {
190
- width: this.width,
191
- height: this.height
192
- };
209
+ return { width: this.width, height: this.height };
193
210
  }
194
211
 
212
+ // ─────────────────────────────────────────
213
+ // DIRTY / CLEAN
214
+ // ─────────────────────────────────────────
215
+
195
216
  /**
196
- * Marque le composant pour redessin
197
- * Appelez cette méthode après avoir modifié une propriété
217
+ * Marque le composant pour redessin.
198
218
  */
199
219
  markDirty() {
200
220
  this._dirty = true;
@@ -203,41 +223,79 @@ class Component {
203
223
  }
204
224
  }
205
225
 
206
- /**
207
- * Marque le composant comme propre (appelé automatiquement après draw)
208
- */
226
+ /** Marque le composant comme propre (appelé après draw). */
209
227
  markClean() {
210
228
  this._dirty = false;
211
229
  }
212
230
 
213
- /**
214
- * Vérifie si le composant est dirty
215
- */
231
+ /** Indique si le composant doit être redessiné. */
216
232
  isDirty() {
217
233
  return this._dirty;
218
234
  }
219
235
 
236
+ // ─────────────────────────────────────────
237
+ // INTERACTION
238
+ // ─────────────────────────────────────────
239
+
220
240
  /**
221
- * Vérifie si un point est dans les limites du composant
241
+ * Vérifie si un point (x, y) est à l'intérieur du composant.
242
+ * @param {number} x
243
+ * @param {number} y
244
+ * @returns {boolean}
222
245
  */
223
246
  isPointInside(x, y) {
224
- return x >= this.x && x <= this.x + this.width &&
225
- y >= this.y && y <= this.y + this.height;
247
+ return (
248
+ x >= this.x &&
249
+ x <= this.x + this.width &&
250
+ y >= this.y &&
251
+ y <= this.y + this.height
252
+ );
226
253
  }
227
254
 
255
+ // ─────────────────────────────────────────
256
+ // DESSIN — À SURCHARGER OBLIGATOIREMENT
257
+ // ─────────────────────────────────────────
258
+
228
259
  /**
229
- * Méthode de dessin implémenter par les sous-classes)
260
+ * Dessine le composant sur le canvas.
261
+ * **Méthode abstraite** — doit être implémentée par chaque sous-classe.
262
+ *
263
+ * @abstract
264
+ * @param {CanvasRenderingContext2D} ctx - Contexte 2D du canvas
230
265
  */
231
266
  draw(ctx) {
232
- // À implémenter par les sous-classes
267
+ // Méthode abstraite — implémenter dans la sous-classe.
268
+ // Ne jamais appeler super.draw(ctx) depuis une sous-classe.
233
269
  }
234
- }
235
-
236
-
237
- export default Component;
238
-
239
-
240
-
241
270
 
271
+ // ─────────────────────────────────────────
272
+ // DESTROY
273
+ // ─────────────────────────────────────────
242
274
 
275
+ /**
276
+ * Libère les ressources du composant.
277
+ *
278
+ * Les sous-classes qui utilisent setInterval, requestAnimationFrame,
279
+ * des EventListeners DOM ou d'autres ressources externes **doivent**
280
+ * surcharger cette méthode et appeler super.destroy() à la fin.
281
+ *
282
+ * Le flag `this._destroyed` est positionné à true AVANT les appels
283
+ * internes : les boucles d'animation doivent tester ce flag pour s'arrêter.
284
+ *
285
+ * @example
286
+ * destroy() {
287
+ * // annuler propre RAF ou timer ici
288
+ * cancelAnimationFrame(this._rafId);
289
+ * super.destroy();
290
+ * }
291
+ */
292
+ destroy() {
293
+ this._destroyed = true;
294
+ this._unmount();
295
+ this.removeAllListeners();
296
+ this.onClick = null;
297
+ this.onPress = null;
298
+ }
299
+ }
243
300
 
301
+ export default Component;