canvasframework 0.5.39 → 0.5.41

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.
@@ -0,0 +1,232 @@
1
+ import Component from '../core/Component.js';
2
+
3
+ /**
4
+ * Container avec système de layout et élévation
5
+ * @class
6
+ * @extends Component
7
+ */
8
+ class Cards extends Component {
9
+ constructor(framework, options = {}, children = []) {
10
+ super(framework, options);
11
+ this.children = [];
12
+ this.padding = options.padding || 0;
13
+ this.gap = options.gap || 0;
14
+ this.direction = options.direction || 'column';
15
+ this.align = options.align || 'start';
16
+ this.bgColor = options.bgColor || 'transparent';
17
+ this.borderRadius = options.borderRadius || 0;
18
+ this.elevation = options.elevation || 0;
19
+ this.shadowColor = options.shadowColor || 'rgba(0,0,0,0.15)';
20
+ this._applyElevationStyles();
21
+
22
+ // Mode de positionnement
23
+ this.positionMode = options.positionMode || 'flow';
24
+
25
+ // Stocker les positions relatives des enfants
26
+ this.childRelativePositions = new Map();
27
+
28
+ // Ajouter les enfants passés dans le constructeur
29
+ if (children && children.length > 0) {
30
+ this.addChildren(children);
31
+ }
32
+ }
33
+
34
+ _applyElevationStyles() {
35
+ const elevationStyles = {
36
+ 0: { blur: 0, offsetY: 0, spread: 0, opacity: 0 },
37
+ 1: { blur: 2, offsetY: 1, spread: 0, opacity: 0.1 },
38
+ 2: { blur: 4, offsetY: 2, spread: 1, opacity: 0.15 },
39
+ 3: { blur: 8, offsetY: 4, spread: 2, opacity: 0.2 },
40
+ 4: { blur: 16, offsetY: 8, spread: 3, opacity: 0.25 },
41
+ 5: { blur: 24, offsetY: 12, spread: 4, opacity: 0.3 }
42
+ };
43
+
44
+ const style = elevationStyles[Math.min(this.elevation, 5)] || elevationStyles[0];
45
+
46
+ this.shadowBlur = style.blur;
47
+ this.shadowOffsetY = style.offsetY;
48
+ this.shadowSpread = style.spread;
49
+ this.shadowOpacity = style.opacity;
50
+
51
+ this._updateShadowColor();
52
+ }
53
+
54
+ _updateShadowColor() {
55
+ const rgbMatch = this.shadowColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
56
+ if (rgbMatch) {
57
+ const r = rgbMatch[1];
58
+ const g = rgbMatch[2];
59
+ const b = rgbMatch[3];
60
+ this._computedShadowColor = `rgba(${r}, ${g}, ${b}, ${this.shadowOpacity})`;
61
+ } else {
62
+ this._computedShadowColor = this.shadowColor;
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Ajoute plusieurs enfants
68
+ * @param {Component[]} children - Tableau d'enfants
69
+ */
70
+ addChildren(children) {
71
+ for (const child of children) {
72
+ this.add(child);
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Ajoute un enfant
78
+ * @param {Component} child - Composant enfant
79
+ * @returns {Component} L'enfant ajouté
80
+ */
81
+ add(child) {
82
+ this.children.push(child);
83
+
84
+ // Si l'enfant a des coordonnées x,y, on les interprète comme relatives au container
85
+ if (child.x !== undefined && child.y !== undefined) {
86
+ // Stocker les positions RELATIVES par rapport au container
87
+ this.childRelativePositions.set(child, {
88
+ relativeX: child.x, // x relatif au container
89
+ relativeY: child.y // y relatif au container
90
+ });
91
+
92
+ // Calculer la position absolue immédiatement
93
+ this.updateChildPosition(child);
94
+ }
95
+
96
+ return child;
97
+ }
98
+
99
+ /**
100
+ * Met à jour la position d'un enfant en fonction de la position du container
101
+ * @param {Component} child - L'enfant à positionner
102
+ * @private
103
+ */
104
+ updateChildPosition(child) {
105
+ const relativePos = this.childRelativePositions.get(child);
106
+
107
+ if (relativePos) {
108
+ // Position ABSOLUE = position container + position relative + padding
109
+ child.x = this.x + this.padding + relativePos.relativeX;
110
+ child.y = this.y + this.padding + relativePos.relativeY;
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Met à jour la position de tous les enfants quand le container bouge
116
+ * @override
117
+ */
118
+ setPosition(x, y) {
119
+ super.setPosition(x, y);
120
+ this.updateAllChildrenPositions();
121
+ }
122
+
123
+ /**
124
+ * Met à jour toutes les positions des enfants
125
+ * @private
126
+ */
127
+ updateAllChildrenPositions() {
128
+ for (const child of this.children) {
129
+ this.updateChildPosition(child);
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Change la position relative d'un enfant dans le container
135
+ * @param {Component} child - L'enfant à repositionner
136
+ * @param {number} relativeX - Nouvelle position X relative
137
+ * @param {number} relativeY - Nouvelle position Y relative
138
+ */
139
+ setChildPosition(child, relativeX, relativeY) {
140
+ this.childRelativePositions.set(child, {
141
+ relativeX: relativeX,
142
+ relativeY: relativeY
143
+ });
144
+ this.updateChildPosition(child);
145
+ }
146
+
147
+ draw(ctx) {
148
+ ctx.save();
149
+
150
+ // Dessiner l'ombre
151
+ if (this.elevation > 0 && this.bgColor !== 'transparent') {
152
+ this.drawShadow(ctx);
153
+ }
154
+
155
+ // Dessiner le fond
156
+ if (this.bgColor !== 'transparent') {
157
+ ctx.fillStyle = this.bgColor;
158
+ if (this.borderRadius > 0) {
159
+ ctx.beginPath();
160
+ this.roundRect(ctx, this.x, this.y, this.width, this.height, this.borderRadius);
161
+ ctx.fill();
162
+ } else {
163
+ ctx.fillRect(this.x, this.y, this.width, this.height);
164
+ }
165
+ }
166
+
167
+ // Dessiner les enfants
168
+ for (let child of this.children) {
169
+ if (child.visible) child.draw(ctx);
170
+ }
171
+
172
+ ctx.restore();
173
+ }
174
+
175
+ drawShadow(ctx) {
176
+ ctx.save();
177
+
178
+ ctx.shadowColor = this._computedShadowColor;
179
+ ctx.shadowBlur = this.shadowBlur;
180
+ ctx.shadowOffsetX = 0;
181
+ ctx.shadowOffsetY = this.shadowOffsetY;
182
+
183
+ ctx.fillStyle = this.bgColor;
184
+
185
+ if (this.borderRadius > 0) {
186
+ ctx.beginPath();
187
+ const spread = this.shadowSpread;
188
+ this.roundRect(
189
+ ctx,
190
+ this.x - spread/2,
191
+ this.y - spread/2,
192
+ this.width + spread,
193
+ this.height + spread,
194
+ this.borderRadius + spread/2
195
+ );
196
+ ctx.fill();
197
+ } else {
198
+ const spread = this.shadowSpread;
199
+ ctx.fillRect(
200
+ this.x - spread/2,
201
+ this.y - spread/2,
202
+ this.width + spread,
203
+ this.height + spread
204
+ );
205
+ }
206
+
207
+ ctx.restore();
208
+ }
209
+
210
+ roundRect(ctx, x, y, width, height, radius) {
211
+ ctx.beginPath();
212
+ ctx.moveTo(x + radius, y);
213
+ ctx.lineTo(x + width - radius, y);
214
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
215
+ ctx.lineTo(x + width, y + height - radius);
216
+ ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
217
+ ctx.lineTo(x + radius, y + height);
218
+ ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
219
+ ctx.lineTo(x, y + radius);
220
+ ctx.quadraticCurveTo(x, y, x + radius, y);
221
+ ctx.closePath();
222
+ }
223
+
224
+ isPointInside(x, y) {
225
+ return x >= this.x &&
226
+ x <= this.x + this.width &&
227
+ y >= this.y &&
228
+ y <= this.y + this.height;
229
+ }
230
+ }
231
+
232
+ export default Cards;
@@ -4,7 +4,8 @@ import Input from '../components/Input.js';
4
4
  import Slider from '../components/Slider.js';
5
5
  import Text from '../components/Text.js';
6
6
  import View from '../components/View.js';
7
- import Card from '../components/Card.js';
7
+ //import Card from '../components/Card.js';
8
+ import Cards from '../components/Cards.js';
8
9
  import FAB from '../components/FAB.js';
9
10
  import SpeedDialFAB from '../components/SpeedDialFAB.js';
10
11
  import MorphingFAB from '../components/MorphingFAB.js';
@@ -2233,7 +2234,7 @@ class CanvasFramework {
2233
2234
  if (comp.visible) {
2234
2235
  const adjustedY = isFixedComponent(comp) ? y : y - this.scrollOffset;
2235
2236
 
2236
- if (comp instanceof Card && comp.clickableChildren && comp.children && comp.children.length > 0) {
2237
+ /* if (comp instanceof Card && comp.clickableChildren && comp.children && comp.children.length > 0) {
2237
2238
  if (comp.isPointInside(x, adjustedY)) {
2238
2239
  const cardAdjustedY = adjustedY - comp.y - comp.padding;
2239
2240
  const cardAdjustedX = x - comp.x - comp.padding;
@@ -2300,7 +2301,7 @@ class CanvasFramework {
2300
2301
  }
2301
2302
  }
2302
2303
  }
2303
- }
2304
+ }*/
2304
2305
 
2305
2306
  if (comp.isPointInside(x, adjustedY)) {
2306
2307
  switch (eventType) {
package/core/UIBuilder.js CHANGED
@@ -5,7 +5,7 @@ import Input from '../components/Input.js';
5
5
  import Slider from '../components/Slider.js';
6
6
  import Text from '../components/Text.js';
7
7
  import View from '../components/View.js';
8
- import Card from '../components/Card.js';
8
+ import Cards from '../components/Cards.js';
9
9
  import FAB from '../components/FAB.js';
10
10
  import SpeedDialFAB from '../components/SpeedDialFAB.js';
11
11
  import MorphingFAB from '../components/MorphingFAB.js';
@@ -82,7 +82,7 @@ const Components = {
82
82
  Slider,
83
83
  Text,
84
84
  View,
85
- Card,
85
+ Cards,
86
86
  FAB,
87
87
  SpeedDialFAB,
88
88
  MorphingFAB,
package/index.js CHANGED
@@ -12,7 +12,7 @@ export { default as Input } from './components/Input.js';
12
12
  export { default as Slider } from './components/Slider.js';
13
13
  export { default as Text } from './components/Text.js';
14
14
  export { default as View } from './components/View.js';
15
- export { default as Card } from './components/Card.js';
15
+ export { default as Cards } from './components/Cards.js';
16
16
  export { default as FAB } from './components/FAB.js';
17
17
  export { default as SpeedDialFAB } from './components/SpeedDialFAB.js';
18
18
  export { default as MorphingFAB } from './components/MorphingFAB.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvasframework",
3
- "version": "0.5.39",
3
+ "version": "0.5.41",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/beyons/CanvasFramework.git"
@@ -1,763 +0,0 @@
1
- import Component from '../core/Component.js';
2
-
3
- /**
4
- * Container avec système de layout et effet d'élévation
5
- * @class
6
- * @extends Component
7
- * @property {Component[]} children - Enfants
8
- * @property {number} padding - Padding interne
9
- * @property {number} gap - Espacement constant entre enfants
10
- * @property {string} direction - Direction ('column' ou 'row')
11
- * @property {string} align - Alignement ('start', 'center', 'end', 'stretch')
12
- * @property {string} bgColor - Couleur de fond
13
- * @property {number} borderRadius - Rayon des coins
14
- * @property {number} elevation - Niveau d'élévation (ombres)
15
- * @property {boolean} autoLayout - Active le layout automatique
16
- */
17
- class Card extends Component {
18
- /**
19
- * Crée une instance de Card
20
- * @param {CanvasFramework} framework - Framework parent
21
- * @param {Object} [options={}] - Options de configuration
22
- * @param {number} [options.padding=0] - Padding interne
23
- * @param {number} [options.gap=0] - Espacement constant entre enfants
24
- * @param {string} [options.direction='column'] - Direction
25
- * @param {string} [options.align='start'] - Alignement
26
- * @param {string} [options.bgColor='transparent'] - Couleur de fond
27
- * @param {number} [options.borderRadius=0] - Rayon des coins
28
- * @param {number} [options.elevation=0] - Niveau d'élévation (0-5)
29
- * @param {boolean} [options.autoLayout=true] - Active le layout automatique
30
- */
31
- constructor(framework, options = {}) {
32
- super(framework, options);
33
- this.children = [];
34
- this.padding = options.padding || 0;
35
- this.gap = options.gap || 0;
36
- this.direction = options.direction || 'column';
37
- this.align = options.align || 'start';
38
- this.bgColor = options.bgColor || 'transparent';
39
- this.borderRadius = options.borderRadius || 0;
40
- this.elevation = options.elevation || 0;
41
- this.autoLayout = options.autoLayout !== undefined ? options.autoLayout : true;
42
-
43
- // Stocker les positions relatives des enfants
44
- this.childPositions = new Map();
45
-
46
- // NOUVEAU: Gestion des événements
47
- this._canvas = null;
48
- this._isTouchDevice = false;
49
- this._clickListeners = new Map();
50
- this._touchListeners = new Map();
51
- this._enabled = options.enabled !== undefined ? options.enabled : true;
52
- this._interactive = options.interactive !== undefined ? options.interactive : true;
53
-
54
- // NOUVEAU: Pour le hot reload, nettoyer les anciens événements
55
- if (typeof window !== 'undefined') {
56
- this._setupEventCleanup();
57
- }
58
- }
59
-
60
- // NOUVEAU: Initialisation des événements
61
- _initEvents() {
62
- if (!this.framework || !this.framework.canvas) return;
63
-
64
- this._canvas = this.framework.canvas;
65
-
66
- // Détecter si on est sur mobile/touch
67
- this._isTouchDevice = 'ontouchstart' in window ||
68
- navigator.maxTouchPoints > 0 ||
69
- navigator.msMaxTouchPoints > 0;
70
-
71
- // Écouter les événements sur le canvas
72
- this._canvas.addEventListener('click', this._handleCanvasClick.bind(this));
73
- this._canvas.addEventListener('touchstart', this._handleCanvasTouch.bind(this), { passive: false });
74
- this._canvas.addEventListener('touchend', this._handleCanvasTouchEnd.bind(this), { passive: false });
75
- }
76
-
77
- // NOUVEAU: Nettoyage pour hot reload Vite
78
- _setupEventCleanup() {
79
- // Avant que la page ne soit déchargée (pour hot reload)
80
- window.addEventListener('beforeunload', () => {
81
- this._cleanupEvents();
82
- });
83
- }
84
-
85
- // NOUVEAU: Nettoyer les événements
86
- _cleanupEvents() {
87
- if (this._canvas) {
88
- this._canvas.removeEventListener('click', this._handleCanvasClick);
89
- this._canvas.removeEventListener('touchstart', this._handleCanvasTouch);
90
- this._canvas.removeEventListener('touchend', this._handleCanvasTouchEnd);
91
- }
92
- this._clickListeners.clear();
93
- this._touchListeners.clear();
94
- }
95
-
96
- // NOUVEAU: Convertir les coordonnées du DOM vers canvas
97
- _getCanvasCoordinates(clientX, clientY) {
98
- if (!this._canvas) return { x: 0, y: 0 };
99
-
100
- const rect = this._canvas.getBoundingClientRect();
101
- const scaleX = this._canvas.width / rect.width;
102
- const scaleY = this._canvas.height / rect.height;
103
-
104
- return {
105
- x: (clientX - rect.left) * scaleX,
106
- y: (clientY - rect.top) * scaleY
107
- };
108
- }
109
-
110
- // NOUVEAU: Gérer les clics sur le canvas
111
- _handleCanvasClick(event) {
112
- if (!this._enabled || !this.visible || !this._interactive) return;
113
-
114
- const coords = this._getCanvasCoordinates(event.clientX, event.clientY);
115
-
116
- // Vérifier si le clic est sur cette carte
117
- if (this.isPointInside(coords.x, coords.y)) {
118
- // Empêcher le comportement par défaut
119
- event.preventDefault();
120
- event.stopPropagation();
121
-
122
- // Déclencher l'événement onClick de la carte
123
- if (this.onClick) {
124
- const localCoords = {
125
- x: coords.x - this.x,
126
- y: coords.y - this.y
127
- };
128
- this.onClick({
129
- type: 'click',
130
- x: localCoords.x,
131
- y: localCoords.y,
132
- clientX: event.clientX,
133
- clientY: event.clientY,
134
- originalEvent: event,
135
- target: this
136
- });
137
- }
138
-
139
- // Vérifier si un enfant est cliqué
140
- this._propagateClickToChildren(coords.x, coords.y, event);
141
- }
142
- }
143
-
144
- // NOUVEAU: Gérer les touches tactiles
145
- _handleCanvasTouch(event) {
146
- if (!this._enabled || !this.visible || !this._interactive) return;
147
-
148
- if (event.touches.length > 0) {
149
- event.preventDefault();
150
- event.stopPropagation();
151
-
152
- const touch = event.touches[0];
153
- const coords = this._getCanvasCoordinates(touch.clientX, touch.clientY);
154
-
155
- // Stocker la position de départ du touch
156
- this._lastTouchStart = { x: coords.x, y: coords.y };
157
-
158
- // Vérifier si le touch est sur cette carte
159
- if (this.isPointInside(coords.x, coords.y)) {
160
- // Déclencher l'événement onTouchStart de la carte
161
- if (this.onTouchStart) {
162
- const localCoords = {
163
- x: coords.x - this.x,
164
- y: coords.y - this.y
165
- };
166
- this.onTouchStart({
167
- type: 'touchstart',
168
- x: localCoords.x,
169
- y: localCoords.y,
170
- clientX: touch.clientX,
171
- clientY: touch.clientY,
172
- originalEvent: event,
173
- target: this
174
- });
175
- }
176
- }
177
- }
178
- }
179
-
180
- // NOUVEAU: Gérer la fin des touches tactiles
181
- _handleCanvasTouchEnd(event) {
182
- if (!this._enabled || !this.visible || !this._interactive) return;
183
-
184
- event.preventDefault();
185
- event.stopPropagation();
186
-
187
- if (event.changedTouches.length > 0) {
188
- const touch = event.changedTouches[0];
189
- const coords = this._getCanvasCoordinates(touch.clientX, touch.clientY);
190
-
191
- // Vérifier si le touch end est sur cette carte
192
- if (this.isPointInside(coords.x, coords.y)) {
193
- // Déclencher l'événement onTouchEnd de la carte
194
- if (this.onTouchEnd) {
195
- const localCoords = {
196
- x: coords.x - this.x,
197
- y: coords.y - this.y
198
- };
199
- this.onTouchEnd({
200
- type: 'touchend',
201
- x: localCoords.x,
202
- y: localCoords.y,
203
- clientX: touch.clientX,
204
- clientY: touch.clientY,
205
- originalEvent: event,
206
- target: this
207
- });
208
- }
209
-
210
- // Simuler un clic si le touch start était sur la même position
211
- if (this._lastTouchStart &&
212
- Math.abs(coords.x - this._lastTouchStart.x) < 10 &&
213
- Math.abs(coords.y - this._lastTouchStart.y) < 10) {
214
- this._propagateClickToChildren(coords.x, coords.y, event);
215
- }
216
-
217
- this._lastTouchStart = null;
218
- }
219
- }
220
- }
221
-
222
- // NOUVEAU: Propager le clic aux enfants
223
- _propagateClickToChildren(canvasX, canvasY, originalEvent) {
224
- // Parcourir les enfants du dernier au premier (z-order)
225
- for (let i = this.children.length - 1; i >= 0; i--) {
226
- const child = this.children[i];
227
-
228
- // Vérifier si l'enfant est visible et interactif
229
- const childEnabled = child._enabled !== undefined ? child._enabled : true;
230
- const childInteractive = child._interactive !== undefined ? child._interactive : true;
231
-
232
- if (child.visible && childEnabled && childInteractive &&
233
- child.isPointInside(canvasX, canvasY)) {
234
-
235
- // Déclencher l'événement onClick de l'enfant
236
- if (child.onClick) {
237
- const localCoords = {
238
- x: canvasX - child.x,
239
- y: canvasY - child.y
240
- };
241
- child.onClick({
242
- type: 'click',
243
- x: localCoords.x,
244
- y: localCoords.y,
245
- clientX: originalEvent.clientX,
246
- clientY: originalEvent.clientY,
247
- originalEvent: originalEvent,
248
- target: child
249
- });
250
- }
251
-
252
- // Stopper la propagation après avoir trouvé un enfant cliqué
253
- break;
254
- }
255
- }
256
- }
257
-
258
- /**
259
- * Ajoute un enfant (position relative convertie en absolue)
260
- * @param {Component} child - Composant enfant
261
- * @returns {Component} L'enfant ajouté
262
- */
263
- add(child) {
264
- this.children.push(child);
265
-
266
- // CONVERTIR les positions relatives en positions absolues
267
- child.x = this.x + child.x;
268
- child.y = this.y + child.y;
269
-
270
- // Stocker la position relative originale
271
- this.childPositions.set(child, {
272
- x: child.x - this.x,
273
- y: child.y - this.y
274
- });
275
-
276
- // NOUVEAU: Initialiser les événements si c'est la première fois
277
- if (this.children.length === 1 && this._canvas === null) {
278
- this._initEvents();
279
- }
280
-
281
- // Si autoLayout est activé, organiser automatiquement
282
- if (this.autoLayout) {
283
- this.layout();
284
- }
285
-
286
- return child;
287
- }
288
-
289
- /**
290
- * Supprime un enfant
291
- * @param {Component} child - Composant enfant à supprimer
292
- * @returns {boolean} True si l'enfant a été supprimé
293
- */
294
- remove(child) {
295
- const index = this.children.indexOf(child);
296
- if (index > -1) {
297
- this.children.splice(index, 1);
298
- this.childPositions.delete(child);
299
- if (this.autoLayout) this.layout();
300
- return true;
301
- }
302
- return false;
303
- }
304
-
305
- /**
306
- * Supprime tous les enfants
307
- */
308
- clear() {
309
- this.children = [];
310
- this.childPositions.clear();
311
- }
312
-
313
- /**
314
- * Organise les enfants selon le layout
315
- * @private
316
- */
317
- layout() {
318
- if (this.children.length === 0 || !this.autoLayout) return;
319
-
320
- if (this.direction === 'column') {
321
- let currentY = this.padding;
322
-
323
- for (let i = 0; i < this.children.length; i++) {
324
- const child = this.children[i];
325
-
326
- // Calculer la position X selon l'alignement
327
- let childX = this.padding;
328
- if (this.align === 'center') {
329
- childX = (this.width - child.width) / 2;
330
- } else if (this.align === 'end') {
331
- childX = this.width - child.width - this.padding;
332
- } else if (this.align === 'stretch') {
333
- childX = this.padding;
334
- child.width = this.width - (this.padding * 2);
335
- }
336
-
337
- // Positionner l'enfant RELATIVEMENT à la Card
338
- child.x = this.x + childX;
339
- child.y = this.y + currentY;
340
-
341
- // Stocker la position relative
342
- this.childPositions.set(child, { x: childX, y: currentY });
343
-
344
- // Mettre à jour la position Y pour l'enfant suivant
345
- currentY += child.height;
346
-
347
- // Ajouter le gap seulement si ce n'est pas le dernier enfant
348
- if (i < this.children.length - 1) {
349
- currentY += this.gap;
350
- }
351
- }
352
- } else {
353
- // Direction 'row'
354
- let currentX = this.padding;
355
-
356
- for (let i = 0; i < this.children.length; i++) {
357
- const child = this.children[i];
358
-
359
- // Calculer la position Y selon l'alignement
360
- let childY = this.padding;
361
- if (this.align === 'center') {
362
- childY = (this.height - child.height) / 2;
363
- } else if (this.align === 'end') {
364
- childY = this.height - child.height - this.padding;
365
- } else if (this.align === 'stretch') {
366
- childY = this.padding;
367
- child.height = this.height - (this.padding * 2);
368
- }
369
-
370
- // Positionner l'enfant RELATIVEMENT à la Card
371
- child.x = this.x + currentX;
372
- child.y = this.y + childY;
373
-
374
- // Stocker la position relative
375
- this.childPositions.set(child, { x: currentX, y: childY });
376
-
377
- // Mettre à jour la position X pour l'enfant suivant
378
- currentX += child.width;
379
-
380
- // Ajouter le gap seulement si ce n'est pas le dernier enfant
381
- if (i < this.children.length - 1) {
382
- currentX += this.gap;
383
- }
384
- }
385
- }
386
- }
387
-
388
- /**
389
- * Met à jour la position de la carte et ajuste les enfants
390
- * @param {number} x - Nouvelle position X
391
- * @param {number} y - Nouvelle position Y
392
- */
393
- setPosition(x, y) {
394
- const deltaX = x - this.x;
395
- const deltaY = y - this.y;
396
-
397
- super.setPosition(x, y);
398
-
399
- // Déplacer tous les enfants avec la carte
400
- for (let child of this.children) {
401
- child.x += deltaX;
402
- child.y += deltaY;
403
- }
404
- }
405
-
406
- /**
407
- * Définit la position d'un enfant dans le système de coordonnées de la Card
408
- * @param {Component} child - L'enfant à positionner
409
- * @param {number} relativeX - Position X relative à la Card
410
- * @param {number} relativeY - Position Y relative à la Card
411
- */
412
- setChildPosition(child, relativeX, relativeY) {
413
- if (this.children.includes(child)) {
414
- child.x = this.x + relativeX;
415
- child.y = this.y + relativeY;
416
- this.childPositions.set(child, { x: relativeX, y: relativeY });
417
- }
418
- }
419
-
420
- /**
421
- * Active/désactive le layout automatique
422
- * @param {boolean} enabled - True pour activer le layout automatique
423
- */
424
- setAutoLayout(enabled) {
425
- this.autoLayout = enabled;
426
- if (enabled) this.layout();
427
- }
428
-
429
- /**
430
- * Force un recalcul du layout
431
- */
432
- updateLayout() {
433
- this.layout();
434
- }
435
-
436
- /**
437
- * Génère les paramètres d'ombre selon le niveau d'élévation
438
- * @param {number} elevation - Niveau d'élévation (0-5)
439
- * @returns {Object} Configuration de l'ombre
440
- * @private
441
- */
442
- getShadowConfig(elevation) {
443
- const shadows = [
444
- { blur: 0, offsetY: 0, color: 'transparent', spread: 0 },
445
- { blur: 2, offsetY: 1, color: 'rgba(0,0,0,0.12)', spread: 0 },
446
- { blur: 3, offsetY: 1, color: 'rgba(0,0,0,0.14)', spread: 0 },
447
- { blur: 4, offsetY: 2, color: 'rgba(0,0,0,0.16)', spread: 0 },
448
- { blur: 6, offsetY: 3, color: 'rgba(0,0,0,0.18)', spread: 0 },
449
- { blur: 8, offsetY: 4, color: 'rgba(0,0,0,0.20)', spread: 0 },
450
- ];
451
-
452
- return shadows[Math.min(elevation, shadows.length - 1)];
453
- }
454
-
455
- /**
456
- * Dessine l'effet d'ombre selon l'élévation
457
- * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
458
- * @private
459
- */
460
- drawShadow(ctx) {
461
- if (this.elevation <= 0) return;
462
-
463
- const shadow = this.getShadowConfig(this.elevation);
464
-
465
- ctx.save();
466
-
467
- ctx.shadowColor = shadow.color;
468
- ctx.shadowBlur = shadow.blur;
469
- ctx.shadowOffsetX = 0;
470
- ctx.shadowOffsetY = shadow.offsetY;
471
-
472
- if (this.borderRadius > 0) {
473
- ctx.beginPath();
474
- this.roundRect(ctx, this.x, this.y + shadow.offsetY, this.width, this.height, this.borderRadius);
475
- ctx.fillStyle = this.bgColor === 'transparent' ? 'white' : this.bgColor;
476
- ctx.fill();
477
- } else {
478
- ctx.fillStyle = this.bgColor === 'transparent' ? 'white' : this.bgColor;
479
- ctx.fillRect(this.x, this.y + shadow.offsetY, this.width, this.height);
480
- }
481
-
482
- ctx.restore();
483
- }
484
-
485
- /**
486
- * Dessine la carte et ses enfants
487
- * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
488
- */
489
- draw(ctx) {
490
- ctx.save();
491
-
492
- // Dessiner l'ombre si elevation > 0
493
- if (this.elevation > 0) {
494
- this.drawShadow(ctx);
495
- }
496
-
497
- // Dessiner le fond de la carte
498
- if (this.bgColor !== 'transparent') {
499
- ctx.fillStyle = this.bgColor;
500
- if (this.borderRadius > 0) {
501
- ctx.beginPath();
502
- this.roundRect(ctx, this.x, this.y, this.width, this.height, this.borderRadius);
503
- ctx.fill();
504
- } else {
505
- ctx.fillRect(this.x, this.y, this.width, this.height);
506
- }
507
- }
508
-
509
- // Dessiner les enfants
510
- for (let child of this.children) {
511
- if (child.visible) child.draw(ctx);
512
- }
513
-
514
- ctx.restore();
515
- }
516
-
517
- /**
518
- * Dessine un rectangle avec coins arrondis
519
- * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
520
- * @param {number} x - Position X
521
- * @param {number} y - Position Y
522
- * @param {number} width - Largeur
523
- * @param {number} height - Hauteur
524
- * @param {number} radius - Rayon des coins
525
- * @private
526
- */
527
- roundRect(ctx, x, y, width, height, radius) {
528
- if (radius === 0) {
529
- ctx.rect(x, y, width, height);
530
- return;
531
- }
532
-
533
- ctx.beginPath();
534
- ctx.moveTo(x + radius, y);
535
- ctx.lineTo(x + width - radius, y);
536
- ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
537
- ctx.lineTo(x + width, y + height - radius);
538
- ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
539
- ctx.lineTo(x + radius, y + height);
540
- ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
541
- ctx.lineTo(x, y + radius);
542
- ctx.quadraticCurveTo(x, y, x + radius, y);
543
- ctx.closePath();
544
- }
545
-
546
- /**
547
- * Vérifie si un point est dans les limites
548
- * @param {number} x - Coordonnée X
549
- * @param {number} y - Coordonnée Y
550
- * @returns {boolean} True si le point est dans la vue
551
- */
552
- isPointInside(x, y) {
553
- // Pour les rectangles arrondis, vérification plus précise
554
- if (this.borderRadius > 0) {
555
- return this._isPointInRoundedRect(x, y);
556
- }
557
-
558
- // Pour les rectangles normaux
559
- return x >= this.x &&
560
- x <= this.x + this.width &&
561
- y >= this.y &&
562
- y <= this.y + this.height;
563
- }
564
-
565
- // NOUVEAU: Vérifier si un point est dans un rectangle arrondi
566
- _isPointInRoundedRect(x, y) {
567
- const rectX = this.x;
568
- const rectY = this.y;
569
- const width = this.width;
570
- const height = this.height;
571
- const radius = this.borderRadius;
572
-
573
- // Vérifier la zone centrale (sans les coins arrondis)
574
- if (x >= rectX + radius && x <= rectX + width - radius &&
575
- y >= rectY && y <= rectY + height) {
576
- return true;
577
- }
578
- if (x >= rectX && x <= rectX + width &&
579
- y >= rectY + radius && y <= rectY + height - radius) {
580
- return true;
581
- }
582
-
583
- // Vérifier les quatre coins arrondis
584
- const checkCorner = (cornerX, cornerY) => {
585
- const dx = x - cornerX;
586
- const dy = y - cornerY;
587
- return (dx * dx + dy * dy) <= (radius * radius);
588
- };
589
-
590
- // Coin supérieur gauche
591
- if (x < rectX + radius && y < rectY + radius) {
592
- return checkCorner(rectX + radius, rectY + radius);
593
- }
594
- // Coin supérieur droit
595
- if (x > rectX + width - radius && y < rectY + radius) {
596
- return checkCorner(rectX + width - radius, rectY + radius);
597
- }
598
- // Coin inférieur gauche
599
- if (x < rectX + radius && y > rectY + height - radius) {
600
- return checkCorner(rectX + radius, rectY + height - radius);
601
- }
602
- // Coin inférieur droit
603
- if (x > rectX + width - radius && y > rectY + height - radius) {
604
- return checkCorner(rectX + width - radius, rectY + height - radius);
605
- }
606
-
607
- return false;
608
- }
609
-
610
- /**
611
- * Définit le niveau d'élévation
612
- * @param {number} elevation - Nouveau niveau d'élévation (0-5)
613
- */
614
- setElevation(elevation) {
615
- this.elevation = Math.max(0, Math.min(elevation, 5));
616
- }
617
-
618
- /**
619
- * Augmente le niveau d'élévation
620
- */
621
- raise() {
622
- this.setElevation(this.elevation + 1);
623
- }
624
-
625
- /**
626
- * Réduit le niveau d'élévation
627
- */
628
- lower() {
629
- this.setElevation(this.elevation - 1);
630
- }
631
-
632
- /**
633
- * Définit l'espacement entre enfants
634
- * @param {number} gap - Nouvel espacement
635
- */
636
- setGap(gap) {
637
- this.gap = Math.max(0, gap);
638
- if (this.autoLayout) this.layout();
639
- }
640
-
641
- /**
642
- * Définit le padding
643
- * @param {number} padding - Nouveau padding
644
- */
645
- setPadding(padding) {
646
- this.padding = Math.max(0, padding);
647
- if (this.autoLayout) this.layout();
648
- }
649
-
650
- /**
651
- * Définit la direction du layout
652
- * @param {string} direction - 'column' ou 'row'
653
- */
654
- setDirection(direction) {
655
- if (direction === 'column' || direction === 'row') {
656
- this.direction = direction;
657
- if (this.autoLayout) this.layout();
658
- }
659
- }
660
-
661
- /**
662
- * Définit l'alignement
663
- * @param {string} align - 'start', 'center', 'end' ou 'stretch'
664
- */
665
- setAlign(align) {
666
- if (['start', 'center', 'end', 'stretch'].includes(align)) {
667
- this.align = align;
668
- if (this.autoLayout) this.layout();
669
- }
670
- }
671
-
672
- /**
673
- * Calcule la hauteur totale nécessaire pour contenir tous les enfants
674
- * @returns {number} Hauteur totale nécessaire
675
- */
676
- getTotalHeight() {
677
- if (this.children.length === 0) return 0;
678
-
679
- if (this.direction === 'column') {
680
- const totalChildrenHeight = this.children.reduce((sum, child) => sum + child.height, 0);
681
- const totalGapHeight = this.gap * Math.max(0, this.children.length - 1);
682
- return totalChildrenHeight + totalGapHeight + (this.padding * 2);
683
- }
684
- return this.height;
685
- }
686
-
687
- /**
688
- * Calcule la largeur totale nécessaire pour contenir tous les enfants
689
- * @returns {number} Largeur totale nécessaire
690
- */
691
- getTotalWidth() {
692
- if (this.children.length === 0) return 0;
693
-
694
- if (this.direction === 'row') {
695
- const totalChildrenWidth = this.children.reduce((sum, child) => sum + child.width, 0);
696
- const totalGapWidth = this.gap * Math.max(0, this.children.length - 1);
697
- return totalChildrenWidth + totalGapWidth + (this.padding * 2);
698
- }
699
- return this.width;
700
- }
701
-
702
- /**
703
- * Ajuste automatiquement la hauteur de la carte pour contenir tous les enfants
704
- */
705
- fitHeight() {
706
- if (this.direction === 'column') {
707
- this.height = this.getTotalHeight();
708
- }
709
- }
710
-
711
- /**
712
- * Ajuste automatiquement la largeur de la carte pour contenir tous les enfants
713
- */
714
- fitWidth() {
715
- if (this.direction === 'row') {
716
- this.width = this.getTotalWidth();
717
- }
718
- }
719
-
720
- /**
721
- * Ajuste automatiquement les dimensions de la carte pour contenir tous les enfants
722
- */
723
- fitSize() {
724
- this.fitWidth();
725
- this.fitHeight();
726
- }
727
-
728
- /**
729
- * Met à jour les dimensions et relayout si autoLayout est activé
730
- * @param {number} width - Nouvelle largeur
731
- * @param {number} height - Nouvelle hauteur
732
- */
733
- setSize(width, height) {
734
- super.setSize(width, height);
735
- if (this.autoLayout) this.layout();
736
- }
737
-
738
- /**
739
- * Obtient la position relative d'un enfant
740
- * @param {Component} child - L'enfant
741
- * @returns {Object|null} Position relative {x, y} ou null si non trouvé
742
- */
743
- getChildPosition(child) {
744
- return this.childPositions.get(child) || null;
745
- }
746
-
747
- // NOUVEAU: Méthodes pour activer/désactiver l'interactivité
748
- setEnabled(enabled) {
749
- this._enabled = enabled;
750
- }
751
-
752
- setInteractive(interactive) {
753
- this._interactive = interactive;
754
- }
755
-
756
- // NOUVEAU: Méthode de nettoyage
757
- destroy() {
758
- this._cleanupEvents();
759
- super.destroy();
760
- }
761
- }
762
-
763
- export default Card;