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,493 @@
|
|
|
1
|
+
import Component from '../core/Component.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Popover (fenêtre contextuelle) avec variantes Material et Cupertino
|
|
5
|
+
* @class
|
|
6
|
+
* @extends Component
|
|
7
|
+
*
|
|
8
|
+
* Material: Carte avec ombre et flèche
|
|
9
|
+
* Cupertino: Style iOS avec blur background
|
|
10
|
+
*/
|
|
11
|
+
class Popover extends Component {
|
|
12
|
+
/**
|
|
13
|
+
* Crée une instance de Popover
|
|
14
|
+
* @param {CanvasFramework} framework - Framework parent
|
|
15
|
+
* @param {Object} [options={}] - Options de configuration
|
|
16
|
+
* @param {string} [options.title] - Titre du popover
|
|
17
|
+
* @param {string} [options.content] - Contenu texte
|
|
18
|
+
* @param {Component} [options.customContent] - Composant personnalisé
|
|
19
|
+
* @param {string} [options.placement='top'] - Position: 'top', 'bottom', 'left', 'right'
|
|
20
|
+
* @param {Object} [options.anchor] - Point d'ancrage {x, y}
|
|
21
|
+
* @param {number} [options.maxWidth=280] - Largeur maximale
|
|
22
|
+
* @param {boolean} [options.showArrow=true] - Afficher la flèche
|
|
23
|
+
* @param {Function} [options.onClose] - Callback fermeture
|
|
24
|
+
*/
|
|
25
|
+
constructor(framework, options = {}) {
|
|
26
|
+
super(framework, options);
|
|
27
|
+
|
|
28
|
+
this.platform = framework.platform;
|
|
29
|
+
this.title = options.title || null;
|
|
30
|
+
this.content = options.content || '';
|
|
31
|
+
this.customContent = options.customContent || null;
|
|
32
|
+
this.placement = options.placement || 'top';
|
|
33
|
+
this.anchor = options.anchor || null;
|
|
34
|
+
this.maxWidth = options.maxWidth || 280;
|
|
35
|
+
this.showArrow = options.showArrow !== false;
|
|
36
|
+
this.onClose = options.onClose || (() => {});
|
|
37
|
+
|
|
38
|
+
this.visible = false;
|
|
39
|
+
this.arrowSize = 12;
|
|
40
|
+
this.padding = this.platform === 'material' ? 16 : 12;
|
|
41
|
+
|
|
42
|
+
// Couleurs
|
|
43
|
+
if (this.platform === 'material') {
|
|
44
|
+
this.backgroundColor = '#FFFFFF';
|
|
45
|
+
this.titleColor = '#000000';
|
|
46
|
+
this.textColor = '#757575';
|
|
47
|
+
this.borderColor = '#E0E0E0';
|
|
48
|
+
this.elevation = 8;
|
|
49
|
+
} else {
|
|
50
|
+
this.backgroundColor = 'rgba(242, 242, 247, 0.98)';
|
|
51
|
+
this.titleColor = '#000000';
|
|
52
|
+
this.textColor = '#3C3C43';
|
|
53
|
+
this.borderColor = 'rgba(0, 0, 0, 0.1)';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Calculer dimensions
|
|
57
|
+
this.calculateDimensions();
|
|
58
|
+
|
|
59
|
+
// Animation
|
|
60
|
+
this.animationProgress = 0;
|
|
61
|
+
this.animating = false;
|
|
62
|
+
// ✅ Flag pour éviter les doubles appels
|
|
63
|
+
this._isShowing = false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Calcule les dimensions du popover
|
|
68
|
+
* @private
|
|
69
|
+
*/
|
|
70
|
+
calculateDimensions() {
|
|
71
|
+
const ctx = this.framework.ctx;
|
|
72
|
+
ctx.save();
|
|
73
|
+
|
|
74
|
+
// Mesurer le texte
|
|
75
|
+
let contentHeight = 0;
|
|
76
|
+
let contentWidth = 0;
|
|
77
|
+
|
|
78
|
+
// Titre
|
|
79
|
+
if (this.title) {
|
|
80
|
+
ctx.font = `600 ${this.platform === 'material' ? 16 : 17}px ${this.platform === 'material' ? 'Roboto' : '-apple-system'}, sans-serif`;
|
|
81
|
+
const titleWidth = ctx.measureText(this.title).width;
|
|
82
|
+
contentWidth = Math.max(contentWidth, titleWidth);
|
|
83
|
+
contentHeight += this.platform === 'material' ? 24 : 22;
|
|
84
|
+
contentHeight += 8; // Espacement
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Contenu
|
|
88
|
+
if (this.content) {
|
|
89
|
+
ctx.font = `400 ${this.platform === 'material' ? 14 : 15}px ${this.platform === 'material' ? 'Roboto' : '-apple-system'}, sans-serif`;
|
|
90
|
+
|
|
91
|
+
// Wrap text
|
|
92
|
+
const words = this.content.split(' ');
|
|
93
|
+
const lines = [];
|
|
94
|
+
let currentLine = '';
|
|
95
|
+
|
|
96
|
+
for (let word of words) {
|
|
97
|
+
const testLine = currentLine + (currentLine ? ' ' : '') + word;
|
|
98
|
+
const metrics = ctx.measureText(testLine);
|
|
99
|
+
|
|
100
|
+
if (metrics.width > this.maxWidth - this.padding * 2 && currentLine) {
|
|
101
|
+
lines.push(currentLine);
|
|
102
|
+
currentLine = word;
|
|
103
|
+
} else {
|
|
104
|
+
currentLine = testLine;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (currentLine) lines.push(currentLine);
|
|
108
|
+
|
|
109
|
+
this.contentLines = lines;
|
|
110
|
+
contentHeight += lines.length * (this.platform === 'material' ? 20 : 21);
|
|
111
|
+
|
|
112
|
+
lines.forEach(line => {
|
|
113
|
+
const lineWidth = ctx.measureText(line).width;
|
|
114
|
+
contentWidth = Math.max(contentWidth, lineWidth);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Custom content
|
|
119
|
+
if (this.customContent) {
|
|
120
|
+
contentWidth = Math.max(contentWidth, this.customContent.width);
|
|
121
|
+
contentHeight += this.customContent.height;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
this.width = Math.min(this.maxWidth, contentWidth + this.padding * 2);
|
|
125
|
+
this.height = contentHeight + this.padding * 2;
|
|
126
|
+
|
|
127
|
+
ctx.restore();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Positionne le popover par rapport à l'ancre
|
|
132
|
+
* @private
|
|
133
|
+
*/
|
|
134
|
+
positionPopover() {
|
|
135
|
+
if (!this.anchor) return;
|
|
136
|
+
|
|
137
|
+
const arrowOffset = this.showArrow ? this.arrowSize : 0;
|
|
138
|
+
|
|
139
|
+
switch (this.placement) {
|
|
140
|
+
case 'top':
|
|
141
|
+
this.x = this.anchor.x - this.width / 2;
|
|
142
|
+
this.y = this.anchor.y - this.height - arrowOffset;
|
|
143
|
+
break;
|
|
144
|
+
|
|
145
|
+
case 'bottom':
|
|
146
|
+
this.x = this.anchor.x - this.width / 2;
|
|
147
|
+
this.y = this.anchor.y + arrowOffset;
|
|
148
|
+
break;
|
|
149
|
+
|
|
150
|
+
case 'left':
|
|
151
|
+
this.x = this.anchor.x - this.width - arrowOffset;
|
|
152
|
+
this.y = this.anchor.y - this.height / 2;
|
|
153
|
+
break;
|
|
154
|
+
|
|
155
|
+
case 'right':
|
|
156
|
+
this.x = this.anchor.x + arrowOffset;
|
|
157
|
+
this.y = this.anchor.y - this.height / 2;
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Ajuster si hors écran
|
|
162
|
+
const canvas = this.framework.canvas;
|
|
163
|
+
this.x = Math.max(10, Math.min(this.x, canvas.width - this.width - 10));
|
|
164
|
+
this.y = Math.max(10, Math.min(this.y, canvas.height - this.height - 10));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Dessine la flèche
|
|
169
|
+
* @private
|
|
170
|
+
*/
|
|
171
|
+
drawArrow(ctx) {
|
|
172
|
+
if (!this.showArrow || !this.anchor) return;
|
|
173
|
+
|
|
174
|
+
const size = this.arrowSize;
|
|
175
|
+
|
|
176
|
+
ctx.fillStyle = this.backgroundColor;
|
|
177
|
+
ctx.strokeStyle = this.borderColor;
|
|
178
|
+
ctx.lineWidth = 1;
|
|
179
|
+
|
|
180
|
+
ctx.beginPath();
|
|
181
|
+
|
|
182
|
+
switch (this.placement) {
|
|
183
|
+
case 'top':
|
|
184
|
+
const bottomX = this.anchor.x;
|
|
185
|
+
ctx.moveTo(bottomX, this.y + this.height);
|
|
186
|
+
ctx.lineTo(bottomX - size / 2, this.y + this.height + size);
|
|
187
|
+
ctx.lineTo(bottomX + size / 2, this.y + this.height + size);
|
|
188
|
+
break;
|
|
189
|
+
|
|
190
|
+
case 'bottom':
|
|
191
|
+
const topX = this.anchor.x;
|
|
192
|
+
ctx.moveTo(topX, this.y);
|
|
193
|
+
ctx.lineTo(topX - size / 2, this.y - size);
|
|
194
|
+
ctx.lineTo(topX + size / 2, this.y - size);
|
|
195
|
+
break;
|
|
196
|
+
|
|
197
|
+
case 'left':
|
|
198
|
+
const rightY = this.anchor.y;
|
|
199
|
+
ctx.moveTo(this.x + this.width, rightY);
|
|
200
|
+
ctx.lineTo(this.x + this.width + size, rightY - size / 2);
|
|
201
|
+
ctx.lineTo(this.x + this.width + size, rightY + size / 2);
|
|
202
|
+
break;
|
|
203
|
+
|
|
204
|
+
case 'right':
|
|
205
|
+
const leftY = this.anchor.y;
|
|
206
|
+
ctx.moveTo(this.x, leftY);
|
|
207
|
+
ctx.lineTo(this.x - size, leftY - size / 2);
|
|
208
|
+
ctx.lineTo(this.x - size, leftY + size / 2);
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
ctx.closePath();
|
|
213
|
+
ctx.fill();
|
|
214
|
+
ctx.stroke();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Dessine le popover Material
|
|
219
|
+
* @private
|
|
220
|
+
*/
|
|
221
|
+
drawMaterial(ctx) {
|
|
222
|
+
ctx.save();
|
|
223
|
+
|
|
224
|
+
// Ombre
|
|
225
|
+
ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
|
|
226
|
+
ctx.shadowBlur = this.elevation * 2;
|
|
227
|
+
ctx.shadowOffsetY = this.elevation;
|
|
228
|
+
|
|
229
|
+
// Background
|
|
230
|
+
ctx.fillStyle = this.backgroundColor;
|
|
231
|
+
this.roundRect(ctx, this.x, this.y, this.width, this.height, 8);
|
|
232
|
+
ctx.fill();
|
|
233
|
+
|
|
234
|
+
ctx.shadowColor = 'transparent';
|
|
235
|
+
|
|
236
|
+
// Bordure légère
|
|
237
|
+
ctx.strokeStyle = this.borderColor;
|
|
238
|
+
ctx.lineWidth = 1;
|
|
239
|
+
ctx.stroke();
|
|
240
|
+
|
|
241
|
+
// Flèche
|
|
242
|
+
this.drawArrow(ctx);
|
|
243
|
+
|
|
244
|
+
ctx.restore();
|
|
245
|
+
|
|
246
|
+
// Contenu
|
|
247
|
+
this.drawContent(ctx);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Dessine le popover Cupertino (iOS)
|
|
252
|
+
* @private
|
|
253
|
+
*/
|
|
254
|
+
drawCupertino(ctx) {
|
|
255
|
+
ctx.save();
|
|
256
|
+
|
|
257
|
+
// Blur background effect (simulé avec semi-transparence)
|
|
258
|
+
ctx.fillStyle = this.backgroundColor;
|
|
259
|
+
this.roundRect(ctx, this.x, this.y, this.width, this.height, 12);
|
|
260
|
+
ctx.fill();
|
|
261
|
+
|
|
262
|
+
// Bordure
|
|
263
|
+
ctx.strokeStyle = this.borderColor;
|
|
264
|
+
ctx.lineWidth = 0.5;
|
|
265
|
+
ctx.stroke();
|
|
266
|
+
|
|
267
|
+
// Flèche
|
|
268
|
+
this.drawArrow(ctx);
|
|
269
|
+
|
|
270
|
+
ctx.restore();
|
|
271
|
+
|
|
272
|
+
// Contenu
|
|
273
|
+
this.drawContent(ctx);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Dessine le contenu
|
|
278
|
+
* @private
|
|
279
|
+
*/
|
|
280
|
+
drawContent(ctx) {
|
|
281
|
+
let currentY = this.y + this.padding;
|
|
282
|
+
|
|
283
|
+
// Titre
|
|
284
|
+
if (this.title) {
|
|
285
|
+
ctx.fillStyle = this.titleColor;
|
|
286
|
+
ctx.font = `600 ${this.platform === 'material' ? 16 : 17}px ${this.platform === 'material' ? 'Roboto' : '-apple-system'}, sans-serif`;
|
|
287
|
+
ctx.textAlign = 'left';
|
|
288
|
+
ctx.textBaseline = 'top';
|
|
289
|
+
ctx.fillText(this.title, this.x + this.padding, currentY);
|
|
290
|
+
currentY += this.platform === 'material' ? 24 : 22;
|
|
291
|
+
currentY += 8;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Contenu texte
|
|
295
|
+
if (this.content && this.contentLines) {
|
|
296
|
+
ctx.fillStyle = this.textColor;
|
|
297
|
+
ctx.font = `400 ${this.platform === 'material' ? 14 : 15}px ${this.platform === 'material' ? 'Roboto' : '-apple-system'}, sans-serif`;
|
|
298
|
+
|
|
299
|
+
this.contentLines.forEach(line => {
|
|
300
|
+
ctx.fillText(line, this.x + this.padding, currentY);
|
|
301
|
+
currentY += this.platform === 'material' ? 20 : 21;
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Custom content
|
|
306
|
+
if (this.customContent) {
|
|
307
|
+
ctx.save();
|
|
308
|
+
ctx.translate(this.x + this.padding, currentY);
|
|
309
|
+
this.customContent.draw(ctx);
|
|
310
|
+
ctx.restore();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Dessine le composant
|
|
316
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
317
|
+
*/
|
|
318
|
+
draw(ctx) {
|
|
319
|
+
if (!this.visible) return;
|
|
320
|
+
|
|
321
|
+
// Animation d'apparition
|
|
322
|
+
if (this.animating) {
|
|
323
|
+
ctx.save();
|
|
324
|
+
ctx.globalAlpha = this.animationProgress;
|
|
325
|
+
|
|
326
|
+
const scale = 0.8 + (this.animationProgress * 0.2);
|
|
327
|
+
const centerX = this.x + this.width / 2;
|
|
328
|
+
const centerY = this.y + this.height / 2;
|
|
329
|
+
|
|
330
|
+
ctx.translate(centerX, centerY);
|
|
331
|
+
ctx.scale(scale, scale);
|
|
332
|
+
ctx.translate(-centerX, -centerY);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (this.platform === 'material') {
|
|
336
|
+
this.drawMaterial(ctx);
|
|
337
|
+
} else {
|
|
338
|
+
this.drawCupertino(ctx);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (this.animating) {
|
|
342
|
+
ctx.restore();
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Affiche le popover
|
|
348
|
+
* @param {Object} anchor - Point d'ancrage {x, y}
|
|
349
|
+
* @param {string} placement - Position
|
|
350
|
+
*/
|
|
351
|
+
show(anchor, placement) {
|
|
352
|
+
// ✅ Éviter les appels multiples
|
|
353
|
+
if (this._isShowing) return;
|
|
354
|
+
this._isShowing = true;
|
|
355
|
+
|
|
356
|
+
if (anchor) this.anchor = anchor;
|
|
357
|
+
if (placement) this.placement = placement;
|
|
358
|
+
|
|
359
|
+
this.calculateDimensions();
|
|
360
|
+
this.positionPopover();
|
|
361
|
+
|
|
362
|
+
this.visible = true;
|
|
363
|
+
this.animating = true;
|
|
364
|
+
this.animationProgress = 0;
|
|
365
|
+
|
|
366
|
+
this.animateIn();
|
|
367
|
+
|
|
368
|
+
// ✅ Réinitialiser le flag après l'animation
|
|
369
|
+
setTimeout(() => {
|
|
370
|
+
this._isShowing = false;
|
|
371
|
+
}, this.splashOptions?.duration || 200);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Cache le popover
|
|
376
|
+
*/
|
|
377
|
+
hide() {
|
|
378
|
+
this.animating = true;
|
|
379
|
+
this.animateOut();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Animation d'apparition
|
|
384
|
+
* @private
|
|
385
|
+
*/
|
|
386
|
+
animateIn() {
|
|
387
|
+
const duration = 200;
|
|
388
|
+
const startTime = Date.now();
|
|
389
|
+
|
|
390
|
+
const animate = () => {
|
|
391
|
+
const elapsed = Date.now() - startTime;
|
|
392
|
+
this.animationProgress = Math.min(1, elapsed / duration);
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
if (this.animationProgress < 1) {
|
|
397
|
+
requestAnimationFrame(animate);
|
|
398
|
+
} else {
|
|
399
|
+
this.animating = false;
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
animate();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Animation de disparition
|
|
408
|
+
* @private
|
|
409
|
+
*/
|
|
410
|
+
animateOut() {
|
|
411
|
+
const duration = 150;
|
|
412
|
+
const startTime = Date.now();
|
|
413
|
+
const startProgress = this.animationProgress;
|
|
414
|
+
|
|
415
|
+
const animate = () => {
|
|
416
|
+
const elapsed = Date.now() - startTime;
|
|
417
|
+
this.animationProgress = startProgress * (1 - elapsed / duration);
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
if (this.animationProgress > 0) {
|
|
421
|
+
requestAnimationFrame(animate);
|
|
422
|
+
} else {
|
|
423
|
+
this.animating = false;
|
|
424
|
+
this.visible = false;
|
|
425
|
+
this.onClose();
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
animate();
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Gère le clic à l'extérieur
|
|
434
|
+
* @param {number} x - Coordonnée X
|
|
435
|
+
* @param {number} y - Coordonnée Y
|
|
436
|
+
* @returns {boolean} True si clic à l'extérieur
|
|
437
|
+
*/
|
|
438
|
+
handleClickOutside(x, y) {
|
|
439
|
+
if (!this.visible) return false;
|
|
440
|
+
|
|
441
|
+
const isInside = x >= this.x && x <= this.x + this.width &&
|
|
442
|
+
y >= this.y && y <= this.y + this.height;
|
|
443
|
+
|
|
444
|
+
if (!isInside) {
|
|
445
|
+
this.hide();
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Définit le contenu
|
|
454
|
+
* @param {string} content - Nouveau contenu
|
|
455
|
+
*/
|
|
456
|
+
setContent(content) {
|
|
457
|
+
this.content = content;
|
|
458
|
+
this.calculateDimensions();
|
|
459
|
+
this.positionPopover();
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Définit le titre
|
|
464
|
+
* @param {string} title - Nouveau titre
|
|
465
|
+
*/
|
|
466
|
+
setTitle(title) {
|
|
467
|
+
this.title = title;
|
|
468
|
+
this.calculateDimensions();
|
|
469
|
+
this.positionPopover();
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
roundRect(ctx, x, y, width, height, radius) {
|
|
473
|
+
ctx.beginPath();
|
|
474
|
+
ctx.moveTo(x + radius, y);
|
|
475
|
+
ctx.lineTo(x + width - radius, y);
|
|
476
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
477
|
+
ctx.lineTo(x + width, y + height - radius);
|
|
478
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
479
|
+
ctx.lineTo(x + radius, y + height);
|
|
480
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
481
|
+
ctx.lineTo(x, y + radius);
|
|
482
|
+
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
483
|
+
ctx.closePath();
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
isPointInside(x, y) {
|
|
487
|
+
if (!this.visible) return false;
|
|
488
|
+
return x >= this.x && x <= this.x + this.width &&
|
|
489
|
+
y >= this.y && y <= this.y + this.height;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export default Popover;
|