canvasframework 0.5.58 → 0.5.59
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/Paginatedcontainer.js +840 -0
- package/core/CanvasFramework.js +1 -0
- package/core/UIBuilder.js +2 -0
- package/index.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,840 @@
|
|
|
1
|
+
import Component from '../core/Component.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Conteneur avec pagination manuelle - l'utilisateur définit le contenu de chaque page
|
|
5
|
+
* @class
|
|
6
|
+
* @extends Component
|
|
7
|
+
*
|
|
8
|
+
* Exemple d'utilisation:
|
|
9
|
+
* const container = new PaginatedContainer(framework, { pageHeight: 400, width: 600 });
|
|
10
|
+
* container.addPage([child1, child2, child3]); // Page 1
|
|
11
|
+
* container.addPage([child4, child5]); // Page 2
|
|
12
|
+
* container.addPage([child6]); // Page 3
|
|
13
|
+
*
|
|
14
|
+
* Material: Boutons Material You avec numéros de page
|
|
15
|
+
* Cupertino: Boutons iOS style avec compteur
|
|
16
|
+
*/
|
|
17
|
+
class PaginatedContainer extends Component {
|
|
18
|
+
/**
|
|
19
|
+
* Crée une instance de PaginatedContainer
|
|
20
|
+
* @param {CanvasFramework} framework - Framework parent
|
|
21
|
+
* @param {Object} [options={}] - Options de configuration
|
|
22
|
+
* @param {number} [options.pageHeight=400] - Hauteur d'une page
|
|
23
|
+
* @param {number} [options.padding=16] - Padding interne
|
|
24
|
+
* @param {number} [options.gap=12] - Espacement entre composants
|
|
25
|
+
* @param {string} [options.bgColor='#FFFFFF'] - Couleur de fond
|
|
26
|
+
* @param {number} [options.elevation=2] - Élévation (Material)
|
|
27
|
+
* @param {number} [options.borderRadius=8] - Rayon des coins
|
|
28
|
+
* @param {boolean} [options.showNavigation=true] - Afficher navigation
|
|
29
|
+
* @param {string} [options.navPosition='bottom'] - Position nav: 'top', 'bottom', 'both'
|
|
30
|
+
* @param {Function} [options.onPageChange] - Callback changement de page
|
|
31
|
+
*/
|
|
32
|
+
constructor(framework, options = {}) {
|
|
33
|
+
super(framework, options);
|
|
34
|
+
|
|
35
|
+
this.platform = framework.platform;
|
|
36
|
+
this.pageHeight = options.pageHeight || 400;
|
|
37
|
+
this.padding = options.padding || 16;
|
|
38
|
+
this.gap = options.gap || 12;
|
|
39
|
+
this.bgColor = options.bgColor || '#FFFFFF';
|
|
40
|
+
this.elevation = options.elevation !== undefined ? options.elevation : 2;
|
|
41
|
+
this.borderRadius = options.borderRadius !== undefined ? options.borderRadius : 8;
|
|
42
|
+
this.showNavigation = options.showNavigation !== false;
|
|
43
|
+
this.navPosition = options.navPosition || 'bottom';
|
|
44
|
+
this.onPageChange = options.onPageChange || (() => {});
|
|
45
|
+
|
|
46
|
+
// Navigation
|
|
47
|
+
this.navHeight = this.platform === 'material' ? 56 : 50;
|
|
48
|
+
this.currentPage = 0;
|
|
49
|
+
|
|
50
|
+
// Pages - tableau de tableaux d'enfants
|
|
51
|
+
this.pages = [];
|
|
52
|
+
|
|
53
|
+
// Calcul hauteur totale
|
|
54
|
+
const navTopHeight = (this.showNavigation && this.navPosition !== 'bottom') ? this.navHeight : 0;
|
|
55
|
+
const navBottomHeight = (this.showNavigation && this.navPosition !== 'top') ? this.navHeight : 0;
|
|
56
|
+
this.height = navTopHeight + this.pageHeight + navBottomHeight;
|
|
57
|
+
|
|
58
|
+
// Couleurs selon plateforme
|
|
59
|
+
if (this.platform === 'material') {
|
|
60
|
+
this.navColor = '#F5F5F5';
|
|
61
|
+
this.borderColor = '#E0E0E0';
|
|
62
|
+
this.activeColor = '#6200EE';
|
|
63
|
+
this.inactiveColor = '#757575';
|
|
64
|
+
this.shadowColor = 'rgba(0,0,0,0.15)';
|
|
65
|
+
this.buttonBgColor = '#FFFFFF';
|
|
66
|
+
this.buttonDisabledColor = '#F5F5F5';
|
|
67
|
+
} else {
|
|
68
|
+
this.navColor = '#F2F2F7';
|
|
69
|
+
this.borderColor = '#C6C6C8';
|
|
70
|
+
this.activeColor = '#007AFF';
|
|
71
|
+
this.inactiveColor = '#8E8E93';
|
|
72
|
+
this.shadowColor = 'rgba(0,0,0,0.1)';
|
|
73
|
+
this.buttonBgColor = '#FFFFFF';
|
|
74
|
+
this.buttonDisabledColor = '#E5E5EA';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Élévation
|
|
78
|
+
this._applyElevationStyles();
|
|
79
|
+
|
|
80
|
+
// Navigation buttons
|
|
81
|
+
this.navButtons = [];
|
|
82
|
+
this.hoveredButton = null;
|
|
83
|
+
this.pressedButton = null;
|
|
84
|
+
|
|
85
|
+
// Pour l'effet ripple
|
|
86
|
+
this.rippleButton = null;
|
|
87
|
+
this.rippleTimer = null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Applique les styles d'élévation
|
|
92
|
+
* @private
|
|
93
|
+
*/
|
|
94
|
+
_applyElevationStyles() {
|
|
95
|
+
const elevationStyles = {
|
|
96
|
+
0: { blur: 0, offsetY: 0, spread: 0, opacity: 0 },
|
|
97
|
+
1: { blur: 2, offsetY: 1, spread: 0, opacity: 0.1 },
|
|
98
|
+
2: { blur: 4, offsetY: 2, spread: 1, opacity: 0.15 },
|
|
99
|
+
3: { blur: 8, offsetY: 4, spread: 2, opacity: 0.2 },
|
|
100
|
+
4: { blur: 16, offsetY: 8, spread: 3, opacity: 0.25 },
|
|
101
|
+
5: { blur: 24, offsetY: 12, spread: 4, opacity: 0.3 }
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const style = elevationStyles[Math.min(this.elevation, 5)] || elevationStyles[0];
|
|
105
|
+
|
|
106
|
+
this.shadowBlur = style.blur;
|
|
107
|
+
this.shadowOffsetY = style.offsetY;
|
|
108
|
+
this.shadowSpread = style.spread;
|
|
109
|
+
this.shadowOpacity = style.opacity;
|
|
110
|
+
|
|
111
|
+
this._updateShadowColor();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Met à jour la couleur de l'ombre
|
|
116
|
+
* @private
|
|
117
|
+
*/
|
|
118
|
+
_updateShadowColor() {
|
|
119
|
+
const rgbMatch = this.shadowColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
|
120
|
+
if (rgbMatch) {
|
|
121
|
+
const r = rgbMatch[1];
|
|
122
|
+
const g = rgbMatch[2];
|
|
123
|
+
const b = rgbMatch[3];
|
|
124
|
+
this._computedShadowColor = `rgba(${r}, ${g}, ${b}, ${this.shadowOpacity})`;
|
|
125
|
+
} else {
|
|
126
|
+
this._computedShadowColor = this.shadowColor;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Ajoute une nouvelle page avec ses enfants
|
|
132
|
+
* @param {Component[]} children - Tableau de composants pour cette page
|
|
133
|
+
* @returns {number} Index de la page ajoutée
|
|
134
|
+
*/
|
|
135
|
+
addPage(children = []) {
|
|
136
|
+
const pageIndex = this.pages.length;
|
|
137
|
+
this.pages.push(children);
|
|
138
|
+
this.updateNavButtons();
|
|
139
|
+
this.updateChildrenPositions();
|
|
140
|
+
return pageIndex;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Définit le contenu d'une page spécifique
|
|
145
|
+
* @param {number} pageIndex - Index de la page
|
|
146
|
+
* @param {Component[]} children - Tableau de composants
|
|
147
|
+
*/
|
|
148
|
+
setPage(pageIndex, children = []) {
|
|
149
|
+
if (pageIndex >= 0) {
|
|
150
|
+
// Étendre le tableau si nécessaire
|
|
151
|
+
while (this.pages.length <= pageIndex) {
|
|
152
|
+
this.pages.push([]);
|
|
153
|
+
}
|
|
154
|
+
this.pages[pageIndex] = children;
|
|
155
|
+
this.updateNavButtons();
|
|
156
|
+
if (pageIndex === this.currentPage) {
|
|
157
|
+
this.updateChildrenPositions();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Récupère le contenu d'une page
|
|
164
|
+
* @param {number} pageIndex - Index de la page
|
|
165
|
+
* @returns {Component[]} Tableau des enfants de cette page
|
|
166
|
+
*/
|
|
167
|
+
getPage(pageIndex) {
|
|
168
|
+
return this.pages[pageIndex] || [];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Retire une page
|
|
173
|
+
* @param {number} pageIndex - Index de la page à retirer
|
|
174
|
+
*/
|
|
175
|
+
removePage(pageIndex) {
|
|
176
|
+
if (pageIndex >= 0 && pageIndex < this.pages.length) {
|
|
177
|
+
this.pages.splice(pageIndex, 1);
|
|
178
|
+
|
|
179
|
+
// Ajuster la page courante si nécessaire
|
|
180
|
+
if (this.currentPage >= this.pages.length && this.pages.length > 0) {
|
|
181
|
+
this.currentPage = this.pages.length - 1;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
this.updateNavButtons();
|
|
185
|
+
this.updateChildrenPositions();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Nombre total de pages
|
|
191
|
+
* @returns {number}
|
|
192
|
+
*/
|
|
193
|
+
get totalPages() {
|
|
194
|
+
return this.pages.length;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Met à jour les positions de tous les enfants de la page courante
|
|
199
|
+
* @private
|
|
200
|
+
*/
|
|
201
|
+
updateChildrenPositions() {
|
|
202
|
+
if (this.currentPage >= this.pages.length) return;
|
|
203
|
+
|
|
204
|
+
const navTopHeight = (this.showNavigation && this.navPosition !== 'bottom') ? this.navHeight : 0;
|
|
205
|
+
const pageY = this.y + navTopHeight;
|
|
206
|
+
|
|
207
|
+
const currentPageChildren = this.pages[this.currentPage];
|
|
208
|
+
let currentY = pageY + this.padding;
|
|
209
|
+
|
|
210
|
+
for (const child of currentPageChildren) {
|
|
211
|
+
// Positionner l'enfant
|
|
212
|
+
child.x = this.x + this.padding;
|
|
213
|
+
child.y = currentY;
|
|
214
|
+
|
|
215
|
+
// Ajuster la largeur si nécessaire (prend toute la largeur disponible)
|
|
216
|
+
if (!child.width || child.width === 0) {
|
|
217
|
+
child.width = this.width - (this.padding * 2);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Incrémenter Y pour le prochain enfant
|
|
221
|
+
currentY += (child.height || 0) + this.gap;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Change de page
|
|
227
|
+
* @param {number} pageIndex - Index de la nouvelle page
|
|
228
|
+
*/
|
|
229
|
+
goToPage(pageIndex) {
|
|
230
|
+
if (pageIndex >= 0 && pageIndex < this.totalPages && pageIndex !== this.currentPage) {
|
|
231
|
+
this.currentPage = pageIndex;
|
|
232
|
+
this.updateChildrenPositions();
|
|
233
|
+
this.updateNavButtons();
|
|
234
|
+
this.onPageChange(this.currentPage, this.totalPages);
|
|
235
|
+
if (this.framework && this.framework.redraw) {
|
|
236
|
+
this.framework.redraw();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Page suivante
|
|
243
|
+
*/
|
|
244
|
+
nextPage() {
|
|
245
|
+
if (this.currentPage < this.totalPages - 1) {
|
|
246
|
+
this.goToPage(this.currentPage + 1);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Page précédente
|
|
252
|
+
*/
|
|
253
|
+
previousPage() {
|
|
254
|
+
if (this.currentPage > 0) {
|
|
255
|
+
this.goToPage(this.currentPage - 1);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Met à jour les boutons de navigation
|
|
261
|
+
* @private
|
|
262
|
+
*/
|
|
263
|
+
updateNavButtons() {
|
|
264
|
+
this.navButtons = [];
|
|
265
|
+
|
|
266
|
+
if (!this.showNavigation || this.totalPages === 0) return;
|
|
267
|
+
|
|
268
|
+
const buttonSize = this.platform === 'material' ? 40 : 36;
|
|
269
|
+
const navTopHeight = (this.showNavigation && this.navPosition !== 'bottom') ? this.navHeight : 0;
|
|
270
|
+
|
|
271
|
+
// Position des boutons
|
|
272
|
+
const leftX = this.x + 16;
|
|
273
|
+
const rightX = this.x + this.width - 16 - buttonSize;
|
|
274
|
+
|
|
275
|
+
// Navigation du bas (par défaut)
|
|
276
|
+
if (this.navPosition === 'bottom' || this.navPosition === 'both') {
|
|
277
|
+
const navBottomY = this.y + navTopHeight + this.pageHeight;
|
|
278
|
+
const buttonY = navBottomY + (this.navHeight - buttonSize) / 2;
|
|
279
|
+
|
|
280
|
+
this.navButtons.push({
|
|
281
|
+
x: leftX,
|
|
282
|
+
y: buttonY,
|
|
283
|
+
size: buttonSize,
|
|
284
|
+
action: 'prev',
|
|
285
|
+
disabled: this.currentPage === 0
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
this.navButtons.push({
|
|
289
|
+
x: rightX,
|
|
290
|
+
y: buttonY,
|
|
291
|
+
size: buttonSize,
|
|
292
|
+
action: 'next',
|
|
293
|
+
disabled: this.currentPage === this.totalPages - 1
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Navigation du haut
|
|
298
|
+
if (this.navPosition === 'top' || this.navPosition === 'both') {
|
|
299
|
+
const navTopY = this.y;
|
|
300
|
+
const buttonY = navTopY + (this.navHeight - buttonSize) / 2;
|
|
301
|
+
|
|
302
|
+
this.navButtons.push({
|
|
303
|
+
x: leftX,
|
|
304
|
+
y: buttonY,
|
|
305
|
+
size: buttonSize,
|
|
306
|
+
action: 'prev',
|
|
307
|
+
disabled: this.currentPage === 0
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
this.navButtons.push({
|
|
311
|
+
x: rightX,
|
|
312
|
+
y: buttonY,
|
|
313
|
+
size: buttonSize,
|
|
314
|
+
action: 'next',
|
|
315
|
+
disabled: this.currentPage === this.totalPages - 1
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Dessine l'ombre
|
|
322
|
+
* @private
|
|
323
|
+
*/
|
|
324
|
+
drawShadow(ctx) {
|
|
325
|
+
if (this.elevation === 0) return;
|
|
326
|
+
|
|
327
|
+
ctx.save();
|
|
328
|
+
|
|
329
|
+
ctx.shadowColor = this._computedShadowColor;
|
|
330
|
+
ctx.shadowBlur = this.shadowBlur;
|
|
331
|
+
ctx.shadowOffsetX = 0;
|
|
332
|
+
ctx.shadowOffsetY = this.shadowOffsetY;
|
|
333
|
+
|
|
334
|
+
ctx.fillStyle = this.bgColor;
|
|
335
|
+
|
|
336
|
+
if (this.borderRadius > 0) {
|
|
337
|
+
ctx.beginPath();
|
|
338
|
+
const spread = this.shadowSpread;
|
|
339
|
+
this.roundRect(
|
|
340
|
+
ctx,
|
|
341
|
+
this.x - spread / 2,
|
|
342
|
+
this.y - spread / 2,
|
|
343
|
+
this.width + spread,
|
|
344
|
+
this.height + spread,
|
|
345
|
+
this.borderRadius + spread / 2
|
|
346
|
+
);
|
|
347
|
+
ctx.fill();
|
|
348
|
+
} else {
|
|
349
|
+
const spread = this.shadowSpread;
|
|
350
|
+
ctx.fillRect(
|
|
351
|
+
this.x - spread / 2,
|
|
352
|
+
this.y - spread / 2,
|
|
353
|
+
this.width + spread,
|
|
354
|
+
this.height + spread
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
ctx.restore();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Dessine la navigation Material
|
|
363
|
+
* @private
|
|
364
|
+
*/
|
|
365
|
+
drawMaterialNavigation(ctx, navY) {
|
|
366
|
+
// Background navigation
|
|
367
|
+
ctx.fillStyle = this.navColor;
|
|
368
|
+
ctx.fillRect(this.x, navY, this.width, this.navHeight);
|
|
369
|
+
|
|
370
|
+
// Ligne de séparation
|
|
371
|
+
ctx.strokeStyle = this.borderColor;
|
|
372
|
+
ctx.lineWidth = 1;
|
|
373
|
+
ctx.beginPath();
|
|
374
|
+
ctx.moveTo(this.x, navY);
|
|
375
|
+
ctx.lineTo(this.x + this.width, navY);
|
|
376
|
+
ctx.stroke();
|
|
377
|
+
|
|
378
|
+
// Boutons navigation
|
|
379
|
+
this.drawMaterialButtons(ctx, navY);
|
|
380
|
+
|
|
381
|
+
// Compteur de pages (centré)
|
|
382
|
+
ctx.fillStyle = this.inactiveColor;
|
|
383
|
+
ctx.font = '14px Roboto, sans-serif';
|
|
384
|
+
ctx.textAlign = 'center';
|
|
385
|
+
ctx.textBaseline = 'middle';
|
|
386
|
+
ctx.fillText(
|
|
387
|
+
`Page ${this.currentPage + 1} / ${this.totalPages}`,
|
|
388
|
+
this.x + this.width / 2,
|
|
389
|
+
navY + this.navHeight / 2
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Dessine les boutons Material avec effet ripple (taille doublée)
|
|
395
|
+
* @private
|
|
396
|
+
*/
|
|
397
|
+
drawMaterialButtons(ctx, navY) {
|
|
398
|
+
const now = Date.now();
|
|
399
|
+
|
|
400
|
+
for (const btn of this.navButtons) {
|
|
401
|
+
// Ne dessiner que les boutons de cette barre de navigation
|
|
402
|
+
const isThisNav = Math.abs(btn.y - (navY + (this.navHeight - btn.size) / 2)) < 1;
|
|
403
|
+
if (!isThisNav) continue;
|
|
404
|
+
|
|
405
|
+
const isHovered = this.hoveredButton === btn.action;
|
|
406
|
+
const isPressed = this.rippleButton === btn.action;
|
|
407
|
+
|
|
408
|
+
ctx.save();
|
|
409
|
+
|
|
410
|
+
// Centre du bouton
|
|
411
|
+
const centerX = btn.x + btn.size / 2;
|
|
412
|
+
const centerY = btn.y + btn.size / 2;
|
|
413
|
+
const buttonRadius = btn.size / 2;
|
|
414
|
+
|
|
415
|
+
// Masque circulaire (garde le ripple à l'intérieur du bouton)
|
|
416
|
+
ctx.beginPath();
|
|
417
|
+
ctx.arc(centerX, centerY, buttonRadius, 0, Math.PI * 2);
|
|
418
|
+
ctx.clip();
|
|
419
|
+
|
|
420
|
+
// Background du bouton
|
|
421
|
+
if (btn.disabled) {
|
|
422
|
+
ctx.fillStyle = this.buttonDisabledColor;
|
|
423
|
+
} else {
|
|
424
|
+
ctx.fillStyle = this.buttonBgColor;
|
|
425
|
+
}
|
|
426
|
+
ctx.beginPath();
|
|
427
|
+
ctx.arc(centerX, centerY, buttonRadius, 0, Math.PI * 2);
|
|
428
|
+
ctx.fill();
|
|
429
|
+
|
|
430
|
+
// EFFET RIPPLE - TAILLE DOUBLÉE
|
|
431
|
+
if (isPressed && !btn.disabled && this.rippleStartTime) {
|
|
432
|
+
const elapsed = now - this.rippleStartTime;
|
|
433
|
+
|
|
434
|
+
if (elapsed < 300) { // Pendant les 300ms
|
|
435
|
+
// Progression de l'animation (0 à 1)
|
|
436
|
+
const progress = elapsed / 300;
|
|
437
|
+
|
|
438
|
+
// 🌟 TAILLE DOUBLÉE: le ripple atteint le DIAMÈTRE complet (btn.size)
|
|
439
|
+
// Au lieu de rayon (btn.size/2), on utilise le diamètre
|
|
440
|
+
const maxRippleRadius = btn.size; // Double de la taille normale
|
|
441
|
+
const rippleRadius = maxRippleRadius * Math.min(progress, 1);
|
|
442
|
+
|
|
443
|
+
// Opacité: forte au début, faible à la fin
|
|
444
|
+
const opacity = 0.5 * (1 - progress * 0.7);
|
|
445
|
+
|
|
446
|
+
// Cercle ripple (plus grand que le bouton, mais clipé)
|
|
447
|
+
ctx.fillStyle = this.hexToRgba(this.activeColor, opacity);
|
|
448
|
+
ctx.beginPath();
|
|
449
|
+
ctx.arc(centerX, centerY, rippleRadius, 0, Math.PI * 2);
|
|
450
|
+
ctx.fill();
|
|
451
|
+
|
|
452
|
+
// Forcer un redessin
|
|
453
|
+
if (this.framework && this.framework.redraw) {
|
|
454
|
+
setTimeout(() => {
|
|
455
|
+
this.framework.redraw();
|
|
456
|
+
}, 16);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Effet hover
|
|
462
|
+
if (isHovered && !btn.disabled && !isPressed) {
|
|
463
|
+
ctx.fillStyle = this.hexToRgba(this.activeColor, 0.1);
|
|
464
|
+
ctx.beginPath();
|
|
465
|
+
ctx.arc(centerX, centerY, buttonRadius, 0, Math.PI * 2);
|
|
466
|
+
ctx.fill();
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
ctx.restore();
|
|
470
|
+
|
|
471
|
+
// Bordure et icône (sans clip)
|
|
472
|
+
ctx.save();
|
|
473
|
+
|
|
474
|
+
// Bordure
|
|
475
|
+
ctx.strokeStyle = btn.disabled ? '#E0E0E0' : this.borderColor;
|
|
476
|
+
ctx.lineWidth = 1;
|
|
477
|
+
ctx.beginPath();
|
|
478
|
+
ctx.arc(centerX, centerY, buttonRadius, 0, Math.PI * 2);
|
|
479
|
+
ctx.stroke();
|
|
480
|
+
|
|
481
|
+
// Icône
|
|
482
|
+
ctx.fillStyle = btn.disabled ? '#BDBDBD' : this.activeColor;
|
|
483
|
+
ctx.font = '28px Roboto, sans-serif';
|
|
484
|
+
ctx.textAlign = 'center';
|
|
485
|
+
ctx.textBaseline = 'middle';
|
|
486
|
+
ctx.fillText(
|
|
487
|
+
btn.action === 'prev' ? '‹' : '›',
|
|
488
|
+
centerX,
|
|
489
|
+
centerY
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
ctx.restore();
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Dessine la navigation Cupertino (iOS)
|
|
498
|
+
* @private
|
|
499
|
+
*/
|
|
500
|
+
drawCupertinoNavigation(ctx, navY) {
|
|
501
|
+
// Background
|
|
502
|
+
ctx.fillStyle = this.navColor;
|
|
503
|
+
ctx.fillRect(this.x, navY, this.width, this.navHeight);
|
|
504
|
+
|
|
505
|
+
// Ligne de séparation
|
|
506
|
+
ctx.strokeStyle = this.borderColor;
|
|
507
|
+
ctx.lineWidth = 0.5;
|
|
508
|
+
ctx.beginPath();
|
|
509
|
+
ctx.moveTo(this.x, navY);
|
|
510
|
+
ctx.lineTo(this.x + this.width, navY);
|
|
511
|
+
ctx.stroke();
|
|
512
|
+
|
|
513
|
+
// Boutons navigation
|
|
514
|
+
this.drawCupertinoButtons(ctx, navY);
|
|
515
|
+
|
|
516
|
+
// Compteur de pages (centré)
|
|
517
|
+
ctx.fillStyle = this.inactiveColor;
|
|
518
|
+
ctx.font = '15px -apple-system, BlinkMacSystemFont, sans-serif';
|
|
519
|
+
ctx.textAlign = 'center';
|
|
520
|
+
ctx.textBaseline = 'middle';
|
|
521
|
+
ctx.fillText(
|
|
522
|
+
`Page ${this.currentPage + 1} / ${this.totalPages}`,
|
|
523
|
+
this.x + this.width / 2,
|
|
524
|
+
navY + this.navHeight / 2
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Dessine les boutons Cupertino (iOS)
|
|
530
|
+
* @private
|
|
531
|
+
*/
|
|
532
|
+
drawCupertinoButtons(ctx, navY) {
|
|
533
|
+
for (const btn of this.navButtons) {
|
|
534
|
+
// Ne dessiner que les boutons de cette barre de navigation
|
|
535
|
+
const isThisNav = Math.abs(btn.y - (navY + (this.navHeight - btn.size) / 2)) < 1;
|
|
536
|
+
if (!isThisNav) continue;
|
|
537
|
+
|
|
538
|
+
const isPressed = this.pressedButton === btn.action;
|
|
539
|
+
|
|
540
|
+
ctx.save();
|
|
541
|
+
|
|
542
|
+
// Background iOS style
|
|
543
|
+
if (btn.disabled) {
|
|
544
|
+
ctx.fillStyle = this.buttonDisabledColor;
|
|
545
|
+
} else {
|
|
546
|
+
ctx.fillStyle = this.buttonBgColor;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Rectangle arrondi (iOS style)
|
|
550
|
+
const padding = 2;
|
|
551
|
+
ctx.beginPath();
|
|
552
|
+
this.roundRect(
|
|
553
|
+
ctx,
|
|
554
|
+
btn.x + padding,
|
|
555
|
+
btn.y + padding,
|
|
556
|
+
btn.size - padding * 2,
|
|
557
|
+
btn.size - padding * 2,
|
|
558
|
+
(btn.size - padding * 2) / 2
|
|
559
|
+
);
|
|
560
|
+
ctx.fill();
|
|
561
|
+
|
|
562
|
+
// Bordure légère
|
|
563
|
+
if (!btn.disabled) {
|
|
564
|
+
ctx.strokeStyle = 'rgba(0, 0, 0, 0.1)';
|
|
565
|
+
ctx.lineWidth = 0.5;
|
|
566
|
+
ctx.stroke();
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Effet pressed
|
|
570
|
+
if (isPressed && !btn.disabled) {
|
|
571
|
+
ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
|
|
572
|
+
ctx.fill();
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Icône chevron iOS
|
|
576
|
+
const chevronColor = btn.disabled ? this.inactiveColor : this.activeColor;
|
|
577
|
+
this.drawChevron(
|
|
578
|
+
ctx,
|
|
579
|
+
btn.x + btn.size / 2,
|
|
580
|
+
btn.y + btn.size / 2,
|
|
581
|
+
btn.action === 'prev' ? 'left' : 'right',
|
|
582
|
+
chevronColor
|
|
583
|
+
);
|
|
584
|
+
|
|
585
|
+
ctx.restore();
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Dessine un chevron iOS
|
|
591
|
+
* @private
|
|
592
|
+
*/
|
|
593
|
+
drawChevron(ctx, cx, cy, direction, color) {
|
|
594
|
+
const size = 10;
|
|
595
|
+
|
|
596
|
+
ctx.save();
|
|
597
|
+
ctx.strokeStyle = color;
|
|
598
|
+
ctx.lineWidth = 2.5;
|
|
599
|
+
ctx.lineCap = 'round';
|
|
600
|
+
ctx.lineJoin = 'round';
|
|
601
|
+
|
|
602
|
+
ctx.beginPath();
|
|
603
|
+
if (direction === 'left') {
|
|
604
|
+
ctx.moveTo(cx + size / 2, cy - size / 2);
|
|
605
|
+
ctx.lineTo(cx - size / 2, cy);
|
|
606
|
+
ctx.lineTo(cx + size / 2, cy + size / 2);
|
|
607
|
+
} else {
|
|
608
|
+
ctx.moveTo(cx - size / 2, cy - size / 2);
|
|
609
|
+
ctx.lineTo(cx + size / 2, cy);
|
|
610
|
+
ctx.lineTo(cx - size / 2, cy + size / 2);
|
|
611
|
+
}
|
|
612
|
+
ctx.stroke();
|
|
613
|
+
ctx.restore();
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Dessine le composant - UNIQUEMENT LA PAGE COURANTE
|
|
618
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
619
|
+
*/
|
|
620
|
+
draw(ctx) {
|
|
621
|
+
ctx.save();
|
|
622
|
+
|
|
623
|
+
// Ombre
|
|
624
|
+
if (this.platform === 'material') {
|
|
625
|
+
this.drawShadow(ctx);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Background principal
|
|
629
|
+
ctx.fillStyle = this.bgColor;
|
|
630
|
+
if (this.borderRadius > 0) {
|
|
631
|
+
ctx.beginPath();
|
|
632
|
+
this.roundRect(ctx, this.x, this.y, this.width, this.height, this.borderRadius);
|
|
633
|
+
ctx.fill();
|
|
634
|
+
} else {
|
|
635
|
+
ctx.fillRect(this.x, this.y, this.width, this.height);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Navigation du haut
|
|
639
|
+
if (this.showNavigation && (this.navPosition === 'top' || this.navPosition === 'both')) {
|
|
640
|
+
if (this.platform === 'material') {
|
|
641
|
+
this.drawMaterialNavigation(ctx, this.y);
|
|
642
|
+
} else {
|
|
643
|
+
this.drawCupertinoNavigation(ctx, this.y);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Zone de contenu avec clipping
|
|
648
|
+
const navTopHeight = (this.showNavigation && this.navPosition !== 'bottom') ? this.navHeight : 0;
|
|
649
|
+
const pageY = this.y + navTopHeight;
|
|
650
|
+
|
|
651
|
+
ctx.save();
|
|
652
|
+
ctx.beginPath();
|
|
653
|
+
ctx.rect(this.x, pageY, this.width, this.pageHeight);
|
|
654
|
+
ctx.clip();
|
|
655
|
+
|
|
656
|
+
// Dessiner UNIQUEMENT les enfants de la page courante
|
|
657
|
+
if (this.currentPage < this.pages.length) {
|
|
658
|
+
const currentPageChildren = this.pages[this.currentPage];
|
|
659
|
+
for (const child of currentPageChildren) {
|
|
660
|
+
if (child.visible !== false && child.draw) {
|
|
661
|
+
child.draw(ctx);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
ctx.restore();
|
|
667
|
+
|
|
668
|
+
// Navigation du bas
|
|
669
|
+
if (this.showNavigation && (this.navPosition === 'bottom' || this.navPosition === 'both')) {
|
|
670
|
+
const navBottomY = this.y + navTopHeight + this.pageHeight;
|
|
671
|
+
if (this.platform === 'material') {
|
|
672
|
+
this.drawMaterialNavigation(ctx, navBottomY);
|
|
673
|
+
} else {
|
|
674
|
+
this.drawCupertinoNavigation(ctx, navBottomY);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
ctx.restore();
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Gère le survol (pour hover effect)
|
|
683
|
+
* @param {number} x - Coordonnée X
|
|
684
|
+
* @param {number} y - Coordonnée Y
|
|
685
|
+
*/
|
|
686
|
+
handleHover(x, y) {
|
|
687
|
+
if (!this.showNavigation) return;
|
|
688
|
+
|
|
689
|
+
let newHovered = null;
|
|
690
|
+
|
|
691
|
+
for (const btn of this.navButtons) {
|
|
692
|
+
if (btn.disabled) continue;
|
|
693
|
+
|
|
694
|
+
const margin = 10;
|
|
695
|
+
if (x >= (btn.x - margin) &&
|
|
696
|
+
x <= (btn.x + btn.size + margin) &&
|
|
697
|
+
y >= (btn.y - margin) &&
|
|
698
|
+
y <= (btn.y + btn.size + margin)) {
|
|
699
|
+
newHovered = btn.action;
|
|
700
|
+
break;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
if (newHovered !== this.hoveredButton) {
|
|
705
|
+
this.hoveredButton = newHovered;
|
|
706
|
+
if (this.framework && this.framework.redraw) {
|
|
707
|
+
this.framework.redraw();
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Override setPosition pour mettre à jour les enfants
|
|
714
|
+
*/
|
|
715
|
+
setPosition(x, y) {
|
|
716
|
+
super.setPosition(x, y);
|
|
717
|
+
this.updateChildrenPositions();
|
|
718
|
+
this.updateNavButtons();
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Utilitaire: Convertit hex en rgba
|
|
723
|
+
* @private
|
|
724
|
+
*/
|
|
725
|
+
hexToRgba(hex, alpha) {
|
|
726
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
727
|
+
if (result) {
|
|
728
|
+
return `rgba(${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(result[3], 16)}, ${alpha})`;
|
|
729
|
+
}
|
|
730
|
+
return `rgba(0, 0, 0, ${alpha})`;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
roundRect(ctx, x, y, width, height, radius) {
|
|
734
|
+
ctx.beginPath();
|
|
735
|
+
ctx.moveTo(x + radius, y);
|
|
736
|
+
ctx.lineTo(x + width - radius, y);
|
|
737
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
738
|
+
ctx.lineTo(x + width, y + height - radius);
|
|
739
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
740
|
+
ctx.lineTo(x + radius, y + height);
|
|
741
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
742
|
+
ctx.lineTo(x, y + radius);
|
|
743
|
+
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
744
|
+
ctx.closePath();
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
isPointInside(x, y) {
|
|
748
|
+
const isInside = x >= this.x &&
|
|
749
|
+
x <= this.x + this.width &&
|
|
750
|
+
y >= this.y &&
|
|
751
|
+
y <= this.y + this.height;
|
|
752
|
+
|
|
753
|
+
if (isInside) {
|
|
754
|
+
if (this.navButtons && this.navButtons.length >= 2) {
|
|
755
|
+
const leftButton = this.navButtons[0];
|
|
756
|
+
const rightButton = this.navButtons[1];
|
|
757
|
+
|
|
758
|
+
const tolerance = 40;
|
|
759
|
+
|
|
760
|
+
// Bouton gauche
|
|
761
|
+
if (Math.abs(x - leftButton.x) < tolerance &&
|
|
762
|
+
Math.abs(y - leftButton.y) < tolerance) {
|
|
763
|
+
|
|
764
|
+
if (!leftButton.disabled) {
|
|
765
|
+
console.log("🎯 Ripple sur bouton gauche"); // LOG DE DEBUG
|
|
766
|
+
|
|
767
|
+
// EFFET RIPPLE
|
|
768
|
+
this.rippleButton = 'prev';
|
|
769
|
+
this.rippleStartTime = Date.now();
|
|
770
|
+
|
|
771
|
+
// Redessiner immédiatement
|
|
772
|
+
if (this.framework && this.framework.redraw) {
|
|
773
|
+
this.framework.redraw();
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// Action après un petit délai
|
|
777
|
+
setTimeout(() => {
|
|
778
|
+
if (this.currentPage > 0) {
|
|
779
|
+
this.currentPage--;
|
|
780
|
+
this.updateChildrenPositions();
|
|
781
|
+
this.updateNavButtons();
|
|
782
|
+
}
|
|
783
|
+
}, 100);
|
|
784
|
+
|
|
785
|
+
// Reset ripple après animation
|
|
786
|
+
setTimeout(() => {
|
|
787
|
+
this.rippleButton = null;
|
|
788
|
+
this.rippleStartTime = null;
|
|
789
|
+
if (this.framework && this.framework.redraw) {
|
|
790
|
+
this.framework.redraw();
|
|
791
|
+
}
|
|
792
|
+
}, 350);
|
|
793
|
+
}
|
|
794
|
+
return true;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Bouton droit
|
|
798
|
+
if (Math.abs(x - rightButton.x) < tolerance &&
|
|
799
|
+
Math.abs(y - rightButton.y) < tolerance) {
|
|
800
|
+
|
|
801
|
+
if (!rightButton.disabled) {
|
|
802
|
+
console.log("🎯 Ripple sur bouton droit"); // LOG DE DEBUG
|
|
803
|
+
|
|
804
|
+
// EFFET RIPPLE
|
|
805
|
+
this.rippleButton = 'next';
|
|
806
|
+
this.rippleStartTime = Date.now();
|
|
807
|
+
|
|
808
|
+
// Redessiner immédiatement
|
|
809
|
+
if (this.framework && this.framework.redraw) {
|
|
810
|
+
this.framework.redraw();
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Action après un petit délai
|
|
814
|
+
setTimeout(() => {
|
|
815
|
+
if (this.currentPage < this.totalPages - 1) {
|
|
816
|
+
this.currentPage++;
|
|
817
|
+
this.updateChildrenPositions();
|
|
818
|
+
this.updateNavButtons();
|
|
819
|
+
}
|
|
820
|
+
}, 100);
|
|
821
|
+
|
|
822
|
+
// Reset ripple après animation
|
|
823
|
+
setTimeout(() => {
|
|
824
|
+
this.rippleButton = null;
|
|
825
|
+
this.rippleStartTime = null;
|
|
826
|
+
if (this.framework && this.framework.redraw) {
|
|
827
|
+
this.framework.redraw();
|
|
828
|
+
}
|
|
829
|
+
}, 350);
|
|
830
|
+
}
|
|
831
|
+
return true;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
return isInside;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
export default PaginatedContainer;
|
package/core/CanvasFramework.js
CHANGED
|
@@ -60,6 +60,7 @@ import FloatedCamera from '../components/FloatedCamera.js';
|
|
|
60
60
|
import TimePicker from '../components/TimePicker.js';
|
|
61
61
|
import QRCodeReader from '../components/QRCodeReader.js';
|
|
62
62
|
import QRCodeGenerator from '../components/QRCodeGenerator.js';
|
|
63
|
+
import PaginatedContainer from '../components/PaginatedContainer.js';
|
|
63
64
|
|
|
64
65
|
// Utils
|
|
65
66
|
import SafeArea from '../utils/SafeArea.js';
|
package/core/UIBuilder.js
CHANGED
|
@@ -60,6 +60,7 @@ import FloatedCamera from '../components/FloatedCamera.js';
|
|
|
60
60
|
import TimePicker from '../components/TimePicker.js';
|
|
61
61
|
import QRCodeReader from '../components/QRCodeReader.js';
|
|
62
62
|
import QRCodeGenerator from '../components/QRCodeGenerator.js';
|
|
63
|
+
import PaginatedContainer from '../components/PaginatedContainer.js';
|
|
63
64
|
|
|
64
65
|
// Features
|
|
65
66
|
import PullToRefresh from '../features/PullToRefresh.js';
|
|
@@ -145,6 +146,7 @@ const Components = {
|
|
|
145
146
|
Column,
|
|
146
147
|
Positioned,
|
|
147
148
|
Banner,
|
|
149
|
+
PaginatedContainer,
|
|
148
150
|
Chart,
|
|
149
151
|
QRCodeGenerator,
|
|
150
152
|
Stack
|
package/index.js
CHANGED
|
@@ -67,6 +67,7 @@ export { default as FloatedCamera } from './components/FloatedCamera.js';
|
|
|
67
67
|
export { default as TimePicker } from './components/TimePicker.js';
|
|
68
68
|
export { default as QRCodeReader } from './components/QRCodeReader.js';
|
|
69
69
|
export { default as QRCodeGenerator } from './components/QRCodeGenerator.js';
|
|
70
|
+
export { default as PaginatedContainer } from './components/PaginatedContainer.js';
|
|
70
71
|
|
|
71
72
|
// Utils
|
|
72
73
|
export { default as SafeArea } from './utils/SafeArea.js';
|