canvasframework 0.5.18 → 0.5.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/components/Accordion.js +265 -0
- package/components/AndroidDatePickerDialog.js +406 -0
- package/components/AppBar.js +398 -0
- package/components/AudioPlayer.js +611 -0
- package/components/Avatar.js +202 -0
- package/components/Banner.js +342 -0
- package/components/BottomNavigationBar.js +433 -0
- package/components/BottomSheet.js +234 -0
- package/components/Button.js +358 -0
- package/components/Camera.js +644 -0
- package/components/Card.js +193 -0
- package/components/Chart.js +700 -0
- package/components/Checkbox.js +166 -0
- package/components/Chip.js +212 -0
- package/components/CircularProgress.js +327 -0
- package/components/ContextMenu.js +116 -0
- package/components/DatePicker.js +298 -0
- package/components/Dialog.js +337 -0
- package/components/Divider.js +125 -0
- package/components/Drawer.js +276 -0
- package/components/FAB.js +270 -0
- package/components/FileUpload.js +315 -0
- package/components/FloatedCamera.js +644 -0
- package/components/IOSDatePickerWheel.js +430 -0
- package/components/ImageCarousel.js +219 -0
- package/components/ImageComponent.js +223 -0
- package/components/Input.js +831 -0
- package/components/InputDatalist.js +723 -0
- package/components/InputTags.js +624 -0
- package/components/List.js +95 -0
- package/components/ListItem.js +269 -0
- package/components/Modal.js +364 -0
- package/components/MorphingFAB.js +428 -0
- package/components/MultiSelectDialog.js +206 -0
- package/components/NumberInput.js +271 -0
- package/components/PasswordInput.js +462 -0
- package/components/ProgressBar.js +88 -0
- package/components/QRCodeReader.js +539 -0
- package/components/RadioButton.js +151 -0
- package/components/SearchInput.js +315 -0
- package/components/SegmentedControl.js +357 -0
- package/components/Select.js +199 -0
- package/components/SelectDialog.js +255 -0
- package/components/Slider.js +113 -0
- package/components/SliverAppBar.js +139 -0
- package/components/Snackbar.js +243 -0
- package/components/SpeedDialFAB.js +397 -0
- package/components/Stepper.js +281 -0
- package/components/SwipeableListItem.js +327 -0
- package/components/Switch.js +147 -0
- package/components/Table.js +492 -0
- package/components/Tabs.js +423 -0
- package/components/Text.js +141 -0
- package/components/TextField.js +151 -0
- package/components/TimePicker.js +934 -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 +3045 -0
- package/core/Component.js +243 -0
- package/core/ThemeManager.js +358 -0
- package/core/UIBuilder.js +267 -0
- package/core/WebGLCanvasAdapter.js +782 -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 +193 -0
- package/features/Stack.js +21 -0
- package/index.js +119 -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 +22 -16
- package/utils/AnimationEngine.js +734 -0
- package/utils/CryptoManager.js +303 -0
- package/utils/DataStore.js +403 -0
- package/utils/DevTools.js +1618 -0
- package/utils/DevToolsConsole.js +201 -0
- package/utils/EventBus.js +407 -0
- package/utils/FetchClient.js +74 -0
- package/utils/FirebaseAuth.js +653 -0
- package/utils/FirebaseCore.js +246 -0
- package/utils/FirebaseFirestore.js +581 -0
- package/utils/FirebaseFunctions.js +97 -0
- package/utils/FirebaseRealtimeDB.js +498 -0
- package/utils/FirebaseStorage.js +612 -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/InspectionOverlay.js +308 -0
- package/utils/NotificationManager.js +60 -0
- package/utils/OfflineSyncManager.js +342 -0
- package/utils/PayPalPayment.js +678 -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/StripePayment.js +552 -0
- package/utils/WebSocketClient.js +66 -0
- package/dist/canvasframework.js +0 -2
- package/dist/canvasframework.js.LICENSE.txt +0 -1
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import Component from '../core/Component.js';
|
|
2
|
+
/**
|
|
3
|
+
* Élément de liste
|
|
4
|
+
* @class
|
|
5
|
+
* @extends Component
|
|
6
|
+
* @property {string} title - Titre
|
|
7
|
+
* @property {string} subtitle - Sous-titre
|
|
8
|
+
* @property {string|null} leftIcon - Icône gauche
|
|
9
|
+
* @property {string|null} leftImage - Image gauche (URL)
|
|
10
|
+
* @property {string|null} rightIcon - Icône droite
|
|
11
|
+
* @property {string|null} rightText - Texte droite
|
|
12
|
+
* @property {boolean} divider - Afficher un diviseur
|
|
13
|
+
* @property {string} platform - Plateforme
|
|
14
|
+
* @property {string} bgColor - Couleur de fond
|
|
15
|
+
* @property {Array} ripples - Effets ripple (Material)
|
|
16
|
+
*/
|
|
17
|
+
class ListItem extends Component {
|
|
18
|
+
/**
|
|
19
|
+
* Crée une instance de ListItem
|
|
20
|
+
* @param {CanvasFramework} framework - Framework parent
|
|
21
|
+
* @param {Object} [options={}] - Options de configuration
|
|
22
|
+
* @param {string} [options.title=''] - Titre
|
|
23
|
+
* @param {string} [options.subtitle=''] - Sous-titre
|
|
24
|
+
* @param {string} [options.leftIcon] - Icône gauche
|
|
25
|
+
* @param {string} [options.leftImage] - URL image gauche
|
|
26
|
+
* @param {string} [options.rightIcon] - Icône droite
|
|
27
|
+
* @param {string} [options.rightText] - Texte droite
|
|
28
|
+
* @param {boolean} [options.divider=true] - Diviseur
|
|
29
|
+
* @param {string} [options.bgColor='#FFFFFF'] - Couleur de fond
|
|
30
|
+
* @param {number} [options.height] - Hauteur (auto selon contenu)
|
|
31
|
+
*/
|
|
32
|
+
constructor(framework, options = {}) {
|
|
33
|
+
super(framework, options);
|
|
34
|
+
this.title = options.title || '';
|
|
35
|
+
this.subtitle = options.subtitle || '';
|
|
36
|
+
this.leftIcon = options.leftIcon || null;
|
|
37
|
+
this.leftImage = options.leftImage || null; // URL d'image
|
|
38
|
+
this.rightIcon = options.rightIcon || null;
|
|
39
|
+
this.rightText = options.rightText || '';
|
|
40
|
+
this.divider = options.divider !== false;
|
|
41
|
+
this.platform = framework.platform;
|
|
42
|
+
this.height = options.height || (this.subtitle ? 72 : 56);
|
|
43
|
+
this.width = options.width || framework.width;
|
|
44
|
+
this.bgColor = options.bgColor || '#FFFFFF';
|
|
45
|
+
this.ripples = []; // Pour l'effet ripple Material
|
|
46
|
+
|
|
47
|
+
this.onPress = this.handlePress.bind(this);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Gère la pression (clic)
|
|
52
|
+
* @param {number} x - Coordonnée X
|
|
53
|
+
* @param {number} y - Coordonnée Y
|
|
54
|
+
* @private
|
|
55
|
+
*/
|
|
56
|
+
handlePress(x, y) {
|
|
57
|
+
if (this.platform === 'material') {
|
|
58
|
+
// ✅ CORRECTION : Calculer les coordonnées LOCALES du composant
|
|
59
|
+
// x et y sont déjà les coordonnées écran après ajustement par le framework
|
|
60
|
+
|
|
61
|
+
// Calculer les coordonnées relatives au composant
|
|
62
|
+
const localX = x - this.x;
|
|
63
|
+
const localY = y - this.y;
|
|
64
|
+
|
|
65
|
+
// ✅ AJOUTER : Vérifier si le point est vraiment dans le composant
|
|
66
|
+
if (localX < 0 || localX > this.width || localY < 0 || localY > this.height) {
|
|
67
|
+
return; // Le clic n'est pas dans le composant
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
this.ripples.push({
|
|
71
|
+
x: localX, // ✅ Coordonnée X relative au composant
|
|
72
|
+
y: localY, // ✅ Coordonnée Y relative au composant
|
|
73
|
+
radius: 0,
|
|
74
|
+
maxRadius: Math.max(this.width, this.height) * 1.5,
|
|
75
|
+
opacity: 1,
|
|
76
|
+
startTime: Date.now() // Pour une animation plus précise
|
|
77
|
+
});
|
|
78
|
+
// ✅ TEMPORAIREMENT mettre pressed à true pour le feedback visuel immédiat
|
|
79
|
+
this.pressed = true;
|
|
80
|
+
|
|
81
|
+
// ✅ MAIS le remettre à false après un court délai
|
|
82
|
+
setTimeout(() => {
|
|
83
|
+
this.pressed = false;
|
|
84
|
+
}, 150); // 150ms de feedback tactile
|
|
85
|
+
this.animateRipple();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Anime les effets ripple
|
|
91
|
+
* @private
|
|
92
|
+
*/
|
|
93
|
+
animateRipple() {
|
|
94
|
+
let animationId = null;
|
|
95
|
+
const startTime = Date.now();
|
|
96
|
+
const duration = 600; // 600ms pour l'animation complète
|
|
97
|
+
|
|
98
|
+
const animate = () => {
|
|
99
|
+
const elapsed = Date.now() - startTime;
|
|
100
|
+
const progress = Math.min(elapsed / duration, 1);
|
|
101
|
+
|
|
102
|
+
let hasActiveRipples = false;
|
|
103
|
+
|
|
104
|
+
for (let ripple of this.ripples) {
|
|
105
|
+
// ✅ Animation plus fluide avec easing
|
|
106
|
+
const easedProgress = this.easeOutCubic(progress);
|
|
107
|
+
ripple.radius = easedProgress * ripple.maxRadius;
|
|
108
|
+
|
|
109
|
+
// Fade out à partir de 50% de progression
|
|
110
|
+
if (progress > 0.5) {
|
|
111
|
+
const fadeProgress = (progress - 0.5) / 0.5;
|
|
112
|
+
ripple.opacity = 1 - fadeProgress;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (progress < 1) {
|
|
116
|
+
hasActiveRipples = true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Filtrer les ripples terminés
|
|
121
|
+
this.ripples = this.ripples.filter(r => r.opacity > 0);
|
|
122
|
+
|
|
123
|
+
if (hasActiveRipples && this.ripples.length > 0) {
|
|
124
|
+
animationId = requestAnimationFrame(animate);
|
|
125
|
+
} else {
|
|
126
|
+
// ✅ Nettoyer quand l'animation est terminée
|
|
127
|
+
if (animationId) {
|
|
128
|
+
cancelAnimationFrame(animationId);
|
|
129
|
+
}
|
|
130
|
+
this.ripples = [];
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
animationId = requestAnimationFrame(animate);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Fonction d'easing pour animation fluide
|
|
139
|
+
*/
|
|
140
|
+
easeOutCubic(t) {
|
|
141
|
+
return 1 - Math.pow(1 - t, 3);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Dessine l'élément de liste
|
|
146
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
147
|
+
*/
|
|
148
|
+
draw(ctx) {
|
|
149
|
+
ctx.save();
|
|
150
|
+
|
|
151
|
+
// 1. Background (toujours opaque)
|
|
152
|
+
ctx.fillStyle = this.pressed ? '#F5F5F5' : this.bgColor;
|
|
153
|
+
ctx.fillRect(this.x, this.y, this.width, this.height);
|
|
154
|
+
|
|
155
|
+
// 2. Ripples (si présents)
|
|
156
|
+
if (this.platform === 'material' && this.ripples.length > 0) {
|
|
157
|
+
ctx.save();
|
|
158
|
+
|
|
159
|
+
// Clip pour contenir les ripples
|
|
160
|
+
ctx.beginPath();
|
|
161
|
+
ctx.rect(this.x, this.y, this.width, this.height);
|
|
162
|
+
ctx.clip();
|
|
163
|
+
|
|
164
|
+
// Dessiner tous les ripples
|
|
165
|
+
for (let ripple of this.ripples) {
|
|
166
|
+
// Utiliser fillStyle avec alpha intégré au lieu de globalAlpha
|
|
167
|
+
ctx.fillStyle = `rgba(0, 0, 0, ${0.1 * ripple.opacity})`;
|
|
168
|
+
ctx.beginPath();
|
|
169
|
+
ctx.arc(this.x + ripple.x, this.y + ripple.y, ripple.radius, 0, Math.PI * 2);
|
|
170
|
+
ctx.fill();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
ctx.restore();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 3. Contenu (texte, icônes, etc.) - toujours avec alpha = 1
|
|
177
|
+
this.drawContent(ctx);
|
|
178
|
+
|
|
179
|
+
ctx.restore();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Dessine le contenu du ListItem (séparé pour plus de clarté)
|
|
184
|
+
*/
|
|
185
|
+
drawContent(ctx) {
|
|
186
|
+
let leftOffset = 16;
|
|
187
|
+
|
|
188
|
+
if (this.leftIcon) {
|
|
189
|
+
ctx.fillStyle = '#757575';
|
|
190
|
+
ctx.font = '24px sans-serif';
|
|
191
|
+
ctx.textAlign = 'left';
|
|
192
|
+
ctx.textBaseline = 'middle';
|
|
193
|
+
ctx.fillText(this.leftIcon, this.x + leftOffset, this.y + this.height / 2);
|
|
194
|
+
leftOffset += 48;
|
|
195
|
+
} else if (this.leftImage) {
|
|
196
|
+
ctx.fillStyle = '#E0E0E0';
|
|
197
|
+
ctx.beginPath();
|
|
198
|
+
ctx.arc(this.x + leftOffset + 20, this.y + this.height / 2, 20, 0, Math.PI * 2);
|
|
199
|
+
ctx.fill();
|
|
200
|
+
|
|
201
|
+
ctx.fillStyle = '#757575';
|
|
202
|
+
ctx.font = '14px sans-serif';
|
|
203
|
+
ctx.textAlign = 'center';
|
|
204
|
+
ctx.textBaseline = 'middle';
|
|
205
|
+
ctx.fillText('👤', this.x + leftOffset + 20, this.y + this.height / 2);
|
|
206
|
+
leftOffset += 56;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const textX = this.x + leftOffset;
|
|
210
|
+
const centerY = this.y + this.height / 2;
|
|
211
|
+
|
|
212
|
+
if (this.subtitle) {
|
|
213
|
+
ctx.fillStyle = '#000000';
|
|
214
|
+
ctx.font = '16px -apple-system, Roboto, sans-serif';
|
|
215
|
+
ctx.textAlign = 'left';
|
|
216
|
+
ctx.textBaseline = 'bottom';
|
|
217
|
+
ctx.fillText(this.title, textX, centerY - 2);
|
|
218
|
+
|
|
219
|
+
ctx.fillStyle = '#757575';
|
|
220
|
+
ctx.font = '14px -apple-system, Roboto, sans-serif';
|
|
221
|
+
ctx.textBaseline = 'top';
|
|
222
|
+
ctx.fillText(this.subtitle, textX, centerY + 2);
|
|
223
|
+
} else {
|
|
224
|
+
ctx.fillStyle = '#000000';
|
|
225
|
+
ctx.font = '16px -apple-system, Roboto, sans-serif';
|
|
226
|
+
ctx.textAlign = 'left';
|
|
227
|
+
ctx.textBaseline = 'middle';
|
|
228
|
+
ctx.fillText(this.title, textX, centerY);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (this.rightText) {
|
|
232
|
+
ctx.fillStyle = '#757575';
|
|
233
|
+
ctx.font = '14px -apple-system, Roboto, sans-serif';
|
|
234
|
+
ctx.textAlign = 'right';
|
|
235
|
+
ctx.textBaseline = 'middle';
|
|
236
|
+
ctx.fillText(this.rightText, this.x + this.width - 16, centerY);
|
|
237
|
+
} else if (this.rightIcon) {
|
|
238
|
+
ctx.fillStyle = '#757575';
|
|
239
|
+
ctx.font = '20px sans-serif';
|
|
240
|
+
ctx.textAlign = 'right';
|
|
241
|
+
ctx.textBaseline = 'middle';
|
|
242
|
+
ctx.fillText(this.rightIcon, this.x + this.width - 16, centerY);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (this.divider) {
|
|
246
|
+
ctx.strokeStyle = '#E0E0E0';
|
|
247
|
+
ctx.lineWidth = 1;
|
|
248
|
+
ctx.beginPath();
|
|
249
|
+
ctx.moveTo(this.x + leftOffset, this.y + this.height);
|
|
250
|
+
ctx.lineTo(this.x + this.width, this.y + this.height);
|
|
251
|
+
ctx.stroke();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Vérifie si un point est dans les limites
|
|
257
|
+
* @param {number} x - Coordonnée X
|
|
258
|
+
* @param {number} y - Coordonnée Y
|
|
259
|
+
* @returns {boolean} True si le point est dans l'élément
|
|
260
|
+
*/
|
|
261
|
+
isPointInside(x, y) {
|
|
262
|
+
return x >= this.x &&
|
|
263
|
+
x <= this.x + this.width &&
|
|
264
|
+
y >= this.y &&
|
|
265
|
+
y <= this.y + this.height;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export default ListItem;
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import Component from '../core/Component.js';
|
|
2
|
+
/**
|
|
3
|
+
* Fenêtre modale
|
|
4
|
+
* @class
|
|
5
|
+
* @extends Component
|
|
6
|
+
* @property {string} title - Titre
|
|
7
|
+
* @property {Component[]} children - Enfants
|
|
8
|
+
* @property {number} padding - Padding interne
|
|
9
|
+
* @property {string} bgColor - Couleur de fond
|
|
10
|
+
* @property {string} overlayColor - Couleur de l'overlay
|
|
11
|
+
* @property {number} borderRadius - Rayon des coins
|
|
12
|
+
* @property {string} shadowColor - Couleur de l'ombre
|
|
13
|
+
* @property {boolean} showCloseButton - Afficher le bouton fermer
|
|
14
|
+
* @property {boolean} closeOnOverlayClick - Fermer au clic sur l'overlay
|
|
15
|
+
* @property {number} modalWidth - Largeur du modal
|
|
16
|
+
* @property {number} modalHeight - Hauteur du modal
|
|
17
|
+
* @property {number} opacity - Opacité
|
|
18
|
+
* @property {number} scale - Échelle (animation)
|
|
19
|
+
* @property {boolean} isVisible - Visibilité
|
|
20
|
+
* @property {boolean} animating - En cours d'animation
|
|
21
|
+
* @property {number} closeButtonSize - Taille du bouton fermer
|
|
22
|
+
* @property {Object|null} closeButtonRect - Rectangle du bouton fermer
|
|
23
|
+
*/
|
|
24
|
+
class Modal extends Component {
|
|
25
|
+
/**
|
|
26
|
+
* Crée une instance de Modal
|
|
27
|
+
* @param {CanvasFramework} framework - Framework parent
|
|
28
|
+
* @param {Object} [options={}] - Options de configuration
|
|
29
|
+
* @param {string} [options.title=''] - Titre
|
|
30
|
+
* @param {number} [options.width] - Largeur (auto selon framework)
|
|
31
|
+
* @param {number} [options.height] - Hauteur
|
|
32
|
+
* @param {number} [options.padding=20] - Padding interne
|
|
33
|
+
* @param {string} [options.bgColor='#FFFFFF'] - Couleur de fond
|
|
34
|
+
* @param {string} [options.overlayColor='rgba(0,0,0,0.7)'] - Couleur overlay
|
|
35
|
+
* @param {number} [options.borderRadius=12] - Rayon des coins
|
|
36
|
+
* @param {string} [options.shadowColor='rgba(0,0,0,0.3)'] - Couleur ombre
|
|
37
|
+
* @param {boolean} [options.showCloseButton=true] - Afficher bouton fermer
|
|
38
|
+
* @param {boolean} [options.closeOnOverlayClick=true] - Fermer sur clic overlay
|
|
39
|
+
*/
|
|
40
|
+
constructor(framework, options = {}) {
|
|
41
|
+
super(framework, {
|
|
42
|
+
x: 0,
|
|
43
|
+
y: 0,
|
|
44
|
+
width: framework.width,
|
|
45
|
+
height: framework.height,
|
|
46
|
+
visible: false,
|
|
47
|
+
...options
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
this.title = options.title || '';
|
|
51
|
+
this.children = []; // Composants enfants
|
|
52
|
+
this.padding = options.padding || 20;
|
|
53
|
+
this.bgColor = options.bgColor || '#FFFFFF';
|
|
54
|
+
this.overlayColor = options.overlayColor || 'rgba(0, 0, 0, 0.7)';
|
|
55
|
+
this.borderRadius = options.borderRadius || 12;
|
|
56
|
+
this.shadowColor = options.shadowColor || 'rgba(0, 0, 0, 0.3)';
|
|
57
|
+
this.showCloseButton = options.showCloseButton !== false;
|
|
58
|
+
this.closeOnOverlayClick = options.closeOnOverlayClick !== false;
|
|
59
|
+
|
|
60
|
+
// Dimensions
|
|
61
|
+
this.modalWidth = options.width || Math.min(400, framework.width - 40);
|
|
62
|
+
this.modalHeight = options.height || 300;
|
|
63
|
+
|
|
64
|
+
// Animation
|
|
65
|
+
this.opacity = 0;
|
|
66
|
+
this.scale = 0.8;
|
|
67
|
+
this.isVisible = false;
|
|
68
|
+
this.animating = false;
|
|
69
|
+
|
|
70
|
+
// Bouton de fermeture
|
|
71
|
+
this.closeButtonSize = 30;
|
|
72
|
+
this.closeButtonRect = null;
|
|
73
|
+
|
|
74
|
+
// Définir onPress pour la fermeture
|
|
75
|
+
this.onPress = this.handlePress.bind(this);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Ajoute un enfant au modal
|
|
80
|
+
* @param {Component} child - Composant enfant
|
|
81
|
+
* @returns {Component} L'enfant ajouté
|
|
82
|
+
*/
|
|
83
|
+
add(child) {
|
|
84
|
+
this.children.push(child);
|
|
85
|
+
return child;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Affiche le modal
|
|
90
|
+
*/
|
|
91
|
+
show() {
|
|
92
|
+
this.isVisible = true;
|
|
93
|
+
this.visible = true;
|
|
94
|
+
this.animateIn();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Cache le modal
|
|
99
|
+
*/
|
|
100
|
+
hide() {
|
|
101
|
+
this.animateOut();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Anime l'entrée
|
|
106
|
+
* @private
|
|
107
|
+
*/
|
|
108
|
+
animateIn() {
|
|
109
|
+
if (this.animating) return;
|
|
110
|
+
this.animating = true;
|
|
111
|
+
|
|
112
|
+
const animate = () => {
|
|
113
|
+
this.opacity += 0.1;
|
|
114
|
+
this.scale += 0.04;
|
|
115
|
+
|
|
116
|
+
if (this.opacity >= 1) {
|
|
117
|
+
this.opacity = 1;
|
|
118
|
+
this.scale = 1;
|
|
119
|
+
this.animating = false;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
requestAnimationFrame(animate);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
animate();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Anime la sortie
|
|
131
|
+
* @private
|
|
132
|
+
*/
|
|
133
|
+
animateOut() {
|
|
134
|
+
if (this.animating) return;
|
|
135
|
+
this.animating = true;
|
|
136
|
+
|
|
137
|
+
const animate = () => {
|
|
138
|
+
this.opacity -= 0.1;
|
|
139
|
+
this.scale -= 0.04;
|
|
140
|
+
|
|
141
|
+
if (this.opacity <= 0) {
|
|
142
|
+
this.opacity = 0;
|
|
143
|
+
this.scale = 0.8;
|
|
144
|
+
this.isVisible = false;
|
|
145
|
+
this.visible = false;
|
|
146
|
+
this.animating = false;
|
|
147
|
+
this.framework.remove(this);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
requestAnimationFrame(animate);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
animate();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Dessine le modal
|
|
159
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
160
|
+
*/
|
|
161
|
+
draw(ctx) {
|
|
162
|
+
if (!this.isVisible || this.opacity <= 0) return;
|
|
163
|
+
|
|
164
|
+
ctx.save();
|
|
165
|
+
ctx.globalAlpha = this.opacity;
|
|
166
|
+
|
|
167
|
+
// Overlay
|
|
168
|
+
ctx.fillStyle = this.overlayColor;
|
|
169
|
+
ctx.fillRect(0, 0, this.framework.width, this.framework.height);
|
|
170
|
+
|
|
171
|
+
// Calculer la position du modal
|
|
172
|
+
const modalX = (this.framework.width - this.modalWidth) / 2;
|
|
173
|
+
const modalY = (this.framework.height - this.modalHeight) / 2;
|
|
174
|
+
|
|
175
|
+
// Appliquer l'animation de scale
|
|
176
|
+
ctx.translate(modalX + this.modalWidth / 2, modalY + this.modalHeight / 2);
|
|
177
|
+
ctx.scale(this.scale, this.scale);
|
|
178
|
+
ctx.translate(-modalX - this.modalWidth / 2, -modalY - this.modalHeight / 2);
|
|
179
|
+
|
|
180
|
+
// Fond du modal
|
|
181
|
+
ctx.fillStyle = this.bgColor;
|
|
182
|
+
ctx.shadowColor = this.shadowColor;
|
|
183
|
+
ctx.shadowBlur = 20;
|
|
184
|
+
ctx.shadowOffsetY = 10;
|
|
185
|
+
|
|
186
|
+
ctx.beginPath();
|
|
187
|
+
this.roundRect(ctx, modalX, modalY, this.modalWidth, this.modalHeight, this.borderRadius);
|
|
188
|
+
ctx.fill();
|
|
189
|
+
|
|
190
|
+
ctx.shadowColor = 'transparent';
|
|
191
|
+
|
|
192
|
+
// Titre
|
|
193
|
+
if (this.title) {
|
|
194
|
+
ctx.fillStyle = '#000000';
|
|
195
|
+
ctx.font = 'bold 18px -apple-system, sans-serif';
|
|
196
|
+
ctx.textAlign = 'center';
|
|
197
|
+
ctx.textBaseline = 'middle';
|
|
198
|
+
ctx.fillText(this.title, modalX + this.modalWidth / 2, modalY + 30);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Bouton de fermeture
|
|
202
|
+
if (this.showCloseButton) {
|
|
203
|
+
const closeX = modalX + this.modalWidth - this.closeButtonSize - 10;
|
|
204
|
+
const closeY = modalY + 10;
|
|
205
|
+
|
|
206
|
+
this.closeButtonRect = {
|
|
207
|
+
x: closeX,
|
|
208
|
+
y: closeY,
|
|
209
|
+
width: this.closeButtonSize,
|
|
210
|
+
height: this.closeButtonSize
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// Cercle du bouton
|
|
214
|
+
ctx.fillStyle = '#F0F0F0';
|
|
215
|
+
ctx.beginPath();
|
|
216
|
+
ctx.arc(closeX + this.closeButtonSize/2, closeY + this.closeButtonSize/2,
|
|
217
|
+
this.closeButtonSize/2, 0, Math.PI * 2);
|
|
218
|
+
ctx.fill();
|
|
219
|
+
|
|
220
|
+
// Croix (X)
|
|
221
|
+
ctx.strokeStyle = '#666666';
|
|
222
|
+
ctx.lineWidth = 2;
|
|
223
|
+
ctx.lineCap = 'round';
|
|
224
|
+
ctx.beginPath();
|
|
225
|
+
ctx.moveTo(closeX + 8, closeY + 8);
|
|
226
|
+
ctx.lineTo(closeX + this.closeButtonSize - 8, closeY + this.closeButtonSize - 8);
|
|
227
|
+
ctx.stroke();
|
|
228
|
+
|
|
229
|
+
ctx.beginPath();
|
|
230
|
+
ctx.moveTo(closeX + this.closeButtonSize - 8, closeY + 8);
|
|
231
|
+
ctx.lineTo(closeX + 8, closeY + this.closeButtonSize - 8);
|
|
232
|
+
ctx.stroke();
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Zone de contenu (clipping)
|
|
236
|
+
const contentX = modalX + this.padding;
|
|
237
|
+
const contentY = modalY + (this.title ? 50 : this.padding);
|
|
238
|
+
const contentWidth = this.modalWidth - (this.padding * 2);
|
|
239
|
+
const contentHeight = this.modalHeight - contentY + modalY - this.padding;
|
|
240
|
+
|
|
241
|
+
ctx.save();
|
|
242
|
+
ctx.beginPath();
|
|
243
|
+
ctx.rect(contentX, contentY, contentWidth, contentHeight);
|
|
244
|
+
ctx.clip();
|
|
245
|
+
|
|
246
|
+
// Dessiner les enfants
|
|
247
|
+
for (let child of this.children) {
|
|
248
|
+
if (child.visible) {
|
|
249
|
+
// Sauvegarder les coordonnées originales
|
|
250
|
+
const originalX = child.x;
|
|
251
|
+
const originalY = child.y;
|
|
252
|
+
|
|
253
|
+
// Ajuster pour la position du modal
|
|
254
|
+
child.x = contentX + originalX;
|
|
255
|
+
child.y = contentY + originalY;
|
|
256
|
+
|
|
257
|
+
child.draw(ctx);
|
|
258
|
+
|
|
259
|
+
// Restaurer les coordonnées
|
|
260
|
+
child.x = originalX;
|
|
261
|
+
child.y = originalY;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
ctx.restore();
|
|
266
|
+
|
|
267
|
+
ctx.restore();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Gère la pression (clic)
|
|
272
|
+
* @param {number} x - Coordonnée X
|
|
273
|
+
* @param {number} y - Coordonnée Y
|
|
274
|
+
* @private
|
|
275
|
+
*/
|
|
276
|
+
handlePress(x, y) {
|
|
277
|
+
const adjustedY = y - this.framework.scrollOffset;
|
|
278
|
+
const modalX = (this.framework.width - this.modalWidth) / 2;
|
|
279
|
+
const modalY = (this.framework.height - this.modalHeight) / 2;
|
|
280
|
+
|
|
281
|
+
// Vérifier le bouton de fermeture
|
|
282
|
+
if (this.showCloseButton && this.closeButtonRect) {
|
|
283
|
+
if (adjustedY >= this.closeButtonRect.y &&
|
|
284
|
+
adjustedY <= this.closeButtonRect.y + this.closeButtonRect.height &&
|
|
285
|
+
x >= this.closeButtonRect.x &&
|
|
286
|
+
x <= this.closeButtonRect.x + this.closeButtonRect.width) {
|
|
287
|
+
this.hide();
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Vérifier si on clique en dehors du modal (overlay)
|
|
293
|
+
if (this.closeOnOverlayClick) {
|
|
294
|
+
const isInModal = x >= modalX && x <= modalX + this.modalWidth &&
|
|
295
|
+
adjustedY >= modalY && adjustedY <= modalY + this.modalHeight;
|
|
296
|
+
|
|
297
|
+
if (!isInModal) {
|
|
298
|
+
this.hide();
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Vérifier les clics sur les enfants
|
|
304
|
+
const contentX = modalX + this.padding;
|
|
305
|
+
const contentY = modalY + (this.title ? 50 : this.padding);
|
|
306
|
+
|
|
307
|
+
for (let child of this.children) {
|
|
308
|
+
const childAbsX = contentX + child.x;
|
|
309
|
+
const childAbsY = contentY + child.y;
|
|
310
|
+
|
|
311
|
+
if (adjustedY >= childAbsY &&
|
|
312
|
+
adjustedY <= childAbsY + child.height &&
|
|
313
|
+
x >= childAbsX &&
|
|
314
|
+
x <= childAbsX + child.width) {
|
|
315
|
+
|
|
316
|
+
// Si l'enfant a un onClick, le déclencher
|
|
317
|
+
if (child.onClick) {
|
|
318
|
+
child.onClick();
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Si l'enfant a un onPress, le déclencher
|
|
323
|
+
if (child.onPress) {
|
|
324
|
+
child.onPress(x - childAbsX, adjustedY - childAbsY);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Dessine un rectangle avec coins arrondis
|
|
333
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
334
|
+
* @param {number} x - Position X
|
|
335
|
+
* @param {number} y - Position Y
|
|
336
|
+
* @param {number} width - Largeur
|
|
337
|
+
* @param {number} height - Hauteur
|
|
338
|
+
* @param {number} radius - Rayon des coins
|
|
339
|
+
* @private
|
|
340
|
+
*/
|
|
341
|
+
roundRect(ctx, x, y, width, height, radius) {
|
|
342
|
+
ctx.beginPath();
|
|
343
|
+
ctx.moveTo(x + radius, y);
|
|
344
|
+
ctx.lineTo(x + width - radius, y);
|
|
345
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
346
|
+
ctx.lineTo(x + width, y + height - radius);
|
|
347
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
348
|
+
ctx.lineTo(x + radius, y + height);
|
|
349
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
350
|
+
ctx.lineTo(x, y + radius);
|
|
351
|
+
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
352
|
+
ctx.closePath();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Vérifie si un point est dans les limites
|
|
357
|
+
* @returns {boolean} True si visible
|
|
358
|
+
*/
|
|
359
|
+
isPointInside(x, y) {
|
|
360
|
+
return this.isVisible;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export default Modal;
|