canvasframework 0.5.57 → 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 +274 -94
- 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';
|
|
@@ -374,7 +375,12 @@ class CanvasFramework {
|
|
|
374
375
|
};
|
|
375
376
|
this._firstRenderDone = false;
|
|
376
377
|
this._startupStartTime = startTime;
|
|
378
|
+
// Dans le constructeur, après this.scrollFriction = 0.95;
|
|
379
|
+
this.scrollFriction = 0.95;
|
|
377
380
|
|
|
381
|
+
// ✅ AJOUTER
|
|
382
|
+
this.overscrollDistance = 0;
|
|
383
|
+
this.isOverscrollAnimating = false;
|
|
378
384
|
// ✅ Créer automatiquement le canvas
|
|
379
385
|
this.canvas = document.createElement('canvas');
|
|
380
386
|
this.canvas.id = canvasId || `canvas-${Date.now()}`;
|
|
@@ -620,6 +626,7 @@ class CanvasFramework {
|
|
|
620
626
|
if (this.optimizations.useSpatialPartitioning) {
|
|
621
627
|
this._initSpatialPartitioning();
|
|
622
628
|
}
|
|
629
|
+
|
|
623
630
|
}
|
|
624
631
|
|
|
625
632
|
/**
|
|
@@ -688,7 +695,7 @@ class CanvasFramework {
|
|
|
688
695
|
* Crée le Worker pour le calcul du scroll
|
|
689
696
|
*/
|
|
690
697
|
createScrollWorker() {
|
|
691
|
-
|
|
698
|
+
const workerCode = `
|
|
692
699
|
let state = {
|
|
693
700
|
scrollOffset: 0,
|
|
694
701
|
scrollVelocity: 0,
|
|
@@ -697,7 +704,10 @@ class CanvasFramework {
|
|
|
697
704
|
maxScroll: 0,
|
|
698
705
|
height: 0,
|
|
699
706
|
lastTouchY: 0,
|
|
700
|
-
components: []
|
|
707
|
+
components: [],
|
|
708
|
+
overscrollDistance: 0,
|
|
709
|
+
maxOverscroll: 150, // ✅ Limite maximale
|
|
710
|
+
overscrollResistance: 0.3
|
|
701
711
|
};
|
|
702
712
|
|
|
703
713
|
const FIXED_COMPONENT_TYPES = [
|
|
@@ -708,13 +718,11 @@ class CanvasFramework {
|
|
|
708
718
|
|
|
709
719
|
const calculateMaxScroll = () => {
|
|
710
720
|
let maxY = 0;
|
|
711
|
-
|
|
712
721
|
for (const comp of state.components) {
|
|
713
722
|
if (FIXED_COMPONENT_TYPES.includes(comp.type) || !comp.visible) continue;
|
|
714
723
|
const bottom = comp.y + comp.height;
|
|
715
724
|
if (bottom > maxY) maxY = bottom;
|
|
716
725
|
}
|
|
717
|
-
|
|
718
726
|
return Math.max(0, maxY - state.height + 50);
|
|
719
727
|
};
|
|
720
728
|
|
|
@@ -722,7 +730,29 @@ class CanvasFramework {
|
|
|
722
730
|
if (Math.abs(state.scrollVelocity) > 0.1 && !state.isDragging) {
|
|
723
731
|
state.scrollOffset += state.scrollVelocity;
|
|
724
732
|
state.maxScroll = calculateMaxScroll();
|
|
725
|
-
|
|
733
|
+
|
|
734
|
+
// ✅ CORRIGER: Limiter l'overscroll pendant l'inertie
|
|
735
|
+
if (state.scrollOffset > 0) {
|
|
736
|
+
// Limiter à maxOverscroll
|
|
737
|
+
if (state.scrollOffset > state.maxOverscroll) {
|
|
738
|
+
state.scrollOffset = state.maxOverscroll;
|
|
739
|
+
state.scrollVelocity = 0;
|
|
740
|
+
}
|
|
741
|
+
state.overscrollDistance = state.scrollOffset;
|
|
742
|
+
state.scrollVelocity *= 0.85;
|
|
743
|
+
} else if (state.scrollOffset < -state.maxScroll) {
|
|
744
|
+
const excess = Math.abs(state.scrollOffset + state.maxScroll);
|
|
745
|
+
// Limiter à maxOverscroll
|
|
746
|
+
if (excess > state.maxOverscroll) {
|
|
747
|
+
state.scrollOffset = -state.maxScroll - state.maxOverscroll;
|
|
748
|
+
state.scrollVelocity = 0;
|
|
749
|
+
}
|
|
750
|
+
state.overscrollDistance = state.scrollOffset + state.maxScroll;
|
|
751
|
+
state.scrollVelocity *= 0.85;
|
|
752
|
+
} else {
|
|
753
|
+
state.overscrollDistance = 0;
|
|
754
|
+
}
|
|
755
|
+
|
|
726
756
|
state.scrollVelocity *= state.scrollFriction;
|
|
727
757
|
} else {
|
|
728
758
|
state.scrollVelocity = 0;
|
|
@@ -731,20 +761,82 @@ class CanvasFramework {
|
|
|
731
761
|
return {
|
|
732
762
|
scrollOffset: state.scrollOffset,
|
|
733
763
|
scrollVelocity: state.scrollVelocity,
|
|
734
|
-
maxScroll: state.maxScroll
|
|
764
|
+
maxScroll: state.maxScroll,
|
|
765
|
+
overscrollDistance: state.overscrollDistance
|
|
735
766
|
};
|
|
736
767
|
};
|
|
737
768
|
|
|
738
769
|
const handleTouchMove = (deltaY) => {
|
|
739
770
|
if (state.isDragging) {
|
|
740
|
-
state.scrollOffset += deltaY;
|
|
741
771
|
state.maxScroll = calculateMaxScroll();
|
|
742
|
-
|
|
772
|
+
const wouldBeOffset = state.scrollOffset + deltaY;
|
|
773
|
+
|
|
774
|
+
let actualDelta = deltaY;
|
|
775
|
+
|
|
776
|
+
// ✅ CORRIGER: Appliquer les limites d'overscroll
|
|
777
|
+
if (wouldBeOffset > 0) {
|
|
778
|
+
// Overscroll en haut
|
|
779
|
+
const currentOverscroll = state.scrollOffset > 0 ? state.scrollOffset : 0;
|
|
780
|
+
|
|
781
|
+
// Si on a déjà atteint la limite, ne plus bouger
|
|
782
|
+
if (currentOverscroll >= state.maxOverscroll) {
|
|
783
|
+
actualDelta = 0;
|
|
784
|
+
state.overscrollDistance = state.maxOverscroll;
|
|
785
|
+
} else {
|
|
786
|
+
// Calculer la résistance progressive
|
|
787
|
+
const overscrollRatio = currentOverscroll / state.maxOverscroll;
|
|
788
|
+
const resistance = state.overscrollResistance * (1 - overscrollRatio * 0.7);
|
|
789
|
+
|
|
790
|
+
actualDelta = deltaY * resistance;
|
|
791
|
+
|
|
792
|
+
// S'assurer qu'on ne dépasse pas la limite
|
|
793
|
+
const newOverscroll = currentOverscroll + actualDelta;
|
|
794
|
+
if (newOverscroll > state.maxOverscroll) {
|
|
795
|
+
actualDelta = state.maxOverscroll - currentOverscroll;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
state.overscrollDistance = currentOverscroll + actualDelta;
|
|
799
|
+
}
|
|
800
|
+
} else if (wouldBeOffset < -state.maxScroll) {
|
|
801
|
+
// Overscroll en bas
|
|
802
|
+
const currentOverscroll = state.scrollOffset < -state.maxScroll
|
|
803
|
+
? Math.abs(state.scrollOffset + state.maxScroll)
|
|
804
|
+
: 0;
|
|
805
|
+
|
|
806
|
+
// Si on a déjà atteint la limite, ne plus bouger
|
|
807
|
+
if (currentOverscroll >= state.maxOverscroll) {
|
|
808
|
+
actualDelta = 0;
|
|
809
|
+
state.overscrollDistance = -state.maxOverscroll;
|
|
810
|
+
} else {
|
|
811
|
+
// Calculer la résistance progressive
|
|
812
|
+
const overscrollRatio = currentOverscroll / state.maxOverscroll;
|
|
813
|
+
const resistance = state.overscrollResistance * (1 - overscrollRatio * 0.7);
|
|
814
|
+
|
|
815
|
+
actualDelta = deltaY * resistance;
|
|
816
|
+
|
|
817
|
+
// S'assurer qu'on ne dépasse pas la limite
|
|
818
|
+
const newOverscroll = currentOverscroll + Math.abs(actualDelta);
|
|
819
|
+
if (newOverscroll > state.maxOverscroll) {
|
|
820
|
+
actualDelta = deltaY > 0
|
|
821
|
+
? state.maxOverscroll - currentOverscroll
|
|
822
|
+
: -(state.maxOverscroll - currentOverscroll);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
state.overscrollDistance = -(currentOverscroll + Math.abs(actualDelta));
|
|
826
|
+
}
|
|
827
|
+
} else {
|
|
828
|
+
// Scroll normal
|
|
829
|
+
state.overscrollDistance = 0;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
state.scrollOffset += actualDelta;
|
|
743
833
|
state.scrollVelocity = deltaY;
|
|
834
|
+
|
|
744
835
|
return {
|
|
745
836
|
scrollOffset: state.scrollOffset,
|
|
746
837
|
scrollVelocity: state.scrollVelocity,
|
|
747
|
-
maxScroll: state.maxScroll
|
|
838
|
+
maxScroll: state.maxScroll,
|
|
839
|
+
overscrollDistance: state.overscrollDistance
|
|
748
840
|
};
|
|
749
841
|
}
|
|
750
842
|
return null;
|
|
@@ -755,16 +847,14 @@ class CanvasFramework {
|
|
|
755
847
|
|
|
756
848
|
switch (type) {
|
|
757
849
|
case 'INIT':
|
|
758
|
-
state = {
|
|
759
|
-
...state,
|
|
760
|
-
...payload
|
|
761
|
-
};
|
|
850
|
+
state = { ...state, ...payload };
|
|
762
851
|
state.maxScroll = calculateMaxScroll();
|
|
763
852
|
self.postMessage({
|
|
764
853
|
type: 'INITIALIZED',
|
|
765
854
|
payload: {
|
|
766
855
|
scrollOffset: state.scrollOffset,
|
|
767
|
-
maxScroll: state.maxScroll
|
|
856
|
+
maxScroll: state.maxScroll,
|
|
857
|
+
overscrollDistance: 0
|
|
768
858
|
}
|
|
769
859
|
});
|
|
770
860
|
break;
|
|
@@ -817,12 +907,43 @@ class CanvasFramework {
|
|
|
817
907
|
case 'SET_SCROLL_OFFSET':
|
|
818
908
|
state.scrollOffset = payload.scrollOffset;
|
|
819
909
|
state.maxScroll = calculateMaxScroll();
|
|
820
|
-
state.
|
|
910
|
+
state.overscrollDistance = 0;
|
|
911
|
+
self.postMessage({
|
|
912
|
+
type: 'SCROLL_UPDATED',
|
|
913
|
+
payload: {
|
|
914
|
+
scrollOffset: state.scrollOffset,
|
|
915
|
+
maxScroll: state.maxScroll,
|
|
916
|
+
overscrollDistance: 0
|
|
917
|
+
}
|
|
918
|
+
});
|
|
919
|
+
break;
|
|
920
|
+
|
|
921
|
+
case 'ANIMATE_RETURN':
|
|
922
|
+
state.maxScroll = calculateMaxScroll();
|
|
923
|
+
|
|
924
|
+
if (state.scrollOffset > 0) {
|
|
925
|
+
state.scrollOffset *= 0.75;
|
|
926
|
+
state.overscrollDistance = state.scrollOffset;
|
|
927
|
+
} else if (state.scrollOffset < -state.maxScroll) {
|
|
928
|
+
const diff = state.scrollOffset + state.maxScroll;
|
|
929
|
+
state.scrollOffset = -state.maxScroll + (diff * 0.75);
|
|
930
|
+
state.overscrollDistance = diff * 0.75;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
const shouldContinue = Math.abs(state.overscrollDistance) > 1;
|
|
934
|
+
|
|
935
|
+
if (!shouldContinue) {
|
|
936
|
+
state.scrollOffset = Math.max(Math.min(state.scrollOffset, 0), -state.maxScroll);
|
|
937
|
+
state.overscrollDistance = 0;
|
|
938
|
+
}
|
|
939
|
+
|
|
821
940
|
self.postMessage({
|
|
822
941
|
type: 'SCROLL_UPDATED',
|
|
823
942
|
payload: {
|
|
824
943
|
scrollOffset: state.scrollOffset,
|
|
825
|
-
maxScroll: state.maxScroll
|
|
944
|
+
maxScroll: state.maxScroll,
|
|
945
|
+
overscrollDistance: state.overscrollDistance,
|
|
946
|
+
shouldContinue
|
|
826
947
|
}
|
|
827
948
|
});
|
|
828
949
|
break;
|
|
@@ -834,7 +955,8 @@ class CanvasFramework {
|
|
|
834
955
|
scrollOffset: state.scrollOffset,
|
|
835
956
|
scrollVelocity: state.scrollVelocity,
|
|
836
957
|
maxScroll: state.maxScroll,
|
|
837
|
-
isDragging: state.isDragging
|
|
958
|
+
isDragging: state.isDragging,
|
|
959
|
+
overscrollDistance: state.overscrollDistance
|
|
838
960
|
}
|
|
839
961
|
});
|
|
840
962
|
break;
|
|
@@ -842,64 +964,71 @@ class CanvasFramework {
|
|
|
842
964
|
};
|
|
843
965
|
`;
|
|
844
966
|
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
return new Worker(URL.createObjectURL(blob));
|
|
849
|
-
}
|
|
967
|
+
const blob = new Blob([workerCode], { type: 'application/javascript' });
|
|
968
|
+
return new Worker(URL.createObjectURL(blob));
|
|
969
|
+
}
|
|
850
970
|
|
|
851
971
|
/**
|
|
852
972
|
* Gère les messages du Scroll Worker
|
|
853
973
|
*/
|
|
854
974
|
handleScrollWorkerMessage(e) {
|
|
855
|
-
|
|
856
|
-
type,
|
|
857
|
-
payload
|
|
858
|
-
} = e.data;
|
|
975
|
+
const { type, payload } = e.data;
|
|
859
976
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
this.
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
this.components.forEach(comp => {
|
|
875
|
-
if (!this.isFixedComponent(comp)) {
|
|
876
|
-
this.markComponentDirty(comp);
|
|
877
|
-
}
|
|
878
|
-
});
|
|
879
|
-
}
|
|
880
|
-
break;
|
|
977
|
+
switch (type) {
|
|
978
|
+
case 'SCROLL_UPDATED':
|
|
979
|
+
this.scrollOffset = payload.scrollOffset;
|
|
980
|
+
this.scrollVelocity = payload.scrollVelocity;
|
|
981
|
+
|
|
982
|
+
// ✅ AJOUTER
|
|
983
|
+
this.overscrollDistance = payload.overscrollDistance || 0;
|
|
984
|
+
|
|
985
|
+
if (Math.abs(payload.scrollVelocity) > 0.5) {
|
|
986
|
+
this.dirtyComponents.clear();
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
this._cachedMaxScroll = payload.maxScroll;
|
|
990
|
+
this._maxScrollDirty = false;
|
|
881
991
|
|
|
882
|
-
|
|
883
|
-
this.
|
|
884
|
-
|
|
885
|
-
|
|
992
|
+
if (Math.abs(payload.scrollVelocity) > 0) {
|
|
993
|
+
this.components.forEach(comp => {
|
|
994
|
+
if (!this.isFixedComponent(comp)) {
|
|
995
|
+
this.markComponentDirty(comp);
|
|
996
|
+
}
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
// ✅ AJOUTER: Continuer l'animation de retour si nécessaire
|
|
1001
|
+
if (payload.shouldContinue !== undefined && payload.shouldContinue) {
|
|
1002
|
+
requestAnimationFrame(() => {
|
|
1003
|
+
this.scrollWorker.postMessage({ type: 'ANIMATE_RETURN' });
|
|
1004
|
+
});
|
|
1005
|
+
} else if (payload.shouldContinue === false) {
|
|
1006
|
+
this.isOverscrollAnimating = false;
|
|
1007
|
+
}
|
|
1008
|
+
break;
|
|
886
1009
|
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
break;
|
|
1010
|
+
case 'MAX_SCROLL_UPDATED':
|
|
1011
|
+
this._cachedMaxScroll = payload.maxScroll;
|
|
1012
|
+
this._maxScrollDirty = false;
|
|
1013
|
+
break;
|
|
892
1014
|
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1015
|
+
case 'INITIALIZED':
|
|
1016
|
+
this.scrollOffset = payload.scrollOffset;
|
|
1017
|
+
this._cachedMaxScroll = payload.maxScroll;
|
|
1018
|
+
this._maxScrollDirty = false;
|
|
1019
|
+
this.overscrollDistance = 0; // ✅ AJOUTER
|
|
1020
|
+
break;
|
|
1021
|
+
|
|
1022
|
+
case 'STATE':
|
|
1023
|
+
this.scrollOffset = payload.scrollOffset;
|
|
1024
|
+
this.scrollVelocity = payload.scrollVelocity;
|
|
1025
|
+
this.isDragging = payload.isDragging;
|
|
1026
|
+
this._cachedMaxScroll = payload.maxScroll;
|
|
1027
|
+
this._maxScrollDirty = false;
|
|
1028
|
+
this.overscrollDistance = payload.overscrollDistance || 0; // ✅ AJOUTER
|
|
1029
|
+
break;
|
|
902
1030
|
}
|
|
1031
|
+
}
|
|
903
1032
|
|
|
904
1033
|
/**
|
|
905
1034
|
* Initialise le Scroll Worker avec les données actuelles
|
|
@@ -2311,23 +2440,29 @@ class CanvasFramework {
|
|
|
2311
2440
|
}
|
|
2312
2441
|
|
|
2313
2442
|
handleTouchEnd(e) {
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2443
|
+
e.preventDefault();
|
|
2444
|
+
const touch = e.changedTouches[0];
|
|
2445
|
+
const pos = this.getTouchPos(touch);
|
|
2317
2446
|
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2447
|
+
if (!this.isDragging) {
|
|
2448
|
+
this.checkComponentsAtPosition(pos.x, pos.y, 'end');
|
|
2449
|
+
} else {
|
|
2450
|
+
this.isDragging = false;
|
|
2451
|
+
this.scrollWorker.postMessage({
|
|
2452
|
+
type: 'SET_DRAGGING',
|
|
2453
|
+
payload: {
|
|
2454
|
+
isDragging: false,
|
|
2455
|
+
lastVelocity: this.scrollVelocity
|
|
2456
|
+
}
|
|
2457
|
+
});
|
|
2458
|
+
|
|
2459
|
+
// ✅ AJOUTER: Démarrer l'animation de retour
|
|
2460
|
+
if (!this.isOverscrollAnimating) {
|
|
2461
|
+
this.isOverscrollAnimating = true;
|
|
2462
|
+
this.scrollWorker.postMessage({ type: 'ANIMATE_RETURN' });
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2331
2466
|
|
|
2332
2467
|
handleMouseDown(e) {
|
|
2333
2468
|
this.isDragging = false;
|
|
@@ -2375,21 +2510,26 @@ class CanvasFramework {
|
|
|
2375
2510
|
}
|
|
2376
2511
|
}
|
|
2377
2512
|
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2513
|
+
handleMouseUp(e) {
|
|
2514
|
+
if (!this.isDragging) {
|
|
2515
|
+
this.checkComponentsAtPosition(e.clientX, e.clientY, 'end');
|
|
2516
|
+
} else {
|
|
2517
|
+
this.isDragging = false;
|
|
2518
|
+
this.scrollWorker.postMessage({
|
|
2519
|
+
type: 'SET_DRAGGING',
|
|
2520
|
+
payload: {
|
|
2521
|
+
isDragging: false,
|
|
2522
|
+
lastVelocity: this.scrollVelocity
|
|
2523
|
+
}
|
|
2524
|
+
});
|
|
2525
|
+
|
|
2526
|
+
// ✅ AJOUTER: Démarrer l'animation de retour
|
|
2527
|
+
if (!this.isOverscrollAnimating) {
|
|
2528
|
+
this.isOverscrollAnimating = true;
|
|
2529
|
+
this.scrollWorker.postMessage({ type: 'ANIMATE_RETURN' });
|
|
2390
2530
|
}
|
|
2391
2531
|
}
|
|
2392
|
-
|
|
2532
|
+
}
|
|
2393
2533
|
|
|
2394
2534
|
getTouchPos(touch) {
|
|
2395
2535
|
const rect = this.canvas.getBoundingClientRect();
|
|
@@ -2988,6 +3128,46 @@ class CanvasFramework {
|
|
|
2988
3128
|
this.renderFull();
|
|
2989
3129
|
}
|
|
2990
3130
|
}
|
|
3131
|
+
|
|
3132
|
+
/**
|
|
3133
|
+
* Dessine l'effet d'overscroll (overlay gris)
|
|
3134
|
+
*/
|
|
3135
|
+
drawOverscrollEffect() {console.log('dessine');
|
|
3136
|
+
if (Math.abs(this.overscrollDistance) < 1) return;
|
|
3137
|
+
|
|
3138
|
+
const ctx = this.ctx;
|
|
3139
|
+
ctx.save();
|
|
3140
|
+
|
|
3141
|
+
// Calculer l'opacité (max 0.4 pour Android)
|
|
3142
|
+
const maxOverscroll = 150;
|
|
3143
|
+
const opacity = Math.min(Math.abs(this.overscrollDistance) / maxOverscroll, 1) * 0.4;
|
|
3144
|
+
|
|
3145
|
+
// Hauteur de l'overlay
|
|
3146
|
+
const overlayHeight = Math.min(Math.abs(this.overscrollDistance) * 1.2, 250);
|
|
3147
|
+
|
|
3148
|
+
let gradient;
|
|
3149
|
+
|
|
3150
|
+
// Overscroll en haut
|
|
3151
|
+
if (this.overscrollDistance > 0) {
|
|
3152
|
+
gradient = ctx.createLinearGradient(0, 0, 0, overlayHeight);
|
|
3153
|
+
gradient.addColorStop(0, `rgba(100, 100, 100, ${opacity})`);
|
|
3154
|
+
gradient.addColorStop(1, 'rgba(100, 100, 100, 0)');
|
|
3155
|
+
|
|
3156
|
+
ctx.fillStyle = gradient;
|
|
3157
|
+
ctx.fillRect(0, 0, this.width, overlayHeight);
|
|
3158
|
+
}
|
|
3159
|
+
// Overscroll en bas
|
|
3160
|
+
else if (this.overscrollDistance < 0) {
|
|
3161
|
+
gradient = ctx.createLinearGradient(0, this.height - overlayHeight, 0, this.height);
|
|
3162
|
+
gradient.addColorStop(0, 'rgba(100, 100, 100, 0)');
|
|
3163
|
+
gradient.addColorStop(1, `rgba(100, 100, 100, ${opacity})`);
|
|
3164
|
+
|
|
3165
|
+
ctx.fillStyle = gradient;
|
|
3166
|
+
ctx.fillRect(0, this.height - overlayHeight, this.width, overlayHeight);
|
|
3167
|
+
}
|
|
3168
|
+
|
|
3169
|
+
ctx.restore();
|
|
3170
|
+
}
|
|
2991
3171
|
|
|
2992
3172
|
/**
|
|
2993
3173
|
* Rendu normal (sans transition)
|
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';
|