canvasframework 0.5.58 → 0.5.60

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,391 @@
1
+ import Component from '../core/Component.js';
2
+
3
+ /**
4
+ * Fil d'Ariane (Breadcrumb) avec variantes Material et Cupertino
5
+ * @class
6
+ * @extends Component
7
+ *
8
+ * Material: Texte avec séparateurs "/"
9
+ * Cupertino: Style iOS avec chevrons "<"
10
+ */
11
+ class Breadcrumb extends Component {
12
+ /**
13
+ * Crée une instance de Breadcrumb
14
+ * @param {CanvasFramework} framework - Framework parent
15
+ * @param {Object} [options={}] - Options de configuration
16
+ * @param {Array} [options.items=[]] - Liste des éléments {label, path}
17
+ * @param {string} [options.separator] - Séparateur personnalisé
18
+ * @param {number} [options.maxItems=5] - Nombre max d'items avant collapse
19
+ * @param {Function} [options.onItemClick] - Callback clic sur item
20
+ */
21
+ constructor(framework, options = {}) {
22
+ super(framework, options);
23
+
24
+ this.platform = framework.platform;
25
+ this.items = options.items || [];
26
+ this.maxItems = options.maxItems || 5;
27
+ this.onItemClick = options.onItemClick || (() => {});
28
+
29
+ // Séparateur selon plateforme
30
+ if (this.platform === 'material') {
31
+ this.separator = options.separator || '/';
32
+ this.fontSize = 14;
33
+ this.activeColor = '#6200EE';
34
+ this.inactiveColor = '#757575';
35
+ this.hoverColor = '#9C47FF';
36
+ } else {
37
+ this.separator = options.separator || '›';
38
+ this.fontSize = 17;
39
+ this.activeColor = '#007AFF';
40
+ this.inactiveColor = '#8E8E93';
41
+ this.hoverColor = '#0051D5';
42
+ }
43
+
44
+ // Hauteur fixe
45
+ this.height = this.platform === 'material' ? 40 : 44;
46
+
47
+ // État hover/pressed
48
+ this.hoveredIndex = null;
49
+ this.pressedIndex = null;
50
+
51
+ // Zones cliquables
52
+ this.clickableAreas = [];
53
+ }
54
+
55
+ /**
56
+ * Obtient les items à afficher (avec collapse si nécessaire)
57
+ * @returns {Array} Items à afficher
58
+ * @private
59
+ */
60
+ getDisplayItems() {
61
+ if (this.items.length <= this.maxItems) {
62
+ return this.items;
63
+ }
64
+
65
+ // Garder premier, dernier, et collapse le milieu
66
+ return [
67
+ this.items[0],
68
+ { label: '...', collapsed: true },
69
+ ...this.items.slice(-(this.maxItems - 2))
70
+ ];
71
+ }
72
+
73
+ /**
74
+ * Dessine le breadcrumb Material
75
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
76
+ * @private
77
+ */
78
+ drawMaterial(ctx) {
79
+ const displayItems = this.getDisplayItems();
80
+ let currentX = this.x + 16;
81
+
82
+ this.clickableAreas = [];
83
+
84
+ displayItems.forEach((item, index) => {
85
+ const isLast = index === displayItems.length - 1;
86
+ const isCollapsed = item.collapsed;
87
+ const isHovered = this.hoveredIndex === index;
88
+ const isPressed = this.pressedIndex === index;
89
+
90
+ // Texte de l'item
91
+ ctx.font = `${isLast ? '500' : '400'} ${this.fontSize}px Roboto, sans-serif`;
92
+ ctx.textAlign = 'left';
93
+ ctx.textBaseline = 'middle';
94
+
95
+ const textWidth = ctx.measureText(item.label).width;
96
+
97
+ // Background hover (sauf dernier et collapsed)
98
+ if (isHovered && !isLast && !isCollapsed) {
99
+ ctx.fillStyle = 'rgba(98, 0, 238, 0.08)';
100
+ this.roundRect(ctx, currentX - 4, this.y + 6, textWidth + 8, 28, 4);
101
+ ctx.fill();
102
+ }
103
+
104
+ // Couleur du texte
105
+ if (isLast) {
106
+ ctx.fillStyle = this.activeColor;
107
+ } else if (isPressed && !isCollapsed) {
108
+ ctx.fillStyle = this.darkenColor(this.inactiveColor);
109
+ } else if (isHovered && !isCollapsed) {
110
+ ctx.fillStyle = this.hoverColor;
111
+ } else {
112
+ ctx.fillStyle = this.inactiveColor;
113
+ }
114
+
115
+ // Texte
116
+ const textY = this.y + this.height / 2;
117
+ ctx.fillText(item.label, currentX, textY);
118
+
119
+ // Stocker zone cliquable (sauf dernier et collapsed)
120
+ if (!isLast && !isCollapsed) {
121
+ this.clickableAreas.push({
122
+ x: currentX - 4,
123
+ y: this.y,
124
+ width: textWidth + 8,
125
+ height: this.height,
126
+ index: index,
127
+ originalIndex: this.getOriginalIndex(index)
128
+ });
129
+ }
130
+
131
+ currentX += textWidth + 12;
132
+
133
+ // Séparateur (sauf dernier)
134
+ if (!isLast) {
135
+ ctx.fillStyle = '#BDBDBD';
136
+ ctx.font = `400 ${this.fontSize}px Roboto, sans-serif`;
137
+ ctx.fillText(this.separator, currentX, textY);
138
+ currentX += ctx.measureText(this.separator).width + 12;
139
+ }
140
+ });
141
+ }
142
+
143
+ /**
144
+ * Dessine le breadcrumb Cupertino (iOS)
145
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
146
+ * @private
147
+ */
148
+ drawCupertino(ctx) {
149
+ const displayItems = this.getDisplayItems();
150
+ let currentX = this.x + 16;
151
+
152
+ this.clickableAreas = [];
153
+
154
+ displayItems.forEach((item, index) => {
155
+ const isLast = index === displayItems.length - 1;
156
+ const isCollapsed = item.collapsed;
157
+ const isHovered = this.hoveredIndex === index;
158
+ const isPressed = this.pressedIndex === index;
159
+
160
+ // Chevron back (sauf premier)
161
+ if (index > 0) {
162
+ const chevronColor = isPressed && index === displayItems.length - 1 ?
163
+ this.darkenColor(this.activeColor) :
164
+ this.activeColor;
165
+
166
+ this.drawChevron(ctx, currentX, this.y + this.height / 2, chevronColor, 'left');
167
+ currentX += 20;
168
+ }
169
+
170
+ // Texte de l'item
171
+ ctx.font = `${isLast ? '600' : '400'} ${this.fontSize}px -apple-system, BlinkMacSystemFont, sans-serif`;
172
+ ctx.textAlign = 'left';
173
+ ctx.textBaseline = 'middle';
174
+
175
+ const textWidth = ctx.measureText(item.label).width;
176
+
177
+ // Couleur du texte
178
+ if (isLast) {
179
+ ctx.fillStyle = isPressed ? this.darkenColor(this.activeColor) : this.activeColor;
180
+ } else if (isPressed && !isCollapsed) {
181
+ ctx.fillStyle = this.darkenColor(this.inactiveColor);
182
+ } else {
183
+ ctx.fillStyle = this.inactiveColor;
184
+ }
185
+
186
+ // Texte
187
+ const textY = this.y + this.height / 2;
188
+ ctx.fillText(item.label, currentX, textY);
189
+
190
+ // Stocker zone cliquable (sauf dernier et collapsed)
191
+ if (!isLast && !isCollapsed) {
192
+ this.clickableAreas.push({
193
+ x: currentX - 20,
194
+ y: this.y,
195
+ width: textWidth + 20,
196
+ height: this.height,
197
+ index: index,
198
+ originalIndex: this.getOriginalIndex(index)
199
+ });
200
+ }
201
+
202
+ currentX += textWidth + 16;
203
+ });
204
+ }
205
+
206
+ /**
207
+ * Dessine un chevron iOS
208
+ * @private
209
+ */
210
+ drawChevron(ctx, x, y, color, direction) {
211
+ const size = 8;
212
+
213
+ ctx.save();
214
+ ctx.strokeStyle = color;
215
+ ctx.lineWidth = 2;
216
+ ctx.lineCap = 'round';
217
+ ctx.lineJoin = 'round';
218
+
219
+ ctx.beginPath();
220
+ if (direction === 'left') {
221
+ ctx.moveTo(x + size / 2, y - size / 2);
222
+ ctx.lineTo(x - size / 2, y);
223
+ ctx.lineTo(x + size / 2, y + size / 2);
224
+ } else {
225
+ ctx.moveTo(x - size / 2, y - size / 2);
226
+ ctx.lineTo(x + size / 2, y);
227
+ ctx.lineTo(x - size / 2, y + size / 2);
228
+ }
229
+ ctx.stroke();
230
+ ctx.restore();
231
+ }
232
+
233
+ /**
234
+ * Obtient l'index original (avant collapse)
235
+ * @private
236
+ */
237
+ getOriginalIndex(displayIndex) {
238
+ if (this.items.length <= this.maxItems) {
239
+ return displayIndex;
240
+ }
241
+
242
+ // Si c'est le premier
243
+ if (displayIndex === 0) return 0;
244
+
245
+ // Si après le collapse
246
+ if (displayIndex > 1) {
247
+ const offset = this.items.length - (this.maxItems - 2);
248
+ return offset + displayIndex - 2;
249
+ }
250
+
251
+ return displayIndex;
252
+ }
253
+
254
+ /**
255
+ * Dessine le composant
256
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
257
+ */
258
+ draw(ctx) {
259
+ ctx.save();
260
+
261
+ if (this.platform === 'material') {
262
+ this.drawMaterial(ctx);
263
+ } else {
264
+ this.drawCupertino(ctx);
265
+ }
266
+
267
+ ctx.restore();
268
+ }
269
+
270
+ /**
271
+ * Gère le clic sur un item
272
+ * @param {number} x - Coordonnée X
273
+ * @param {number} y - Coordonnée Y
274
+ */
275
+ handleClick(x, y) {
276
+ for (let area of this.clickableAreas) {
277
+ if (x >= area.x && x <= area.x + area.width &&
278
+ y >= area.y && y <= area.y + area.height) {
279
+
280
+ this.pressedIndex = area.index;
281
+ this.framework.redraw();
282
+
283
+ setTimeout(() => {
284
+ this.pressedIndex = null;
285
+ const item = this.items[area.originalIndex];
286
+ this.onItemClick(item, area.originalIndex);
287
+ this.framework.redraw();
288
+ }, 100);
289
+
290
+ return true;
291
+ }
292
+ }
293
+ return false;
294
+ }
295
+
296
+ /**
297
+ * Gère le survol
298
+ * @param {number} x - Coordonnée X
299
+ * @param {number} y - Coordonnée Y
300
+ */
301
+ handleHover(x, y) {
302
+ let newHoveredIndex = null;
303
+
304
+ for (let area of this.clickableAreas) {
305
+ if (x >= area.x && x <= area.x + area.width &&
306
+ y >= area.y && y <= area.y + area.height) {
307
+ newHoveredIndex = area.index;
308
+ break;
309
+ }
310
+ }
311
+
312
+ if (newHoveredIndex !== this.hoveredIndex) {
313
+ this.hoveredIndex = newHoveredIndex;
314
+ this.framework.redraw();
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Ajoute un item au breadcrumb
320
+ * @param {string} label - Libellé
321
+ * @param {string} path - Chemin
322
+ */
323
+ addItem(label, path) {
324
+ this.items.push({ label, path });
325
+ this.framework.redraw();
326
+ }
327
+
328
+ /**
329
+ * Retire les items après un index
330
+ * @param {number} index - Index à partir duquel supprimer
331
+ */
332
+ removeAfter(index) {
333
+ this.items = this.items.slice(0, index + 1);
334
+ this.framework.redraw();
335
+ }
336
+
337
+ /**
338
+ * Définit les items
339
+ * @param {Array} items - Nouveaux items
340
+ */
341
+ setItems(items) {
342
+ this.items = items;
343
+ this.framework.redraw();
344
+ }
345
+
346
+ /**
347
+ * Assombrit une couleur
348
+ * @private
349
+ */
350
+ darkenColor(color) {
351
+ if (color.startsWith('rgb')) {
352
+ return color.replace(/[\d.]+\)$/g, match => {
353
+ const val = parseFloat(match);
354
+ return `${Math.max(0, val - 0.2)})`;
355
+ });
356
+ }
357
+
358
+ const rgb = this.hexToRgb(color);
359
+ return `rgb(${Math.max(0, rgb.r - 50)}, ${Math.max(0, rgb.g - 50)}, ${Math.max(0, rgb.b - 50)})`;
360
+ }
361
+
362
+ hexToRgb(hex) {
363
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
364
+ return result ? {
365
+ r: parseInt(result[1], 16),
366
+ g: parseInt(result[2], 16),
367
+ b: parseInt(result[3], 16)
368
+ } : { r: 0, g: 0, b: 0 };
369
+ }
370
+
371
+ roundRect(ctx, x, y, width, height, radius) {
372
+ ctx.beginPath();
373
+ ctx.moveTo(x + radius, y);
374
+ ctx.lineTo(x + width - radius, y);
375
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
376
+ ctx.lineTo(x + width, y + height - radius);
377
+ ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
378
+ ctx.lineTo(x + radius, y + height);
379
+ ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
380
+ ctx.lineTo(x, y + radius);
381
+ ctx.quadraticCurveTo(x, y, x + radius, y);
382
+ ctx.closePath();
383
+ }
384
+
385
+ isPointInside(x, y) {
386
+ return x >= this.x && x <= this.x + this.width &&
387
+ y >= this.y && y <= this.y + this.height;
388
+ }
389
+ }
390
+
391
+ export default Breadcrumb;