canvasframework 0.5.59 → 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.
- package/components/Breadcrumb.js +391 -0
- package/components/ColorPicker.js +891 -0
- package/components/Popover.js +493 -0
- package/components/Rating.js +494 -0
- package/core/CanvasFramework.js +4 -0
- package/core/UIBuilder.js +8 -0
- package/index.js +4 -0
- package/package.json +1 -1
|
@@ -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;
|