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,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Système de stockage sécurisé avec chiffrement AES-GCM
|
|
3
|
+
* @class
|
|
4
|
+
* @example
|
|
5
|
+
* const storage = new SecureStorage('myapp_');
|
|
6
|
+
* await storage.init('password123');
|
|
7
|
+
* await storage.setSecure('token', 'secret-token');
|
|
8
|
+
* const token = await storage.getSecure('token');
|
|
9
|
+
*/
|
|
10
|
+
class SecureStorage {
|
|
11
|
+
/**
|
|
12
|
+
* @constructs SecureStorage
|
|
13
|
+
* @param {string} [prefix='app_'] - Préfixe pour les clés localStorage
|
|
14
|
+
*/
|
|
15
|
+
constructor(prefix = 'app_') {
|
|
16
|
+
/** @type {string} */
|
|
17
|
+
this.prefix = prefix;
|
|
18
|
+
/** @type {CryptoKey|null} */
|
|
19
|
+
this.encryptionKey = null;
|
|
20
|
+
/** @type {Map<string, any>} */
|
|
21
|
+
this.memoryCache = new Map();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Initialiser avec une clé de chiffrement
|
|
26
|
+
* @param {string} password - Mot de passe pour générer la clé de chiffrement
|
|
27
|
+
* @returns {Promise<boolean>} True si l'initialisation a réussi
|
|
28
|
+
*/
|
|
29
|
+
async init(password) {
|
|
30
|
+
try {
|
|
31
|
+
// Générer une clé de chiffrement à partir du password
|
|
32
|
+
const encoder = new TextEncoder();
|
|
33
|
+
const data = encoder.encode(password);
|
|
34
|
+
const hash = await crypto.subtle.digest('SHA-256', data);
|
|
35
|
+
|
|
36
|
+
this.encryptionKey = await crypto.subtle.importKey(
|
|
37
|
+
'raw',
|
|
38
|
+
hash,
|
|
39
|
+
{ name: 'AES-GCM' },
|
|
40
|
+
false,
|
|
41
|
+
['encrypt', 'decrypt']
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return true;
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error('Failed to initialize SecureStorage:', error);
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Chiffrer des données
|
|
53
|
+
* @param {*} data - Données à chiffrer
|
|
54
|
+
* @returns {Promise<string>} Données chiffrées en base64
|
|
55
|
+
* @throws {Error} Si SecureStorage n'est pas initialisé
|
|
56
|
+
* @private
|
|
57
|
+
*/
|
|
58
|
+
async encrypt(data) {
|
|
59
|
+
if (!this.encryptionKey) {
|
|
60
|
+
throw new Error('SecureStorage not initialized. Call init() first.');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const encoder = new TextEncoder();
|
|
65
|
+
const dataBuffer = encoder.encode(JSON.stringify(data));
|
|
66
|
+
|
|
67
|
+
// Générer un IV aléatoire
|
|
68
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
69
|
+
|
|
70
|
+
// Chiffrer
|
|
71
|
+
const encrypted = await crypto.subtle.encrypt(
|
|
72
|
+
{ name: 'AES-GCM', iv },
|
|
73
|
+
this.encryptionKey,
|
|
74
|
+
dataBuffer
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// Combiner IV et données chiffrées
|
|
78
|
+
const combined = new Uint8Array(iv.length + encrypted.byteLength);
|
|
79
|
+
combined.set(iv);
|
|
80
|
+
combined.set(new Uint8Array(encrypted), iv.length);
|
|
81
|
+
|
|
82
|
+
// Convertir en base64
|
|
83
|
+
return btoa(String.fromCharCode(...combined));
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error('Encryption failed:', error);
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Déchiffrer des données
|
|
92
|
+
* @param {string} encryptedData - Données chiffrées en base64
|
|
93
|
+
* @returns {Promise<*>} Données déchiffrées
|
|
94
|
+
* @throws {Error} Si SecureStorage n'est pas initialisé
|
|
95
|
+
* @private
|
|
96
|
+
*/
|
|
97
|
+
async decrypt(encryptedData) {
|
|
98
|
+
if (!this.encryptionKey) {
|
|
99
|
+
throw new Error('SecureStorage not initialized. Call init() first.');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
// Décoder base64
|
|
104
|
+
const combined = new Uint8Array(
|
|
105
|
+
atob(encryptedData).split('').map(char => char.charCodeAt(0))
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// Extraire IV et données
|
|
109
|
+
const iv = combined.slice(0, 12);
|
|
110
|
+
const data = combined.slice(12);
|
|
111
|
+
|
|
112
|
+
// Déchiffrer
|
|
113
|
+
const decrypted = await crypto.subtle.decrypt(
|
|
114
|
+
{ name: 'AES-GCM', iv },
|
|
115
|
+
this.encryptionKey,
|
|
116
|
+
data
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// Décoder et parser
|
|
120
|
+
const decoder = new TextDecoder();
|
|
121
|
+
return JSON.parse(decoder.decode(decrypted));
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error('Decryption failed:', error);
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Sauvegarder une valeur de manière sécurisée (chiffrée)
|
|
130
|
+
* @param {string} key - Clé de stockage
|
|
131
|
+
* @param {*} value - Valeur à sauvegarder
|
|
132
|
+
* @returns {Promise<boolean>} True si la sauvegarde a réussi
|
|
133
|
+
*/
|
|
134
|
+
async setSecure(key, value) {
|
|
135
|
+
try {
|
|
136
|
+
const encrypted = await this.encrypt(value);
|
|
137
|
+
localStorage.setItem(this.prefix + key, encrypted);
|
|
138
|
+
this.memoryCache.set(key, value);
|
|
139
|
+
return true;
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.error('Failed to save secure data:', error);
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Récupérer une valeur de manière sécurisée (déchiffrée)
|
|
148
|
+
* @param {string} key - Clé de stockage
|
|
149
|
+
* @param {*} [defaultValue=null] - Valeur par défaut si la clé n'existe pas
|
|
150
|
+
* @returns {Promise<*>} Valeur déchiffrée ou valeur par défaut
|
|
151
|
+
*/
|
|
152
|
+
async getSecure(key, defaultValue = null) {
|
|
153
|
+
try {
|
|
154
|
+
// Vérifier le cache mémoire
|
|
155
|
+
if (this.memoryCache.has(key)) {
|
|
156
|
+
return this.memoryCache.get(key);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const encrypted = localStorage.getItem(this.prefix + key);
|
|
160
|
+
if (!encrypted) {
|
|
161
|
+
return defaultValue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const value = await this.decrypt(encrypted);
|
|
165
|
+
this.memoryCache.set(key, value);
|
|
166
|
+
return value;
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.error('Failed to get secure data:', error);
|
|
169
|
+
return defaultValue;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Sauvegarder une valeur en clair (pour données non sensibles)
|
|
175
|
+
* @param {string} key - Clé de stockage
|
|
176
|
+
* @param {*} value - Valeur à sauvegarder
|
|
177
|
+
* @returns {boolean} True si la sauvegarde a réussi
|
|
178
|
+
*/
|
|
179
|
+
set(key, value) {
|
|
180
|
+
try {
|
|
181
|
+
localStorage.setItem(this.prefix + key, JSON.stringify(value));
|
|
182
|
+
return true;
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.error('Failed to save data:', error);
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Récupérer une valeur en clair
|
|
191
|
+
* @param {string} key - Clé de stockage
|
|
192
|
+
* @param {*} [defaultValue=null] - Valeur par défaut si la clé n'existe pas
|
|
193
|
+
* @returns {*} Valeur ou valeur par défaut
|
|
194
|
+
*/
|
|
195
|
+
get(key, defaultValue = null) {
|
|
196
|
+
try {
|
|
197
|
+
const item = localStorage.getItem(this.prefix + key);
|
|
198
|
+
return item ? JSON.parse(item) : defaultValue;
|
|
199
|
+
} catch (error) {
|
|
200
|
+
console.error('Failed to get data:', error);
|
|
201
|
+
return defaultValue;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Supprimer une clé
|
|
207
|
+
* @param {string} key - Clé à supprimer
|
|
208
|
+
*/
|
|
209
|
+
remove(key) {
|
|
210
|
+
localStorage.removeItem(this.prefix + key);
|
|
211
|
+
this.memoryCache.delete(key);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Vérifier si une clé existe
|
|
216
|
+
* @param {string} key - Clé à vérifier
|
|
217
|
+
* @returns {boolean} True si la clé existe
|
|
218
|
+
*/
|
|
219
|
+
has(key) {
|
|
220
|
+
return localStorage.getItem(this.prefix + key) !== null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Vider le cache mémoire
|
|
225
|
+
*/
|
|
226
|
+
clearCache() {
|
|
227
|
+
this.memoryCache.clear();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Tout supprimer (données avec le préfixe)
|
|
232
|
+
*/
|
|
233
|
+
clear() {
|
|
234
|
+
const keys = Object.keys(localStorage);
|
|
235
|
+
keys.forEach(key => {
|
|
236
|
+
if (key.startsWith(this.prefix)) {
|
|
237
|
+
localStorage.removeItem(key);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
this.clearCache();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Obtenir toutes les clés
|
|
245
|
+
* @returns {string[]} Liste des clés (sans le préfixe)
|
|
246
|
+
*/
|
|
247
|
+
keys() {
|
|
248
|
+
return Object.keys(localStorage)
|
|
249
|
+
.filter(key => key.startsWith(this.prefix))
|
|
250
|
+
.map(key => key.substring(this.prefix.length));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Exporter toutes les données de manière sécurisée
|
|
255
|
+
* @returns {Promise<string>} Données chiffrées contenant toutes les clés/valeurs
|
|
256
|
+
*/
|
|
257
|
+
async exportSecure() {
|
|
258
|
+
const data = {};
|
|
259
|
+
const keys = this.keys();
|
|
260
|
+
|
|
261
|
+
for (const key of keys) {
|
|
262
|
+
data[key] = await this.getSecure(key);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return await this.encrypt(data);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Importer des données de manière sécurisée
|
|
270
|
+
* @param {string} encryptedData - Données chiffrées exportées
|
|
271
|
+
* @returns {Promise<boolean>} True si l'import a réussi
|
|
272
|
+
*/
|
|
273
|
+
async importSecure(encryptedData) {
|
|
274
|
+
try {
|
|
275
|
+
const data = await this.decrypt(encryptedData);
|
|
276
|
+
|
|
277
|
+
for (const [key, value] of Object.entries(data)) {
|
|
278
|
+
await this.setSecure(key, value);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return true;
|
|
282
|
+
} catch (error) {
|
|
283
|
+
console.error('Failed to import data:', error);
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export default SecureStorage;
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Système de gestion d'état avec historique (undo/redo) et observateurs
|
|
3
|
+
* @class
|
|
4
|
+
* @example
|
|
5
|
+
* const state = new StateManager();
|
|
6
|
+
* state.set('user.name', 'John');
|
|
7
|
+
* const unsubscribe = state.subscribe('user.name', (newValue, oldValue) => {
|
|
8
|
+
* console.log('Name changed:', oldValue, '->', newValue);
|
|
9
|
+
* });
|
|
10
|
+
*/
|
|
11
|
+
class StateManager {
|
|
12
|
+
/**
|
|
13
|
+
* @constructs StateManager
|
|
14
|
+
*/
|
|
15
|
+
constructor() {
|
|
16
|
+
/** @type {Object} */
|
|
17
|
+
this.state = {};
|
|
18
|
+
/** @type {Map<string, Function[]>} */
|
|
19
|
+
this.listeners = new Map();
|
|
20
|
+
/** @type {Array<Object>} */
|
|
21
|
+
this.history = [];
|
|
22
|
+
/** @type {number} */
|
|
23
|
+
this.historyIndex = -1;
|
|
24
|
+
/** @type {number} */
|
|
25
|
+
this.maxHistory = 50;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Définir une valeur dans le state
|
|
30
|
+
* @param {string} key - Clé à définir
|
|
31
|
+
* @param {*} value - Valeur à assigner
|
|
32
|
+
* @param {boolean} [saveToHistory=true] - Sauvegarder dans l'historique
|
|
33
|
+
*/
|
|
34
|
+
set(key, value, saveToHistory = true) {
|
|
35
|
+
const oldValue = this.state[key];
|
|
36
|
+
|
|
37
|
+
if (saveToHistory && oldValue !== value) {
|
|
38
|
+
this.addToHistory(key, oldValue, value);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
this.state[key] = value;
|
|
42
|
+
this.notify(key, value, oldValue);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Récupérer une valeur du state
|
|
47
|
+
* @param {string} key - Clé à récupérer
|
|
48
|
+
* @param {*} [defaultValue=null] - Valeur par défaut si la clé n'existe pas
|
|
49
|
+
* @returns {*} La valeur de la clé ou la valeur par défaut
|
|
50
|
+
*/
|
|
51
|
+
get(key, defaultValue = null) {
|
|
52
|
+
return this.state.hasOwnProperty(key) ? this.state[key] : defaultValue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Mettre à jour plusieurs valeurs à la fois
|
|
57
|
+
* @param {Object} updates - Objet avec les clés/valeurs à mettre à jour
|
|
58
|
+
* @param {boolean} [saveToHistory=true] - Sauvegarder dans l'historique
|
|
59
|
+
*/
|
|
60
|
+
update(updates, saveToHistory = true) {
|
|
61
|
+
Object.keys(updates).forEach(key => {
|
|
62
|
+
this.set(key, updates[key], saveToHistory);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Supprimer une clé du state
|
|
68
|
+
* @param {string} key - Clé à supprimer
|
|
69
|
+
*/
|
|
70
|
+
delete(key) {
|
|
71
|
+
const oldValue = this.state[key];
|
|
72
|
+
delete this.state[key];
|
|
73
|
+
this.notify(key, undefined, oldValue);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* S'abonner aux changements d'une clé
|
|
78
|
+
* @param {string} key - Clé à observer (ou '*' pour toutes les clés)
|
|
79
|
+
* @param {Function} callback - Fonction appelée lors des changements
|
|
80
|
+
* @returns {Function} Fonction de désabonnement
|
|
81
|
+
*/
|
|
82
|
+
subscribe(key, callback) {
|
|
83
|
+
if (!this.listeners.has(key)) {
|
|
84
|
+
this.listeners.set(key, []);
|
|
85
|
+
}
|
|
86
|
+
this.listeners.get(key).push(callback);
|
|
87
|
+
|
|
88
|
+
// Retourner une fonction de désabonnement
|
|
89
|
+
return () => {
|
|
90
|
+
const callbacks = this.listeners.get(key);
|
|
91
|
+
const index = callbacks.indexOf(callback);
|
|
92
|
+
if (index > -1) {
|
|
93
|
+
callbacks.splice(index, 1);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Notifier les observateurs d'un changement
|
|
100
|
+
* @param {string} key - Clé qui a changé
|
|
101
|
+
* @param {*} newValue - Nouvelle valeur
|
|
102
|
+
* @param {*} oldValue - Ancienne valeur
|
|
103
|
+
* @private
|
|
104
|
+
*/
|
|
105
|
+
notify(key, newValue, oldValue) {
|
|
106
|
+
if (this.listeners.has(key)) {
|
|
107
|
+
this.listeners.get(key).forEach(callback => {
|
|
108
|
+
callback(newValue, oldValue);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Notifier les listeners globaux (*)
|
|
113
|
+
if (this.listeners.has('*')) {
|
|
114
|
+
this.listeners.get('*').forEach(callback => {
|
|
115
|
+
callback(key, newValue, oldValue);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Ajouter une modification à l'historique
|
|
122
|
+
* @param {string} key - Clé modifiée
|
|
123
|
+
* @param {*} oldValue - Ancienne valeur
|
|
124
|
+
* @param {*} newValue - Nouvelle valeur
|
|
125
|
+
* @private
|
|
126
|
+
*/
|
|
127
|
+
addToHistory(key, oldValue, newValue) {
|
|
128
|
+
// Supprimer l'historique après l'index actuel
|
|
129
|
+
this.history = this.history.slice(0, this.historyIndex + 1);
|
|
130
|
+
|
|
131
|
+
this.history.push({ key, oldValue, newValue, timestamp: Date.now() });
|
|
132
|
+
|
|
133
|
+
// Limiter la taille de l'historique
|
|
134
|
+
if (this.history.length > this.maxHistory) {
|
|
135
|
+
this.history.shift();
|
|
136
|
+
} else {
|
|
137
|
+
this.historyIndex++;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Annuler la dernière modification
|
|
143
|
+
* @returns {boolean} True si une opération a été annulée
|
|
144
|
+
*/
|
|
145
|
+
undo() {
|
|
146
|
+
if (this.historyIndex >= 0) {
|
|
147
|
+
const { key, oldValue } = this.history[this.historyIndex];
|
|
148
|
+
this.set(key, oldValue, false);
|
|
149
|
+
this.historyIndex--;
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Rétablir la dernière modification annulée
|
|
157
|
+
* @returns {boolean} True si une opération a été rétablie
|
|
158
|
+
*/
|
|
159
|
+
redo() {
|
|
160
|
+
if (this.historyIndex < this.history.length - 1) {
|
|
161
|
+
this.historyIndex++;
|
|
162
|
+
const { key, newValue } = this.history[this.historyIndex];
|
|
163
|
+
this.set(key, newValue, false);
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Réinitialiser complètement le state
|
|
171
|
+
*/
|
|
172
|
+
reset() {
|
|
173
|
+
const keys = Object.keys(this.state);
|
|
174
|
+
this.state = {};
|
|
175
|
+
keys.forEach(key => this.notify(key, undefined));
|
|
176
|
+
this.history = [];
|
|
177
|
+
this.historyIndex = -1;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Exporter le state en JSON
|
|
182
|
+
* @returns {string} Le state sérialisé en JSON
|
|
183
|
+
*/
|
|
184
|
+
export() {
|
|
185
|
+
return JSON.stringify(this.state);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Importer un state depuis du JSON
|
|
190
|
+
* @param {string} jsonState - State sérialisé en JSON
|
|
191
|
+
* @returns {boolean} True si l'import a réussi
|
|
192
|
+
*/
|
|
193
|
+
import(jsonState) {
|
|
194
|
+
try {
|
|
195
|
+
const newState = JSON.parse(jsonState);
|
|
196
|
+
Object.keys(newState).forEach(key => {
|
|
197
|
+
this.set(key, newState[key], false);
|
|
198
|
+
});
|
|
199
|
+
return true;
|
|
200
|
+
} catch (error) {
|
|
201
|
+
console.error('Error importing state:', error);
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export default StateManager;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client WebSocket avec events et JSON automatique
|
|
3
|
+
* @class
|
|
4
|
+
* @property {string} url - URL du serveur WebSocket
|
|
5
|
+
* @property {WebSocket|null} ws - Instance WebSocket
|
|
6
|
+
* @property {Object<string, Function[]>} listeners - Listeners par event ('open','message','close','error')
|
|
7
|
+
*/
|
|
8
|
+
class WebSocketClient {
|
|
9
|
+
/**
|
|
10
|
+
* Crée une instance de WebSocketClient
|
|
11
|
+
* @param {string} url - URL du serveur WS
|
|
12
|
+
*/
|
|
13
|
+
constructor(url) {
|
|
14
|
+
this.url = url;
|
|
15
|
+
this.ws = null;
|
|
16
|
+
this.listeners = { open: [], message: [], close: [], error: [] };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Connexion au serveur WebSocket
|
|
21
|
+
*/
|
|
22
|
+
connect() {
|
|
23
|
+
this.ws = new WebSocket(this.url);
|
|
24
|
+
|
|
25
|
+
this.ws.onopen = (e) => this.listeners.open.forEach(fn => fn(e));
|
|
26
|
+
this.ws.onmessage = (e) => {
|
|
27
|
+
try {
|
|
28
|
+
const data = JSON.parse(e.data);
|
|
29
|
+
this.listeners.message.forEach(fn => fn(data));
|
|
30
|
+
} catch {
|
|
31
|
+
this.listeners.message.forEach(fn => fn(e.data));
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
this.ws.onclose = (e) => this.listeners.close.forEach(fn => fn(e));
|
|
35
|
+
this.ws.onerror = (e) => this.listeners.error.forEach(fn => fn(e));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Envoie des données
|
|
40
|
+
* @param {any} data - Données JSON ou string
|
|
41
|
+
*/
|
|
42
|
+
send(data) {
|
|
43
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
44
|
+
throw new Error("WebSocket not connected");
|
|
45
|
+
}
|
|
46
|
+
this.ws.send(JSON.stringify(data));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Écoute un event
|
|
51
|
+
* @param {'open'|'message'|'close'|'error'} event - Nom de l'event
|
|
52
|
+
* @param {Function} callback - Callback
|
|
53
|
+
*/
|
|
54
|
+
on(event, callback) {
|
|
55
|
+
if (this.listeners[event]) this.listeners[event].push(callback);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Déconnecte le WebSocket
|
|
60
|
+
*/
|
|
61
|
+
disconnect() {
|
|
62
|
+
if (this.ws) this.ws.close();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export default WebSocketClient;
|