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,374 @@
|
|
|
1
|
+
import Component from '../core/Component.js';
|
|
2
|
+
/**
|
|
3
|
+
* Composant BottomSheet (feuille modale depuis le bas) avec drag & drop
|
|
4
|
+
* @class
|
|
5
|
+
* @extends Component
|
|
6
|
+
* @param {Framework} framework - Instance du framework
|
|
7
|
+
* @param {Object} [options={}] - Options de configuration
|
|
8
|
+
* @param {number} [options.height=framework.height * 0.6] - Hauteur du bottom sheet
|
|
9
|
+
* @param {boolean} [options.dragHandle=true] - Afficher la poignée de drag
|
|
10
|
+
* @param {boolean} [options.closeOnOverlayClick=true] - Fermer au clic sur l'overlay
|
|
11
|
+
* @param {string} [options.bgColor='#FFFFFF'] - Couleur de fond
|
|
12
|
+
* @param {number} [options.borderRadius=16] - Rayon des coins arrondis
|
|
13
|
+
* @example
|
|
14
|
+
* const bottomSheet = new BottomSheet(framework, {
|
|
15
|
+
* height: 400,
|
|
16
|
+
* bgColor: '#F5F5F5',
|
|
17
|
+
* borderRadius: 20
|
|
18
|
+
* });
|
|
19
|
+
*/
|
|
20
|
+
class BottomSheet extends Component {
|
|
21
|
+
/**
|
|
22
|
+
* @constructs BottomSheet
|
|
23
|
+
*/
|
|
24
|
+
constructor(framework, options = {}) {
|
|
25
|
+
super(framework, {
|
|
26
|
+
x: 0,
|
|
27
|
+
y: framework.height,
|
|
28
|
+
width: framework.width,
|
|
29
|
+
height: options.height || framework.height * 0.6,
|
|
30
|
+
visible: false
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/** @type {Component[]} */
|
|
34
|
+
this.children = [];
|
|
35
|
+
/** @type {boolean} */
|
|
36
|
+
this.dragHandle = options.dragHandle !== false;
|
|
37
|
+
/** @type {boolean} */
|
|
38
|
+
this.closeOnOverlayClick = options.closeOnOverlayClick !== false;
|
|
39
|
+
/** @type {string} */
|
|
40
|
+
this.bgColor = options.bgColor || '#FFFFFF';
|
|
41
|
+
/** @type {number} */
|
|
42
|
+
this.borderRadius = options.borderRadius || 16;
|
|
43
|
+
/** @type {number} */
|
|
44
|
+
this.targetY = framework.height;
|
|
45
|
+
/** @type {boolean} */
|
|
46
|
+
this.isOpen = false;
|
|
47
|
+
/** @type {boolean} */
|
|
48
|
+
this.animating = false;
|
|
49
|
+
/** @type {boolean} */
|
|
50
|
+
this.dragging = false;
|
|
51
|
+
/** @type {number} */
|
|
52
|
+
this.dragStartY = 0;
|
|
53
|
+
/** @type {number} */
|
|
54
|
+
this.dragOffset = 0;
|
|
55
|
+
/** @type {number} */
|
|
56
|
+
this.overlayOpacity = 0;
|
|
57
|
+
|
|
58
|
+
// IMPORTANT: Supprimer les bindings ici et les gérer différemment
|
|
59
|
+
this.onPress = this.handlePress.bind(this);
|
|
60
|
+
this.onMove = this.handleMove.bind(this);
|
|
61
|
+
this.onRelease = this.handleRelease.bind(this);
|
|
62
|
+
|
|
63
|
+
// Pour suivre le dernier clic
|
|
64
|
+
/** @type {number} */
|
|
65
|
+
this.lastClickTime = 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Ajoute un enfant au bottom sheet
|
|
70
|
+
* @param {Component} child - Composant enfant à ajouter
|
|
71
|
+
* @returns {Component} L'enfant ajouté
|
|
72
|
+
*/
|
|
73
|
+
add(child) {
|
|
74
|
+
this.children.push(child);
|
|
75
|
+
return child;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Ouvre le bottom sheet avec animation
|
|
80
|
+
*/
|
|
81
|
+
open() {
|
|
82
|
+
this.visible = true;
|
|
83
|
+
this.isOpen = true;
|
|
84
|
+
this.targetY = this.framework.height - this.height;
|
|
85
|
+
this.animate();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Ferme le bottom sheet avec animation
|
|
90
|
+
*/
|
|
91
|
+
close() {
|
|
92
|
+
this.isOpen = false;
|
|
93
|
+
this.targetY = this.framework.height;
|
|
94
|
+
this.animate(() => {
|
|
95
|
+
this.visible = false;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Anime le bottom sheet vers sa position cible
|
|
101
|
+
* @param {Function} [callback] - Callback appelé à la fin de l'animation
|
|
102
|
+
* @private
|
|
103
|
+
*/
|
|
104
|
+
animate(callback) {
|
|
105
|
+
if (this.animating) return;
|
|
106
|
+
this.animating = true;
|
|
107
|
+
|
|
108
|
+
const step = () => {
|
|
109
|
+
const diff = this.targetY - this.y;
|
|
110
|
+
|
|
111
|
+
if (Math.abs(diff) < 1) {
|
|
112
|
+
this.y = this.targetY;
|
|
113
|
+
this.overlayOpacity = this.isOpen ? 0.5 : 0;
|
|
114
|
+
this.animating = false;
|
|
115
|
+
if (callback) callback();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
this.y += diff * 0.2;
|
|
120
|
+
|
|
121
|
+
// Animer l'opacité de l'overlay
|
|
122
|
+
const progress = 1 - ((this.y - (this.framework.height - this.height)) / this.height);
|
|
123
|
+
this.overlayOpacity = Math.max(0, Math.min(0.5, progress * 0.5));
|
|
124
|
+
|
|
125
|
+
requestAnimationFrame(step);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
step();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Gère le clic/touch sur le bottom sheet
|
|
133
|
+
* @param {number} x - Position X du clic
|
|
134
|
+
* @param {number} y - Position Y du clic
|
|
135
|
+
*/
|
|
136
|
+
handlePress(x, y) {
|
|
137
|
+
// Empêcher les doubles clics rapides
|
|
138
|
+
const now = Date.now();
|
|
139
|
+
if (now - this.lastClickTime < 300) return;
|
|
140
|
+
this.lastClickTime = now;
|
|
141
|
+
|
|
142
|
+
// Calculer les coordonnées dans le sheet (sans scrollOffset car le sheet est fixe)
|
|
143
|
+
const adjustedY = y; // Pas d'ajustement de scroll pour le BottomSheet
|
|
144
|
+
|
|
145
|
+
// Clic sur l'overlay (zone sombre)
|
|
146
|
+
if (adjustedY < this.y && this.closeOnOverlayClick) {
|
|
147
|
+
this.close();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Début du drag sur la poignée
|
|
152
|
+
if (this.dragHandle && adjustedY >= this.y && adjustedY <= this.y + 40) {
|
|
153
|
+
this.dragging = true;
|
|
154
|
+
this.dragStartY = adjustedY;
|
|
155
|
+
this.dragOffset = 0;
|
|
156
|
+
this.framework.activeComponent = this;
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Vérifier les clics sur les enfants
|
|
161
|
+
const contentY = this.y + (this.dragHandle ? 40 : 16);
|
|
162
|
+
|
|
163
|
+
// Parcourir les enfants dans l'ordre inverse (du dernier au premier)
|
|
164
|
+
for (let i = this.children.length - 1; i >= 0; i--) {
|
|
165
|
+
const child = this.children[i];
|
|
166
|
+
|
|
167
|
+
if (!child.visible) continue;
|
|
168
|
+
|
|
169
|
+
// Calculer les coordonnées absolues de l'enfant
|
|
170
|
+
const childAbsX = this.x + 16 + child.x;
|
|
171
|
+
const childAbsY = contentY + child.y;
|
|
172
|
+
|
|
173
|
+
// Vérifier si le clic est dans l'enfant
|
|
174
|
+
if (adjustedY >= childAbsY &&
|
|
175
|
+
adjustedY <= childAbsY + child.height &&
|
|
176
|
+
x >= childAbsX &&
|
|
177
|
+
x <= childAbsX + child.width) {
|
|
178
|
+
|
|
179
|
+
// Si l'enfant a un onClick, le déclencher
|
|
180
|
+
if (child.onClick) {
|
|
181
|
+
child.onClick();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Si l'enfant a un onPress, le déclencher
|
|
186
|
+
if (child.onPress) {
|
|
187
|
+
// Calculer les coordonnées relatives pour l'enfant
|
|
188
|
+
const relativeX = x - childAbsX;
|
|
189
|
+
const relativeY = adjustedY - childAbsY;
|
|
190
|
+
child.onPress(relativeX, relativeY);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Marquer l'enfant comme pressé pour l'effet visuel
|
|
195
|
+
child.pressed = true;
|
|
196
|
+
|
|
197
|
+
// Si c'est un bouton, déclencher son onClick après un délai
|
|
198
|
+
if (child instanceof Button || child instanceof FAB) {
|
|
199
|
+
setTimeout(() => {
|
|
200
|
+
if (child.onClick) child.onClick();
|
|
201
|
+
child.pressed = false;
|
|
202
|
+
}, 150);
|
|
203
|
+
}
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Gère le déplacement pendant le drag
|
|
211
|
+
* @param {number} x - Position X actuelle
|
|
212
|
+
* @param {number} y - Position Y actuelle
|
|
213
|
+
*/
|
|
214
|
+
handleMove(x, y) {
|
|
215
|
+
if (this.dragging) {
|
|
216
|
+
const adjustedY = y; // Pas d'ajustement de scroll
|
|
217
|
+
this.dragOffset = adjustedY - this.dragStartY;
|
|
218
|
+
|
|
219
|
+
// Limiter le drag vers le haut
|
|
220
|
+
const newY = (this.framework.height - this.height) + this.dragOffset;
|
|
221
|
+
if (newY >= this.framework.height - this.height) {
|
|
222
|
+
this.y = newY;
|
|
223
|
+
|
|
224
|
+
// Mettre à jour l'opacité de l'overlay
|
|
225
|
+
const progress = 1 - ((this.y - (this.framework.height - this.height)) / this.height);
|
|
226
|
+
this.overlayOpacity = Math.max(0, Math.min(0.5, progress * 0.5));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Gère le relâchement après un drag
|
|
233
|
+
* @param {number} x - Position X du relâchement
|
|
234
|
+
* @param {number} y - Position Y du relâchement
|
|
235
|
+
*/
|
|
236
|
+
handleRelease(x, y) {
|
|
237
|
+
if (this.dragging) {
|
|
238
|
+
this.dragging = false;
|
|
239
|
+
this.framework.activeComponent = null;
|
|
240
|
+
|
|
241
|
+
// Si on a dragué plus de 30% vers le bas, fermer
|
|
242
|
+
if (this.dragOffset > this.height * 0.3) {
|
|
243
|
+
this.close();
|
|
244
|
+
} else {
|
|
245
|
+
// Sinon, revenir à la position ouverte
|
|
246
|
+
this.targetY = this.framework.height - this.height;
|
|
247
|
+
this.animate();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Réinitialiser l'état pressed pour tous les enfants
|
|
252
|
+
for (let child of this.children) {
|
|
253
|
+
child.pressed = false;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Dessine le bottom sheet
|
|
259
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
260
|
+
*/
|
|
261
|
+
draw(ctx) {
|
|
262
|
+
if (!this.visible) return;
|
|
263
|
+
|
|
264
|
+
ctx.save();
|
|
265
|
+
|
|
266
|
+
// Overlay sombre
|
|
267
|
+
ctx.fillStyle = `rgba(0, 0, 0, ${this.overlayOpacity})`;
|
|
268
|
+
ctx.fillRect(0, 0, this.framework.width, this.framework.height);
|
|
269
|
+
|
|
270
|
+
// BottomSheet
|
|
271
|
+
ctx.fillStyle = this.bgColor;
|
|
272
|
+
ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
|
|
273
|
+
ctx.shadowBlur = 20;
|
|
274
|
+
ctx.shadowOffsetY = -5;
|
|
275
|
+
|
|
276
|
+
ctx.beginPath();
|
|
277
|
+
this.roundRectTop(ctx, this.x, this.y, this.width, this.height, this.borderRadius);
|
|
278
|
+
ctx.fill();
|
|
279
|
+
|
|
280
|
+
ctx.shadowColor = 'transparent';
|
|
281
|
+
|
|
282
|
+
// Drag Handle
|
|
283
|
+
if (this.dragHandle) {
|
|
284
|
+
ctx.fillStyle = '#CCCCCC';
|
|
285
|
+
ctx.beginPath();
|
|
286
|
+
this.roundRect(ctx, this.width / 2 - 20, this.y + 12, 40, 4, 2);
|
|
287
|
+
ctx.fill();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Contenu (avec clipping)
|
|
291
|
+
const contentY = this.y + (this.dragHandle ? 40 : 16);
|
|
292
|
+
const contentHeight = this.height - (this.dragHandle ? 40 : 16);
|
|
293
|
+
|
|
294
|
+
ctx.save();
|
|
295
|
+
ctx.beginPath();
|
|
296
|
+
ctx.rect(this.x, contentY, this.width, contentHeight);
|
|
297
|
+
ctx.clip();
|
|
298
|
+
|
|
299
|
+
// Dessiner les enfants
|
|
300
|
+
for (let child of this.children) {
|
|
301
|
+
if (child.visible) {
|
|
302
|
+
const originalX = child.x;
|
|
303
|
+
const originalY = child.y;
|
|
304
|
+
|
|
305
|
+
child.x = this.x + 16 + originalX;
|
|
306
|
+
child.y = contentY + originalY;
|
|
307
|
+
|
|
308
|
+
child.draw(ctx);
|
|
309
|
+
|
|
310
|
+
child.x = originalX;
|
|
311
|
+
child.y = originalY;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
ctx.restore();
|
|
316
|
+
ctx.restore();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Dessine un rectangle avec seulement les coins supérieurs arrondis
|
|
321
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
322
|
+
* @param {number} x - Position X
|
|
323
|
+
* @param {number} y - Position Y
|
|
324
|
+
* @param {number} width - Largeur
|
|
325
|
+
* @param {number} height - Hauteur
|
|
326
|
+
* @param {number} radius - Rayon des coins
|
|
327
|
+
* @private
|
|
328
|
+
*/
|
|
329
|
+
roundRectTop(ctx, x, y, width, height, radius) {
|
|
330
|
+
ctx.moveTo(x + radius, y);
|
|
331
|
+
ctx.lineTo(x + width - radius, y);
|
|
332
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
333
|
+
ctx.lineTo(x + width, y + height);
|
|
334
|
+
ctx.lineTo(x, y + height);
|
|
335
|
+
ctx.lineTo(x, y + radius);
|
|
336
|
+
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Dessine un rectangle avec des coins arrondis
|
|
341
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
342
|
+
* @param {number} x - Position X
|
|
343
|
+
* @param {number} y - Position Y
|
|
344
|
+
* @param {number} width - Largeur
|
|
345
|
+
* @param {number} height - Hauteur
|
|
346
|
+
* @param {number} radius - Rayon des coins
|
|
347
|
+
* @private
|
|
348
|
+
*/
|
|
349
|
+
roundRect(ctx, x, y, width, height, radius) {
|
|
350
|
+
ctx.beginPath();
|
|
351
|
+
ctx.moveTo(x + radius, y);
|
|
352
|
+
ctx.lineTo(x + width - radius, y);
|
|
353
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
354
|
+
ctx.lineTo(x + width, y + height - radius);
|
|
355
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
356
|
+
ctx.lineTo(x + radius, y + height);
|
|
357
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
358
|
+
ctx.lineTo(x, y + radius);
|
|
359
|
+
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
360
|
+
ctx.closePath();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Vérifie si un point est à l'intérieur du composant
|
|
365
|
+
* @param {number} x - Position X
|
|
366
|
+
* @param {number} y - Position Y
|
|
367
|
+
* @returns {boolean} Toujours true si visible (le composant occupe tout l'écran)
|
|
368
|
+
*/
|
|
369
|
+
isPointInside(x, y) {
|
|
370
|
+
return this.visible;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export default BottomSheet;
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import Component from '../core/Component.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Bouton cliquable
|
|
5
|
+
* @class
|
|
6
|
+
* @extends Component
|
|
7
|
+
* @property {string} text - Texte du bouton
|
|
8
|
+
* @property {number} fontSize - Taille de la police
|
|
9
|
+
* @property {string} platform - Plateforme
|
|
10
|
+
* @property {string} bgColor - Couleur de fond
|
|
11
|
+
* @property {string} textColor - Couleur du texte
|
|
12
|
+
* @property {string} rippleColor - Couleur du ripple
|
|
13
|
+
* @property {number} elevation - Élévation (ombre)
|
|
14
|
+
* @property {Array} ripples - Effets ripple
|
|
15
|
+
*/
|
|
16
|
+
class Button extends Component {
|
|
17
|
+
/**
|
|
18
|
+
* Crée une instance de Button
|
|
19
|
+
* @param {CanvasFramework} framework - Framework parent
|
|
20
|
+
* @param {Object} [options={}] - Options de configuration
|
|
21
|
+
* @param {string} [options.text='Button'] - Texte
|
|
22
|
+
* @param {number} [options.fontSize=16] - Taille de police
|
|
23
|
+
* @param {string} [options.bgColor] - Couleur de fond (auto selon platform)
|
|
24
|
+
* @param {string} [options.textColor] - Couleur du texte (auto selon platform)
|
|
25
|
+
* @param {number} [options.elevation=2] - Élévation (Material)
|
|
26
|
+
*/
|
|
27
|
+
constructor(framework, options = {}) {
|
|
28
|
+
super(framework, options);
|
|
29
|
+
this.text = options.text || 'Button';
|
|
30
|
+
this.fontSize = options.fontSize || 16;
|
|
31
|
+
this.platform = framework.platform;
|
|
32
|
+
|
|
33
|
+
// Couleurs selon la plateforme
|
|
34
|
+
if (this.platform === 'material') {
|
|
35
|
+
this.bgColor = options.bgColor || '#6200EE';
|
|
36
|
+
this.textColor = options.textColor || '#FFFFFF';
|
|
37
|
+
this.rippleColor = 'rgba(255, 255, 255, 0.3)';
|
|
38
|
+
this.elevation = options.elevation || 2;
|
|
39
|
+
} else {
|
|
40
|
+
this.bgColor = options.bgColor || '#007AFF';
|
|
41
|
+
this.textColor = options.textColor || '#FFFFFF';
|
|
42
|
+
this.borderRadius = 10;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Effet Ripple
|
|
46
|
+
this.ripples = [];
|
|
47
|
+
|
|
48
|
+
// Bind des méthodes
|
|
49
|
+
this.onPress = this.handlePress.bind(this);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Gère la pression sur le bouton
|
|
54
|
+
* @param {number} x - Coordonnée X
|
|
55
|
+
* @param {number} y - Coordonnée Y
|
|
56
|
+
* @private
|
|
57
|
+
*/
|
|
58
|
+
handlePress(x, y) {
|
|
59
|
+
// Créer un ripple au point de clic
|
|
60
|
+
if (this.platform === 'material') {
|
|
61
|
+
const adjustedY = y - this.framework.scrollOffset;
|
|
62
|
+
this.ripples.push({
|
|
63
|
+
x: x - this.x,
|
|
64
|
+
y: adjustedY - this.y,
|
|
65
|
+
radius: 0,
|
|
66
|
+
maxRadius: Math.max(this.width, this.height) * 1.5,
|
|
67
|
+
opacity: 1
|
|
68
|
+
});
|
|
69
|
+
this.animateRipple();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Anime les effets ripple
|
|
75
|
+
* @private
|
|
76
|
+
*/
|
|
77
|
+
animateRipple() {
|
|
78
|
+
const animate = () => {
|
|
79
|
+
let hasActiveRipples = false;
|
|
80
|
+
|
|
81
|
+
for (let ripple of this.ripples) {
|
|
82
|
+
if (ripple.radius < ripple.maxRadius) {
|
|
83
|
+
ripple.radius += ripple.maxRadius / 15;
|
|
84
|
+
hasActiveRipples = true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (ripple.radius >= ripple.maxRadius * 0.5) {
|
|
88
|
+
ripple.opacity -= 0.05;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Nettoyer les ripples terminés
|
|
93
|
+
this.ripples = this.ripples.filter(r => r.opacity > 0);
|
|
94
|
+
|
|
95
|
+
if (hasActiveRipples) {
|
|
96
|
+
requestAnimationFrame(animate);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
animate();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Dessine le bouton
|
|
105
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
106
|
+
*/
|
|
107
|
+
draw(ctx) {
|
|
108
|
+
ctx.save();
|
|
109
|
+
|
|
110
|
+
if (this.platform === 'material') {
|
|
111
|
+
// Material Design
|
|
112
|
+
// Ombre (elevation)
|
|
113
|
+
if (this.elevation > 0 && !this.pressed) {
|
|
114
|
+
ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
|
|
115
|
+
ctx.shadowBlur = this.elevation * 2;
|
|
116
|
+
ctx.shadowOffsetY = this.elevation;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
ctx.fillStyle = this.pressed ? this.darkenColor(this.bgColor) : this.bgColor;
|
|
120
|
+
ctx.beginPath();
|
|
121
|
+
this.roundRect(ctx, this.x, this.y, this.width, this.height, 4);
|
|
122
|
+
ctx.fill();
|
|
123
|
+
|
|
124
|
+
ctx.shadowColor = 'transparent';
|
|
125
|
+
ctx.shadowBlur = 0;
|
|
126
|
+
ctx.shadowOffsetY = 0;
|
|
127
|
+
|
|
128
|
+
// Clipping pour les ripples
|
|
129
|
+
ctx.save();
|
|
130
|
+
ctx.beginPath();
|
|
131
|
+
this.roundRect(ctx, this.x, this.y, this.width, this.height, 4);
|
|
132
|
+
ctx.clip();
|
|
133
|
+
|
|
134
|
+
// Dessiner les ripples
|
|
135
|
+
for (let ripple of this.ripples) {
|
|
136
|
+
ctx.globalAlpha = ripple.opacity;
|
|
137
|
+
ctx.fillStyle = this.rippleColor;
|
|
138
|
+
ctx.beginPath();
|
|
139
|
+
ctx.arc(this.x + ripple.x, this.y + ripple.y, ripple.radius, 0, Math.PI * 2);
|
|
140
|
+
ctx.fill();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
ctx.restore();
|
|
144
|
+
|
|
145
|
+
} else {
|
|
146
|
+
// Cupertino (iOS)
|
|
147
|
+
ctx.fillStyle = this.pressed ? this.darkenColor(this.bgColor) : this.bgColor;
|
|
148
|
+
ctx.beginPath();
|
|
149
|
+
this.roundRect(ctx, this.x, this.y, this.width, this.height, this.borderRadius);
|
|
150
|
+
ctx.fill();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Texte
|
|
154
|
+
ctx.fillStyle = this.textColor;
|
|
155
|
+
ctx.font = `${this.fontSize}px -apple-system, BlinkMacSystemFont, 'Roboto', sans-serif`;
|
|
156
|
+
ctx.textAlign = 'center';
|
|
157
|
+
ctx.textBaseline = 'middle';
|
|
158
|
+
ctx.fillText(this.text, this.x + this.width / 2, this.y + this.height / 2);
|
|
159
|
+
|
|
160
|
+
ctx.restore();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Dessine un rectangle avec coins arrondis
|
|
165
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
166
|
+
* @param {number} x - Position X
|
|
167
|
+
* @param {number} y - Position Y
|
|
168
|
+
* @param {number} width - Largeur
|
|
169
|
+
* @param {number} height - Hauteur
|
|
170
|
+
* @param {number} radius - Rayon des coins
|
|
171
|
+
* @private
|
|
172
|
+
*/
|
|
173
|
+
roundRect(ctx, x, y, width, height, radius) {
|
|
174
|
+
ctx.moveTo(x + radius, y);
|
|
175
|
+
ctx.lineTo(x + width - radius, y);
|
|
176
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
177
|
+
ctx.lineTo(x + width, y + height - radius);
|
|
178
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
179
|
+
ctx.lineTo(x + radius, y + height);
|
|
180
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
181
|
+
ctx.lineTo(x, y + radius);
|
|
182
|
+
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Assombrit une couleur
|
|
187
|
+
* @param {string} color - Couleur hexadécimale
|
|
188
|
+
* @returns {string} Couleur assombrie
|
|
189
|
+
* @private
|
|
190
|
+
*/
|
|
191
|
+
darkenColor(color) {
|
|
192
|
+
const rgb = this.hexToRgb(color);
|
|
193
|
+
return `rgb(${Math.max(0, rgb.r - 30)}, ${Math.max(0, rgb.g - 30)}, ${Math.max(0, rgb.b - 30)})`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Convertit une couleur hex en RGB
|
|
198
|
+
* @param {string} hex - Couleur hexadécimale
|
|
199
|
+
* @returns {{r: number, g: number, b: number}} Objet RGB
|
|
200
|
+
* @private
|
|
201
|
+
*/
|
|
202
|
+
hexToRgb(hex) {
|
|
203
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
204
|
+
return result ? {
|
|
205
|
+
r: parseInt(result[1], 16),
|
|
206
|
+
g: parseInt(result[2], 16),
|
|
207
|
+
b: parseInt(result[3], 16)
|
|
208
|
+
} : { r: 0, g: 0, b: 0 };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Vérifie si un point est dans les limites
|
|
213
|
+
* @param {number} x - Coordonnée X
|
|
214
|
+
* @param {number} y - Coordonnée Y
|
|
215
|
+
* @returns {boolean} True si le point est dans le bouton
|
|
216
|
+
*/
|
|
217
|
+
isPointInside(x, y) {
|
|
218
|
+
return x >= this.x &&
|
|
219
|
+
x <= this.x + this.width &&
|
|
220
|
+
y >= this.y &&
|
|
221
|
+
y <= this.y + this.height;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export default Button;
|