canvasframework 0.3.6

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.
Files changed (85) hide show
  1. package/README.md +554 -0
  2. package/components/Accordion.js +252 -0
  3. package/components/AndroidDatePickerDialog.js +398 -0
  4. package/components/AppBar.js +225 -0
  5. package/components/Avatar.js +202 -0
  6. package/components/BottomNavigationBar.js +205 -0
  7. package/components/BottomSheet.js +374 -0
  8. package/components/Button.js +225 -0
  9. package/components/Card.js +193 -0
  10. package/components/Checkbox.js +180 -0
  11. package/components/Chip.js +212 -0
  12. package/components/CircularProgress.js +143 -0
  13. package/components/ContextMenu.js +116 -0
  14. package/components/DatePicker.js +257 -0
  15. package/components/Dialog.js +367 -0
  16. package/components/Divider.js +125 -0
  17. package/components/Drawer.js +261 -0
  18. package/components/FAB.js +270 -0
  19. package/components/FileUpload.js +315 -0
  20. package/components/IOSDatePickerWheel.js +268 -0
  21. package/components/ImageCarousel.js +193 -0
  22. package/components/ImageComponent.js +223 -0
  23. package/components/Input.js +309 -0
  24. package/components/List.js +94 -0
  25. package/components/ListItem.js +223 -0
  26. package/components/Modal.js +364 -0
  27. package/components/MultiSelectDialog.js +206 -0
  28. package/components/NumberInput.js +271 -0
  29. package/components/ProgressBar.js +88 -0
  30. package/components/RadioButton.js +142 -0
  31. package/components/SearchInput.js +315 -0
  32. package/components/SegmentedControl.js +202 -0
  33. package/components/Select.js +199 -0
  34. package/components/SelectDialog.js +255 -0
  35. package/components/Slider.js +113 -0
  36. package/components/Snackbar.js +243 -0
  37. package/components/Stepper.js +281 -0
  38. package/components/SwipeableListItem.js +179 -0
  39. package/components/Switch.js +147 -0
  40. package/components/Table.js +492 -0
  41. package/components/Tabs.js +125 -0
  42. package/components/Text.js +141 -0
  43. package/components/TextField.js +331 -0
  44. package/components/Toast.js +236 -0
  45. package/components/TreeView.js +420 -0
  46. package/components/Video.js +397 -0
  47. package/components/View.js +140 -0
  48. package/components/VirtualList.js +120 -0
  49. package/core/CanvasFramework.js +1271 -0
  50. package/core/CanvasWork.js +32 -0
  51. package/core/Component.js +153 -0
  52. package/core/LogicWorker.js +25 -0
  53. package/core/WebGLCanvasAdapter.js +1369 -0
  54. package/features/Column.js +43 -0
  55. package/features/Grid.js +47 -0
  56. package/features/LayoutComponent.js +43 -0
  57. package/features/OpenStreetMap.js +310 -0
  58. package/features/Positioned.js +33 -0
  59. package/features/PullToRefresh.js +328 -0
  60. package/features/Row.js +40 -0
  61. package/features/SignaturePad.js +257 -0
  62. package/features/Skeleton.js +84 -0
  63. package/features/Stack.js +21 -0
  64. package/index.js +101 -0
  65. package/manager/AccessibilityManager.js +107 -0
  66. package/manager/ErrorHandler.js +59 -0
  67. package/manager/FeatureFlags.js +60 -0
  68. package/manager/MemoryManager.js +107 -0
  69. package/manager/PerformanceMonitor.js +84 -0
  70. package/manager/SecurityManager.js +54 -0
  71. package/package.json +28 -0
  72. package/utils/AnimationEngine.js +428 -0
  73. package/utils/DataStore.js +403 -0
  74. package/utils/EventBus.js +407 -0
  75. package/utils/FetchClient.js +74 -0
  76. package/utils/FormValidator.js +355 -0
  77. package/utils/GeoLocationService.js +62 -0
  78. package/utils/I18n.js +207 -0
  79. package/utils/IndexedDBManager.js +273 -0
  80. package/utils/OfflineSyncManager.js +342 -0
  81. package/utils/QueryBuilder.js +478 -0
  82. package/utils/SafeArea.js +64 -0
  83. package/utils/SecureStorage.js +289 -0
  84. package/utils/StateManager.js +207 -0
  85. package/utils/WebSocketClient.js +66 -0
@@ -0,0 +1,397 @@
1
+ import Component from '../core/Component.js';
2
+ /**
3
+ * Lecteur vidéo
4
+ * @class
5
+ * @extends Component
6
+ * @property {string} src - URL de la vidéo
7
+ * @property {string} poster - URL de l'image d'affiche
8
+ * @property {boolean} playing - En cours de lecture
9
+ * @property {string} platform - Plateforme
10
+ * @property {boolean} showControls - Afficher les contrôles
11
+ * @property {number|null} controlsTimeout - Timeout des contrôles
12
+ * @property {number} currentTime - Temps actuel
13
+ * @property {number} duration - Durée totale
14
+ * @property {number} progress - Progression (0-100)
15
+ * @property {number} volume - Volume (0-1)
16
+ * @property {boolean} showVolume - Afficher le contrôle de volume
17
+ * @property {boolean} fullscreen - Plein écran
18
+ * @property {boolean} loaded - Vidéo chargée
19
+ * @property {HTMLVideoElement} videoElement - Élément vidéo HTML
20
+ * @property {number} controlsHeight - Hauteur des contrôles
21
+ * @property {number} volumeHeight - Hauteur du contrôle de volume
22
+ * @property {Function} onPlay - Callback à la lecture
23
+ * @property {Function} onPause - Callback à la pause
24
+ * @property {Function} onEnded - Callback à la fin
25
+ * @property {Function} onFullscreen - Callback au plein écran
26
+ */
27
+ class Video extends Component {
28
+ /**
29
+ * Crée une instance de Video
30
+ * @param {CanvasFramework} framework - Framework parent
31
+ * @param {Object} [options={}] - Options de configuration
32
+ * @param {string} [options.src=''] - URL de la vidéo
33
+ * @param {string} [options.poster=''] - URL de l'image d'affiche
34
+ * @param {boolean} [options.playing=false] - Lecture initiale
35
+ * @param {boolean} [options.showControls=true] - Afficher les contrôles
36
+ * @param {Function} [options.onPlay] - Callback à la lecture
37
+ * @param {Function} [options.onPause] - Callback à la pause
38
+ * @param {Function} [options.onEnded] - Callback à la fin
39
+ * @param {Function} [options.onFullscreen] - Callback au plein écran
40
+ */
41
+ constructor(framework, options = {}) {
42
+ super(framework, options);
43
+ this.src = options.src || '';
44
+ this.poster = options.poster || '';
45
+ this.playing = false;
46
+ this.platform = framework.platform;
47
+ this.showControls = true;
48
+ this.controlsTimeout = null;
49
+ this.currentTime = 0;
50
+ this.duration = 0;
51
+ this.progress = 0;
52
+ this.volume = 1;
53
+ this.showVolume = false;
54
+ this.fullscreen = false;
55
+ this.loaded = false;
56
+
57
+ // Éléments de contrôle
58
+ this.controlsHeight = 50;
59
+ this.volumeHeight = 100;
60
+
61
+ // Créer l'élément vidéo HTML5
62
+ this.videoElement = document.createElement('video');
63
+ this.videoElement.style.position = 'fixed';
64
+ this.videoElement.style.left = '-9999px'; // Caché
65
+ this.videoElement.style.top = '-9999px';
66
+ this.videoElement.style.width = '0';
67
+ this.videoElement.style.height = '0';
68
+ this.videoElement.src = this.src;
69
+ this.videoElement.poster = this.poster;
70
+ this.videoElement.preload = 'auto';
71
+ this.videoElement.crossOrigin = 'anonymous'; // Important pour les vidéos externes
72
+ this.videoElement.controls = false; // Nous gérons nos propres contrôles
73
+
74
+ document.body.appendChild(this.videoElement);
75
+
76
+ // Événements de la vidéo
77
+ this.videoElement.addEventListener('loadedmetadata', () => {
78
+ this.duration = this.videoElement.duration;
79
+ this.loaded = true;
80
+ });
81
+
82
+ this.videoElement.addEventListener('timeupdate', () => {
83
+ this.currentTime = this.videoElement.currentTime;
84
+ this.progress = (this.currentTime / this.duration) * 100;
85
+ });
86
+
87
+ this.videoElement.addEventListener('ended', () => {
88
+ this.playing = false;
89
+ if (this.onEnded) this.onEnded();
90
+ });
91
+
92
+ this.videoElement.addEventListener('play', () => {
93
+ this.playing = true;
94
+ if (this.onPlay) this.onPlay();
95
+ });
96
+
97
+ this.videoElement.addEventListener('pause', () => {
98
+ this.playing = false;
99
+ if (this.onPause) this.onPause();
100
+ });
101
+
102
+ // Événements
103
+ this.onPlay = options.onPlay;
104
+ this.onPause = options.onPause;
105
+ this.onEnded = options.onEnded;
106
+ this.onFullscreen = options.onFullscreen;
107
+
108
+ // Définir onPress pour les contrôles
109
+ this.onPress = this.handlePress.bind(this);
110
+ this.onMove = this.handleMove.bind(this);
111
+ }
112
+
113
+ /**
114
+ * Démarre la lecture
115
+ */
116
+ play() {
117
+ if (this.videoElement) {
118
+ this.videoElement.play();
119
+ this.playing = true;
120
+ this.showControlsTemporarily();
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Met en pause
126
+ */
127
+ pause() {
128
+ if (this.videoElement) {
129
+ this.videoElement.pause();
130
+ this.playing = false;
131
+ this.showControlsTemporarily();
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Alterne lecture/pause
137
+ */
138
+ togglePlay() {
139
+ if (this.playing) {
140
+ this.pause();
141
+ } else {
142
+ this.play();
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Affiche temporairement les contrôles
148
+ * @private
149
+ */
150
+ showControlsTemporarily() {
151
+ this.showControls = true;
152
+ clearTimeout(this.controlsTimeout);
153
+ this.controlsTimeout = setTimeout(() => {
154
+ if (this.playing) {
155
+ this.showControls = false;
156
+ }
157
+ }, 3000);
158
+ }
159
+
160
+ /**
161
+ * Gère la pression (clic)
162
+ * @param {number} x - Coordonnée X
163
+ * @param {number} y - Coordonnée Y
164
+ * @private
165
+ */
166
+ handlePress(x, y) {
167
+ const adjustedY = y - this.framework.scrollOffset;
168
+
169
+ // Montrer les contrôles
170
+ this.showControls = true;
171
+ this.showControlsTemporarily();
172
+
173
+ // Bouton play/pause central
174
+ const centerX = this.x + this.width / 2;
175
+ const centerY = this.y + this.height / 2;
176
+
177
+ if (x >= centerX - 30 && x <= centerX + 30 &&
178
+ adjustedY >= centerY - 30 && adjustedY <= centerY + 30) {
179
+ this.togglePlay();
180
+ return;
181
+ }
182
+
183
+ // Barre de progression
184
+ if (this.showControls && this.loaded) {
185
+ const progressBarY = this.y + this.height - 30;
186
+ if (adjustedY >= progressBarY && adjustedY <= progressBarY + 10) {
187
+ const clickX = x - this.x;
188
+ this.progress = (clickX / this.width) * 100;
189
+ this.currentTime = (this.duration * this.progress) / 100;
190
+ if (this.videoElement) {
191
+ this.videoElement.currentTime = this.currentTime;
192
+ }
193
+ return;
194
+ }
195
+ }
196
+
197
+ // Bouton plein écran
198
+ const fullscreenX = this.x + this.width - 40;
199
+ const fullscreenY = this.y + this.height - 40;
200
+
201
+ if (x >= fullscreenX && x <= fullscreenX + 30 &&
202
+ adjustedY >= fullscreenY && adjustedY <= fullscreenY + 30) {
203
+ this.toggleFullscreen();
204
+ return;
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Gère le mouvement
210
+ * @param {number} x - Coordonnée X
211
+ * @param {number} y - Coordonnée Y
212
+ * @private
213
+ */
214
+ handleMove(x, y) {
215
+ // Pour des interactions supplémentaires
216
+ }
217
+
218
+ /**
219
+ * Alterne le plein écran
220
+ */
221
+ toggleFullscreen() {
222
+ if (!document.fullscreenElement && this.videoElement.requestFullscreen) {
223
+ this.videoElement.requestFullscreen();
224
+ this.fullscreen = true;
225
+ if (this.onFullscreen) this.onFullscreen(true);
226
+ } else if (document.exitFullscreen) {
227
+ document.exitFullscreen();
228
+ this.fullscreen = false;
229
+ if (this.onFullscreen) this.onFullscreen(false);
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Dessine le lecteur vidéo
235
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
236
+ */
237
+ draw(ctx) {
238
+ ctx.save();
239
+
240
+ try {
241
+ // Dessiner la vidéo sur le canvas
242
+ if (this.loaded && this.videoElement.readyState >= 2) {
243
+ ctx.drawImage(this.videoElement, this.x, this.y, this.width, this.height);
244
+ } else {
245
+ // Affichage de chargement
246
+ ctx.fillStyle = '#000000';
247
+ ctx.fillRect(this.x, this.y, this.width, this.height);
248
+
249
+ ctx.fillStyle = '#FFFFFF';
250
+ ctx.font = '16px Arial';
251
+ ctx.textAlign = 'center';
252
+ ctx.textBaseline = 'middle';
253
+ ctx.fillText('Chargement de la vidéo...',
254
+ this.x + this.width/2, this.y + this.height/2);
255
+ }
256
+ } catch (error) {
257
+ // En cas d'erreur CORS ou autre
258
+ ctx.fillStyle = '#000000';
259
+ ctx.fillRect(this.x, this.y, this.width, this.height);
260
+
261
+ ctx.fillStyle = '#FFFFFF';
262
+ ctx.font = '14px Arial';
263
+ ctx.textAlign = 'center';
264
+ ctx.textBaseline = 'middle';
265
+ ctx.fillText('Vidéo: ' + (this.src.substring(0, 30) + '...'),
266
+ this.x + this.width/2, this.y + this.height/2 - 10);
267
+ ctx.fillText('Cliquez pour lire',
268
+ this.x + this.width/2, this.y + this.height/2 + 10);
269
+ }
270
+
271
+ // Overlay sombre quand en pause
272
+ if (!this.playing || this.showControls) {
273
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
274
+ ctx.fillRect(this.x, this.y, this.width, this.height);
275
+ }
276
+
277
+ // Bouton play/pause au centre (quand pause ou contrôles visibles)
278
+ if (!this.playing || this.showControls) {
279
+ const centerX = this.x + this.width / 2;
280
+ const centerY = this.y + this.height / 2;
281
+
282
+ // Fond rond pour le bouton
283
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
284
+ ctx.beginPath();
285
+ ctx.arc(centerX, centerY, 30, 0, Math.PI * 2);
286
+ ctx.fill();
287
+
288
+ // Icône play/pause
289
+ ctx.fillStyle = '#FFFFFF';
290
+ if (this.playing) {
291
+ // Icône pause
292
+ ctx.fillRect(centerX - 8, centerY - 15, 6, 30);
293
+ ctx.fillRect(centerX + 2, centerY - 15, 6, 30);
294
+ } else {
295
+ // Icône play (triangle)
296
+ ctx.beginPath();
297
+ ctx.moveTo(centerX - 5, centerY - 15);
298
+ ctx.lineTo(centerX - 5, centerY + 15);
299
+ ctx.lineTo(centerX + 20, centerY);
300
+ ctx.closePath();
301
+ ctx.fill();
302
+ }
303
+ }
304
+
305
+ // Contrôles en bas
306
+ if (this.showControls && this.loaded) {
307
+ // Overlay pour les contrôles
308
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
309
+ ctx.fillRect(this.x, this.y + this.height - this.controlsHeight,
310
+ this.width, this.controlsHeight);
311
+
312
+ // Barre de progression
313
+ const progressX = this.x + 10;
314
+ const progressY = this.y + this.height - 25;
315
+ const progressWidth = this.width - 60; // Réduit pour laisser place au bouton plein écran
316
+
317
+ // Fond de la barre
318
+ ctx.fillStyle = '#555555';
319
+ ctx.fillRect(progressX, progressY, progressWidth, 4);
320
+
321
+ // Progression actuelle
322
+ ctx.fillStyle = '#FF0000';
323
+ const currentProgressWidth = (progressWidth * this.progress) / 100;
324
+ ctx.fillRect(progressX, progressY, currentProgressWidth, 4);
325
+
326
+ // Curseur de progression
327
+ const thumbX = progressX + currentProgressWidth;
328
+ ctx.fillStyle = '#FFFFFF';
329
+ ctx.beginPath();
330
+ ctx.arc(thumbX, progressY + 2, 6, 0, Math.PI * 2);
331
+ ctx.fill();
332
+
333
+ // Temps
334
+ const currentTimeStr = this.formatTime(this.currentTime);
335
+ const totalTimeStr = this.formatTime(this.duration);
336
+
337
+ ctx.fillStyle = '#FFFFFF';
338
+ ctx.font = '12px Arial';
339
+ ctx.textAlign = 'left';
340
+ ctx.fillText(currentTimeStr, this.x + 10, this.y + this.height - 35);
341
+
342
+ ctx.textAlign = 'right';
343
+ ctx.fillText(totalTimeStr, this.x + progressWidth + 10, this.y + this.height - 35);
344
+
345
+ // Bouton plein écran
346
+ const fullscreenX = this.x + this.width - 40;
347
+ const fullscreenY = this.y + this.height - 40;
348
+
349
+ ctx.strokeStyle = '#FFFFFF';
350
+ ctx.lineWidth = 2;
351
+ ctx.strokeRect(fullscreenX, fullscreenY, 20, 20);
352
+ ctx.beginPath();
353
+ ctx.moveTo(fullscreenX + 5, fullscreenY + 5);
354
+ ctx.lineTo(fullscreenX + 5, fullscreenY + 15);
355
+ ctx.lineTo(fullscreenX + 15, fullscreenY + 15);
356
+ ctx.lineTo(fullscreenX + 15, fullscreenY + 5);
357
+ ctx.stroke();
358
+ }
359
+
360
+ ctx.restore();
361
+ }
362
+
363
+ /**
364
+ * Formate un temps en minutes:secondes
365
+ * @param {number} seconds - Secondes
366
+ * @returns {string} Temps formaté
367
+ * @private
368
+ */
369
+ formatTime(seconds) {
370
+ const mins = Math.floor(seconds / 60);
371
+ const secs = Math.floor(seconds % 60);
372
+ return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
373
+ }
374
+
375
+ /**
376
+ * Vérifie si un point est dans les limites
377
+ * @param {number} x - Coordonnée X
378
+ * @param {number} y - Coordonnée Y
379
+ * @returns {boolean} True si le point est dans le lecteur
380
+ */
381
+ isPointInside(x, y) {
382
+ // Le VideoPlayer est cliquable pour les contrôles
383
+ return x >= this.x && x <= this.x + this.width &&
384
+ y >= this.y && y <= this.y + this.height;
385
+ }
386
+
387
+ /**
388
+ * Nettoie l'élément vidéo du DOM
389
+ */
390
+ remove() {
391
+ if (this.videoElement && this.videoElement.parentNode) {
392
+ this.videoElement.parentNode.removeChild(this.videoElement);
393
+ }
394
+ }
395
+ }
396
+
397
+ export default Video;
@@ -0,0 +1,140 @@
1
+ import Component from '../core/Component.js';
2
+ /**
3
+ * Container avec système de layout
4
+ * @class
5
+ * @extends Component
6
+ * @property {Component[]} children - Enfants
7
+ * @property {number} padding - Padding interne
8
+ * @property {number} gap - Espacement entre enfants
9
+ * @property {string} direction - Direction ('column' ou 'row')
10
+ * @property {string} align - Alignement ('start', 'center', 'end')
11
+ * @property {string} bgColor - Couleur de fond
12
+ * @property {number} borderRadius - Rayon des coins
13
+ */
14
+ class View extends Component {
15
+ /**
16
+ * Crée une instance de View
17
+ * @param {CanvasFramework} framework - Framework parent
18
+ * @param {Object} [options={}] - Options de configuration
19
+ * @param {number} [options.padding=0] - Padding interne
20
+ * @param {number} [options.gap=0] - Espacement entre enfants
21
+ * @param {string} [options.direction='column'] - Direction
22
+ * @param {string} [options.align='start'] - Alignement
23
+ * @param {string} [options.bgColor='transparent'] - Couleur de fond
24
+ * @param {number} [options.borderRadius=0] - Rayon des coins
25
+ */
26
+ constructor(framework, options = {}) {
27
+ super(framework, options);
28
+ this.children = [];
29
+ this.padding = options.padding || 0;
30
+ this.gap = options.gap || 0;
31
+ this.direction = options.direction || 'column'; // 'column' ou 'row'
32
+ this.align = options.align || 'start'; // 'start', 'center', 'end'
33
+ this.bgColor = options.bgColor || 'transparent';
34
+ this.borderRadius = options.borderRadius || 0;
35
+ }
36
+
37
+ /**
38
+ * Ajoute un enfant
39
+ * @param {Component} child - Composant enfant
40
+ * @returns {Component} L'enfant ajouté
41
+ */
42
+ add(child) {
43
+ this.children.push(child);
44
+ this.layout();
45
+ return child;
46
+ }
47
+
48
+ /**
49
+ * Organise les enfants selon le layout
50
+ * @private
51
+ */
52
+ layout() {
53
+ let currentX = this.x + this.padding;
54
+ let currentY = this.y + this.padding;
55
+
56
+ for (let child of this.children) {
57
+ if (this.direction === 'column') {
58
+ child.x = currentX;
59
+ child.y = currentY;
60
+ if (this.align === 'center') {
61
+ child.x = this.x + (this.width - child.width) / 2;
62
+ } else if (this.align === 'end') {
63
+ child.x = this.x + this.width - child.width - this.padding;
64
+ }
65
+ currentY += child.height + this.gap;
66
+ } else {
67
+ child.x = currentX;
68
+ child.y = currentY;
69
+ if (this.align === 'center') {
70
+ child.y = this.y + (this.height - child.height) / 2;
71
+ } else if (this.align === 'end') {
72
+ child.y = this.y + this.height - child.height - this.padding;
73
+ }
74
+ currentX += child.width + this.gap;
75
+ }
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Dessine la vue et ses enfants
81
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
82
+ */
83
+ draw(ctx) {
84
+ ctx.save();
85
+
86
+ if (this.bgColor !== 'transparent') {
87
+ ctx.fillStyle = this.bgColor;
88
+ if (this.borderRadius > 0) {
89
+ ctx.beginPath();
90
+ this.roundRect(ctx, this.x, this.y, this.width, this.height, this.borderRadius);
91
+ ctx.fill();
92
+ } else {
93
+ ctx.fillRect(this.x, this.y, this.width, this.height);
94
+ }
95
+ }
96
+
97
+ for (let child of this.children) {
98
+ if (child.visible) child.draw(ctx);
99
+ }
100
+
101
+ ctx.restore();
102
+ }
103
+
104
+ /**
105
+ * Dessine un rectangle avec coins arrondis
106
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
107
+ * @param {number} x - Position X
108
+ * @param {number} y - Position Y
109
+ * @param {number} width - Largeur
110
+ * @param {number} height - Hauteur
111
+ * @param {number} radius - Rayon des coins
112
+ * @private
113
+ */
114
+ roundRect(ctx, x, y, width, height, radius) {
115
+ ctx.moveTo(x + radius, y);
116
+ ctx.lineTo(x + width - radius, y);
117
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
118
+ ctx.lineTo(x + width, y + height - radius);
119
+ ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
120
+ ctx.lineTo(x + radius, y + height);
121
+ ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
122
+ ctx.lineTo(x, y + radius);
123
+ ctx.quadraticCurveTo(x, y, x + radius, y);
124
+ }
125
+
126
+ /**
127
+ * Vérifie si un point est dans les limites
128
+ * @param {number} x - Coordonnée X
129
+ * @param {number} y - Coordonnée Y
130
+ * @returns {boolean} True si le point est dans la vue
131
+ */
132
+ isPointInside(x, y) {
133
+ return x >= this.x &&
134
+ x <= this.x + this.width &&
135
+ y >= this.y &&
136
+ y <= this.y + this.height;
137
+ }
138
+ }
139
+
140
+ export default View;
@@ -0,0 +1,120 @@
1
+ import Component from '../core/Component.js';
2
+ import ListItem from '../components/ListItem.js';
3
+
4
+ /**
5
+ * Virtual List : optimise le rendu pour les longues listes
6
+ * @class
7
+ * @extends Component
8
+ */
9
+ class VirtualList extends Component {
10
+ constructor(framework, options = {}) {
11
+ super(framework, options);
12
+
13
+ this.allItemsData = []; // Stocke toutes les données des items (mais pas tous les objets)
14
+ this.visibleItems = []; // Liste des ListItem réellement créés/dessinés
15
+ this.itemHeight = options.itemHeight || 56;
16
+ this.onItemClick = options.onItemClick;
17
+ this.y = options.y || 0;
18
+
19
+ this.viewportHeight = options.height || framework.height; // Hauteur visible
20
+ this.scrollOffset = 0; // Position de scroll
21
+ }
22
+
23
+ /**
24
+ * Ajoute un item (seule la data est stockée)
25
+ * @param {Object} itemOptions
26
+ */
27
+ addItem(itemOptions) {
28
+ this.allItemsData.push(itemOptions);
29
+ this.updateVisibleItems();
30
+ }
31
+
32
+ /**
33
+ * Supprime tous les items
34
+ */
35
+ clear() {
36
+ for (let item of this.visibleItems) {
37
+ this.framework.remove(item);
38
+ }
39
+ this.allItemsData = [];
40
+ this.visibleItems = [];
41
+ this.height = 0;
42
+ }
43
+
44
+ /**
45
+ * Met à jour la liste des items visibles selon scrollOffset
46
+ */
47
+ updateVisibleItems() {
48
+ const firstIndex = Math.floor(this.scrollOffset / this.itemHeight);
49
+ const lastIndex = Math.min(this.allItemsData.length - 1, Math.ceil((this.scrollOffset + this.viewportHeight) / this.itemHeight));
50
+
51
+ const newVisibleItems = [];
52
+
53
+ for (let i = firstIndex; i <= lastIndex; i++) {
54
+ let item = this.visibleItems.find(v => v.__virtualIndex === i);
55
+ if (!item) {
56
+ // Crée un nouvel item si pas existant
57
+ const data = this.allItemsData[i];
58
+ item = new ListItem(this.framework, {
59
+ ...data,
60
+ x: this.x,
61
+ y: this.y + i * this.itemHeight - this.scrollOffset,
62
+ width: this.width,
63
+ height: this.itemHeight,
64
+ onClick: () => {
65
+ if (this.onItemClick) this.onItemClick(i, data);
66
+ if (data.onClick) data.onClick();
67
+ }
68
+ });
69
+ item.__virtualIndex = i;
70
+ this.framework.add(item);
71
+ } else {
72
+ // Met à jour la position Y si déjà existant
73
+ item.y = this.y + i * this.itemHeight - this.scrollOffset;
74
+ }
75
+ newVisibleItems.push(item);
76
+ }
77
+
78
+ // Supprime les items qui ne sont plus visibles
79
+ for (let item of this.visibleItems) {
80
+ if (!newVisibleItems.includes(item)) {
81
+ this.framework.remove(item);
82
+ }
83
+ }
84
+
85
+ this.visibleItems = newVisibleItems;
86
+ this.height = this.allItemsData.length * this.itemHeight;
87
+ }
88
+
89
+ /**
90
+ * Scroll la liste
91
+ * @param {number} deltaY
92
+ */
93
+ scroll(deltaY) {
94
+ this.scrollOffset += deltaY;
95
+ if (this.scrollOffset < 0) this.scrollOffset = 0;
96
+ const maxScroll = Math.max(0, this.height - this.viewportHeight);
97
+ if (this.scrollOffset > maxScroll) this.scrollOffset = maxScroll;
98
+
99
+ this.updateVisibleItems();
100
+ }
101
+
102
+ /**
103
+ * Dessine les items visibles
104
+ * @param {CanvasRenderingContext2D} ctx
105
+ */
106
+ draw(ctx) {
107
+ for (let item of this.visibleItems) {
108
+ item.draw(ctx);
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Toujours false : les ListItems gèrent leurs clics
114
+ */
115
+ isPointInside() {
116
+ return false;
117
+ }
118
+ }
119
+
120
+ export default VirtualList;