canvasframework 0.3.18 → 0.3.20
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/Banner.js +342 -0
- package/components/RadioButton.js +17 -9
- package/components/Tabs.js +309 -149
- package/core/CanvasFramework.js +80 -9
- package/core/UIBuilder.js +2 -0
- package/index.js +2 -1
- package/package.json +1 -1
- package/core/CanvasWork.js +0 -32
- package/core/LogicWorker.js +0 -25
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
// components/Banner.js
|
|
2
|
+
import Component from '../core/Component.js';
|
|
3
|
+
|
|
4
|
+
export default class Banner extends Component {
|
|
5
|
+
constructor(framework, options = {}) {
|
|
6
|
+
super(framework, options);
|
|
7
|
+
|
|
8
|
+
this.text = options.text || '';
|
|
9
|
+
this.type = options.type || 'info';
|
|
10
|
+
this.actions = options.actions || [];
|
|
11
|
+
this.dismissible = options.dismissible === true;
|
|
12
|
+
|
|
13
|
+
this.platform = framework.platform || 'material';
|
|
14
|
+
|
|
15
|
+
this.width = options.width || framework.width || window.innerWidth;
|
|
16
|
+
this.height = options.height || 64;
|
|
17
|
+
this.x = options.x || 0;
|
|
18
|
+
this.y = options.y || 0;
|
|
19
|
+
|
|
20
|
+
this.visible = options.visible !== false;
|
|
21
|
+
this.progress = this.visible ? 1 : 0;
|
|
22
|
+
this.animSpeed = 0.18;
|
|
23
|
+
|
|
24
|
+
this._lastUpdate = performance.now();
|
|
25
|
+
this._colors = this._resolveColors();
|
|
26
|
+
|
|
27
|
+
// Bounds calculées à chaque frame
|
|
28
|
+
this._actionBounds = [];
|
|
29
|
+
this._dismissBounds = null;
|
|
30
|
+
|
|
31
|
+
// Pour indiquer qu'on gère nos propres clics
|
|
32
|
+
this.selfManagedClicks = true;
|
|
33
|
+
|
|
34
|
+
// Écouter les événements directement sur le canvas
|
|
35
|
+
this._setupEventListeners();
|
|
36
|
+
|
|
37
|
+
// Ref si fourni
|
|
38
|
+
if (options.ref) options.ref.current = this;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* ===================== Setup ===================== */
|
|
42
|
+
_setupEventListeners() {
|
|
43
|
+
// Stocker les références pour pouvoir les retirer plus tard
|
|
44
|
+
this._boundHandleClick = this._handleClick.bind(this);
|
|
45
|
+
|
|
46
|
+
// Écouter les événements sur le canvas parent
|
|
47
|
+
if (this.framework && this.framework.canvas) {
|
|
48
|
+
this.framework.canvas.addEventListener('click', this._boundHandleClick);
|
|
49
|
+
this.framework.canvas.addEventListener('touchend', this._boundHandleClick);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
_removeEventListeners() {
|
|
54
|
+
if (this.framework && this.framework.canvas && this._boundHandleClick) {
|
|
55
|
+
this.framework.canvas.removeEventListener('click', this._boundHandleClick);
|
|
56
|
+
this.framework.canvas.removeEventListener('touchend', this._boundHandleClick);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* ===================== Lifecycle ===================== */
|
|
61
|
+
onMount() {
|
|
62
|
+
this._setupEventListeners();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
onUnmount() {
|
|
66
|
+
this._removeEventListeners();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* ===================== Colors ===================== */
|
|
70
|
+
_resolveColors() {
|
|
71
|
+
if (this.platform === 'cupertino') {
|
|
72
|
+
return {
|
|
73
|
+
bg: 'rgba(250,250,250,0.95)',
|
|
74
|
+
fg: '#000',
|
|
75
|
+
accent: '#007AFF',
|
|
76
|
+
divider: 'rgba(60,60,67,0.15)'
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Material v3
|
|
81
|
+
const map = {
|
|
82
|
+
info: '#E8F0FE',
|
|
83
|
+
success: '#E6F4EA',
|
|
84
|
+
warning: '#FEF7E0',
|
|
85
|
+
error: '#FCE8E6'
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
bg: map[this.type] || map.info,
|
|
90
|
+
fg: '#1F1F1F',
|
|
91
|
+
accent: '#1A73E8'
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* ===================== Show/Hide ===================== */
|
|
96
|
+
show() {
|
|
97
|
+
this.visible = true;
|
|
98
|
+
this.markDirty();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
hide() {
|
|
102
|
+
this.visible = false;
|
|
103
|
+
this.markDirty();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* ===================== Update ===================== */
|
|
107
|
+
update() {
|
|
108
|
+
const now = performance.now();
|
|
109
|
+
const dt = Math.min((now - this._lastUpdate) / 16.6, 3);
|
|
110
|
+
|
|
111
|
+
const target = this.visible ? 1 : 0;
|
|
112
|
+
this.progress += (target - this.progress) * this.animSpeed * dt;
|
|
113
|
+
this.progress = Math.max(0, Math.min(1, this.progress));
|
|
114
|
+
|
|
115
|
+
if (Math.abs(target - this.progress) > 0.01) this.markDirty();
|
|
116
|
+
|
|
117
|
+
this._lastUpdate = now;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* ===================== Draw ===================== */
|
|
121
|
+
draw(ctx) {
|
|
122
|
+
this.update();
|
|
123
|
+
if (this.progress <= 0.01) return;
|
|
124
|
+
|
|
125
|
+
const h = this.height * this.progress;
|
|
126
|
+
const visibleHeight = h;
|
|
127
|
+
|
|
128
|
+
ctx.save();
|
|
129
|
+
|
|
130
|
+
// Background
|
|
131
|
+
if (this.platform === 'material') {
|
|
132
|
+
ctx.shadowColor = 'rgba(0,0,0,0.18)';
|
|
133
|
+
ctx.shadowBlur = 8;
|
|
134
|
+
ctx.shadowOffsetY = 2;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
ctx.fillStyle = this._colors.bg;
|
|
138
|
+
ctx.fillRect(this.x, this.y, this.width, visibleHeight);
|
|
139
|
+
ctx.shadowColor = 'transparent';
|
|
140
|
+
|
|
141
|
+
// Divider iOS
|
|
142
|
+
if (this.platform === 'cupertino') {
|
|
143
|
+
ctx.strokeStyle = this._colors.divider;
|
|
144
|
+
ctx.beginPath();
|
|
145
|
+
ctx.moveTo(this.x, this.y + visibleHeight);
|
|
146
|
+
ctx.lineTo(this.x + this.width, this.y + visibleHeight);
|
|
147
|
+
ctx.stroke();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Text
|
|
151
|
+
ctx.fillStyle = this._colors.fg;
|
|
152
|
+
ctx.font =
|
|
153
|
+
this.platform === 'cupertino'
|
|
154
|
+
? '400 15px -apple-system'
|
|
155
|
+
: '400 14px Roboto, sans-serif';
|
|
156
|
+
ctx.textBaseline = 'middle';
|
|
157
|
+
ctx.textAlign = 'left';
|
|
158
|
+
ctx.fillText(this.text, this.x + 16, this.y + visibleHeight / 2);
|
|
159
|
+
|
|
160
|
+
// Actions - calculer et stocker les bounds
|
|
161
|
+
this._actionBounds = [];
|
|
162
|
+
let x = this.width - 16;
|
|
163
|
+
|
|
164
|
+
for (let i = this.actions.length - 1; i >= 0; i--) {
|
|
165
|
+
const action = this.actions[i];
|
|
166
|
+
const textWidth = ctx.measureText(action.label).width + 20;
|
|
167
|
+
x -= textWidth;
|
|
168
|
+
|
|
169
|
+
ctx.fillStyle = this._colors.accent;
|
|
170
|
+
ctx.textAlign = 'center';
|
|
171
|
+
ctx.textBaseline = 'middle';
|
|
172
|
+
ctx.fillText(action.label, this.x + x + textWidth / 2, this.y + visibleHeight / 2);
|
|
173
|
+
|
|
174
|
+
// Stocker la hitbox (en coordonnées écran, pas canvas)
|
|
175
|
+
this._actionBounds.push({
|
|
176
|
+
action: action,
|
|
177
|
+
bounds: {
|
|
178
|
+
x: this.x + x,
|
|
179
|
+
y: this.y + (visibleHeight - 44) / 2,
|
|
180
|
+
w: textWidth,
|
|
181
|
+
h: 44
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
x -= 12;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Dismiss button
|
|
189
|
+
if (this.dismissible) {
|
|
190
|
+
const hitSize = 44;
|
|
191
|
+
const cx = this.width - 28;
|
|
192
|
+
const cy = this.y + visibleHeight / 2;
|
|
193
|
+
|
|
194
|
+
ctx.fillStyle =
|
|
195
|
+
this.platform === 'cupertino'
|
|
196
|
+
? 'rgba(60,60,67,0.6)'
|
|
197
|
+
: this._colors.fg;
|
|
198
|
+
|
|
199
|
+
ctx.font =
|
|
200
|
+
this.platform === 'cupertino'
|
|
201
|
+
? '600 16px -apple-system'
|
|
202
|
+
: '500 16px Roboto';
|
|
203
|
+
ctx.textAlign = 'center';
|
|
204
|
+
ctx.textBaseline = 'middle';
|
|
205
|
+
ctx.fillText('×', cx, cy);
|
|
206
|
+
|
|
207
|
+
this._dismissBounds = {
|
|
208
|
+
x: cx - hitSize / 2,
|
|
209
|
+
y: cy - hitSize / 2,
|
|
210
|
+
w: hitSize,
|
|
211
|
+
h: hitSize
|
|
212
|
+
};
|
|
213
|
+
} else {
|
|
214
|
+
this._dismissBounds = null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
ctx.restore();
|
|
218
|
+
|
|
219
|
+
// DEBUG: Dessiner les hitboxes
|
|
220
|
+
if (this.framework && this.framework.debbug) {
|
|
221
|
+
this._drawDebugHitboxes(ctx);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/* ===================== Debug ===================== */
|
|
226
|
+
_drawDebugHitboxes(ctx) {
|
|
227
|
+
ctx.save();
|
|
228
|
+
ctx.strokeStyle = 'red';
|
|
229
|
+
ctx.lineWidth = 1;
|
|
230
|
+
ctx.fillStyle = 'rgba(255, 0, 0, 0.1)';
|
|
231
|
+
|
|
232
|
+
// Dessiner la hitbox principale du banner
|
|
233
|
+
const h = this.height * this.progress;
|
|
234
|
+
ctx.strokeRect(this.x, this.y, this.width, h);
|
|
235
|
+
|
|
236
|
+
// Dessiner les hitboxes des actions
|
|
237
|
+
if (this._actionBounds && this._actionBounds.length > 0) {
|
|
238
|
+
for (const item of this._actionBounds) {
|
|
239
|
+
const b = item.bounds;
|
|
240
|
+
ctx.fillRect(b.x, b.y, b.w, b.h);
|
|
241
|
+
ctx.strokeRect(b.x, b.y, b.w, b.h);
|
|
242
|
+
|
|
243
|
+
// Texte de debug
|
|
244
|
+
ctx.fillStyle = 'red';
|
|
245
|
+
ctx.font = '10px monospace';
|
|
246
|
+
ctx.fillText(item.action.label, b.x + 5, b.y + 12);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Dessiner la hitbox du dismiss button
|
|
251
|
+
if (this._dismissBounds) {
|
|
252
|
+
const b = this._dismissBounds;
|
|
253
|
+
ctx.fillRect(b.x, b.y, b.w, b.h);
|
|
254
|
+
ctx.strokeRect(b.x, b.y, b.w, b.h);
|
|
255
|
+
ctx.fillText('X', b.x + 5, b.y + 12);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
ctx.restore();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/* ===================== Click Handling ===================== */
|
|
262
|
+
_handleClick(event) {
|
|
263
|
+
if (this.progress < 0.95) return;
|
|
264
|
+
|
|
265
|
+
// Obtenir les coordonnées du clic/touch
|
|
266
|
+
let clientX, clientY;
|
|
267
|
+
|
|
268
|
+
if (event.type === 'touchend') {
|
|
269
|
+
const touch = event.changedTouches[0];
|
|
270
|
+
clientX = touch.clientX;
|
|
271
|
+
clientY = touch.clientY;
|
|
272
|
+
} else {
|
|
273
|
+
clientX = event.clientX;
|
|
274
|
+
clientY = event.clientY;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Convertir en coordonnées canvas SIMPLIFIÉ
|
|
278
|
+
const canvasRect = this.framework.canvas.getBoundingClientRect();
|
|
279
|
+
|
|
280
|
+
// Coordonnées relatives au canvas (en pixels CSS, pas en pixels canvas)
|
|
281
|
+
const x = clientX - canvasRect.left;
|
|
282
|
+
const y = clientY - canvasRect.top;
|
|
283
|
+
|
|
284
|
+
console.log('Click converted:', {
|
|
285
|
+
clientX, clientY,
|
|
286
|
+
canvasLeft: canvasRect.left,
|
|
287
|
+
canvasTop: canvasRect.top,
|
|
288
|
+
x, y,
|
|
289
|
+
bannerX: this.x,
|
|
290
|
+
bannerY: this.y,
|
|
291
|
+
bannerWidth: this.width,
|
|
292
|
+
bannerHeight: this.height * this.progress
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// Vérifier si on clique sur le banner (en coordonnées CSS)
|
|
296
|
+
const bannerBottom = this.y + (this.height * this.progress);
|
|
297
|
+
if (x < this.x || x > this.x + this.width || y < this.y || y > bannerBottom) {
|
|
298
|
+
console.log('Click outside banner');
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
console.log('Click INSIDE banner!');
|
|
303
|
+
|
|
304
|
+
// Empêcher la propagation
|
|
305
|
+
event.stopPropagation();
|
|
306
|
+
|
|
307
|
+
// 1️⃣ Dismiss button
|
|
308
|
+
if (this.dismissible && this._dismissBounds) {
|
|
309
|
+
const b = this._dismissBounds;
|
|
310
|
+
console.log('Checking dismiss bounds:', b, 'click:', {x, y});
|
|
311
|
+
if (x >= b.x && x <= b.x + b.w &&
|
|
312
|
+
y >= b.y && y <= b.y + b.h) {
|
|
313
|
+
console.log('Dismiss clicked!');
|
|
314
|
+
this.hide();
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// 2️⃣ Actions
|
|
320
|
+
if (this._actionBounds && this._actionBounds.length > 0) {
|
|
321
|
+
console.log('Checking', this._actionBounds.length, 'action bounds');
|
|
322
|
+
for (const item of this._actionBounds) {
|
|
323
|
+
const b = item.bounds;
|
|
324
|
+
console.log('Checking action:', item.action.label, 'bounds:', b);
|
|
325
|
+
if (x >= b.x && x <= b.x + b.w &&
|
|
326
|
+
y >= b.y && y <= b.y + b.h) {
|
|
327
|
+
console.log('Action clicked:', item.action.label);
|
|
328
|
+
item.action.onClick?.();
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
console.log('Click on banner but not on any button');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/* ===================== Resize ===================== */
|
|
338
|
+
_resize(width) {
|
|
339
|
+
this.width = width;
|
|
340
|
+
this.markDirty();
|
|
341
|
+
}
|
|
342
|
+
}
|
|
@@ -97,19 +97,27 @@ class RadioButton extends Component {
|
|
|
97
97
|
ctx.arc(centerX, centerY, 5, 0, Math.PI * 2);
|
|
98
98
|
ctx.fill();
|
|
99
99
|
}
|
|
100
|
-
|
|
101
|
-
// Cupertino
|
|
102
|
-
ctx.strokeStyle = this.checked ? '#007AFF' : '#C7C7CC';
|
|
103
|
-
ctx.lineWidth = 2;
|
|
104
|
-
ctx.beginPath();
|
|
105
|
-
ctx.arc(centerX, centerY, this.circleRadius, 0, Math.PI * 2);
|
|
106
|
-
ctx.stroke();
|
|
107
|
-
|
|
100
|
+
} else {
|
|
101
|
+
// Cupertino (iOS style)
|
|
108
102
|
if (this.checked) {
|
|
103
|
+
// Cercle bleu rempli
|
|
109
104
|
ctx.fillStyle = '#007AFF';
|
|
110
105
|
ctx.beginPath();
|
|
111
|
-
ctx.arc(centerX, centerY,
|
|
106
|
+
ctx.arc(centerX, centerY, this.circleRadius, 0, Math.PI * 2);
|
|
107
|
+
ctx.fill();
|
|
108
|
+
|
|
109
|
+
// Point blanc au centre
|
|
110
|
+
ctx.fillStyle = '#FFFFFF';
|
|
111
|
+
ctx.beginPath();
|
|
112
|
+
ctx.arc(centerX, centerY, 4, 0, Math.PI * 2);
|
|
112
113
|
ctx.fill();
|
|
114
|
+
} else {
|
|
115
|
+
// Cercle gris clair
|
|
116
|
+
ctx.strokeStyle = '#D1D1D6';
|
|
117
|
+
ctx.lineWidth = 1.5;
|
|
118
|
+
ctx.beginPath();
|
|
119
|
+
ctx.arc(centerX, centerY, this.circleRadius, 0, Math.PI * 2);
|
|
120
|
+
ctx.stroke();
|
|
113
121
|
}
|
|
114
122
|
}
|
|
115
123
|
|
package/components/Tabs.js
CHANGED
|
@@ -8,248 +8,408 @@ import Component from '../core/Component.js';
|
|
|
8
8
|
class Tabs extends Component {
|
|
9
9
|
constructor(framework, options = {}) {
|
|
10
10
|
super(framework, options);
|
|
11
|
+
|
|
11
12
|
this.tabs = options.tabs || [];
|
|
12
13
|
this.selectedIndex = options.selectedIndex || 0;
|
|
13
14
|
this.onChange = options.onChange;
|
|
14
15
|
this.platform = framework.platform;
|
|
15
|
-
this.height = options.height ||
|
|
16
|
-
|
|
17
|
-
this.
|
|
16
|
+
this.height = options.height || 56;
|
|
17
|
+
|
|
18
|
+
this.indicatorColor = options.indicatorColor ||
|
|
19
|
+
(this.platform === 'material' ? '#6200EE' : '#007AFF');
|
|
20
|
+
this.textColor = options.textColor ||
|
|
21
|
+
(this.platform === 'material' ? '#000000' : '#8E8E93');
|
|
18
22
|
this.selectedTextColor = options.selectedTextColor || this.indicatorColor;
|
|
19
23
|
|
|
20
|
-
//
|
|
24
|
+
// Ripple pour Material
|
|
21
25
|
this.ripples = [];
|
|
22
26
|
this.animationFrame = null;
|
|
23
27
|
this.lastAnimationTime = 0;
|
|
24
28
|
|
|
25
|
-
//
|
|
26
|
-
this.
|
|
27
|
-
this.
|
|
29
|
+
// Animation pour Cupertino
|
|
30
|
+
this.pressedTabIndex = -1;
|
|
31
|
+
this.pressAnimation = 0;
|
|
32
|
+
|
|
33
|
+
// ✅ Structure: tableau de tableaux d'enfants
|
|
34
|
+
// tabChildren[0] = [enfants du tab 0]
|
|
35
|
+
// tabChildren[1] = [enfants du tab 1]
|
|
36
|
+
this.tabChildren = this.tabs.map(() => []);
|
|
28
37
|
|
|
38
|
+
// ✅ Configuration: nombre d'enfants par tab
|
|
39
|
+
// Si défini, distribue automatiquement les enfants
|
|
40
|
+
// Ex: childrenPerTab = [3, 2] => 3 enfants pour tab 0, 2 pour tab 1
|
|
41
|
+
this.childrenPerTab = options.childrenPerTab || null;
|
|
42
|
+
this.currentTabIndex = 0;
|
|
43
|
+
this.childAddCount = 0; // Compteur d'enfants ajoutés
|
|
44
|
+
|
|
45
|
+
// Gestionnaire de clic
|
|
29
46
|
this.onPress = this.handlePress.bind(this);
|
|
47
|
+
|
|
48
|
+
// Position par défaut
|
|
49
|
+
this.position = options.position || (this.platform === 'cupertino' ? 'bottom' : 'top');
|
|
50
|
+
|
|
51
|
+
if (this.position === 'bottom' && !options.y) {
|
|
52
|
+
this.y = framework.height - this.height;
|
|
53
|
+
} else if (this.position === 'top' && !options.y) {
|
|
54
|
+
this.y = options.appbar || 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Zone de contenu (sous les tabs)
|
|
58
|
+
this.contentY = this.y + this.height;
|
|
59
|
+
this.contentHeight = framework.height - this.height;
|
|
30
60
|
}
|
|
31
61
|
|
|
32
62
|
/**
|
|
33
|
-
*
|
|
34
|
-
* @
|
|
63
|
+
* ✅ Définit le tab actuel pour l'ajout d'enfants (appelé par UIBuilder)
|
|
64
|
+
* @param {number} tabIndex - Index du tab
|
|
35
65
|
*/
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
let needsUpdate = false;
|
|
43
|
-
|
|
44
|
-
// Mettre à jour chaque ripple
|
|
45
|
-
for (let i = this.ripples.length - 1; i >= 0; i--) {
|
|
46
|
-
const ripple = this.ripples[i];
|
|
47
|
-
|
|
48
|
-
// Animer le rayon (expansion)
|
|
49
|
-
if (ripple.radius < ripple.maxRadius) {
|
|
50
|
-
ripple.radius += (ripple.maxRadius / 300) * deltaTime;
|
|
51
|
-
needsUpdate = true;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Animer l'opacité (fade out)
|
|
55
|
-
if (ripple.radius >= ripple.maxRadius * 0.4) {
|
|
56
|
-
ripple.opacity -= (0.003 * deltaTime);
|
|
57
|
-
if (ripple.opacity < 0) ripple.opacity = 0;
|
|
58
|
-
needsUpdate = true;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Supprimer les ripples terminés
|
|
62
|
-
if (ripple.opacity <= 0 && ripple.radius >= ripple.maxRadius * 0.95) {
|
|
63
|
-
this.ripples.splice(i, 1);
|
|
64
|
-
needsUpdate = true;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
66
|
+
setCurrentTab(tabIndex) {
|
|
67
|
+
if (tabIndex >= 0 && tabIndex < this.tabChildren.length) {
|
|
68
|
+
this.currentTabIndex = tabIndex;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
67
71
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
72
|
+
/**
|
|
73
|
+
* ✅ Ajoute un enfant au tab en cours
|
|
74
|
+
* Distribution automatique: divise les enfants équitablement entre les tabs
|
|
75
|
+
* @param {Component} child - Composant enfant
|
|
76
|
+
* @returns {Component} L'enfant ajouté
|
|
77
|
+
*/
|
|
78
|
+
add(child) {
|
|
79
|
+
// Coordonnées relatives à la zone de contenu
|
|
80
|
+
child.x = child.x || 0;
|
|
81
|
+
child.y = child.y || 0;
|
|
82
|
+
|
|
83
|
+
// Dimensions par défaut
|
|
84
|
+
if (!child.width) child.width = this.framework.width;
|
|
85
|
+
|
|
86
|
+
// Marquer l'enfant comme appartenant à ce Tabs
|
|
87
|
+
child.parentTabs = this;
|
|
88
|
+
|
|
89
|
+
// ✅ Calculer quel tab doit recevoir cet enfant
|
|
90
|
+
// On distribue équitablement les enfants entre les tabs
|
|
91
|
+
const totalChildren = this.tabChildren.reduce((sum, arr) => sum + arr.length, 0);
|
|
92
|
+
const childrenPerTab = Math.ceil(totalChildren / this.tabs.length);
|
|
93
|
+
|
|
94
|
+
// Trouver le premier tab qui n'est pas encore plein
|
|
95
|
+
let targetTabIndex = 0;
|
|
96
|
+
for (let i = 0; i < this.tabChildren.length; i++) {
|
|
97
|
+
if (this.tabChildren[i].length < childrenPerTab) {
|
|
98
|
+
targetTabIndex = i;
|
|
99
|
+
break;
|
|
71
100
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
this.animationFrame = requestAnimationFrame(animate);
|
|
76
|
-
} else {
|
|
77
|
-
this.animationFrame = null;
|
|
78
|
-
this.lastAnimationTime = 0;
|
|
101
|
+
// Si tous les tabs ont childrenPerTab enfants, recommencer à 0
|
|
102
|
+
if (i === this.tabChildren.length - 1) {
|
|
103
|
+
targetTabIndex = totalChildren % this.tabs.length;
|
|
79
104
|
}
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
if (this.ripples.length > 0 && !this.animationFrame) {
|
|
83
|
-
this.animationFrame = requestAnimationFrame(animate);
|
|
84
105
|
}
|
|
106
|
+
|
|
107
|
+
// Ajouter au tableau du tab calculé
|
|
108
|
+
this.tabChildren[targetTabIndex].push(child);
|
|
109
|
+
|
|
110
|
+
// Visibilité selon le tab sélectionné
|
|
111
|
+
child.visible = (targetTabIndex === this.selectedIndex);
|
|
112
|
+
|
|
113
|
+
return child;
|
|
85
114
|
}
|
|
86
115
|
|
|
87
116
|
/**
|
|
88
|
-
*
|
|
89
|
-
* @private
|
|
117
|
+
* ✅ Met à jour la visibilité des enfants selon l'onglet sélectionné
|
|
90
118
|
*/
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
this.
|
|
94
|
-
|
|
119
|
+
updateChildrenVisibility() {
|
|
120
|
+
this.tabChildren.forEach((children, tabIdx) => {
|
|
121
|
+
const isVisible = (tabIdx === this.selectedIndex);
|
|
122
|
+
children.forEach(child => {
|
|
123
|
+
child.visible = isVisible;
|
|
124
|
+
});
|
|
125
|
+
});
|
|
95
126
|
}
|
|
96
127
|
|
|
97
128
|
/**
|
|
98
|
-
*
|
|
129
|
+
* ✅ Retourne tous les enfants du tab sélectionné
|
|
99
130
|
*/
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
cancelAnimationFrame(this.animationFrame);
|
|
103
|
-
this.animationFrame = null;
|
|
104
|
-
}
|
|
105
|
-
if (super.destroy) {
|
|
106
|
-
super.destroy();
|
|
107
|
-
}
|
|
131
|
+
getActiveChildren() {
|
|
132
|
+
return this.tabChildren[this.selectedIndex] || [];
|
|
108
133
|
}
|
|
109
134
|
|
|
110
135
|
handlePress(x, y) {
|
|
111
|
-
|
|
136
|
+
// D'abord vérifier les clics sur les enfants
|
|
137
|
+
if (y > this.y + this.height && this.checkChildClick(x, y)) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Ensuite vérifier les clics sur la barre de tabs
|
|
142
|
+
if (!this.isPointInside(x, y)) return;
|
|
112
143
|
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
const deltaX = Math.abs(x - this.lastEventCoords.x);
|
|
116
|
-
const deltaY = Math.abs(y - this.lastEventCoords.y);
|
|
144
|
+
const tabWidth = this.width / this.tabs.length;
|
|
145
|
+
const index = Math.floor((x - this.x) / tabWidth);
|
|
117
146
|
|
|
118
|
-
|
|
147
|
+
if (index < 0 || index >= this.tabs.length) return;
|
|
119
148
|
|
|
120
|
-
|
|
121
|
-
|
|
149
|
+
// Ripple pour Material
|
|
150
|
+
if (this.platform === 'material') {
|
|
151
|
+
const rippleCenterX = this.x + index * tabWidth + tabWidth / 2;
|
|
152
|
+
const maxRippleRadius = Math.min(tabWidth * 0.6, this.height * 0.8);
|
|
153
|
+
|
|
154
|
+
this.ripples.push({
|
|
155
|
+
x: rippleCenterX,
|
|
156
|
+
y: this.y + this.height / 2,
|
|
157
|
+
radius: 0,
|
|
158
|
+
maxRadius: maxRippleRadius,
|
|
159
|
+
opacity: 1
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (!this.animationFrame) this.startRippleAnimation();
|
|
163
|
+
}
|
|
164
|
+
// Animation Cupertino
|
|
165
|
+
else if (this.platform === 'cupertino') {
|
|
166
|
+
this.pressedTabIndex = index;
|
|
167
|
+
this.pressAnimation = 1;
|
|
168
|
+
this.requestRender();
|
|
169
|
+
setTimeout(() => this.animatePressRelease(), 100);
|
|
122
170
|
}
|
|
123
171
|
|
|
124
|
-
//
|
|
125
|
-
this.
|
|
126
|
-
|
|
172
|
+
// Changement d'onglet
|
|
173
|
+
if (index !== this.selectedIndex) {
|
|
174
|
+
this.selectedIndex = index;
|
|
175
|
+
this.updateChildrenVisibility();
|
|
176
|
+
if (this.onChange) this.onChange(index, this.tabs[index]);
|
|
177
|
+
}
|
|
127
178
|
|
|
128
|
-
|
|
129
|
-
|
|
179
|
+
this.requestRender();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* ✅ Vérifie les clics sur les enfants du tab actif
|
|
184
|
+
*/
|
|
185
|
+
checkChildClick(x, y) {
|
|
186
|
+
const adjustedY = y - (this.framework.scrollOffset || 0);
|
|
187
|
+
const activeChildren = this.getActiveChildren();
|
|
130
188
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
tabIndex: index
|
|
147
|
-
});
|
|
189
|
+
// Parcourir en ordre inverse (derniers ajoutés = au dessus)
|
|
190
|
+
for (let i = activeChildren.length - 1; i >= 0; i--) {
|
|
191
|
+
const child = activeChildren[i];
|
|
192
|
+
|
|
193
|
+
if (!child.visible) continue;
|
|
194
|
+
|
|
195
|
+
// Calculer les coordonnées absolues de l'enfant
|
|
196
|
+
const childX = this.x + child.x;
|
|
197
|
+
const childY = this.contentY + child.y;
|
|
198
|
+
|
|
199
|
+
// Vérifier si le clic est dans l'enfant
|
|
200
|
+
if (adjustedY >= childY &&
|
|
201
|
+
adjustedY <= childY + child.height &&
|
|
202
|
+
x >= childX &&
|
|
203
|
+
x <= childX + child.width) {
|
|
148
204
|
|
|
149
|
-
//
|
|
150
|
-
if (
|
|
151
|
-
|
|
205
|
+
// Si l'enfant a un onClick ou onPress, le déclencher
|
|
206
|
+
if (child.onClick) {
|
|
207
|
+
child.onClick();
|
|
208
|
+
return true;
|
|
209
|
+
} else if (child.onPress) {
|
|
210
|
+
child.onPress(x, adjustedY);
|
|
211
|
+
return true;
|
|
152
212
|
}
|
|
153
|
-
|
|
154
|
-
// Forcer un redessin
|
|
155
|
-
this.requestRender();
|
|
156
213
|
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
157
218
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
219
|
+
animatePressRelease() {
|
|
220
|
+
let startTime = null;
|
|
221
|
+
const duration = 150;
|
|
222
|
+
|
|
223
|
+
const animate = (timestamp) => {
|
|
224
|
+
if (!startTime) startTime = timestamp;
|
|
225
|
+
const progress = Math.min((timestamp - startTime) / duration, 1);
|
|
163
226
|
|
|
227
|
+
this.pressAnimation = 1 - progress;
|
|
164
228
|
this.requestRender();
|
|
165
|
-
|
|
229
|
+
|
|
230
|
+
if (progress < 1) requestAnimationFrame(animate);
|
|
231
|
+
else {
|
|
232
|
+
this.pressAnimation = 0;
|
|
233
|
+
this.pressedTabIndex = -1;
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
requestAnimationFrame(animate);
|
|
166
238
|
}
|
|
167
239
|
|
|
168
240
|
isPointInside(x, y) {
|
|
169
|
-
return x >= this.x && x <= this.x + this.width &&
|
|
241
|
+
return x >= this.x && x <= this.x + this.width &&
|
|
170
242
|
y >= this.y && y <= this.y + this.height;
|
|
171
243
|
}
|
|
172
244
|
|
|
245
|
+
startRippleAnimation() {
|
|
246
|
+
const animate = (timestamp) => {
|
|
247
|
+
if (!this.lastAnimationTime) this.lastAnimationTime = timestamp;
|
|
248
|
+
const deltaTime = timestamp - this.lastAnimationTime;
|
|
249
|
+
this.lastAnimationTime = timestamp;
|
|
250
|
+
|
|
251
|
+
let needsUpdate = false;
|
|
252
|
+
|
|
253
|
+
for (let i = this.ripples.length - 1; i >= 0; i--) {
|
|
254
|
+
const ripple = this.ripples[i];
|
|
255
|
+
|
|
256
|
+
if (ripple.radius < ripple.maxRadius)
|
|
257
|
+
ripple.radius += (ripple.maxRadius / 300) * deltaTime;
|
|
258
|
+
|
|
259
|
+
if (ripple.radius >= ripple.maxRadius * 0.4) {
|
|
260
|
+
ripple.opacity -= (0.003 * deltaTime);
|
|
261
|
+
if (ripple.opacity < 0) ripple.opacity = 0;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (ripple.opacity <= 0 && ripple.radius >= ripple.maxRadius * 0.95)
|
|
265
|
+
this.ripples.splice(i, 1);
|
|
266
|
+
|
|
267
|
+
needsUpdate = true;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (needsUpdate) this.requestRender();
|
|
271
|
+
|
|
272
|
+
if (this.ripples.length > 0)
|
|
273
|
+
this.animationFrame = requestAnimationFrame(animate);
|
|
274
|
+
else
|
|
275
|
+
this.animationFrame = null;
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
if (this.ripples.length && !this.animationFrame)
|
|
279
|
+
this.animationFrame = requestAnimationFrame(animate);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
requestRender() {
|
|
283
|
+
if (this.framework && this.framework.requestRender)
|
|
284
|
+
this.framework.requestRender();
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* ✅ Dessine les tabs et les enfants du tab actif
|
|
289
|
+
*/
|
|
173
290
|
draw(ctx) {
|
|
174
291
|
ctx.save();
|
|
175
292
|
|
|
293
|
+
// ===== DESSINER LA BARRE DE TABS =====
|
|
294
|
+
|
|
176
295
|
// Background
|
|
177
|
-
ctx.fillStyle = '#
|
|
296
|
+
ctx.fillStyle = this.platform === 'material' ? '#FFF' : '#F2F2F7';
|
|
178
297
|
ctx.fillRect(this.x, this.y, this.width, this.height);
|
|
179
298
|
|
|
180
|
-
// Bordure inférieure
|
|
181
|
-
ctx.strokeStyle = '#E0E0E0';
|
|
182
|
-
ctx.lineWidth = 1;
|
|
183
|
-
ctx.beginPath();
|
|
184
|
-
ctx.moveTo(this.x, this.y + this.height);
|
|
185
|
-
ctx.lineTo(this.x + this.width, this.y + this.height);
|
|
186
|
-
ctx.stroke();
|
|
187
|
-
|
|
188
|
-
const tabWidth = this.width / this.tabs.length;
|
|
189
|
-
|
|
190
|
-
// 🔹 Dessiner les ripples Material EN PREMIER
|
|
191
299
|
if (this.platform === 'material') {
|
|
192
|
-
|
|
300
|
+
ctx.strokeStyle = '#E0E0E0';
|
|
301
|
+
ctx.lineWidth = 1;
|
|
302
|
+
ctx.beginPath();
|
|
303
|
+
ctx.moveTo(this.x, this.y + this.height);
|
|
304
|
+
ctx.lineTo(this.x + this.width, this.y + this.height);
|
|
305
|
+
ctx.stroke();
|
|
193
306
|
}
|
|
194
|
-
|
|
195
|
-
|
|
307
|
+
|
|
308
|
+
const tabWidth = this.width / this.tabs.length;
|
|
309
|
+
|
|
310
|
+
// Ripples
|
|
311
|
+
if (this.platform === 'material') this.drawRipples(ctx, tabWidth);
|
|
312
|
+
|
|
196
313
|
for (let i = 0; i < this.tabs.length; i++) {
|
|
197
314
|
const tab = this.tabs[i];
|
|
198
315
|
const tabX = this.x + i * tabWidth;
|
|
199
316
|
const isSelected = i === this.selectedIndex;
|
|
317
|
+
|
|
318
|
+
// Cupertino pressed effect
|
|
319
|
+
if (this.platform === 'cupertino' && i === this.pressedTabIndex) {
|
|
320
|
+
ctx.fillStyle = `rgba(0,122,255,${0.1 * this.pressAnimation})`;
|
|
321
|
+
ctx.fillRect(tabX, this.y, tabWidth, this.height);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Indicators
|
|
325
|
+
if (this.platform === 'cupertino' && isSelected) {
|
|
326
|
+
ctx.fillStyle = '#007AFF';
|
|
327
|
+
ctx.fillRect(tabX + tabWidth/2 - 15, this.y + this.height - 2, 30, 2);
|
|
328
|
+
}
|
|
329
|
+
|
|
200
330
|
const color = isSelected ? this.selectedTextColor : this.textColor;
|
|
201
|
-
|
|
202
|
-
//
|
|
331
|
+
|
|
332
|
+
// Icon
|
|
203
333
|
if (tab.icon) {
|
|
204
|
-
ctx.font = '
|
|
334
|
+
ctx.font = '24px -apple-system, sans-serif';
|
|
205
335
|
ctx.textAlign = 'center';
|
|
206
336
|
ctx.textBaseline = 'middle';
|
|
207
337
|
ctx.fillStyle = color;
|
|
208
|
-
|
|
338
|
+
const iconY = this.platform === 'material' ? this.y + 18 : this.y + 20;
|
|
339
|
+
ctx.fillText(tab.icon, tabX + tabWidth/2, iconY);
|
|
209
340
|
}
|
|
210
|
-
|
|
341
|
+
|
|
211
342
|
// Label
|
|
212
|
-
|
|
343
|
+
const fontSize = this.platform === 'material' ? 14 : 12;
|
|
344
|
+
const fontWeight = isSelected ? '600' : '400';
|
|
345
|
+
ctx.font = `${fontWeight} ${fontSize}px -apple-system, Roboto, sans-serif`;
|
|
213
346
|
ctx.fillStyle = color;
|
|
214
347
|
ctx.textAlign = 'center';
|
|
215
348
|
ctx.textBaseline = 'middle';
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
349
|
+
|
|
350
|
+
const labelY = this.platform === 'material'
|
|
351
|
+
? (tab.icon ? this.y + 36 : this.y + this.height / 2)
|
|
352
|
+
: (tab.icon ? this.y + 42 : this.y + this.height / 2);
|
|
353
|
+
|
|
354
|
+
ctx.fillText(tab.label, tabX + tabWidth/2, labelY);
|
|
355
|
+
|
|
356
|
+
// Material indicator
|
|
220
357
|
if (isSelected && this.platform === 'material') {
|
|
221
358
|
ctx.fillStyle = this.indicatorColor;
|
|
222
|
-
ctx.fillRect(tabX, this.y + this.height -
|
|
359
|
+
ctx.fillRect(tabX, this.y + this.height - 3, tabWidth, 3);
|
|
223
360
|
}
|
|
224
361
|
}
|
|
225
|
-
|
|
362
|
+
|
|
226
363
|
ctx.restore();
|
|
364
|
+
|
|
365
|
+
// ===== DESSINER LES ENFANTS DU TAB ACTIF =====
|
|
366
|
+
const activeChildren = this.getActiveChildren();
|
|
367
|
+
|
|
368
|
+
for (let child of activeChildren) {
|
|
369
|
+
if (child.visible) {
|
|
370
|
+
ctx.save();
|
|
371
|
+
|
|
372
|
+
// Sauvegarder les coordonnées originales
|
|
373
|
+
const originalX = child.x;
|
|
374
|
+
const originalY = child.y;
|
|
375
|
+
|
|
376
|
+
// Ajuster les coordonnées pour être absolues
|
|
377
|
+
child.x = this.x + originalX;
|
|
378
|
+
child.y = this.contentY + originalY;
|
|
379
|
+
|
|
380
|
+
// Dessiner l'enfant
|
|
381
|
+
child.draw(ctx);
|
|
382
|
+
|
|
383
|
+
// Restaurer les coordonnées originales
|
|
384
|
+
child.x = originalX;
|
|
385
|
+
child.y = originalY;
|
|
386
|
+
|
|
387
|
+
ctx.restore();
|
|
388
|
+
}
|
|
389
|
+
}
|
|
227
390
|
}
|
|
228
391
|
|
|
229
|
-
/**
|
|
230
|
-
* Dessine les ripples (Material)
|
|
231
|
-
* @private
|
|
232
|
-
*/
|
|
233
392
|
drawRipples(ctx, tabWidth) {
|
|
234
|
-
// Sauvegarder le contexte
|
|
235
393
|
ctx.save();
|
|
236
|
-
|
|
237
|
-
// Créer un masque de clipping pour limiter les ripples aux onglets
|
|
238
394
|
ctx.beginPath();
|
|
239
395
|
ctx.rect(this.x, this.y, this.width, this.height);
|
|
240
396
|
ctx.clip();
|
|
241
397
|
|
|
242
398
|
for (let ripple of this.ripples) {
|
|
243
399
|
ctx.globalAlpha = ripple.opacity;
|
|
244
|
-
ctx.fillStyle = this.indicatorColor
|
|
400
|
+
ctx.fillStyle = this.indicatorColor;
|
|
245
401
|
ctx.beginPath();
|
|
246
|
-
ctx.arc(ripple.x, ripple.y, ripple.radius, 0, Math.PI
|
|
402
|
+
ctx.arc(ripple.x, ripple.y, ripple.radius, 0, Math.PI*2);
|
|
247
403
|
ctx.fill();
|
|
248
404
|
}
|
|
249
405
|
|
|
250
|
-
// Restaurer le contexte
|
|
251
406
|
ctx.restore();
|
|
252
407
|
}
|
|
408
|
+
|
|
409
|
+
destroy() {
|
|
410
|
+
if (this.animationFrame) cancelAnimationFrame(this.animationFrame);
|
|
411
|
+
if (super.destroy) super.destroy();
|
|
412
|
+
}
|
|
253
413
|
}
|
|
254
414
|
|
|
255
415
|
export default Tabs;
|
package/core/CanvasFramework.js
CHANGED
|
@@ -50,6 +50,7 @@ import ImageCarousel from '../components/ImageCarousel.js';
|
|
|
50
50
|
import PasswordInput from '../components/PasswordInput.js';
|
|
51
51
|
import InputTags from '../components/InputTags.js';
|
|
52
52
|
import InputDatalist from '../components/InputDatalist.js';
|
|
53
|
+
import Banner from '../components/Banner.js';
|
|
53
54
|
|
|
54
55
|
// Utils
|
|
55
56
|
import SafeArea from '../utils/SafeArea.js';
|
|
@@ -122,6 +123,7 @@ const FIXED_COMPONENT_TYPES = new Set([
|
|
|
122
123
|
Modal,
|
|
123
124
|
FAB,
|
|
124
125
|
Toast,
|
|
126
|
+
Banner,
|
|
125
127
|
BottomSheet,
|
|
126
128
|
ContextMenu,
|
|
127
129
|
OpenStreetMap,
|
|
@@ -187,16 +189,17 @@ class CanvasFramework {
|
|
|
187
189
|
this._lastFpsTime = performance.now();
|
|
188
190
|
this.showFps = options.showFps || false; // false par défaut
|
|
189
191
|
this.debbug = options.debug || false; // false par défaut (et correction de la faute de frappe)
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
this.worker
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
// Worker logique pour calculs séparés
|
|
192
|
+
|
|
193
|
+
// Worker pour multithreading Canvas Worker
|
|
194
|
+
this.worker = this.createCanvasWorker();
|
|
195
|
+
this.worker.onmessage = this.handleWorkerMessage.bind(this);
|
|
196
|
+
this.worker.postMessage({ type: 'INIT', payload: { components: [] } });
|
|
196
197
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
198
|
+
// Logic Worker
|
|
199
|
+
this.logicWorker = this.createLogicWorker();
|
|
200
|
+
this.logicWorker.onmessage = this.handleLogicWorkerMessage.bind(this);
|
|
201
|
+
this.logicWorkerState = {};
|
|
202
|
+
this.logicWorker.postMessage({ type: 'SET_STATE', payload: this.state });
|
|
200
203
|
|
|
201
204
|
// Envoyer l'état initial au worker
|
|
202
205
|
this.logicWorker.postMessage({ type: 'SET_STATE', payload: this.state });
|
|
@@ -254,6 +257,74 @@ class CanvasFramework {
|
|
|
254
257
|
get: () => originalFillStyle.get.call(ctx)
|
|
255
258
|
});
|
|
256
259
|
}
|
|
260
|
+
|
|
261
|
+
createCanvasWorker() {
|
|
262
|
+
const workerCode = `
|
|
263
|
+
let components = [];
|
|
264
|
+
|
|
265
|
+
self.onmessage = function(e) {
|
|
266
|
+
const { type, payload } = e.data;
|
|
267
|
+
|
|
268
|
+
switch(type) {
|
|
269
|
+
case 'INIT':
|
|
270
|
+
components = payload.components;
|
|
271
|
+
self.postMessage({ type: 'READY' });
|
|
272
|
+
break;
|
|
273
|
+
|
|
274
|
+
case 'UPDATE_LAYOUT':
|
|
275
|
+
const updated = components.map(comp => {
|
|
276
|
+
if (comp.dynamicHeight && comp.calculateHeight) {
|
|
277
|
+
comp.height = comp.calculateHeight();
|
|
278
|
+
}
|
|
279
|
+
return { id: comp.id, height: comp.height };
|
|
280
|
+
});
|
|
281
|
+
self.postMessage({ type: 'LAYOUT_DONE', payload: updated });
|
|
282
|
+
break;
|
|
283
|
+
|
|
284
|
+
case 'SCROLL_INERTIA':
|
|
285
|
+
let { offset, velocity, friction, maxScroll } = payload;
|
|
286
|
+
offset += velocity;
|
|
287
|
+
offset = Math.max(Math.min(offset, 0), -maxScroll);
|
|
288
|
+
velocity *= friction;
|
|
289
|
+
self.postMessage({ type: 'SCROLL_UPDATED', payload: { offset, velocity } });
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
`;
|
|
294
|
+
|
|
295
|
+
const blob = new Blob([workerCode], { type: 'application/javascript' });
|
|
296
|
+
return new Worker(URL.createObjectURL(blob));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
createLogicWorker() {
|
|
300
|
+
const workerCode = `
|
|
301
|
+
let state = {};
|
|
302
|
+
|
|
303
|
+
self.onmessage = async function(e) {
|
|
304
|
+
const { type, payload } = e.data;
|
|
305
|
+
|
|
306
|
+
switch(type) {
|
|
307
|
+
case 'SET_STATE':
|
|
308
|
+
state = payload;
|
|
309
|
+
self.postMessage({ type: 'STATE_UPDATED', payload: state });
|
|
310
|
+
break;
|
|
311
|
+
|
|
312
|
+
case 'EXECUTE':
|
|
313
|
+
try {
|
|
314
|
+
const fn = new Function('state', 'args', payload.fnString);
|
|
315
|
+
const result = await fn(state, payload.args);
|
|
316
|
+
self.postMessage({ type: 'EXECUTION_RESULT', payload: result });
|
|
317
|
+
} catch (err) {
|
|
318
|
+
self.postMessage({ type: 'EXECUTION_ERROR', payload: err.message });
|
|
319
|
+
}
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
`;
|
|
324
|
+
|
|
325
|
+
const blob = new Blob([workerCode], { type: 'application/javascript' });
|
|
326
|
+
return new Worker(URL.createObjectURL(blob));
|
|
327
|
+
}
|
|
257
328
|
|
|
258
329
|
// Set Theme dynamique
|
|
259
330
|
setTheme(theme) {
|
package/core/UIBuilder.js
CHANGED
|
@@ -51,6 +51,7 @@ import ImageCarousel from '../components/ImageCarousel.js';
|
|
|
51
51
|
import PasswordInput from '../components/PasswordInput.js';
|
|
52
52
|
import InputTags from '../components/InputTags.js';
|
|
53
53
|
import InputDatalist from '../components/InputDatalist.js';
|
|
54
|
+
import Banner from '../components/Banner.js';
|
|
54
55
|
|
|
55
56
|
// Features
|
|
56
57
|
import PullToRefresh from '../features/PullToRefresh.js';
|
|
@@ -129,6 +130,7 @@ const Components = {
|
|
|
129
130
|
Row,
|
|
130
131
|
Column,
|
|
131
132
|
Positioned,
|
|
133
|
+
Banner,
|
|
132
134
|
Stack
|
|
133
135
|
};
|
|
134
136
|
|
package/index.js
CHANGED
|
@@ -58,6 +58,7 @@ export { default as ImageCarousel } from './components/ImageCarousel.js';
|
|
|
58
58
|
export { default as PasswordInput } from './components/PasswordInput.js';
|
|
59
59
|
export { default as InputTags } from './components/InputTags.js';
|
|
60
60
|
export { default as InputDatalist } from './components/InputDatalist.js';
|
|
61
|
+
export { default as Banner } from './components/Banner.js';
|
|
61
62
|
|
|
62
63
|
// Utils
|
|
63
64
|
export { default as SafeArea } from './utils/SafeArea.js';
|
|
@@ -99,7 +100,7 @@ export { default as FeatureFlags } from './manager/FeatureFlags.js';
|
|
|
99
100
|
|
|
100
101
|
// Version du framework
|
|
101
102
|
|
|
102
|
-
export const VERSION = '0.3.
|
|
103
|
+
export const VERSION = '0.3.20';
|
|
103
104
|
|
|
104
105
|
|
|
105
106
|
|
package/package.json
CHANGED
package/core/CanvasWork.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
// CanvasWorker.js
|
|
2
|
-
let components = [];
|
|
3
|
-
|
|
4
|
-
self.onmessage = (e) => {
|
|
5
|
-
const { type, payload } = e.data;
|
|
6
|
-
|
|
7
|
-
switch(type) {
|
|
8
|
-
case 'INIT':
|
|
9
|
-
components = payload.components;
|
|
10
|
-
self.postMessage({ type: 'READY' });
|
|
11
|
-
break;
|
|
12
|
-
|
|
13
|
-
case 'UPDATE_LAYOUT':
|
|
14
|
-
// Recalculer la hauteur des composants dynamiques
|
|
15
|
-
const updated = components.map(comp => {
|
|
16
|
-
if (comp.dynamicHeight && comp.calculateHeight) {
|
|
17
|
-
comp.height = comp.calculateHeight();
|
|
18
|
-
}
|
|
19
|
-
return { id: comp.id, height: comp.height };
|
|
20
|
-
});
|
|
21
|
-
self.postMessage({ type: 'LAYOUT_DONE', payload: updated });
|
|
22
|
-
break;
|
|
23
|
-
|
|
24
|
-
case 'SCROLL_INERTIA':
|
|
25
|
-
let { offset, velocity, friction, maxScroll } = payload;
|
|
26
|
-
offset += velocity;
|
|
27
|
-
offset = Math.max(Math.min(offset, 0), -maxScroll);
|
|
28
|
-
velocity *= friction;
|
|
29
|
-
self.postMessage({ type: 'SCROLL_UPDATED', payload: { offset, velocity } });
|
|
30
|
-
break;
|
|
31
|
-
}
|
|
32
|
-
};
|
package/core/LogicWorker.js
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
// LogicWorker.js
|
|
2
|
-
let state = {};
|
|
3
|
-
|
|
4
|
-
self.onmessage = async (e) => {
|
|
5
|
-
const { type, payload } = e.data;
|
|
6
|
-
|
|
7
|
-
switch(type) {
|
|
8
|
-
case 'SET_STATE':
|
|
9
|
-
state = payload;
|
|
10
|
-
self.postMessage({ type: 'STATE_UPDATED', payload: state });
|
|
11
|
-
break;
|
|
12
|
-
|
|
13
|
-
case 'EXECUTE':
|
|
14
|
-
// payload: { fnString: string, args: array }
|
|
15
|
-
// Attention : on envoie la fonction en string et on l'exécute ici
|
|
16
|
-
try {
|
|
17
|
-
const fn = new Function('state', 'args', payload.fnString);
|
|
18
|
-
const result = await fn(state, payload.args);
|
|
19
|
-
self.postMessage({ type: 'EXECUTION_RESULT', payload: result });
|
|
20
|
-
} catch (err) {
|
|
21
|
-
self.postMessage({ type: 'EXECUTION_ERROR', payload: err.message });
|
|
22
|
-
}
|
|
23
|
-
break;
|
|
24
|
-
}
|
|
25
|
-
};
|