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,255 @@
|
|
|
1
|
+
import Modal from '../components/Modal.js';
|
|
2
|
+
/**
|
|
3
|
+
* Modal pour la sélection d'une option parmi une liste
|
|
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 une option'] - Titre du modal
|
|
9
|
+
* @param {string[]} [options.options=[]] - Liste des options
|
|
10
|
+
* @param {number} [options.selectedIndex=0] - Index de l'option sélectionnée par défaut
|
|
11
|
+
* @param {Function} [options.onSelect] - Callback lors de la sélection
|
|
12
|
+
* @example
|
|
13
|
+
* const dialog = new SelectDialog(framework, {
|
|
14
|
+
* title: 'Choisir une couleur',
|
|
15
|
+
* options: ['Rouge', 'Vert', 'Bleu'],
|
|
16
|
+
* selectedIndex: 1,
|
|
17
|
+
* onSelect: (index, value) => console.log('Selected:', value)
|
|
18
|
+
* });
|
|
19
|
+
*/
|
|
20
|
+
class SelectDialog extends Modal {
|
|
21
|
+
/**
|
|
22
|
+
* @constructs SelectDialog
|
|
23
|
+
*/
|
|
24
|
+
constructor(framework, options = {}) {
|
|
25
|
+
// Calculer la hauteur en fonction du nombre d'options
|
|
26
|
+
const optionsCount = options.options?.length || 0;
|
|
27
|
+
const itemHeight = 50;
|
|
28
|
+
const dialogHeight = Math.min(
|
|
29
|
+
400, // Hauteur max
|
|
30
|
+
Math.max(200, optionsCount * itemHeight + 100) // Hauteur min + espace pour titre
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
// Appeler le constructeur parent
|
|
34
|
+
super(framework, {
|
|
35
|
+
title: options.title || 'Sélectionner une option',
|
|
36
|
+
width: Math.min(350, framework.width - 40),
|
|
37
|
+
height: dialogHeight,
|
|
38
|
+
showCloseButton: true,
|
|
39
|
+
closeOnOverlayClick: true,
|
|
40
|
+
padding: 0, // Pas de padding, on gère nous-même
|
|
41
|
+
...options
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/** @type {string[]} */
|
|
45
|
+
this.options = options.options || [];
|
|
46
|
+
/** @type {number} */
|
|
47
|
+
this.selectedIndex = options.selectedIndex || 0;
|
|
48
|
+
/** @type {Function|undefined} */
|
|
49
|
+
this.onSelect = options.onSelect;
|
|
50
|
+
/** @type {number} */
|
|
51
|
+
this.itemHeight = itemHeight;
|
|
52
|
+
/** @type {number} */
|
|
53
|
+
this.hoveredIndex = -1;
|
|
54
|
+
|
|
55
|
+
// Désactiver les animations
|
|
56
|
+
this.opacity = 1;
|
|
57
|
+
this.scale = 1;
|
|
58
|
+
this.isVisible = true;
|
|
59
|
+
this.visible = true;
|
|
60
|
+
|
|
61
|
+
// AJOUTER: Définir onPress et onMove pour que le framework les appelle
|
|
62
|
+
this.onPress = this.handlePress.bind(this);
|
|
63
|
+
this.onMove = this.handleMove.bind(this);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Dessine le modal de sélection
|
|
68
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
69
|
+
*/
|
|
70
|
+
draw(ctx) {
|
|
71
|
+
if (!this.isVisible) return;
|
|
72
|
+
|
|
73
|
+
ctx.save();
|
|
74
|
+
ctx.globalAlpha = this.opacity;
|
|
75
|
+
|
|
76
|
+
// Overlay sombre
|
|
77
|
+
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
|
|
78
|
+
ctx.fillRect(0, 0, this.framework.width, this.framework.height);
|
|
79
|
+
|
|
80
|
+
// Calculer la position du modal (centré)
|
|
81
|
+
const modalX = (this.framework.width - this.modalWidth) / 2;
|
|
82
|
+
const modalY = (this.framework.height - this.modalHeight) / 2;
|
|
83
|
+
|
|
84
|
+
// Fond du modal
|
|
85
|
+
ctx.fillStyle = '#FFFFFF';
|
|
86
|
+
ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
|
|
87
|
+
ctx.shadowBlur = 20;
|
|
88
|
+
ctx.shadowOffsetY = 10;
|
|
89
|
+
|
|
90
|
+
ctx.beginPath();
|
|
91
|
+
this.roundRect(ctx, modalX, modalY, this.modalWidth, this.modalHeight, 12);
|
|
92
|
+
ctx.fill();
|
|
93
|
+
|
|
94
|
+
ctx.shadowColor = 'transparent';
|
|
95
|
+
|
|
96
|
+
// Titre
|
|
97
|
+
if (this.title) {
|
|
98
|
+
ctx.fillStyle = '#000000';
|
|
99
|
+
ctx.font = 'bold 18px -apple-system, sans-serif';
|
|
100
|
+
ctx.textAlign = 'center';
|
|
101
|
+
ctx.textBaseline = 'middle';
|
|
102
|
+
ctx.fillText(this.title, modalX + this.modalWidth / 2, modalY + 30);
|
|
103
|
+
|
|
104
|
+
// Ligne de séparation sous le titre
|
|
105
|
+
ctx.strokeStyle = '#E0E0E0';
|
|
106
|
+
ctx.lineWidth = 1;
|
|
107
|
+
ctx.beginPath();
|
|
108
|
+
ctx.moveTo(modalX, modalY + 50);
|
|
109
|
+
ctx.lineTo(modalX + this.modalWidth, modalY + 50);
|
|
110
|
+
ctx.stroke();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Zone de contenu (avec scroll si nécessaire)
|
|
114
|
+
const contentX = modalX;
|
|
115
|
+
const contentY = modalY + 55; // Après le titre et la ligne
|
|
116
|
+
const contentHeight = this.modalHeight - 55;
|
|
117
|
+
|
|
118
|
+
ctx.save();
|
|
119
|
+
ctx.beginPath();
|
|
120
|
+
ctx.rect(contentX, contentY, this.modalWidth, contentHeight);
|
|
121
|
+
ctx.clip();
|
|
122
|
+
|
|
123
|
+
// Options
|
|
124
|
+
for (let i = 0; i < this.options.length; i++) {
|
|
125
|
+
const optionY = contentY + i * this.itemHeight;
|
|
126
|
+
|
|
127
|
+
// Si l'option est en dehors de la zone visible, passer à la suivante
|
|
128
|
+
if (optionY + this.itemHeight < contentY || optionY > contentY + contentHeight) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Option sélectionnée
|
|
133
|
+
if (i === this.selectedIndex) {
|
|
134
|
+
ctx.fillStyle = this.framework.platform === 'material' ? 'rgba(98, 0, 238, 0.1)' : 'rgba(0, 122, 255, 0.1)';
|
|
135
|
+
ctx.fillRect(contentX, optionY, this.modalWidth, this.itemHeight);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Effet hover
|
|
139
|
+
if (this.hoveredIndex === i) {
|
|
140
|
+
ctx.fillStyle = '#F5F5F5';
|
|
141
|
+
ctx.fillRect(contentX, optionY, this.modalWidth, this.itemHeight);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Texte de l'option
|
|
145
|
+
ctx.fillStyle = i === this.selectedIndex ?
|
|
146
|
+
(this.framework.platform === 'material' ? '#6200EE' : '#007AFF') :
|
|
147
|
+
'#000000';
|
|
148
|
+
ctx.font = i === this.selectedIndex ? 'bold 16px -apple-system, sans-serif' : '16px -apple-system, sans-serif';
|
|
149
|
+
ctx.textAlign = 'left';
|
|
150
|
+
ctx.textBaseline = 'middle';
|
|
151
|
+
ctx.fillText(this.options[i], contentX + 20, optionY + this.itemHeight / 2);
|
|
152
|
+
|
|
153
|
+
// Divider entre les options
|
|
154
|
+
if (i < this.options.length - 1) {
|
|
155
|
+
ctx.strokeStyle = '#E0E0E0';
|
|
156
|
+
ctx.lineWidth = 1;
|
|
157
|
+
ctx.beginPath();
|
|
158
|
+
ctx.moveTo(contentX + 20, optionY + this.itemHeight);
|
|
159
|
+
ctx.lineTo(contentX + this.modalWidth - 20, optionY + this.itemHeight);
|
|
160
|
+
ctx.stroke();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
ctx.restore();
|
|
165
|
+
ctx.restore();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Gère le clic dans le modal
|
|
170
|
+
* @param {number} x - Position X du clic
|
|
171
|
+
* @param {number} y - Position Y du clic
|
|
172
|
+
*/
|
|
173
|
+
handlePress(x, y) {
|
|
174
|
+
// POUR LES MODALS: utiliser les coordonnées brutes car le modal est un composant FIXE
|
|
175
|
+
// Les modals ne sont pas affectés par le scroll
|
|
176
|
+
|
|
177
|
+
const modalX = (this.framework.width - this.modalWidth) / 2;
|
|
178
|
+
const modalY = (this.framework.height - this.modalHeight) / 2;
|
|
179
|
+
const contentY = modalY + 55;
|
|
180
|
+
|
|
181
|
+
// Vérifier si une option a été cliquée
|
|
182
|
+
for (let i = 0; i < this.options.length; i++) {
|
|
183
|
+
const optionY = contentY + i * this.itemHeight;
|
|
184
|
+
|
|
185
|
+
// IMPORTANT: Pas d'ajustement de scroll pour les modals
|
|
186
|
+
if (y >= optionY && y <= optionY + this.itemHeight &&
|
|
187
|
+
x >= modalX && x <= modalX + this.modalWidth) {
|
|
188
|
+
|
|
189
|
+
this.selectedIndex = i;
|
|
190
|
+
if (this.onSelect) {
|
|
191
|
+
this.onSelect(i, this.options[i]);
|
|
192
|
+
}
|
|
193
|
+
this.hide();
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Sinon, laisser le parent gérer (bouton de fermeture, overlay)
|
|
199
|
+
super.handlePress(x, y);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Gère le survol dans le modal
|
|
204
|
+
* @param {number} x - Position X actuelle
|
|
205
|
+
* @param {number} y - Position Y actuelle
|
|
206
|
+
*/
|
|
207
|
+
handleMove(x, y) {
|
|
208
|
+
const modalX = (this.framework.width - this.modalWidth) / 2;
|
|
209
|
+
const modalY = (this.framework.height - this.modalHeight) / 2;
|
|
210
|
+
const contentY = modalY + 55;
|
|
211
|
+
|
|
212
|
+
this.hoveredIndex = -1;
|
|
213
|
+
|
|
214
|
+
// Vérifier si on survole une option
|
|
215
|
+
for (let i = 0; i < this.options.length; i++) {
|
|
216
|
+
const optionY = contentY + i * this.itemHeight;
|
|
217
|
+
|
|
218
|
+
// IMPORTANT: Pas d'ajustement de scroll pour les modals
|
|
219
|
+
if (y >= optionY && y <= optionY + this.itemHeight &&
|
|
220
|
+
x >= modalX && x <= modalX + this.modalWidth) {
|
|
221
|
+
this.hoveredIndex = i;
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Vérifie si un point est à l'intérieur du modal
|
|
229
|
+
* @param {number} x - Position X
|
|
230
|
+
* @param {number} y - Position Y
|
|
231
|
+
* @returns {boolean} True si le point est à l'intérieur du modal
|
|
232
|
+
*/
|
|
233
|
+
isPointInside(x, y) {
|
|
234
|
+
const modalX = (this.framework.width - this.modalWidth) / 2;
|
|
235
|
+
const modalY = (this.framework.height - this.modalHeight) / 2;
|
|
236
|
+
|
|
237
|
+
// Les modals sont des composants fixes, donc pas d'ajustement de scroll
|
|
238
|
+
return x >= modalX &&
|
|
239
|
+
x <= modalX + this.modalWidth &&
|
|
240
|
+
y >= modalY &&
|
|
241
|
+
y <= modalY + this.modalHeight;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Affiche le modal
|
|
246
|
+
*/
|
|
247
|
+
show() {
|
|
248
|
+
this.isVisible = true;
|
|
249
|
+
this.visible = true;
|
|
250
|
+
this.opacity = 1;
|
|
251
|
+
this.scale = 1;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export default SelectDialog;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import Component from '../core/Component.js';
|
|
2
|
+
|
|
3
|
+
class Slider extends Component {
|
|
4
|
+
constructor(framework, options = {}) {
|
|
5
|
+
super(framework, options);
|
|
6
|
+
this.min = options.min || 0;
|
|
7
|
+
this.max = options.max || 100;
|
|
8
|
+
this.value = options.value || 50;
|
|
9
|
+
this.platform = framework.platform;
|
|
10
|
+
this.onChange = options.onChange;
|
|
11
|
+
this.dragging = false;
|
|
12
|
+
|
|
13
|
+
// Animation state
|
|
14
|
+
this.thumbScale = 1.0;
|
|
15
|
+
this.targetScale = 1.0;
|
|
16
|
+
|
|
17
|
+
this.onPress = this.handlePress.bind(this);
|
|
18
|
+
this.onMove = this.handleMove.bind(this);
|
|
19
|
+
this.onClick = this.handleClick.bind(this);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
handlePress(x, y) {
|
|
23
|
+
this.dragging = true;
|
|
24
|
+
this.targetScale = 1.5; // Agrandissement de 50%
|
|
25
|
+
this.updateValue(x);
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
handleMove(x, y) {
|
|
30
|
+
if (this.dragging) {
|
|
31
|
+
this.updateValue(x);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
handleClick() {
|
|
36
|
+
this.dragging = false;
|
|
37
|
+
this.targetScale = 1.0; // Retour à la taille normale
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
updateValue(x) {
|
|
41
|
+
const relativeX = Math.max(0, Math.min(this.width, x - this.x));
|
|
42
|
+
const newValue = this.min + (relativeX / this.width) * (this.max - this.min);
|
|
43
|
+
|
|
44
|
+
if (newValue !== this.value) {
|
|
45
|
+
this.value = newValue;
|
|
46
|
+
if (this.onChange) this.onChange(this.value);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
draw(ctx) {
|
|
51
|
+
ctx.save();
|
|
52
|
+
|
|
53
|
+
// Animation du scale
|
|
54
|
+
this.thumbScale += (this.targetScale - this.thumbScale) * 0.2;
|
|
55
|
+
|
|
56
|
+
const progress = (this.value - this.min) / (this.max - this.min);
|
|
57
|
+
const thumbX = this.x + progress * this.width;
|
|
58
|
+
|
|
59
|
+
// Track
|
|
60
|
+
ctx.strokeStyle = this.platform === 'material' ? '#E0E0E0' : '#C7C7CC';
|
|
61
|
+
ctx.lineWidth = 2;
|
|
62
|
+
ctx.beginPath();
|
|
63
|
+
ctx.moveTo(this.x, this.y + this.height / 2);
|
|
64
|
+
ctx.lineTo(this.x + this.width, this.y + this.height / 2);
|
|
65
|
+
ctx.stroke();
|
|
66
|
+
|
|
67
|
+
// Track rempli
|
|
68
|
+
const trackColor = this.platform === 'material' ? '#6200EE' : '#007AFF';
|
|
69
|
+
ctx.strokeStyle = trackColor;
|
|
70
|
+
ctx.lineWidth = 2;
|
|
71
|
+
ctx.beginPath();
|
|
72
|
+
ctx.moveTo(this.x, this.y + this.height / 2);
|
|
73
|
+
ctx.lineTo(thumbX, this.y + this.height / 2);
|
|
74
|
+
ctx.stroke();
|
|
75
|
+
|
|
76
|
+
// Curseur avec animation
|
|
77
|
+
const baseRadius = 8;
|
|
78
|
+
const currentRadius = baseRadius * this.thumbScale;
|
|
79
|
+
|
|
80
|
+
// Effet d'ombre pendant le drag
|
|
81
|
+
if (this.dragging) {
|
|
82
|
+
ctx.shadowColor = trackColor;
|
|
83
|
+
ctx.shadowBlur = 10;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
ctx.fillStyle = trackColor;
|
|
87
|
+
ctx.beginPath();
|
|
88
|
+
ctx.arc(thumbX, this.y + this.height / 2, currentRadius, 0, Math.PI * 2);
|
|
89
|
+
ctx.fill();
|
|
90
|
+
|
|
91
|
+
if (this.dragging) {
|
|
92
|
+
ctx.shadowColor = 'transparent';
|
|
93
|
+
ctx.shadowBlur = 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
ctx.restore();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
isPointInside(x, y) {
|
|
100
|
+
const progress = (this.value - this.min) / (this.max - this.min);
|
|
101
|
+
const thumbX = this.x + progress * this.width;
|
|
102
|
+
const thumbY = this.y + this.height / 2;
|
|
103
|
+
const maxRadius = 8 * 1.5 + 5; // Rayon max + marge
|
|
104
|
+
|
|
105
|
+
// Zone de clic plus large pour faciliter l'utilisation
|
|
106
|
+
const distance = Math.sqrt((x - thumbX) ** 2 + (y - thumbY) ** 2);
|
|
107
|
+
return distance <= maxRadius ||
|
|
108
|
+
(x >= this.x && x <= this.x + this.width &&
|
|
109
|
+
y >= this.y && y <= this.y + this.height);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export default Slider;
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import Component from '../core/Component.js';
|
|
2
|
+
/**
|
|
3
|
+
* Snackbar (notification avec action)
|
|
4
|
+
* @class
|
|
5
|
+
* @extends Component
|
|
6
|
+
* @property {string} message - Message
|
|
7
|
+
* @property {string|null} actionText - Texte de l'action
|
|
8
|
+
* @property {Function} onAction - Callback de l'action
|
|
9
|
+
* @property {number} duration - Durée d'affichage
|
|
10
|
+
* @property {string} platform - Plateforme
|
|
11
|
+
* @property {number} padding - Padding interne
|
|
12
|
+
* @property {number} minWidth - Largeur minimale
|
|
13
|
+
* @property {number} maxWidth - Largeur maximale
|
|
14
|
+
* @property {number} targetY - Position Y cible
|
|
15
|
+
* @property {number} opacity - Opacité
|
|
16
|
+
* @property {boolean} isVisible - Visibilité
|
|
17
|
+
* @property {Object|null} actionRect - Rectangle de l'action
|
|
18
|
+
* @property {boolean} actionHovered - Action survolée
|
|
19
|
+
*/
|
|
20
|
+
class Snackbar extends Component {
|
|
21
|
+
/**
|
|
22
|
+
* Crée une instance de Snackbar
|
|
23
|
+
* @param {CanvasFramework} framework - Framework parent
|
|
24
|
+
* @param {Object} [options={}] - Options de configuration
|
|
25
|
+
* @param {string} [options.message=''] - Message
|
|
26
|
+
* @param {string} [options.actionText] - Texte de l'action
|
|
27
|
+
* @param {Function} [options.onAction] - Callback de l'action
|
|
28
|
+
* @param {number} [options.duration=4000] - Durée en ms
|
|
29
|
+
*/
|
|
30
|
+
constructor(framework, options = {}) {
|
|
31
|
+
super(framework, options);
|
|
32
|
+
this.message = options.message || '';
|
|
33
|
+
this.actionText = options.actionText || null;
|
|
34
|
+
this.onAction = options.onAction;
|
|
35
|
+
this.duration = options.duration || 4000;
|
|
36
|
+
this.platform = framework.platform;
|
|
37
|
+
|
|
38
|
+
// Dimensions
|
|
39
|
+
this.height = 48;
|
|
40
|
+
this.padding = 16;
|
|
41
|
+
this.minWidth = 344;
|
|
42
|
+
this.maxWidth = Math.min(672, framework.width - 32);
|
|
43
|
+
|
|
44
|
+
// Calculer la largeur
|
|
45
|
+
const ctx = framework.ctx;
|
|
46
|
+
ctx.font = '14px -apple-system, Roboto, sans-serif';
|
|
47
|
+
const messageWidth = ctx.measureText(this.message).width;
|
|
48
|
+
const actionWidth = this.actionText ? ctx.measureText(this.actionText).width + 40 : 0;
|
|
49
|
+
this.width = Math.min(this.maxWidth, Math.max(this.minWidth, messageWidth + actionWidth + this.padding * 3));
|
|
50
|
+
|
|
51
|
+
// Position (centré en bas)
|
|
52
|
+
this.x = (framework.width - this.width) / 2;
|
|
53
|
+
this.y = framework.height; // Commence hors écran
|
|
54
|
+
this.targetY = framework.height - this.height - 37;
|
|
55
|
+
|
|
56
|
+
this.opacity = 0;
|
|
57
|
+
this.isVisible = false;
|
|
58
|
+
|
|
59
|
+
// Zone du bouton d'action
|
|
60
|
+
this.actionRect = null;
|
|
61
|
+
|
|
62
|
+
this.onPress = this.handlePress.bind(this);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Affiche la snackbar
|
|
67
|
+
*/
|
|
68
|
+
show() {
|
|
69
|
+
this.isVisible = true;
|
|
70
|
+
this.visible = true;
|
|
71
|
+
this.animateIn();
|
|
72
|
+
|
|
73
|
+
// Auto-hide après duration
|
|
74
|
+
setTimeout(() => {
|
|
75
|
+
if (this.isVisible) {
|
|
76
|
+
this.hide();
|
|
77
|
+
}
|
|
78
|
+
}, this.duration);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Cache la snackbar
|
|
83
|
+
*/
|
|
84
|
+
hide() {
|
|
85
|
+
this.animateOut();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Anime l'entrée
|
|
90
|
+
* @private
|
|
91
|
+
*/
|
|
92
|
+
animateIn() {
|
|
93
|
+
const animate = () => {
|
|
94
|
+
if (this.y > this.targetY) {
|
|
95
|
+
this.y -= (this.y - this.targetY) * 0.2;
|
|
96
|
+
this.opacity = Math.min(1, this.opacity + 0.1);
|
|
97
|
+
requestAnimationFrame(animate);
|
|
98
|
+
} else {
|
|
99
|
+
this.y = this.targetY;
|
|
100
|
+
this.opacity = 1;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
animate();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Anime la sortie
|
|
108
|
+
* @private
|
|
109
|
+
*/
|
|
110
|
+
animateOut() {
|
|
111
|
+
const animate = () => {
|
|
112
|
+
if (this.opacity > 0) {
|
|
113
|
+
this.y += 5;
|
|
114
|
+
this.opacity -= 0.1;
|
|
115
|
+
requestAnimationFrame(animate);
|
|
116
|
+
} else {
|
|
117
|
+
this.isVisible = false;
|
|
118
|
+
this.visible = false;
|
|
119
|
+
this.framework.remove(this);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
animate();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Dessine la snackbar
|
|
127
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
128
|
+
*/
|
|
129
|
+
draw(ctx) {
|
|
130
|
+
if (!this.isVisible || this.opacity <= 0) return;
|
|
131
|
+
|
|
132
|
+
ctx.save();
|
|
133
|
+
ctx.globalAlpha = this.opacity;
|
|
134
|
+
|
|
135
|
+
// Background
|
|
136
|
+
ctx.fillStyle = this.platform === 'material' ? '#323232' : 'rgba(0, 0, 0, 0.9)';
|
|
137
|
+
ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
|
|
138
|
+
ctx.shadowBlur = 8;
|
|
139
|
+
ctx.shadowOffsetY = 4;
|
|
140
|
+
|
|
141
|
+
ctx.beginPath();
|
|
142
|
+
this.roundRect(ctx, this.x, this.y, this.width, this.height, 4);
|
|
143
|
+
ctx.fill();
|
|
144
|
+
|
|
145
|
+
ctx.shadowColor = 'transparent';
|
|
146
|
+
|
|
147
|
+
// Message
|
|
148
|
+
ctx.fillStyle = '#FFFFFF';
|
|
149
|
+
ctx.font = '14px -apple-system, Roboto, sans-serif';
|
|
150
|
+
ctx.textAlign = 'left';
|
|
151
|
+
ctx.textBaseline = 'middle';
|
|
152
|
+
ctx.fillText(this.message, this.x + this.padding, this.y + this.height / 2);
|
|
153
|
+
|
|
154
|
+
// Bouton d'action
|
|
155
|
+
if (this.actionText) {
|
|
156
|
+
const ctx2 = this.framework.ctx;
|
|
157
|
+
ctx2.font = '14px -apple-system, Roboto, sans-serif';
|
|
158
|
+
const actionWidth = ctx2.measureText(this.actionText).width;
|
|
159
|
+
const actionX = this.x + this.width - actionWidth - this.padding * 2;
|
|
160
|
+
const actionY = this.y;
|
|
161
|
+
|
|
162
|
+
this.actionRect = {
|
|
163
|
+
x: actionX,
|
|
164
|
+
y: actionY,
|
|
165
|
+
width: actionWidth + this.padding * 2,
|
|
166
|
+
height: this.height
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// Highlight si hover
|
|
170
|
+
if (this.actionHovered) {
|
|
171
|
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.1)';
|
|
172
|
+
ctx.fillRect(actionX, actionY, actionWidth + this.padding * 2, this.height);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Texte du bouton
|
|
176
|
+
const actionColor = this.platform === 'material' ? '#BB86FC' : '#0A84FF';
|
|
177
|
+
ctx.fillStyle = actionColor;
|
|
178
|
+
ctx.font = 'bold 14px -apple-system, Roboto, sans-serif';
|
|
179
|
+
ctx.textAlign = 'center';
|
|
180
|
+
ctx.fillText(this.actionText, actionX + (actionWidth + this.padding * 2) / 2, this.y + this.height / 2);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
ctx.restore();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Gère la pression (clic)
|
|
188
|
+
* @param {number} x - Coordonnée X
|
|
189
|
+
* @param {number} y - Coordonnée Y
|
|
190
|
+
* @private
|
|
191
|
+
*/
|
|
192
|
+
handlePress(x, y) {
|
|
193
|
+
if (this.actionRect && this.actionText) {
|
|
194
|
+
if (x >= this.actionRect.x &&
|
|
195
|
+
x <= this.actionRect.x + this.actionRect.width &&
|
|
196
|
+
y >= this.actionRect.y &&
|
|
197
|
+
y <= this.actionRect.y + this.actionRect.height) {
|
|
198
|
+
if (this.onAction) {
|
|
199
|
+
this.onAction();
|
|
200
|
+
}
|
|
201
|
+
this.hide();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Dessine un rectangle avec coins arrondis
|
|
208
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
209
|
+
* @param {number} x - Position X
|
|
210
|
+
* @param {number} y - Position Y
|
|
211
|
+
* @param {number} width - Largeur
|
|
212
|
+
* @param {number} height - Hauteur
|
|
213
|
+
* @param {number} radius - Rayon des coins
|
|
214
|
+
* @private
|
|
215
|
+
*/
|
|
216
|
+
roundRect(ctx, x, y, width, height, radius) {
|
|
217
|
+
ctx.beginPath();
|
|
218
|
+
ctx.moveTo(x + radius, y);
|
|
219
|
+
ctx.lineTo(x + width - radius, y);
|
|
220
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
221
|
+
ctx.lineTo(x + width, y + height - radius);
|
|
222
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
223
|
+
ctx.lineTo(x + radius, y + height);
|
|
224
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
225
|
+
ctx.lineTo(x, y + radius);
|
|
226
|
+
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
227
|
+
ctx.closePath();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Vérifie si un point est dans les limites
|
|
232
|
+
* @param {number} x - Coordonnée X
|
|
233
|
+
* @param {number} y - Coordonnée Y
|
|
234
|
+
* @returns {boolean} True si le point est dans la snackbar
|
|
235
|
+
*/
|
|
236
|
+
isPointInside(x, y) {
|
|
237
|
+
return this.isVisible &&
|
|
238
|
+
x >= this.x && x <= this.x + this.width &&
|
|
239
|
+
y >= this.y && y <= this.y + this.height;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export default Snackbar;
|