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.
- package/README.md +554 -0
- package/components/Accordion.js +252 -0
- package/components/AndroidDatePickerDialog.js +398 -0
- package/components/AppBar.js +225 -0
- package/components/Avatar.js +202 -0
- package/components/BottomNavigationBar.js +205 -0
- package/components/BottomSheet.js +374 -0
- package/components/Button.js +225 -0
- package/components/Card.js +193 -0
- package/components/Checkbox.js +180 -0
- package/components/Chip.js +212 -0
- package/components/CircularProgress.js +143 -0
- package/components/ContextMenu.js +116 -0
- package/components/DatePicker.js +257 -0
- package/components/Dialog.js +367 -0
- package/components/Divider.js +125 -0
- package/components/Drawer.js +261 -0
- package/components/FAB.js +270 -0
- package/components/FileUpload.js +315 -0
- package/components/IOSDatePickerWheel.js +268 -0
- package/components/ImageCarousel.js +193 -0
- package/components/ImageComponent.js +223 -0
- package/components/Input.js +309 -0
- package/components/List.js +94 -0
- package/components/ListItem.js +223 -0
- package/components/Modal.js +364 -0
- package/components/MultiSelectDialog.js +206 -0
- package/components/NumberInput.js +271 -0
- package/components/ProgressBar.js +88 -0
- package/components/RadioButton.js +142 -0
- package/components/SearchInput.js +315 -0
- package/components/SegmentedControl.js +202 -0
- package/components/Select.js +199 -0
- package/components/SelectDialog.js +255 -0
- package/components/Slider.js +113 -0
- package/components/Snackbar.js +243 -0
- package/components/Stepper.js +281 -0
- package/components/SwipeableListItem.js +179 -0
- package/components/Switch.js +147 -0
- package/components/Table.js +492 -0
- package/components/Tabs.js +125 -0
- package/components/Text.js +141 -0
- package/components/TextField.js +331 -0
- package/components/Toast.js +236 -0
- package/components/TreeView.js +420 -0
- package/components/Video.js +397 -0
- package/components/View.js +140 -0
- package/components/VirtualList.js +120 -0
- package/core/CanvasFramework.js +1271 -0
- package/core/CanvasWork.js +32 -0
- package/core/Component.js +153 -0
- package/core/LogicWorker.js +25 -0
- package/core/WebGLCanvasAdapter.js +1369 -0
- package/features/Column.js +43 -0
- package/features/Grid.js +47 -0
- package/features/LayoutComponent.js +43 -0
- package/features/OpenStreetMap.js +310 -0
- package/features/Positioned.js +33 -0
- package/features/PullToRefresh.js +328 -0
- package/features/Row.js +40 -0
- package/features/SignaturePad.js +257 -0
- package/features/Skeleton.js +84 -0
- package/features/Stack.js +21 -0
- package/index.js +101 -0
- package/manager/AccessibilityManager.js +107 -0
- package/manager/ErrorHandler.js +59 -0
- package/manager/FeatureFlags.js +60 -0
- package/manager/MemoryManager.js +107 -0
- package/manager/PerformanceMonitor.js +84 -0
- package/manager/SecurityManager.js +54 -0
- package/package.json +28 -0
- package/utils/AnimationEngine.js +428 -0
- package/utils/DataStore.js +403 -0
- package/utils/EventBus.js +407 -0
- package/utils/FetchClient.js +74 -0
- package/utils/FormValidator.js +355 -0
- package/utils/GeoLocationService.js +62 -0
- package/utils/I18n.js +207 -0
- package/utils/IndexedDBManager.js +273 -0
- package/utils/OfflineSyncManager.js +342 -0
- package/utils/QueryBuilder.js +478 -0
- package/utils/SafeArea.js +64 -0
- package/utils/SecureStorage.js +289 -0
- package/utils/StateManager.js +207 -0
- package/utils/WebSocketClient.js +66 -0
|
@@ -0,0 +1,1271 @@
|
|
|
1
|
+
import Button from '../components/Button.js';
|
|
2
|
+
import SegmentedControl from '../components/SegmentedControl.js';
|
|
3
|
+
import Input from '../components/Input.js';
|
|
4
|
+
import Slider from '../components/Slider.js';
|
|
5
|
+
import Text from '../components/Text.js';
|
|
6
|
+
import View from '../components/View.js';
|
|
7
|
+
import Card from '../components/Card.js';
|
|
8
|
+
import FAB from '../components/FAB.js';
|
|
9
|
+
import CircularProgress from '../components/CircularProgress.js';
|
|
10
|
+
import ImageComponent from '../components/ImageComponent.js';
|
|
11
|
+
import DatePicker from '../components/DatePicker.js';
|
|
12
|
+
import IOSDatePickerWheel from '../components/IOSDatePickerWheel.js';
|
|
13
|
+
import AndroidDatePickerDialog from '../components/AndroidDatePickerDialog.js';
|
|
14
|
+
import Avatar from '../components/Avatar.js';
|
|
15
|
+
import Snackbar from '../components/Snackbar.js';
|
|
16
|
+
import BottomNavigationBar from '../components/BottomNavigationBar.js';
|
|
17
|
+
import Video from '../components/Video.js';
|
|
18
|
+
import Modal from '../components/Modal.js';
|
|
19
|
+
import Drawer from '../components/Drawer.js';
|
|
20
|
+
import AppBar from '../components/AppBar.js';
|
|
21
|
+
import Chip from '../components/Chip.js';
|
|
22
|
+
import Stepper from '../components/Stepper.js';
|
|
23
|
+
import Accordion from '../components/Accordion.js';
|
|
24
|
+
import Tabs from '../components/Tabs.js';
|
|
25
|
+
import Switch from '../components/Switch.js';
|
|
26
|
+
import SwipeableListItem from '../components/SwipeableListItem.js';
|
|
27
|
+
import ListItem from '../components/ListItem.js';
|
|
28
|
+
import List from '../components/List.js';
|
|
29
|
+
import VirtualList from '../components/VirtualList.js';
|
|
30
|
+
import BottomSheet from '../components/BottomSheet.js';
|
|
31
|
+
import ProgressBar from '../components/ProgressBar.js';
|
|
32
|
+
import RadioButton from '../components/RadioButton.js';
|
|
33
|
+
import Dialog from '../components/Dialog.js';
|
|
34
|
+
import ContextMenu from '../components/ContextMenu.js';
|
|
35
|
+
import Checkbox from '../components/Checkbox.js';
|
|
36
|
+
import Toast from '../components/Toast.js';
|
|
37
|
+
import NumberInput from '../components/NumberInput.js';
|
|
38
|
+
import TextField from '../components/TextField.js';
|
|
39
|
+
import SelectDialog from '../components/SelectDialog.js';
|
|
40
|
+
import Select from '../components/Select.js';
|
|
41
|
+
import MultiSelectDialog from '../components/MultiSelectDialog.js';
|
|
42
|
+
import Divider from '../components/Divider.js';
|
|
43
|
+
import FileUpload from '../components/FileUpload.js';
|
|
44
|
+
import Table from '../components/Table.js';
|
|
45
|
+
import TreeView from '../components/TreeView.js';
|
|
46
|
+
import SearchInput from '../components/SearchInput.js';
|
|
47
|
+
import ImageCarousel from '../components/ImageCarousel.js';
|
|
48
|
+
|
|
49
|
+
// Utils
|
|
50
|
+
import SafeArea from '../utils/SafeArea.js';
|
|
51
|
+
import StateManager from '../utils/StateManager.js';
|
|
52
|
+
import I18n from '../utils/I18n.js';
|
|
53
|
+
import SecureStorage from '../utils/SecureStorage.js';
|
|
54
|
+
import FormValidator from '../utils/FormValidator.js';
|
|
55
|
+
import DataStore from '../utils/DataStore.js';
|
|
56
|
+
import EventBus from '../utils/EventBus.js';
|
|
57
|
+
import IndexedDBManager from '../utils/IndexedDBManager.js';
|
|
58
|
+
import QueryBuilder from '../utils/QueryBuilder.js';
|
|
59
|
+
import OfflineSyncManager from '../utils/OfflineSyncManager.js';
|
|
60
|
+
import FetchClient from '../utils/FetchClient.js';
|
|
61
|
+
import GeoLocationService from '../utils/GeoLocationService.js';
|
|
62
|
+
import WebSocketClient from '../utils/WebSocketClient.js';
|
|
63
|
+
import AnimationEngine from '../utils/AnimationEngine.js';
|
|
64
|
+
|
|
65
|
+
// Features
|
|
66
|
+
import PullToRefresh from '../features/PullToRefresh.js';
|
|
67
|
+
import Skeleton from '../features/Skeleton.js';
|
|
68
|
+
import SignaturePad from '../features/SignaturePad.js';
|
|
69
|
+
import OpenStreetMap from '../features/OpenStreetMap.js';
|
|
70
|
+
import LayoutComponent from '../features/LayoutComponent.js';
|
|
71
|
+
import Grid from '../features/Grid.js';
|
|
72
|
+
import Row from '../features/Row.js';
|
|
73
|
+
import Column from '../features/Column.js';
|
|
74
|
+
import Positioned from '../features/Positioned.js';
|
|
75
|
+
import Stack from '../features/Stack.js';
|
|
76
|
+
|
|
77
|
+
// Manager
|
|
78
|
+
import ErrorHandler from '../manager/ErrorHandler.js';
|
|
79
|
+
import PerformanceMonitor from '../manager/PerformanceMonitor.js';
|
|
80
|
+
import AccessibilityManager from '../manager/AccessibilityManager.js';
|
|
81
|
+
import MemoryManager from '../manager/MemoryManager.js';
|
|
82
|
+
import SecurityManager from '../manager/SecurityManager.js';
|
|
83
|
+
import FeatureFlags from '../manager/FeatureFlags.js';
|
|
84
|
+
|
|
85
|
+
// WebGL Adapter
|
|
86
|
+
import WebGLCanvasAdapter from './WebGLCanvasAdapter.js';
|
|
87
|
+
|
|
88
|
+
// theme
|
|
89
|
+
export const lightTheme = {
|
|
90
|
+
background: '#FFFFFF',
|
|
91
|
+
text: '#000000',
|
|
92
|
+
primary: '#6200EE',
|
|
93
|
+
secondary: '#03DAC6',
|
|
94
|
+
buttonText: '#FFFFFF',
|
|
95
|
+
buttonBackground: '#6200EE',
|
|
96
|
+
border: '#E0E0E0'
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const darkTheme = {
|
|
100
|
+
background: '#121212',
|
|
101
|
+
text: '#FFFFFF',
|
|
102
|
+
primary: '#BB86FC',
|
|
103
|
+
secondary: '#03DAC6',
|
|
104
|
+
buttonText: '#000000',
|
|
105
|
+
buttonBackground: '#BB86FC',
|
|
106
|
+
border: '#333333'
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Framework principal pour créer des interfaces utilisateur basées sur Canvas
|
|
111
|
+
* @class
|
|
112
|
+
* @property {HTMLCanvasElement} canvas - Élément canvas HTML
|
|
113
|
+
* @property {CanvasRenderingContext2D} ctx - Contexte 2D du canvas
|
|
114
|
+
* @property {number} width - Largeur du canvas
|
|
115
|
+
* @property {number} height - Hauteur du canvas
|
|
116
|
+
* @property {number} dpr - Device Pixel Ratio
|
|
117
|
+
* @property {string} platform - Plateforme détectée ('material' ou 'cupertino')
|
|
118
|
+
* @property {Component[]} components - Liste des composants
|
|
119
|
+
* @property {Map} routes - Routes de navigation
|
|
120
|
+
* @property {string} currentRoute - Route actuelle
|
|
121
|
+
* @property {Object} state - État global
|
|
122
|
+
* @property {boolean} isDragging - Indique si un drag est en cours
|
|
123
|
+
* @property {number} lastTouchY - Dernière position Y du touch
|
|
124
|
+
* @property {number} scrollOffset - Offset de défilement
|
|
125
|
+
* @property {number} scrollVelocity - Vitesse de défilement
|
|
126
|
+
* @property {number} scrollFriction - Friction du défilement
|
|
127
|
+
*/
|
|
128
|
+
class CanvasFramework {
|
|
129
|
+
/**
|
|
130
|
+
* Crée une instance de CanvasFramework
|
|
131
|
+
* @param {string} canvasId - ID de l'élément canvas
|
|
132
|
+
*/
|
|
133
|
+
constructor(canvasId, options = {}) {
|
|
134
|
+
this.canvas = document.getElementById(canvasId);
|
|
135
|
+
this.ctx = this.canvas.getContext('2d');
|
|
136
|
+
this.width = window.innerWidth;
|
|
137
|
+
this.height = window.innerHeight;
|
|
138
|
+
this.dpr = window.devicePixelRatio || 1;
|
|
139
|
+
|
|
140
|
+
this.platform = this.detectPlatform();
|
|
141
|
+
|
|
142
|
+
// Thèmes
|
|
143
|
+
this.lightTheme = lightTheme;
|
|
144
|
+
this.darkTheme = darkTheme;
|
|
145
|
+
this.theme = lightTheme; // thème par défaut
|
|
146
|
+
|
|
147
|
+
this.components = [];
|
|
148
|
+
this.state = {};
|
|
149
|
+
// NOUVELLE OPTION: choisir entre Canvas 2D et WebGL
|
|
150
|
+
this.useWebGL = options.useWebGL !== false; // true par défaut
|
|
151
|
+
// Initialiser le contexte approprié
|
|
152
|
+
if (this.useWebGL) {
|
|
153
|
+
try {
|
|
154
|
+
this.ctx = new WebGLCanvasAdapter(this.canvas);
|
|
155
|
+
} catch (e) {
|
|
156
|
+
this.ctx = this.canvas.getContext('2d');
|
|
157
|
+
this.useWebGL = false;
|
|
158
|
+
}
|
|
159
|
+
} else {
|
|
160
|
+
this.ctx = this.canvas.getContext('2d');
|
|
161
|
+
}
|
|
162
|
+
// Calcule FPS
|
|
163
|
+
this.fps = 0;
|
|
164
|
+
this._frames = 0;
|
|
165
|
+
this._lastFpsTime = performance.now();
|
|
166
|
+
this.showFps = options.showFps || false; // false par défaut
|
|
167
|
+
this.debbug = options.debug || false; // false par défaut (et correction de la faute de frappe)
|
|
168
|
+
// Worker pour multithreading
|
|
169
|
+
this.worker = new Worker('./CanvasWorker.js', { type: 'module' });
|
|
170
|
+
this.worker.onmessage = this.handleWorkerMessage.bind(this);
|
|
171
|
+
this.worker.postMessage({ type: 'INIT', payload: { components: [] } });
|
|
172
|
+
|
|
173
|
+
// Worker logique pour calculs séparés
|
|
174
|
+
this.logicWorker = new Worker('./LogicWorker.js', { type: 'module' });
|
|
175
|
+
this.logicWorker.onmessage = this.handleLogicWorkerMessage.bind(this);
|
|
176
|
+
this.logicWorkerState = {};
|
|
177
|
+
|
|
178
|
+
// Envoyer l'état initial au worker
|
|
179
|
+
this.logicWorker.postMessage({ type: 'SET_STATE', payload: this.state });
|
|
180
|
+
|
|
181
|
+
// Gestion des événements
|
|
182
|
+
this.isDragging = false;
|
|
183
|
+
this.lastTouchY = 0;
|
|
184
|
+
this.scrollOffset = 0;
|
|
185
|
+
this.scrollVelocity = 0;
|
|
186
|
+
this.scrollFriction = 0.95;
|
|
187
|
+
|
|
188
|
+
// Optimisation
|
|
189
|
+
this.dirtyComponents = new Set();
|
|
190
|
+
this.optimizationEnabled = false;
|
|
191
|
+
|
|
192
|
+
// AJOUTER CETTE LIGNE
|
|
193
|
+
this.animator = new AnimationEngine();
|
|
194
|
+
|
|
195
|
+
// ===== NOUVEAU SYSTÈME DE ROUTING =====
|
|
196
|
+
this.routes = new Map();
|
|
197
|
+
this.currentRoute = '/';
|
|
198
|
+
this.currentParams = {};
|
|
199
|
+
this.currentQuery = {};
|
|
200
|
+
this.history = [];
|
|
201
|
+
this.historyIndex = -1;
|
|
202
|
+
|
|
203
|
+
// Animation de transition
|
|
204
|
+
this.transitionState = {
|
|
205
|
+
isTransitioning: false,
|
|
206
|
+
progress: 0,
|
|
207
|
+
duration: 300,
|
|
208
|
+
type: 'slide', // 'slide', 'fade', 'none'
|
|
209
|
+
direction: 'forward', // 'forward', 'back'
|
|
210
|
+
oldComponents: [],
|
|
211
|
+
newComponents: []
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
this.setupCanvas();
|
|
215
|
+
this.setupEventListeners();
|
|
216
|
+
this.setupHistoryListener();
|
|
217
|
+
this.startRenderLoop();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
wrapContext(ctx, theme) {
|
|
221
|
+
const originalFillStyle = Object.getOwnPropertyDescriptor(CanvasRenderingContext2D.prototype, 'fillStyle');
|
|
222
|
+
Object.defineProperty(ctx, 'fillStyle', {
|
|
223
|
+
set: (value) => {
|
|
224
|
+
// Si value est blanc/noir ou une couleur “neutre”, tu remplaces par theme
|
|
225
|
+
if (value === '#FFFFFF' || value === '#000000') {
|
|
226
|
+
originalFillStyle.set.call(ctx, theme.text);
|
|
227
|
+
} else {
|
|
228
|
+
originalFillStyle.set.call(ctx, value);
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
get: () => originalFillStyle.get.call(ctx)
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Set Theme dynamique
|
|
236
|
+
setTheme(theme) {
|
|
237
|
+
this.theme = theme;
|
|
238
|
+
|
|
239
|
+
// Intercepter le context pour remplacer les couleurs globalement
|
|
240
|
+
if (!this.useWebGL) {
|
|
241
|
+
this.wrapContext(this.ctx, theme);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// marque tous les composants dirty pour redraw
|
|
245
|
+
for (let comp of this.components) comp.markDirty();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Switch Theme
|
|
249
|
+
toggleDarkMode() {
|
|
250
|
+
if (this.theme === lightTheme) {
|
|
251
|
+
this.setTheme(darkTheme);
|
|
252
|
+
} else {
|
|
253
|
+
this.setTheme(lightTheme);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
enableFpsDisplay(enable = true) {
|
|
258
|
+
this.showFps = enable;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// AJOUTER CETTE MÉTHODE (optionnel - pour faciliter l'accès)
|
|
262
|
+
animate(component, options) {
|
|
263
|
+
return this.animator.animate(component, options);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ----- Worker UI -----
|
|
267
|
+
handleWorkerMessage(e) {
|
|
268
|
+
const { type, payload } = e.data;
|
|
269
|
+
switch(type) {
|
|
270
|
+
case 'LAYOUT_DONE':
|
|
271
|
+
for (let update of payload) {
|
|
272
|
+
const comp = this.components.find(c => c.id === update.id);
|
|
273
|
+
if (comp) comp.height = update.height;
|
|
274
|
+
}
|
|
275
|
+
break;
|
|
276
|
+
case 'SCROLL_UPDATED':
|
|
277
|
+
this.scrollOffset = payload.offset;
|
|
278
|
+
this.scrollVelocity = payload.velocity;
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
updateLayoutAsync() {
|
|
284
|
+
this.worker.postMessage({ type: 'UPDATE_LAYOUT' });
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
updateScrollInertia() {
|
|
288
|
+
const maxScroll = this.getMaxScroll();
|
|
289
|
+
this.worker.postMessage({
|
|
290
|
+
type: 'SCROLL_INERTIA',
|
|
291
|
+
payload: {
|
|
292
|
+
offset: this.scrollOffset,
|
|
293
|
+
velocity: this.scrollVelocity,
|
|
294
|
+
friction: this.scrollFriction,
|
|
295
|
+
maxScroll
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ------ Logic Worker --------
|
|
301
|
+
handleLogicWorkerMessage(e) {
|
|
302
|
+
const { type, payload } = e.data;
|
|
303
|
+
switch(type) {
|
|
304
|
+
case 'STATE_UPDATED':
|
|
305
|
+
// Le worker a renvoyé le nouvel état global
|
|
306
|
+
this.logicWorkerState = payload;
|
|
307
|
+
break;
|
|
308
|
+
|
|
309
|
+
case 'EXECUTION_RESULT':
|
|
310
|
+
// Résultat d'une tâche spécifique envoyée au worker
|
|
311
|
+
if (this.onWorkerResult) this.onWorkerResult(payload);
|
|
312
|
+
break;
|
|
313
|
+
|
|
314
|
+
case 'EXECUTION_ERROR':
|
|
315
|
+
console.error('Logic Worker Error:', payload);
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
runLogicTask(taskName, taskData) {
|
|
321
|
+
this.logicWorker.postMessage({
|
|
322
|
+
type: 'EXECUTE_TASK',
|
|
323
|
+
payload: { taskName, taskData }
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
updateLogicWorkerState(newState) {
|
|
328
|
+
this.logicWorkerState = { ...this.logicWorkerState, ...newState };
|
|
329
|
+
this.logicWorker.postMessage({ type: 'SET_STATE', payload: this.logicWorkerState });
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
detectPlatform() {
|
|
333
|
+
const ua = navigator.userAgent.toLowerCase();
|
|
334
|
+
if (/android/.test(ua)) return 'material';
|
|
335
|
+
if (/iphone|ipad|ipod/.test(ua)) return 'cupertino';
|
|
336
|
+
return 'material';
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
setupCanvas() {
|
|
340
|
+
this.canvas.width = this.width * this.dpr;
|
|
341
|
+
this.canvas.height = this.height * this.dpr;
|
|
342
|
+
this.canvas.style.width = this.width + 'px';
|
|
343
|
+
this.canvas.style.height = this.height + 'px';
|
|
344
|
+
|
|
345
|
+
// Échelle uniquement pour Canvas 2D
|
|
346
|
+
if (!this.useWebGL) {
|
|
347
|
+
this.ctx.scale(this.dpr, this.dpr);
|
|
348
|
+
} else {
|
|
349
|
+
// WebGL gère le DPR automatiquement via la matrice de projection
|
|
350
|
+
this.ctx.updateProjectionMatrix();
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
setupEventListeners() {
|
|
355
|
+
this.canvas.addEventListener('touchstart', this.handleTouchStart.bind(this));
|
|
356
|
+
this.canvas.addEventListener('touchmove', this.handleTouchMove.bind(this));
|
|
357
|
+
this.canvas.addEventListener('touchend', this.handleTouchEnd.bind(this));
|
|
358
|
+
this.canvas.addEventListener('mousedown', this.handleMouseDown.bind(this));
|
|
359
|
+
this.canvas.addEventListener('mousemove', this.handleMouseMove.bind(this));
|
|
360
|
+
this.canvas.addEventListener('mouseup', this.handleMouseUp.bind(this));
|
|
361
|
+
window.addEventListener('resize', this.handleResize.bind(this));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Configure l'écoute de l'historique du navigateur
|
|
366
|
+
* @private
|
|
367
|
+
*/
|
|
368
|
+
setupHistoryListener() {
|
|
369
|
+
window.addEventListener('popstate', (e) => {
|
|
370
|
+
if (e.state && e.state.route) {
|
|
371
|
+
this.navigateTo(e.state.route, {
|
|
372
|
+
replace: true,
|
|
373
|
+
animate: true,
|
|
374
|
+
direction: 'back'
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ===== MÉTHODES DE ROUTING =====
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Définit une route avec pattern de paramètres
|
|
384
|
+
* @param {string} pattern - Pattern de la route (ex: '/user/:id', '/posts/:category/:id')
|
|
385
|
+
* @param {Function} component - Fonction qui crée les composants
|
|
386
|
+
* @param {Object} options - Options de la route
|
|
387
|
+
* @returns {CanvasFramework}
|
|
388
|
+
*/
|
|
389
|
+
route(pattern, component, options = {}) {
|
|
390
|
+
const route = {
|
|
391
|
+
pattern,
|
|
392
|
+
component,
|
|
393
|
+
regex: this.patternToRegex(pattern),
|
|
394
|
+
paramNames: this.extractParamNames(pattern),
|
|
395
|
+
beforeEnter: options.beforeEnter,
|
|
396
|
+
afterEnter: options.afterEnter,
|
|
397
|
+
beforeLeave: options.beforeLeave,
|
|
398
|
+
transition: options.transition || 'slide'
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
this.routes.set(pattern, route);
|
|
402
|
+
return this;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Convertit un pattern de route en regex
|
|
407
|
+
* @private
|
|
408
|
+
*/
|
|
409
|
+
patternToRegex(pattern) {
|
|
410
|
+
const regexPattern = pattern
|
|
411
|
+
.replace(/\//g, '\\/')
|
|
412
|
+
.replace(/:([^\/]+)/g, '([^\\/]+)');
|
|
413
|
+
return new RegExp(`^${regexPattern}$`);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Extrait les noms des paramètres d'un pattern
|
|
418
|
+
* @private
|
|
419
|
+
*/
|
|
420
|
+
extractParamNames(pattern) {
|
|
421
|
+
const matches = pattern.match(/:([^\/]+)/g);
|
|
422
|
+
return matches ? matches.map(m => m.slice(1)) : [];
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Trouve la route correspondant à un path
|
|
427
|
+
* @private
|
|
428
|
+
*/
|
|
429
|
+
matchRoute(path) {
|
|
430
|
+
// Séparer le path et la query string
|
|
431
|
+
const [pathname, queryString] = path.split('?');
|
|
432
|
+
|
|
433
|
+
for (let [pattern, route] of this.routes) {
|
|
434
|
+
const match = pathname.match(route.regex);
|
|
435
|
+
if (match) {
|
|
436
|
+
const params = {};
|
|
437
|
+
route.paramNames.forEach((name, index) => {
|
|
438
|
+
params[name] = match[index + 1];
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
const query = this.parseQueryString(queryString);
|
|
442
|
+
|
|
443
|
+
return { route, params, query, pathname };
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Parse une query string
|
|
451
|
+
* @private
|
|
452
|
+
*/
|
|
453
|
+
parseQueryString(queryString) {
|
|
454
|
+
if (!queryString) return {};
|
|
455
|
+
|
|
456
|
+
const params = {};
|
|
457
|
+
queryString.split('&').forEach(param => {
|
|
458
|
+
const [key, value] = param.split('=');
|
|
459
|
+
params[decodeURIComponent(key)] = decodeURIComponent(value || '');
|
|
460
|
+
});
|
|
461
|
+
return params;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Navigue vers une route
|
|
466
|
+
* @param {string} path - Chemin de destination (ex: '/user/123', '/posts/tech/456?sort=date')
|
|
467
|
+
* @param {Object} options - Options de navigation
|
|
468
|
+
*/
|
|
469
|
+
navigate(path, options = {}) {
|
|
470
|
+
this.navigateTo(path, options);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Méthode interne de navigation
|
|
475
|
+
* @private
|
|
476
|
+
*/
|
|
477
|
+
async navigateTo(path, options = {}) {
|
|
478
|
+
const {
|
|
479
|
+
replace = false,
|
|
480
|
+
animate = true,
|
|
481
|
+
direction = 'forward',
|
|
482
|
+
transition = null,
|
|
483
|
+
state = {}
|
|
484
|
+
} = options;
|
|
485
|
+
|
|
486
|
+
const match = this.matchRoute(path);
|
|
487
|
+
if (!match) {
|
|
488
|
+
console.warn(`Route not found: ${path}`);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const { route, params, query, pathname } = match;
|
|
493
|
+
|
|
494
|
+
// Hook beforeLeave de la route actuelle
|
|
495
|
+
const currentRouteData = this.routes.get(this.currentRoute);
|
|
496
|
+
if (currentRouteData?.beforeLeave) {
|
|
497
|
+
const canLeave = await currentRouteData.beforeLeave(this.currentParams, this.currentQuery);
|
|
498
|
+
if (canLeave === false) return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Hook beforeEnter de la nouvelle route
|
|
502
|
+
if (route.beforeEnter) {
|
|
503
|
+
const canEnter = await route.beforeEnter(params, query);
|
|
504
|
+
if (canEnter === false) return;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Sauvegarder l'ancienne route pour l'animation
|
|
508
|
+
const oldComponents = [...this.components];
|
|
509
|
+
|
|
510
|
+
// Mettre à jour l'état
|
|
511
|
+
this.currentRoute = pathname;
|
|
512
|
+
this.currentParams = params;
|
|
513
|
+
this.currentQuery = query;
|
|
514
|
+
|
|
515
|
+
// Gérer l'historique
|
|
516
|
+
if (!replace) {
|
|
517
|
+
this.historyIndex++;
|
|
518
|
+
this.history = this.history.slice(0, this.historyIndex);
|
|
519
|
+
this.history.push({ path, params, query, state });
|
|
520
|
+
|
|
521
|
+
// Mettre à jour l'historique du navigateur
|
|
522
|
+
window.history.pushState(
|
|
523
|
+
{ route: path, params, query, state },
|
|
524
|
+
'',
|
|
525
|
+
path
|
|
526
|
+
);
|
|
527
|
+
} else {
|
|
528
|
+
this.history[this.historyIndex] = { path, params, query, state };
|
|
529
|
+
window.history.replaceState(
|
|
530
|
+
{ route: path, params, query, state },
|
|
531
|
+
'',
|
|
532
|
+
path
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Créer les nouveaux composants
|
|
537
|
+
this.components = [];
|
|
538
|
+
if (typeof route.component === 'function') {
|
|
539
|
+
route.component(this, params, query);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Lancer l'animation de transition
|
|
543
|
+
if (animate && !this.transitionState.isTransitioning) {
|
|
544
|
+
const transitionType = transition || route.transition || 'slide';
|
|
545
|
+
this.startTransition(oldComponents, this.components, transitionType, direction);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Hook afterEnter
|
|
549
|
+
if (route.afterEnter) {
|
|
550
|
+
route.afterEnter(params, query);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Démarre une animation de transition
|
|
556
|
+
* @private
|
|
557
|
+
*/
|
|
558
|
+
startTransition(oldComponents, newComponents, type, direction) {
|
|
559
|
+
this.transitionState = {
|
|
560
|
+
isTransitioning: true,
|
|
561
|
+
progress: 0,
|
|
562
|
+
duration: 300,
|
|
563
|
+
type,
|
|
564
|
+
direction,
|
|
565
|
+
oldComponents: [...oldComponents],
|
|
566
|
+
newComponents: [...newComponents],
|
|
567
|
+
startTime: Date.now()
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Met à jour l'animation de transition
|
|
573
|
+
* @private
|
|
574
|
+
*/
|
|
575
|
+
updateTransition() {
|
|
576
|
+
if (!this.transitionState.isTransitioning) return;
|
|
577
|
+
|
|
578
|
+
const elapsed = Date.now() - this.transitionState.startTime;
|
|
579
|
+
this.transitionState.progress = Math.min(elapsed / this.transitionState.duration, 1);
|
|
580
|
+
|
|
581
|
+
// Fonction d'easing (ease-in-out)
|
|
582
|
+
const eased = this.easeInOutCubic(this.transitionState.progress);
|
|
583
|
+
|
|
584
|
+
// Appliquer la transformation selon le type
|
|
585
|
+
this.ctx.save();
|
|
586
|
+
this.applyTransitionTransform(eased);
|
|
587
|
+
this.ctx.restore();
|
|
588
|
+
|
|
589
|
+
// Terminer la transition
|
|
590
|
+
if (this.transitionState.progress >= 1) {
|
|
591
|
+
this.transitionState.isTransitioning = false;
|
|
592
|
+
this.transitionState.oldComponents = [];
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Applique la transformation de transition
|
|
598
|
+
* @private
|
|
599
|
+
*/
|
|
600
|
+
applyTransitionTransform(progress) {
|
|
601
|
+
const { type, direction, oldComponents, newComponents } = this.transitionState;
|
|
602
|
+
const directionMultiplier = direction === 'forward' ? 1 : -1;
|
|
603
|
+
|
|
604
|
+
switch (type) {
|
|
605
|
+
case 'slide':
|
|
606
|
+
// Dessiner l'ancienne vue qui sort
|
|
607
|
+
this.ctx.save();
|
|
608
|
+
this.ctx.translate(-this.width * progress * directionMultiplier, 0);
|
|
609
|
+
this.ctx.globalAlpha = 1 - progress * 0.3;
|
|
610
|
+
for (let comp of oldComponents) {
|
|
611
|
+
if (comp.visible) comp.draw(this.ctx);
|
|
612
|
+
}
|
|
613
|
+
this.ctx.restore();
|
|
614
|
+
|
|
615
|
+
// Dessiner la nouvelle vue qui entre
|
|
616
|
+
this.ctx.save();
|
|
617
|
+
this.ctx.translate(this.width * (1 - progress) * directionMultiplier, 0);
|
|
618
|
+
for (let comp of newComponents) {
|
|
619
|
+
if (comp.visible) comp.draw(this.ctx);
|
|
620
|
+
}
|
|
621
|
+
this.ctx.restore();
|
|
622
|
+
break;
|
|
623
|
+
|
|
624
|
+
case 'fade':
|
|
625
|
+
// Dessiner l'ancienne vue qui fade out
|
|
626
|
+
this.ctx.save();
|
|
627
|
+
this.ctx.globalAlpha = 1 - progress;
|
|
628
|
+
for (let comp of oldComponents) {
|
|
629
|
+
if (comp.visible) comp.draw(this.ctx);
|
|
630
|
+
}
|
|
631
|
+
this.ctx.restore();
|
|
632
|
+
|
|
633
|
+
// Dessiner la nouvelle vue qui fade in
|
|
634
|
+
this.ctx.save();
|
|
635
|
+
this.ctx.globalAlpha = progress;
|
|
636
|
+
for (let comp of newComponents) {
|
|
637
|
+
if (comp.visible) comp.draw(this.ctx);
|
|
638
|
+
}
|
|
639
|
+
this.ctx.restore();
|
|
640
|
+
break;
|
|
641
|
+
|
|
642
|
+
case 'none':
|
|
643
|
+
// Pas d'animation, juste afficher la nouvelle vue
|
|
644
|
+
for (let comp of newComponents) {
|
|
645
|
+
if (comp.visible) comp.draw(this.ctx);
|
|
646
|
+
}
|
|
647
|
+
break;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Fonction d'easing
|
|
653
|
+
* @private
|
|
654
|
+
*/
|
|
655
|
+
easeInOutCubic(t) {
|
|
656
|
+
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Retour en arrière dans l'historique
|
|
661
|
+
*/
|
|
662
|
+
goBack() {
|
|
663
|
+
if (this.historyIndex > 0) {
|
|
664
|
+
this.historyIndex--;
|
|
665
|
+
const historyEntry = this.history[this.historyIndex];
|
|
666
|
+
this.navigateTo(historyEntry.path, {
|
|
667
|
+
replace: true,
|
|
668
|
+
animate: true,
|
|
669
|
+
direction: 'back'
|
|
670
|
+
});
|
|
671
|
+
window.history.back();
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Avancer dans l'historique
|
|
677
|
+
*/
|
|
678
|
+
goForward() {
|
|
679
|
+
if (this.historyIndex < this.history.length - 1) {
|
|
680
|
+
this.historyIndex++;
|
|
681
|
+
const historyEntry = this.history[this.historyIndex];
|
|
682
|
+
this.navigateTo(historyEntry.path, {
|
|
683
|
+
replace: true,
|
|
684
|
+
animate: true,
|
|
685
|
+
direction: 'forward'
|
|
686
|
+
});
|
|
687
|
+
window.history.forward();
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Obtient les paramètres de la route actuelle
|
|
693
|
+
* @returns {Object}
|
|
694
|
+
*/
|
|
695
|
+
getParams() {
|
|
696
|
+
return { ...this.currentParams };
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Obtient la query string de la route actuelle
|
|
701
|
+
* @returns {Object}
|
|
702
|
+
*/
|
|
703
|
+
getQuery() {
|
|
704
|
+
return { ...this.currentQuery };
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Obtient un paramètre spécifique
|
|
709
|
+
* @param {string} name
|
|
710
|
+
* @returns {string|undefined}
|
|
711
|
+
*/
|
|
712
|
+
getParam(name) {
|
|
713
|
+
return this.currentParams[name];
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Obtient un paramètre de query spécifique
|
|
718
|
+
* @param {string} name
|
|
719
|
+
* @returns {string|undefined}
|
|
720
|
+
*/
|
|
721
|
+
getQueryParam(name) {
|
|
722
|
+
return this.currentQuery[name];
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// ===== FIN DES MÉTHODES DE ROUTING =====
|
|
726
|
+
|
|
727
|
+
handleTouchStart(e) {
|
|
728
|
+
e.preventDefault();
|
|
729
|
+
this.isDragging = false;
|
|
730
|
+
const touch = e.touches[0];
|
|
731
|
+
const pos = this.getTouchPos(touch);
|
|
732
|
+
this.lastTouchY = pos.y;
|
|
733
|
+
this.checkComponentsAtPosition(pos.x, pos.y, 'start');
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
handleTouchMove(e) {
|
|
737
|
+
e.preventDefault();
|
|
738
|
+
const touch = e.touches[0];
|
|
739
|
+
const pos = this.getTouchPos(touch);
|
|
740
|
+
|
|
741
|
+
if (!this.isDragging) {
|
|
742
|
+
const deltaY = Math.abs(pos.y - this.lastTouchY);
|
|
743
|
+
if (deltaY > 5) {
|
|
744
|
+
this.isDragging = true;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
if (this.isDragging) {
|
|
749
|
+
const deltaY = pos.y - this.lastTouchY;
|
|
750
|
+
this.scrollOffset += deltaY;
|
|
751
|
+
const maxScroll = this.getMaxScroll();
|
|
752
|
+
this.scrollOffset = Math.max(Math.min(this.scrollOffset, 0), -maxScroll);
|
|
753
|
+
this.scrollVelocity = deltaY;
|
|
754
|
+
this.lastTouchY = pos.y;
|
|
755
|
+
} else {
|
|
756
|
+
this.checkComponentsAtPosition(pos.x, pos.y, 'move');
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
handleTouchEnd(e) {
|
|
761
|
+
e.preventDefault();
|
|
762
|
+
const touch = e.changedTouches[0];
|
|
763
|
+
const pos = this.getTouchPos(touch);
|
|
764
|
+
|
|
765
|
+
if (!this.isDragging) {
|
|
766
|
+
this.checkComponentsAtPosition(pos.x, pos.y, 'end');
|
|
767
|
+
} else {
|
|
768
|
+
this.isDragging = false;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
handleMouseDown(e) {
|
|
773
|
+
this.isDragging = false;
|
|
774
|
+
this.lastTouchY = e.clientY;
|
|
775
|
+
this.checkComponentsAtPosition(e.clientX, e.clientY, 'start');
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
handleMouseMove(e) {
|
|
779
|
+
if (!this.isDragging) {
|
|
780
|
+
const deltaY = Math.abs(e.clientY - this.lastTouchY);
|
|
781
|
+
if (deltaY > 5) {
|
|
782
|
+
this.isDragging = true;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
if (this.isDragging) {
|
|
787
|
+
const deltaY = e.clientY - this.lastTouchY;
|
|
788
|
+
this.scrollOffset += deltaY;
|
|
789
|
+
const maxScroll = this.getMaxScroll();
|
|
790
|
+
this.scrollOffset = Math.max(Math.min(this.scrollOffset, 0), -maxScroll);
|
|
791
|
+
this.scrollVelocity = deltaY;
|
|
792
|
+
this.lastTouchY = e.clientY;
|
|
793
|
+
} else {
|
|
794
|
+
this.checkComponentsAtPosition(e.clientX, e.clientY, 'move');
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
handleMouseUp(e) {
|
|
799
|
+
if (!this.isDragging) {
|
|
800
|
+
this.checkComponentsAtPosition(e.clientX, e.clientY, 'end');
|
|
801
|
+
} else {
|
|
802
|
+
this.isDragging = false;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
getTouchPos(touch) {
|
|
807
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
808
|
+
return {
|
|
809
|
+
x: touch.clientX - rect.left,
|
|
810
|
+
y: touch.clientY - rect.top
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
checkComponentsAtPosition(x, y, eventType) {
|
|
815
|
+
const isFixedComponent = (comp) => {
|
|
816
|
+
return comp instanceof AppBar ||
|
|
817
|
+
comp instanceof BottomNavigationBar ||
|
|
818
|
+
comp instanceof Drawer ||
|
|
819
|
+
comp instanceof Dialog ||
|
|
820
|
+
comp instanceof Modal ||
|
|
821
|
+
comp instanceof FAB ||
|
|
822
|
+
comp instanceof BottomSheet ||
|
|
823
|
+
comp instanceof ContextMenu ||
|
|
824
|
+
comp instanceof OpenStreetMap ||
|
|
825
|
+
comp instanceof SelectDialog;
|
|
826
|
+
};
|
|
827
|
+
|
|
828
|
+
for (let i = this.components.length - 1; i >= 0; i--) {
|
|
829
|
+
const comp = this.components[i];
|
|
830
|
+
|
|
831
|
+
if (comp.visible) {
|
|
832
|
+
const adjustedY = isFixedComponent(comp) ? y : y - this.scrollOffset;
|
|
833
|
+
|
|
834
|
+
if (comp instanceof Card && comp.clickableChildren && comp.children && comp.children.length > 0) {
|
|
835
|
+
if (comp.isPointInside(x, adjustedY)) {
|
|
836
|
+
const cardAdjustedY = adjustedY - comp.y - comp.padding;
|
|
837
|
+
const cardAdjustedX = x - comp.x - comp.padding;
|
|
838
|
+
|
|
839
|
+
for (let j = comp.children.length - 1; j >= 0; j--) {
|
|
840
|
+
const child = comp.children[j];
|
|
841
|
+
|
|
842
|
+
if (child.visible &&
|
|
843
|
+
cardAdjustedY >= child.y &&
|
|
844
|
+
cardAdjustedY <= child.y + child.height &&
|
|
845
|
+
cardAdjustedX >= child.x &&
|
|
846
|
+
cardAdjustedX <= child.x + child.width) {
|
|
847
|
+
|
|
848
|
+
const relativeX = cardAdjustedX - child.x;
|
|
849
|
+
const relativeY = cardAdjustedY - child.y;
|
|
850
|
+
|
|
851
|
+
switch (eventType) {
|
|
852
|
+
case 'start':
|
|
853
|
+
child.pressed = true;
|
|
854
|
+
if (child.onPress) child.onPress(relativeX, relativeY);
|
|
855
|
+
break;
|
|
856
|
+
|
|
857
|
+
case 'move':
|
|
858
|
+
if (!child.hovered) {
|
|
859
|
+
child.hovered = true;
|
|
860
|
+
if (child.onHover) child.onHover();
|
|
861
|
+
}
|
|
862
|
+
if (child.onMove) child.onMove(relativeX, relativeY);
|
|
863
|
+
break;
|
|
864
|
+
|
|
865
|
+
case 'end':
|
|
866
|
+
if (child.pressed) {
|
|
867
|
+
child.pressed = false;
|
|
868
|
+
|
|
869
|
+
if (child instanceof Input) {
|
|
870
|
+
for (let other of this.components) {
|
|
871
|
+
if (other instanceof Input && other !== child && other.focused) {
|
|
872
|
+
other.focused = false;
|
|
873
|
+
other.cursorVisible = false;
|
|
874
|
+
if (other.onBlur) other.onBlur();
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
child.focused = true;
|
|
879
|
+
child.cursorVisible = true;
|
|
880
|
+
if (child.onFocus) child.onFocus();
|
|
881
|
+
} else if (child.onClick) {
|
|
882
|
+
child.onClick();
|
|
883
|
+
} else if (child.onPress) {
|
|
884
|
+
child.onPress(relativeX, relativeY);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
break;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
if (comp.isPointInside(x, adjustedY)) {
|
|
897
|
+
switch (eventType) {
|
|
898
|
+
case 'start':
|
|
899
|
+
comp.pressed = true;
|
|
900
|
+
if (comp.onPress) comp.onPress(x, adjustedY);
|
|
901
|
+
break;
|
|
902
|
+
|
|
903
|
+
case 'move':
|
|
904
|
+
if (!comp.hovered) {
|
|
905
|
+
comp.hovered = true;
|
|
906
|
+
if (comp.onHover) comp.onHover();
|
|
907
|
+
}
|
|
908
|
+
if (comp.onMove) comp.onMove(x, adjustedY);
|
|
909
|
+
break;
|
|
910
|
+
|
|
911
|
+
case 'end':
|
|
912
|
+
if (comp.pressed) {
|
|
913
|
+
comp.pressed = false;
|
|
914
|
+
|
|
915
|
+
if (comp instanceof Input) {
|
|
916
|
+
for (let other of this.components) {
|
|
917
|
+
if (other instanceof Input && other !== comp && other.focused) {
|
|
918
|
+
other.focused = false;
|
|
919
|
+
other.cursorVisible = false;
|
|
920
|
+
if (other.onBlur) other.onBlur();
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
comp.focused = true;
|
|
925
|
+
comp.cursorVisible = true;
|
|
926
|
+
if (comp.onFocus) comp.onFocus();
|
|
927
|
+
} else if (comp.onClick) {
|
|
928
|
+
comp.onClick();
|
|
929
|
+
} else if (comp.onPress) {
|
|
930
|
+
comp.onPress(x, adjustedY);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
break;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
return;
|
|
937
|
+
} else {
|
|
938
|
+
comp.hovered = false;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
getMaxScroll() {
|
|
945
|
+
let maxY = 0;
|
|
946
|
+
for (let comp of this.components) {
|
|
947
|
+
if (!this.isFixedComponent(comp)) {
|
|
948
|
+
maxY = Math.max(maxY, comp.y + comp.height);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
return Math.max(0, maxY - this.height + 50);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
handleResize() {
|
|
955
|
+
this.width = window.innerWidth;
|
|
956
|
+
this.height = window.innerHeight;
|
|
957
|
+
this.setupCanvas();
|
|
958
|
+
for (const comp of this.components) {
|
|
959
|
+
comp._resize(this.width, this.height);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
add(component) {
|
|
964
|
+
this.components.push(component);
|
|
965
|
+
component._mount();
|
|
966
|
+
return component;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
remove(component) {
|
|
970
|
+
const index = this.components.indexOf(component);
|
|
971
|
+
if (index > -1) {
|
|
972
|
+
component._unmount();
|
|
973
|
+
this.components.splice(index, 1);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
markComponentDirty(component) {
|
|
978
|
+
if (this.optimizationEnabled) {
|
|
979
|
+
this.dirtyComponents.add(component);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
enableOptimization() {
|
|
984
|
+
this.optimizationEnabled = true;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
/**
|
|
988
|
+
* Dessine un petit triangle rouge pour indiquer overflow (style Flutter)
|
|
989
|
+
*/
|
|
990
|
+
drawOverflowIndicators() {
|
|
991
|
+
const ctx = this.ctx;
|
|
992
|
+
|
|
993
|
+
// Pour chaque composant
|
|
994
|
+
for (let comp of this.components) {
|
|
995
|
+
if (!comp.visible) continue;
|
|
996
|
+
|
|
997
|
+
// Position réelle à l'écran
|
|
998
|
+
const isFixed = this.isFixedComponent(comp);
|
|
999
|
+
const screenY = isFixed ? comp.y : comp.y + this.scrollOffset;
|
|
1000
|
+
const screenX = comp.x;
|
|
1001
|
+
|
|
1002
|
+
// Vérifier si le composant TEXT a une largeur/hauteur incorrecte
|
|
1003
|
+
let actualWidth = comp.width;
|
|
1004
|
+
let actualHeight = comp.height;
|
|
1005
|
+
|
|
1006
|
+
// Si c'est un Text, vérifier la taille réelle du texte
|
|
1007
|
+
if (comp instanceof Text && comp.text && ctx.measureText) {
|
|
1008
|
+
try {
|
|
1009
|
+
// Sauvegarder le style actuel
|
|
1010
|
+
ctx.save();
|
|
1011
|
+
|
|
1012
|
+
// Appliquer le style du texte
|
|
1013
|
+
if (comp.fontSize) {
|
|
1014
|
+
ctx.font = `${comp.fontSize}px ${comp.fontFamily || 'Arial'}`;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// Mesurer la taille réelle
|
|
1018
|
+
const metrics = ctx.measureText(comp.text);
|
|
1019
|
+
actualWidth = metrics.width + (comp.padding || 0) * 2;
|
|
1020
|
+
actualHeight = (comp.fontSize || 16) + (comp.padding || 0) * 2;
|
|
1021
|
+
|
|
1022
|
+
ctx.restore();
|
|
1023
|
+
} catch (e) {
|
|
1024
|
+
// En cas d'erreur, garder les dimensions par défaut
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// Calculer les limites RÉELLES du composant
|
|
1029
|
+
const compLeft = screenX;
|
|
1030
|
+
const compRight = screenX + actualWidth;
|
|
1031
|
+
const compTop = screenY;
|
|
1032
|
+
const compBottom = screenY + actualHeight;
|
|
1033
|
+
|
|
1034
|
+
// Vérifier les débordements avec les dimensions RÉELLES
|
|
1035
|
+
const overflow = {
|
|
1036
|
+
left: compLeft < 0,
|
|
1037
|
+
right: compRight > this.width,
|
|
1038
|
+
top: compTop < 0,
|
|
1039
|
+
bottom: compBottom > this.height
|
|
1040
|
+
};
|
|
1041
|
+
|
|
1042
|
+
// Si aucun débordement, passer au suivant
|
|
1043
|
+
if (!overflow.left && !overflow.right && !overflow.top && !overflow.bottom) {
|
|
1044
|
+
continue;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// DEBUG: Afficher les infos du composant
|
|
1048
|
+
if (this.debbug) {
|
|
1049
|
+
console.table({
|
|
1050
|
+
type: comp.constructor?.name,
|
|
1051
|
+
x: comp.x,
|
|
1052
|
+
y: comp.y,
|
|
1053
|
+
declaredSize: `${comp.width}x${comp.height}`,
|
|
1054
|
+
actualSize: `${actualWidth}x${actualHeight}`,
|
|
1055
|
+
screenPos: `(${screenX}, ${screenY})`,
|
|
1056
|
+
overflow
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// Dessiner les indicateurs
|
|
1061
|
+
ctx.save();
|
|
1062
|
+
|
|
1063
|
+
// 1. Bordures rouges sur les parties qui débordent
|
|
1064
|
+
ctx.strokeStyle = 'red';
|
|
1065
|
+
ctx.lineWidth = 2;
|
|
1066
|
+
ctx.fillStyle = 'rgba(255, 0, 0, 0.2)';
|
|
1067
|
+
|
|
1068
|
+
// Gauche
|
|
1069
|
+
if (overflow.left) {
|
|
1070
|
+
const overflowWidth = Math.min(actualWidth, -compLeft);
|
|
1071
|
+
ctx.fillRect(compLeft, compTop, overflowWidth, actualHeight);
|
|
1072
|
+
ctx.strokeRect(compLeft, compTop, overflowWidth, actualHeight);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// Droite
|
|
1076
|
+
if (overflow.right) {
|
|
1077
|
+
const overflowStart = Math.max(0, this.width - compLeft);
|
|
1078
|
+
const overflowWidth = Math.min(actualWidth, compRight - this.width);
|
|
1079
|
+
ctx.fillRect(this.width - overflowWidth, compTop, overflowWidth, actualHeight);
|
|
1080
|
+
ctx.strokeRect(this.width - overflowWidth, compTop, overflowWidth, actualHeight);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// Haut
|
|
1084
|
+
if (overflow.top) {
|
|
1085
|
+
const overflowHeight = Math.min(actualHeight, -compTop);
|
|
1086
|
+
ctx.fillRect(compLeft, compTop, actualWidth, overflowHeight);
|
|
1087
|
+
ctx.strokeRect(compLeft, compTop, actualWidth, overflowHeight);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// Bas
|
|
1091
|
+
if (overflow.bottom) {
|
|
1092
|
+
const overflowStart = Math.max(0, this.height - compTop);
|
|
1093
|
+
const overflowHeight = Math.min(actualHeight, compBottom - this.height);
|
|
1094
|
+
ctx.fillRect(compLeft, this.height - overflowHeight, actualWidth, overflowHeight);
|
|
1095
|
+
ctx.strokeRect(compLeft, this.height - overflowHeight, actualWidth, overflowHeight);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// 2. Points rouges aux coins
|
|
1099
|
+
ctx.fillStyle = 'red';
|
|
1100
|
+
const markerSize = 6;
|
|
1101
|
+
|
|
1102
|
+
// Coin supérieur gauche
|
|
1103
|
+
if (overflow.left || overflow.top) {
|
|
1104
|
+
ctx.fillRect(compLeft, compTop, markerSize, markerSize);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// Coin supérieur droit
|
|
1108
|
+
if (overflow.right || overflow.top) {
|
|
1109
|
+
ctx.fillRect(compRight - markerSize, compTop, markerSize, markerSize);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// Coin inférieur gauche
|
|
1113
|
+
if (overflow.left || overflow.bottom) {
|
|
1114
|
+
ctx.fillRect(compLeft, compBottom - markerSize, markerSize, markerSize);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// Coin inférieur droit
|
|
1118
|
+
if (overflow.right || overflow.bottom) {
|
|
1119
|
+
ctx.fillRect(compRight - markerSize, compBottom - markerSize, markerSize, markerSize);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// 3. Texte d'information (optionnel)
|
|
1123
|
+
if (this.debbug && comp.text) {
|
|
1124
|
+
ctx.fillStyle = 'red';
|
|
1125
|
+
ctx.font = '10px monospace';
|
|
1126
|
+
ctx.textAlign = 'left';
|
|
1127
|
+
|
|
1128
|
+
const overflowText = [];
|
|
1129
|
+
if (overflow.left) overflowText.push('←');
|
|
1130
|
+
if (overflow.right) overflowText.push('→');
|
|
1131
|
+
if (overflow.top) overflowText.push('↑');
|
|
1132
|
+
if (overflow.bottom) overflowText.push('↓');
|
|
1133
|
+
|
|
1134
|
+
if (overflowText.length > 0) {
|
|
1135
|
+
ctx.fillText(
|
|
1136
|
+
`"${comp.text.substring(0, 10)}${comp.text.length > 10 ? '...' : ''}" ${overflowText.join('')}`,
|
|
1137
|
+
compLeft + 5,
|
|
1138
|
+
compTop - 5
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
ctx.restore();
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
startRenderLoop() {
|
|
1148
|
+
const render = () => {
|
|
1149
|
+
// 1️⃣ Scroll inertia
|
|
1150
|
+
if (Math.abs(this.scrollVelocity) > 0.1 && !this.isDragging) {
|
|
1151
|
+
this.scrollOffset += this.scrollVelocity;
|
|
1152
|
+
this.scrollOffset = Math.max(Math.min(this.scrollOffset, 0), -this.getMaxScroll());
|
|
1153
|
+
this.scrollVelocity *= this.scrollFriction;
|
|
1154
|
+
} else {
|
|
1155
|
+
this.scrollVelocity = 0;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// 2️⃣ Clear canvas
|
|
1159
|
+
this.ctx.clearRect(0, 0, this.width, this.height);
|
|
1160
|
+
|
|
1161
|
+
// 3️⃣ Transition handling
|
|
1162
|
+
if (this.transitionState.isTransitioning) {
|
|
1163
|
+
this.updateTransition();
|
|
1164
|
+
} else if (this.optimizationEnabled && this.dirtyComponents.size > 0) {
|
|
1165
|
+
// Dirty components redraw
|
|
1166
|
+
for (let comp of this.dirtyComponents) {
|
|
1167
|
+
if (comp.visible) {
|
|
1168
|
+
const isFixed = this.isFixedComponent(comp);
|
|
1169
|
+
const y = isFixed ? comp.y : comp.y + this.scrollOffset;
|
|
1170
|
+
|
|
1171
|
+
this.ctx.clearRect(comp.x - 2, y - 2, comp.width + 4, comp.height + 4);
|
|
1172
|
+
|
|
1173
|
+
this.ctx.save();
|
|
1174
|
+
if (!isFixed) this.ctx.translate(0, this.scrollOffset);
|
|
1175
|
+
comp.draw(this.ctx);
|
|
1176
|
+
this.ctx.restore();
|
|
1177
|
+
|
|
1178
|
+
// Overflow indicator style Flutter
|
|
1179
|
+
const overflow = comp.getOverflow?.();
|
|
1180
|
+
if (comp.markClean) comp.markClean();
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
this.dirtyComponents.clear();
|
|
1184
|
+
} else {
|
|
1185
|
+
// Full redraw
|
|
1186
|
+
const scrollableComponents = [];
|
|
1187
|
+
const fixedComponents = [];
|
|
1188
|
+
|
|
1189
|
+
for (let comp of this.components) {
|
|
1190
|
+
if (this.isFixedComponent(comp)) fixedComponents.push(comp);
|
|
1191
|
+
else scrollableComponents.push(comp);
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// Scrollable
|
|
1195
|
+
this.ctx.save();
|
|
1196
|
+
this.ctx.translate(0, this.scrollOffset);
|
|
1197
|
+
for (let comp of scrollableComponents) {
|
|
1198
|
+
if (comp.visible) {
|
|
1199
|
+
comp.draw(this.ctx);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
this.ctx.restore();
|
|
1203
|
+
|
|
1204
|
+
// Fixed
|
|
1205
|
+
for (let comp of fixedComponents) {
|
|
1206
|
+
if (comp.visible) {
|
|
1207
|
+
comp.draw(this.ctx);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
// 4️⃣ FPS
|
|
1213
|
+
this._frames++;
|
|
1214
|
+
const now = performance.now();
|
|
1215
|
+
if (now - this._lastFpsTime >= 1000) {
|
|
1216
|
+
this.fps = this._frames;
|
|
1217
|
+
this._frames = 0;
|
|
1218
|
+
this._lastFpsTime = now;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
if (this.showFps) {
|
|
1222
|
+
this.ctx.save();
|
|
1223
|
+
this.ctx.fillStyle = 'lime';
|
|
1224
|
+
this.ctx.font = '16px monospace';
|
|
1225
|
+
this.ctx.fillText(`FPS: ${this.fps}`, 10, 20);
|
|
1226
|
+
this.ctx.restore();
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
if(this.debbug) {
|
|
1230
|
+
this.drawOverflowIndicators();
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
requestAnimationFrame(render);
|
|
1234
|
+
};
|
|
1235
|
+
|
|
1236
|
+
render();
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
isFixedComponent(comp) {
|
|
1240
|
+
return comp instanceof AppBar ||
|
|
1241
|
+
comp instanceof BottomNavigationBar ||
|
|
1242
|
+
comp instanceof Drawer ||
|
|
1243
|
+
comp instanceof Dialog ||
|
|
1244
|
+
comp instanceof Modal ||
|
|
1245
|
+
comp instanceof FAB ||
|
|
1246
|
+
comp instanceof BottomSheet ||
|
|
1247
|
+
comp instanceof ContextMenu ||
|
|
1248
|
+
comp instanceof OpenStreetMap ||
|
|
1249
|
+
comp instanceof SelectDialog;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
showToast(message, duration = 3000) {
|
|
1253
|
+
const toast = new Toast(this, {
|
|
1254
|
+
text: message,
|
|
1255
|
+
duration: duration,
|
|
1256
|
+
x: this.width / 2,
|
|
1257
|
+
y: this.height - 100
|
|
1258
|
+
});
|
|
1259
|
+
this.add(toast);
|
|
1260
|
+
toast.show();
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
export default CanvasFramework;
|
|
1265
|
+
|
|
1266
|
+
|
|
1267
|
+
|
|
1268
|
+
|
|
1269
|
+
|
|
1270
|
+
|
|
1271
|
+
|