canvasframework 0.5.16 → 0.5.18
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/dist/canvasframework.js +2 -0
- package/dist/canvasframework.js.LICENSE.txt +1 -0
- package/package.json +18 -17
- package/components/Accordion.js +0 -265
- package/components/AndroidDatePickerDialog.js +0 -406
- package/components/AppBar.js +0 -398
- package/components/AudioPlayer.js +0 -611
- package/components/Avatar.js +0 -202
- package/components/Banner.js +0 -342
- package/components/BottomNavigationBar.js +0 -433
- package/components/BottomSheet.js +0 -234
- package/components/Button.js +0 -360
- package/components/Camera.js +0 -644
- package/components/Card.js +0 -193
- package/components/Chart.js +0 -700
- package/components/Checkbox.js +0 -166
- package/components/Chip.js +0 -212
- package/components/CircularProgress.js +0 -327
- package/components/ContextMenu.js +0 -116
- package/components/DatePicker.js +0 -298
- package/components/Dialog.js +0 -337
- package/components/Divider.js +0 -125
- package/components/Drawer.js +0 -276
- package/components/FAB.js +0 -270
- package/components/FileUpload.js +0 -315
- package/components/FloatedCamera.js +0 -644
- package/components/IOSDatePickerWheel.js +0 -430
- package/components/ImageCarousel.js +0 -219
- package/components/ImageComponent.js +0 -223
- package/components/Input.js +0 -831
- package/components/InputDatalist.js +0 -723
- package/components/InputTags.js +0 -624
- package/components/List.js +0 -95
- package/components/ListItem.js +0 -269
- package/components/Modal.js +0 -364
- package/components/MorphingFAB.js +0 -428
- package/components/MultiSelectDialog.js +0 -206
- package/components/NumberInput.js +0 -271
- package/components/PasswordInput.js +0 -462
- package/components/ProgressBar.js +0 -88
- package/components/QRCodeReader.js +0 -539
- package/components/RadioButton.js +0 -151
- package/components/SearchInput.js +0 -315
- package/components/SegmentedControl.js +0 -357
- package/components/Select.js +0 -199
- package/components/SelectDialog.js +0 -255
- package/components/Slider.js +0 -113
- package/components/SliverAppBar.js +0 -139
- package/components/Snackbar.js +0 -243
- package/components/SpeedDialFAB.js +0 -397
- package/components/Stepper.js +0 -281
- package/components/SwipeableListItem.js +0 -327
- package/components/Switch.js +0 -147
- package/components/Table.js +0 -492
- package/components/Tabs.js +0 -423
- package/components/Text.js +0 -141
- package/components/TextField.js +0 -151
- package/components/TimePicker.js +0 -934
- package/components/Toast.js +0 -236
- package/components/TreeView.js +0 -420
- package/components/Video.js +0 -397
- package/components/View.js +0 -140
- package/components/VirtualList.js +0 -120
- package/core/CanvasFramework.js +0 -3034
- package/core/Component.js +0 -243
- package/core/ThemeManager.js +0 -358
- package/core/UIBuilder.js +0 -267
- package/core/WebGLCanvasAdapter.js +0 -782
- package/features/Column.js +0 -43
- package/features/Grid.js +0 -47
- package/features/LayoutComponent.js +0 -43
- package/features/OpenStreetMap.js +0 -310
- package/features/Positioned.js +0 -33
- package/features/PullToRefresh.js +0 -328
- package/features/Row.js +0 -40
- package/features/SignaturePad.js +0 -257
- package/features/Skeleton.js +0 -193
- package/features/Stack.js +0 -21
- package/index.js +0 -119
- package/manager/AccessibilityManager.js +0 -107
- package/manager/ErrorHandler.js +0 -59
- package/manager/FeatureFlags.js +0 -60
- package/manager/MemoryManager.js +0 -107
- package/manager/PerformanceMonitor.js +0 -84
- package/manager/SecurityManager.js +0 -54
- package/utils/AnimationEngine.js +0 -734
- package/utils/CryptoManager.js +0 -303
- package/utils/DataStore.js +0 -403
- package/utils/DevTools.js +0 -1618
- package/utils/DevToolsConsole.js +0 -201
- package/utils/EventBus.js +0 -407
- package/utils/FetchClient.js +0 -74
- package/utils/FirebaseAuth.js +0 -653
- package/utils/FirebaseCore.js +0 -246
- package/utils/FirebaseFirestore.js +0 -581
- package/utils/FirebaseFunctions.js +0 -97
- package/utils/FirebaseRealtimeDB.js +0 -498
- package/utils/FirebaseStorage.js +0 -612
- package/utils/FormValidator.js +0 -355
- package/utils/GeoLocationService.js +0 -62
- package/utils/I18n.js +0 -207
- package/utils/IndexedDBManager.js +0 -273
- package/utils/InspectionOverlay.js +0 -308
- package/utils/NotificationManager.js +0 -60
- package/utils/OfflineSyncManager.js +0 -342
- package/utils/PayPalPayment.js +0 -678
- package/utils/QueryBuilder.js +0 -478
- package/utils/SafeArea.js +0 -64
- package/utils/SecureStorage.js +0 -289
- package/utils/StateManager.js +0 -207
- package/utils/StripePayment.js +0 -552
- package/utils/WebSocketClient.js +0 -66
|
@@ -1,428 +0,0 @@
|
|
|
1
|
-
import FAB from './FAB.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Morphing FAB - FAB qui se transforme en barre d'actions
|
|
5
|
-
* @class
|
|
6
|
-
* @extends FAB
|
|
7
|
-
* @property {boolean} isMorphed - État transformé
|
|
8
|
-
* @property {Array} actions - Actions disponibles dans la barre
|
|
9
|
-
* @property {number} morphProgress - Progression de l'animation (0-1)
|
|
10
|
-
* @property {number} targetWidth - Largeur cible en mode morphé
|
|
11
|
-
*/
|
|
12
|
-
class MorphingFAB extends FAB {
|
|
13
|
-
/**
|
|
14
|
-
* Crée une instance de MorphingFAB
|
|
15
|
-
* @param {CanvasFramework} framework - Framework parent
|
|
16
|
-
* @param {Object} [options={}] - Options de configuration
|
|
17
|
-
* @param {Array} [options.actions=[]] - Actions de la barre
|
|
18
|
-
* @param {string} [options.morphType='toolbar'] - Type: 'toolbar', 'searchbar'
|
|
19
|
-
* @example
|
|
20
|
-
* // Toolbar
|
|
21
|
-
* actions: [
|
|
22
|
-
* { icon: '🏠', label: 'Home', action: () => {...} },
|
|
23
|
-
* { icon: '⭐', label: 'Favorites', action: () => {...} },
|
|
24
|
-
* { icon: '⚙', label: 'Settings', action: () => {...} }
|
|
25
|
-
* ]
|
|
26
|
-
*
|
|
27
|
-
* // Searchbar
|
|
28
|
-
* morphType: 'searchbar'
|
|
29
|
-
*/
|
|
30
|
-
constructor(framework, options = {}) {
|
|
31
|
-
super(framework, {
|
|
32
|
-
...options,
|
|
33
|
-
icon: options.icon || '+'
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
this.actions = options.actions || [];
|
|
37
|
-
this.morphType = options.morphType || 'toolbar';
|
|
38
|
-
this.isMorphed = false;
|
|
39
|
-
this.morphProgress = 0;
|
|
40
|
-
|
|
41
|
-
// Dimensions
|
|
42
|
-
this.originalWidth = this.width;
|
|
43
|
-
this.originalHeight = this.height;
|
|
44
|
-
this.originalX = this.x;
|
|
45
|
-
this.originalY = this.y;
|
|
46
|
-
|
|
47
|
-
// Calculer la largeur cible selon le type
|
|
48
|
-
if (this.morphType === 'searchbar') {
|
|
49
|
-
this.targetWidth = Math.min(framework.width - 32, 400);
|
|
50
|
-
this.targetHeight = 56;
|
|
51
|
-
this.targetX = (framework.width - this.targetWidth) / 2;
|
|
52
|
-
this.targetY = 16;
|
|
53
|
-
} else {
|
|
54
|
-
// toolbar
|
|
55
|
-
this.targetWidth = Math.min(framework.width - 32, this.actions.length * 80 + 32);
|
|
56
|
-
this.targetHeight = 56;
|
|
57
|
-
this.targetX = (framework.width - this.targetWidth) / 2;
|
|
58
|
-
this.targetY = framework.height - 80;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// État des boutons d'actions
|
|
62
|
-
this.actionButtons = this.actions.map((action, index) => ({
|
|
63
|
-
...action,
|
|
64
|
-
x: 0,
|
|
65
|
-
y: 0,
|
|
66
|
-
width: 60,
|
|
67
|
-
height: 48,
|
|
68
|
-
alpha: 0,
|
|
69
|
-
pressed: false
|
|
70
|
-
}));
|
|
71
|
-
|
|
72
|
-
// État de la searchbar
|
|
73
|
-
this.searchText = '';
|
|
74
|
-
this.searchFocused = false;
|
|
75
|
-
|
|
76
|
-
// Input caché pour la searchbar
|
|
77
|
-
if (this.morphType === 'searchbar') {
|
|
78
|
-
this.setupHiddenInput();
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Bind
|
|
82
|
-
this.onPress = this.handlePress.bind(this);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Configure l'input HTML caché pour la searchbar
|
|
87
|
-
* @private
|
|
88
|
-
*/
|
|
89
|
-
setupHiddenInput() {
|
|
90
|
-
this.hiddenInput = document.createElement('input');
|
|
91
|
-
this.hiddenInput.style.position = 'fixed';
|
|
92
|
-
this.hiddenInput.style.opacity = '0';
|
|
93
|
-
this.hiddenInput.style.pointerEvents = 'none';
|
|
94
|
-
this.hiddenInput.style.top = '-100px';
|
|
95
|
-
document.body.appendChild(this.hiddenInput);
|
|
96
|
-
|
|
97
|
-
this.hiddenInput.addEventListener('input', (e) => {
|
|
98
|
-
if (this.searchFocused) {
|
|
99
|
-
this.searchText = e.target.value;
|
|
100
|
-
}
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
this.hiddenInput.addEventListener('blur', () => {
|
|
104
|
-
this.searchFocused = false;
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Gère la pression
|
|
110
|
-
*/
|
|
111
|
-
handlePress(x, y) {
|
|
112
|
-
const adjustedY = y - this.framework.scrollOffset;
|
|
113
|
-
|
|
114
|
-
if (this.isMorphed) {
|
|
115
|
-
// Mode morphé
|
|
116
|
-
if (this.morphType === 'searchbar') {
|
|
117
|
-
// Zone de l'input searchbar
|
|
118
|
-
const searchInputX = this.x + 48;
|
|
119
|
-
const searchInputWidth = this.width - 96;
|
|
120
|
-
|
|
121
|
-
if (x >= searchInputX && x <= searchInputX + searchInputWidth &&
|
|
122
|
-
adjustedY >= this.y && adjustedY <= this.y + this.height) {
|
|
123
|
-
// Clic sur l'input - activer le focus
|
|
124
|
-
this.searchFocused = true;
|
|
125
|
-
if (this.hiddenInput) {
|
|
126
|
-
this.hiddenInput.value = this.searchText;
|
|
127
|
-
this.hiddenInput.style.top = `${this.y}px`;
|
|
128
|
-
this.hiddenInput.focus();
|
|
129
|
-
}
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Bouton fermer (X)
|
|
134
|
-
const closeX = this.x + this.width - 40;
|
|
135
|
-
if (x >= closeX && x <= closeX + 40 &&
|
|
136
|
-
adjustedY >= this.y && adjustedY <= this.y + this.height) {
|
|
137
|
-
this.searchText = '';
|
|
138
|
-
this.toggle();
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
} else {
|
|
142
|
-
// Toolbar - vérifier les actions
|
|
143
|
-
for (let btn of this.actionButtons) {
|
|
144
|
-
if (x >= btn.x && x <= btn.x + btn.width &&
|
|
145
|
-
adjustedY >= btn.y && adjustedY <= btn.y + btn.height) {
|
|
146
|
-
btn.pressed = true;
|
|
147
|
-
setTimeout(() => {
|
|
148
|
-
btn.pressed = false;
|
|
149
|
-
if (btn.action) btn.action();
|
|
150
|
-
}, 150);
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Clic en dehors -> fermer
|
|
157
|
-
if (!this.isPointInside(x, y)) {
|
|
158
|
-
this.toggle();
|
|
159
|
-
}
|
|
160
|
-
} else {
|
|
161
|
-
// Mode normal - ouvrir
|
|
162
|
-
this.toggle();
|
|
163
|
-
super.handlePress(x, y);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Toggle entre FAB et barre
|
|
169
|
-
*/
|
|
170
|
-
toggle() {
|
|
171
|
-
this.isMorphed = !this.isMorphed;
|
|
172
|
-
|
|
173
|
-
// Si on ferme et que c'est une searchbar, nettoyer l'input
|
|
174
|
-
if (!this.isMorphed && this.morphType === 'searchbar') {
|
|
175
|
-
this.searchText = '';
|
|
176
|
-
this.searchFocused = false;
|
|
177
|
-
if (this.hiddenInput) {
|
|
178
|
-
this.hiddenInput.blur();
|
|
179
|
-
this.hiddenInput.remove();
|
|
180
|
-
this.hiddenInput = null;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Si on ouvre une searchbar, recréer l'input
|
|
185
|
-
if (this.isMorphed && this.morphType === 'searchbar' && !this.hiddenInput) {
|
|
186
|
-
this.setupHiddenInput();
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
this.animate();
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Anime la transformation
|
|
194
|
-
* @private
|
|
195
|
-
*/
|
|
196
|
-
animate() {
|
|
197
|
-
const startTime = Date.now();
|
|
198
|
-
const duration = 400;
|
|
199
|
-
|
|
200
|
-
const step = () => {
|
|
201
|
-
const elapsed = Date.now() - startTime;
|
|
202
|
-
const progress = Math.min(elapsed / duration, 1);
|
|
203
|
-
|
|
204
|
-
// Easing out cubic
|
|
205
|
-
const eased = 1 - Math.pow(1 - progress, 3);
|
|
206
|
-
|
|
207
|
-
this.morphProgress = this.isMorphed ? eased : 1 - eased;
|
|
208
|
-
|
|
209
|
-
// Interpoler les dimensions
|
|
210
|
-
this.width = this.lerp(this.originalWidth, this.targetWidth, this.morphProgress);
|
|
211
|
-
this.height = this.lerp(this.originalHeight, this.targetHeight, this.morphProgress);
|
|
212
|
-
this.x = this.lerp(this.originalX, this.targetX, this.morphProgress);
|
|
213
|
-
this.y = this.lerp(this.originalY, this.targetY, this.morphProgress);
|
|
214
|
-
|
|
215
|
-
// Mettre à jour les positions des actions
|
|
216
|
-
if (this.morphType === 'toolbar') {
|
|
217
|
-
this.updateActionPositions();
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
if (progress < 1) {
|
|
221
|
-
requestAnimationFrame(step);
|
|
222
|
-
}
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
step();
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Met à jour les positions des boutons d'action
|
|
230
|
-
* @private
|
|
231
|
-
*/
|
|
232
|
-
updateActionPositions() {
|
|
233
|
-
const spacing = this.targetWidth / (this.actionButtons.length + 1);
|
|
234
|
-
|
|
235
|
-
this.actionButtons.forEach((btn, index) => {
|
|
236
|
-
btn.x = this.x + spacing * (index + 1) - btn.width / 2;
|
|
237
|
-
btn.y = this.y + (this.height - btn.height) / 2;
|
|
238
|
-
btn.alpha = this.morphProgress;
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Interpolation linéaire
|
|
244
|
-
* @private
|
|
245
|
-
*/
|
|
246
|
-
lerp(start, end, t) {
|
|
247
|
-
return start + (end - start) * t;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Dessine le Morphing FAB
|
|
252
|
-
*/
|
|
253
|
-
draw(ctx) {
|
|
254
|
-
ctx.save();
|
|
255
|
-
|
|
256
|
-
if (this.isMorphed || this.morphProgress > 0) {
|
|
257
|
-
this.drawMorphed(ctx);
|
|
258
|
-
} else {
|
|
259
|
-
super.draw(ctx);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
ctx.restore();
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Dessine l'état morphé
|
|
267
|
-
* @private
|
|
268
|
-
*/
|
|
269
|
-
drawMorphed(ctx) {
|
|
270
|
-
// Ombre
|
|
271
|
-
ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
|
|
272
|
-
ctx.shadowBlur = 12;
|
|
273
|
-
ctx.shadowOffsetY = 4;
|
|
274
|
-
|
|
275
|
-
// Background de la barre
|
|
276
|
-
ctx.fillStyle = this.bgColor;
|
|
277
|
-
ctx.beginPath();
|
|
278
|
-
const radius = this.lerp(this.borderRadius, 28, this.morphProgress);
|
|
279
|
-
this.roundRect(ctx, this.x, this.y, this.width, this.height, radius);
|
|
280
|
-
ctx.fill();
|
|
281
|
-
|
|
282
|
-
ctx.shadowColor = 'transparent';
|
|
283
|
-
ctx.shadowBlur = 0;
|
|
284
|
-
ctx.shadowOffsetY = 0;
|
|
285
|
-
|
|
286
|
-
if (this.morphType === 'searchbar') {
|
|
287
|
-
this.drawSearchBar(ctx);
|
|
288
|
-
} else {
|
|
289
|
-
this.drawToolbar(ctx);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* Dessine la toolbar
|
|
295
|
-
* @private
|
|
296
|
-
*/
|
|
297
|
-
drawToolbar(ctx) {
|
|
298
|
-
// Dessiner les actions
|
|
299
|
-
for (let btn of this.actionButtons) {
|
|
300
|
-
if (btn.alpha > 0.01) {
|
|
301
|
-
ctx.save();
|
|
302
|
-
ctx.globalAlpha = btn.alpha;
|
|
303
|
-
|
|
304
|
-
// Highlight si pressed
|
|
305
|
-
if (btn.pressed) {
|
|
306
|
-
ctx.fillStyle = 'rgba(255, 255, 255, 0.2)';
|
|
307
|
-
ctx.beginPath();
|
|
308
|
-
ctx.arc(btn.x + btn.width / 2, btn.y + btn.height / 2, 24, 0, Math.PI * 2);
|
|
309
|
-
ctx.fill();
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Icône
|
|
313
|
-
ctx.fillStyle = this.iconColor;
|
|
314
|
-
ctx.font = 'bold 20px sans-serif';
|
|
315
|
-
ctx.textAlign = 'center';
|
|
316
|
-
ctx.textBaseline = 'middle';
|
|
317
|
-
ctx.fillText(btn.icon, btn.x + btn.width / 2, btn.y + btn.height / 2 - 8);
|
|
318
|
-
|
|
319
|
-
// Label
|
|
320
|
-
if (btn.label) {
|
|
321
|
-
ctx.font = '11px -apple-system, sans-serif';
|
|
322
|
-
ctx.fillText(btn.label, btn.x + btn.width / 2, btn.y + btn.height / 2 + 12);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
ctx.restore();
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* Dessine la search bar
|
|
332
|
-
* @private
|
|
333
|
-
*/
|
|
334
|
-
drawSearchBar(ctx) {
|
|
335
|
-
ctx.save();
|
|
336
|
-
ctx.globalAlpha = this.morphProgress;
|
|
337
|
-
|
|
338
|
-
// Icône de recherche
|
|
339
|
-
ctx.fillStyle = this.iconColor;
|
|
340
|
-
ctx.font = 'bold 20px sans-serif';
|
|
341
|
-
ctx.textAlign = 'left';
|
|
342
|
-
ctx.textBaseline = 'middle';
|
|
343
|
-
ctx.fillText('🔍', this.x + 16, this.y + this.height / 2);
|
|
344
|
-
|
|
345
|
-
// Curseur clignotant si focus
|
|
346
|
-
let showCursor = false;
|
|
347
|
-
if (this.searchFocused) {
|
|
348
|
-
showCursor = Math.floor(Date.now() / 500) % 2 === 0;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
// Placeholder ou texte
|
|
352
|
-
ctx.font = '16px -apple-system, sans-serif';
|
|
353
|
-
ctx.fillStyle = this.searchText ? this.iconColor : 'rgba(255, 255, 255, 0.6)';
|
|
354
|
-
const displayText = this.searchText || 'Search...';
|
|
355
|
-
ctx.fillText(displayText, this.x + 48, this.y + this.height / 2);
|
|
356
|
-
|
|
357
|
-
// Curseur
|
|
358
|
-
if (showCursor && this.searchText) {
|
|
359
|
-
const textWidth = ctx.measureText(this.searchText).width;
|
|
360
|
-
ctx.fillStyle = this.iconColor;
|
|
361
|
-
ctx.fillRect(this.x + 48 + textWidth + 2, this.y + this.height / 2 - 10, 2, 20);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// Bouton fermer
|
|
365
|
-
if (this.searchText || this.isMorphed) {
|
|
366
|
-
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
|
|
367
|
-
ctx.font = 'bold 18px sans-serif';
|
|
368
|
-
ctx.textAlign = 'right';
|
|
369
|
-
ctx.fillText('✕', this.x + this.width - 16, this.y + this.height / 2);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
ctx.restore();
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* Dessine un rectangle arrondi
|
|
377
|
-
* @private
|
|
378
|
-
*/
|
|
379
|
-
roundRect(ctx, x, y, width, height, radius) {
|
|
380
|
-
ctx.beginPath();
|
|
381
|
-
ctx.moveTo(x + radius, y);
|
|
382
|
-
ctx.lineTo(x + width - radius, y);
|
|
383
|
-
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
384
|
-
ctx.lineTo(x + width, y + height - radius);
|
|
385
|
-
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
386
|
-
ctx.lineTo(x + radius, y + height);
|
|
387
|
-
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
388
|
-
ctx.lineTo(x, y + radius);
|
|
389
|
-
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/**
|
|
393
|
-
* Vérifie si un point est dans le composant
|
|
394
|
-
*/
|
|
395
|
-
isPointInside(x, y) {
|
|
396
|
-
const adjustedY = y - this.framework.scrollOffset;
|
|
397
|
-
return x >= this.x && x <= this.x + this.width &&
|
|
398
|
-
adjustedY >= this.y && adjustedY <= this.y + this.height;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
/**
|
|
402
|
-
* Gère l'input texte (pour searchbar)
|
|
403
|
-
* @param {string} char - Caractère à ajouter
|
|
404
|
-
*/
|
|
405
|
-
addChar(char) {
|
|
406
|
-
if (this.morphType === 'searchbar' && this.isMorphed) {
|
|
407
|
-
this.searchText += char;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
/**
|
|
412
|
-
* Efface un caractère
|
|
413
|
-
*/
|
|
414
|
-
backspace() {
|
|
415
|
-
if (this.morphType === 'searchbar' && this.isMorphed) {
|
|
416
|
-
this.searchText = this.searchText.slice(0, -1);
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
/**
|
|
421
|
-
* Réinitialise la recherche
|
|
422
|
-
*/
|
|
423
|
-
clearSearch() {
|
|
424
|
-
this.searchText = '';
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
export default MorphingFAB;
|
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
import Modal from '../components/Modal.js';
|
|
2
|
-
/**
|
|
3
|
-
* Modal pour la sélection multiple d'options avec checkboxes
|
|
4
|
-
* @class
|
|
5
|
-
* @extends Modal
|
|
6
|
-
* @param {Framework} framework - Instance du framework
|
|
7
|
-
* @param {Object} [options={}] - Options de configuration
|
|
8
|
-
* @param {string} [options.title='Sélectionner'] - Titre du modal
|
|
9
|
-
* @param {string[]} [options.options=[]] - Liste des options
|
|
10
|
-
* @param {number[]} [options.selectedIndices=[]] - Indices des options sélectionnées
|
|
11
|
-
* @param {Function} [options.onSelect] - Callback lors de la validation
|
|
12
|
-
* @example
|
|
13
|
-
* const multiSelect = new MultiSelectDialog(framework, {
|
|
14
|
-
* title: 'Choisir vos intérêts',
|
|
15
|
-
* options: ['Sport', 'Musique', 'Lecture', 'Voyage'],
|
|
16
|
-
* selectedIndices: [0, 2],
|
|
17
|
-
* onSelect: (indices, values) => console.log('Selected:', values)
|
|
18
|
-
* });
|
|
19
|
-
*/
|
|
20
|
-
class MultiSelectDialog extends Modal {
|
|
21
|
-
/**
|
|
22
|
-
* @constructs MultiSelectDialog
|
|
23
|
-
*/
|
|
24
|
-
constructor(framework, options = {}) {
|
|
25
|
-
const optionsCount = options.options?.length || 0;
|
|
26
|
-
const itemHeight = 50;
|
|
27
|
-
const dialogHeight = Math.min(400, Math.max(200, optionsCount * itemHeight + 150));
|
|
28
|
-
|
|
29
|
-
super(framework, {
|
|
30
|
-
title: options.title || 'Sélectionner',
|
|
31
|
-
width: Math.min(350, framework.width - 40),
|
|
32
|
-
height: dialogHeight,
|
|
33
|
-
showCloseButton: true,
|
|
34
|
-
closeOnOverlayClick: true,
|
|
35
|
-
padding: 0,
|
|
36
|
-
...options
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
/** @type {string[]} */
|
|
40
|
-
this.options = options.options || [];
|
|
41
|
-
/** @type {number[]} */
|
|
42
|
-
this.selectedIndices = options.selectedIndices || [];
|
|
43
|
-
/** @type {Function|undefined} */
|
|
44
|
-
this.onSelect = options.onSelect;
|
|
45
|
-
/** @type {number} */
|
|
46
|
-
this.itemHeight = itemHeight;
|
|
47
|
-
|
|
48
|
-
/** @type {string[]} */
|
|
49
|
-
this.buttons = ['Annuler', 'Valider'];
|
|
50
|
-
// AJOUTER: Définir onPress pour que le framework l'appelle
|
|
51
|
-
this.onPress = this.handlePress.bind(this);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Dessine le modal de sélection multiple
|
|
56
|
-
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
57
|
-
*/
|
|
58
|
-
draw(ctx) {
|
|
59
|
-
if (!this.isVisible) return;
|
|
60
|
-
|
|
61
|
-
ctx.save();
|
|
62
|
-
ctx.globalAlpha = this.opacity;
|
|
63
|
-
|
|
64
|
-
// Overlay
|
|
65
|
-
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
|
|
66
|
-
ctx.fillRect(0, 0, this.framework.width, this.framework.height);
|
|
67
|
-
|
|
68
|
-
const modalX = (this.framework.width - this.modalWidth) / 2;
|
|
69
|
-
const modalY = (this.framework.height - this.modalHeight) / 2;
|
|
70
|
-
|
|
71
|
-
// Fond du modal
|
|
72
|
-
ctx.fillStyle = '#FFFFFF';
|
|
73
|
-
ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
|
|
74
|
-
ctx.shadowBlur = 20;
|
|
75
|
-
ctx.shadowOffsetY = 10;
|
|
76
|
-
|
|
77
|
-
ctx.beginPath();
|
|
78
|
-
this.roundRect(ctx, modalX, modalY, this.modalWidth, this.modalHeight, 12);
|
|
79
|
-
ctx.fill();
|
|
80
|
-
|
|
81
|
-
ctx.shadowColor = 'transparent';
|
|
82
|
-
|
|
83
|
-
// Titre
|
|
84
|
-
if (this.title) {
|
|
85
|
-
ctx.fillStyle = '#000000';
|
|
86
|
-
ctx.font = 'bold 18px -apple-system, sans-serif';
|
|
87
|
-
ctx.textAlign = 'center';
|
|
88
|
-
ctx.textBaseline = 'middle';
|
|
89
|
-
ctx.fillText(this.title, modalX + this.modalWidth / 2, modalY + 30);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Options avec checkboxes
|
|
93
|
-
const contentX = modalX + 20;
|
|
94
|
-
const contentY = modalY + 60;
|
|
95
|
-
|
|
96
|
-
for (let i = 0; i < this.options.length; i++) {
|
|
97
|
-
const optionY = contentY + i * this.itemHeight;
|
|
98
|
-
const isSelected = this.selectedIndices.includes(i);
|
|
99
|
-
|
|
100
|
-
// Checkbox
|
|
101
|
-
ctx.strokeStyle = isSelected ? '#6200EE' : '#666666';
|
|
102
|
-
ctx.lineWidth = 2;
|
|
103
|
-
ctx.strokeRect(contentX, optionY + 15, 20, 20);
|
|
104
|
-
|
|
105
|
-
if (isSelected) {
|
|
106
|
-
ctx.fillStyle = '#6200EE';
|
|
107
|
-
ctx.fillRect(contentX + 4, optionY + 19, 12, 12);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Texte
|
|
111
|
-
ctx.fillStyle = '#000000';
|
|
112
|
-
ctx.font = '16px -apple-system, sans-serif';
|
|
113
|
-
ctx.textAlign = 'left';
|
|
114
|
-
ctx.textBaseline = 'middle';
|
|
115
|
-
ctx.fillText(this.options[i], contentX + 35, optionY + 25);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Boutons
|
|
119
|
-
const buttonY = modalY + this.modalHeight - 60;
|
|
120
|
-
const buttonWidth = (this.modalWidth - 60) / 2;
|
|
121
|
-
|
|
122
|
-
// Bouton Annuler
|
|
123
|
-
ctx.fillStyle = '#666666';
|
|
124
|
-
ctx.font = 'bold 16px -apple-system, sans-serif';
|
|
125
|
-
ctx.textAlign = 'center';
|
|
126
|
-
ctx.textBaseline = 'middle';
|
|
127
|
-
ctx.fillText('Annuler', modalX + buttonWidth / 2 + 10, buttonY);
|
|
128
|
-
|
|
129
|
-
// Bouton Valider
|
|
130
|
-
ctx.fillStyle = '#007AFF';
|
|
131
|
-
ctx.fillText('Valider', modalX + this.modalWidth - buttonWidth / 2 - 10, buttonY);
|
|
132
|
-
|
|
133
|
-
ctx.restore();
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Gère le clic dans le modal de sélection multiple
|
|
138
|
-
* @param {number} x - Position X du clic
|
|
139
|
-
* @param {number} y - Position Y du clic
|
|
140
|
-
*/
|
|
141
|
-
handlePress(x, y) {
|
|
142
|
-
// Coordonnées brutes pour les modals fixes
|
|
143
|
-
const modalX = (this.framework.width - this.modalWidth) / 2;
|
|
144
|
-
const modalY = (this.framework.height - this.modalHeight) / 2;
|
|
145
|
-
const contentX = modalX + 20;
|
|
146
|
-
const contentY = modalY + 60;
|
|
147
|
-
const buttonY = modalY + this.modalHeight - 60;
|
|
148
|
-
const buttonWidth = (this.modalWidth - 60) / 2;
|
|
149
|
-
|
|
150
|
-
// Vérifier les clics sur les options (coordonnées brutes)
|
|
151
|
-
for (let i = 0; i < this.options.length; i++) {
|
|
152
|
-
const optionY = contentY + i * this.itemHeight;
|
|
153
|
-
|
|
154
|
-
if (y >= optionY && y <= optionY + this.itemHeight &&
|
|
155
|
-
x >= contentX && x <= modalX + this.modalWidth - 20) {
|
|
156
|
-
|
|
157
|
-
const index = this.selectedIndices.indexOf(i);
|
|
158
|
-
if (index > -1) {
|
|
159
|
-
this.selectedIndices.splice(index, 1);
|
|
160
|
-
} else {
|
|
161
|
-
this.selectedIndices.push(i);
|
|
162
|
-
}
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Bouton Annuler (coordonnées brutes)
|
|
168
|
-
if (y >= buttonY - 20 && y <= buttonY + 20 &&
|
|
169
|
-
x >= modalX + 10 && x <= modalX + buttonWidth + 10) {
|
|
170
|
-
this.hide();
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Bouton Valider (coordonnées brutes)
|
|
175
|
-
if (y >= buttonY - 20 && y <= buttonY + 20 &&
|
|
176
|
-
x >= modalX + this.modalWidth - buttonWidth - 10 && x <= modalX + this.modalWidth - 10) {
|
|
177
|
-
if (this.onSelect) {
|
|
178
|
-
this.onSelect(this.selectedIndices, this.selectedIndices.map(i => this.options[i]));
|
|
179
|
-
}
|
|
180
|
-
this.hide();
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Overlay ou bouton de fermeture
|
|
185
|
-
super.handlePress(x, y);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Vérifie si un point est à l'intérieur du modal
|
|
190
|
-
* @param {number} x - Position X
|
|
191
|
-
* @param {number} y - Position Y
|
|
192
|
-
* @returns {boolean} True si le point est à l'intérieur du modal
|
|
193
|
-
*/
|
|
194
|
-
isPointInside(x, y) {
|
|
195
|
-
const modalX = (this.framework.width - this.modalWidth) / 2;
|
|
196
|
-
const modalY = (this.framework.height - this.modalHeight) / 2;
|
|
197
|
-
|
|
198
|
-
// Les modals sont des composants fixes, donc pas d'ajustement de scroll
|
|
199
|
-
return x >= modalX &&
|
|
200
|
-
x <= modalX + this.modalWidth &&
|
|
201
|
-
y >= modalY &&
|
|
202
|
-
y <= modalY + this.modalHeight;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export default MultiSelectDialog;
|