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,315 @@
|
|
|
1
|
+
import Input from './Input.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Champ de saisie pour la recherche
|
|
5
|
+
* @class
|
|
6
|
+
* @extends Input
|
|
7
|
+
* @property {Function} onSearch - Callback lors de la recherche
|
|
8
|
+
* @property {Function} onClear - Callback lors de l'effacement
|
|
9
|
+
* @property {boolean} showClearButton - Afficher le bouton d'effacement
|
|
10
|
+
*/
|
|
11
|
+
class SearchInput extends Input {
|
|
12
|
+
/**
|
|
13
|
+
* Crée une instance de SearchInput
|
|
14
|
+
* @param {CanvasFramework} framework - Framework parent
|
|
15
|
+
* @param {Object} [options={}] - Options de configuration
|
|
16
|
+
* @param {Function} [options.onSearch] - Callback lors de la recherche
|
|
17
|
+
* @param {Function} [options.onClear] - Callback lors de l'effacement
|
|
18
|
+
* @param {string} [options.searchIcon] - Icône de recherche
|
|
19
|
+
*/
|
|
20
|
+
constructor(framework, options = {}) {
|
|
21
|
+
super(framework, {
|
|
22
|
+
placeholder: options.placeholder || 'Rechercher...',
|
|
23
|
+
...options
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
this.onSearch = options.onSearch;
|
|
27
|
+
this.onClear = options.onClear;
|
|
28
|
+
this.searchIcon = options.searchIcon || '🔍';
|
|
29
|
+
this.clearIcon = '×';
|
|
30
|
+
this.showClearButton = false;
|
|
31
|
+
|
|
32
|
+
// Bind des méthodes supplémentaires
|
|
33
|
+
this.handleKeyDown = this.handleKeyDown.bind(this);
|
|
34
|
+
this.handleClearClick = this.handleClearClick.bind(this);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Configure l'input HTML caché (surcharge)
|
|
39
|
+
*/
|
|
40
|
+
setupHiddenInput() {
|
|
41
|
+
if (this.hiddenInput) return;
|
|
42
|
+
|
|
43
|
+
// Appeler la méthode parent
|
|
44
|
+
super.setupHiddenInput();
|
|
45
|
+
|
|
46
|
+
if (this.hiddenInput) {
|
|
47
|
+
// Ajouter l'événement keydown pour la touche Entrée
|
|
48
|
+
this.hiddenInput.addEventListener('keydown', this.handleKeyDown);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Gère la pression des touches
|
|
54
|
+
* @param {KeyboardEvent} e - Événement clavier
|
|
55
|
+
* @private
|
|
56
|
+
*/
|
|
57
|
+
handleKeyDown(e) {
|
|
58
|
+
if (e.key === 'Enter' && this.onSearch) {
|
|
59
|
+
this.onSearch(this.value);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Gère le clic sur le bouton d'effacement
|
|
65
|
+
* @private
|
|
66
|
+
*/
|
|
67
|
+
handleClearClick() {
|
|
68
|
+
const hadValue = this.value.length > 0;
|
|
69
|
+
this.value = '';
|
|
70
|
+
|
|
71
|
+
if (this.hiddenInput) {
|
|
72
|
+
this.hiddenInput.value = '';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Appeler les callbacks
|
|
76
|
+
if (hadValue && this.onClear) {
|
|
77
|
+
this.onClear();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Mettre à jour l'affichage
|
|
81
|
+
this.showClearButton = false;
|
|
82
|
+
|
|
83
|
+
// Redonner le focus
|
|
84
|
+
this.onFocus();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Gère la saisie (surcharge)
|
|
89
|
+
*/
|
|
90
|
+
onInputUpdate() {
|
|
91
|
+
// Mettre à jour l'affichage du bouton d'effacement
|
|
92
|
+
this.showClearButton = this.value.length > 0;
|
|
93
|
+
|
|
94
|
+
// Appeler le callback de recherche au fur et à mesure (optionnel)
|
|
95
|
+
if (this.value.length > 0 && this.onSearch) {
|
|
96
|
+
// Délai pour éviter de déclencher à chaque frappe
|
|
97
|
+
clearTimeout(this._searchTimeout);
|
|
98
|
+
this._searchTimeout = setTimeout(() => {
|
|
99
|
+
if (this.onSearch) {
|
|
100
|
+
this.onSearch(this.value);
|
|
101
|
+
}
|
|
102
|
+
}, 300);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Gère le clic (surcharge)
|
|
108
|
+
*/
|
|
109
|
+
onClick() {
|
|
110
|
+
// Vérifier si on a cliqué sur le bouton d'effacement
|
|
111
|
+
if (this.showClearButton && this.isPointInClearButton(this.lastClickX, this.lastClickY)) {
|
|
112
|
+
this.handleClearClick();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Sinon, focus normal
|
|
117
|
+
super.onFocus();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Gère le focus (surcharge)
|
|
122
|
+
*/
|
|
123
|
+
onFocus() {
|
|
124
|
+
super.onFocus();
|
|
125
|
+
|
|
126
|
+
// Mettre à jour l'état du bouton d'effacement
|
|
127
|
+
this.showClearButton = this.value.length > 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Gère la pression (surcharge pour capturer les coordonnées)
|
|
132
|
+
* @param {number} x - Coordonnée X
|
|
133
|
+
* @param {number} y - Coordonnée Y
|
|
134
|
+
*/
|
|
135
|
+
onPress(x, y) {
|
|
136
|
+
// Stocker les coordonnées du dernier clic
|
|
137
|
+
this.lastClickX = x;
|
|
138
|
+
this.lastClickY = y;
|
|
139
|
+
|
|
140
|
+
// Appeler la méthode parent
|
|
141
|
+
super.onPress(x, y);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Vérifie si un point est dans le bouton d'effacement
|
|
146
|
+
* @param {number} x - Coordonnée X
|
|
147
|
+
* @param {number} y - Coordonnée Y
|
|
148
|
+
* @returns {boolean} True si le point est dans le bouton
|
|
149
|
+
*/
|
|
150
|
+
isPointInClearButton(x, y) {
|
|
151
|
+
if (!this.showClearButton) return false;
|
|
152
|
+
|
|
153
|
+
// Position du bouton d'effacement (à droite)
|
|
154
|
+
const clearButtonSize = this.height * 0.6;
|
|
155
|
+
const clearButtonX = this.x + this.width - clearButtonSize - 10;
|
|
156
|
+
const clearButtonY = this.y + (this.height - clearButtonSize) / 2;
|
|
157
|
+
|
|
158
|
+
return x >= clearButtonX &&
|
|
159
|
+
x <= clearButtonX + clearButtonSize &&
|
|
160
|
+
y >= clearButtonY &&
|
|
161
|
+
y <= clearButtonY + clearButtonSize;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Dessine le champ de recherche (surcharge)
|
|
166
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
167
|
+
*/
|
|
168
|
+
draw(ctx) {
|
|
169
|
+
ctx.save();
|
|
170
|
+
|
|
171
|
+
// Style selon la plateforme
|
|
172
|
+
if (this.platform === 'material') {
|
|
173
|
+
ctx.strokeStyle = this.focused ? '#6200EE' : '#CCCCCC';
|
|
174
|
+
ctx.lineWidth = this.focused ? 2 : 1;
|
|
175
|
+
ctx.beginPath();
|
|
176
|
+
ctx.moveTo(this.x, this.y + this.height);
|
|
177
|
+
ctx.lineTo(this.x + this.width, this.y + this.height);
|
|
178
|
+
ctx.stroke();
|
|
179
|
+
} else {
|
|
180
|
+
// iOS style avec coins arrondis
|
|
181
|
+
ctx.fillStyle = this.focused ? '#FFFFFF' : '#F2F2F7';
|
|
182
|
+
ctx.strokeStyle = this.focused ? '#007AFF' : '#C7C7CC';
|
|
183
|
+
ctx.lineWidth = 1;
|
|
184
|
+
|
|
185
|
+
// Fond
|
|
186
|
+
ctx.beginPath();
|
|
187
|
+
this.roundRect(ctx, this.x, this.y, this.width, this.height, 10);
|
|
188
|
+
ctx.fill();
|
|
189
|
+
|
|
190
|
+
// Bordure
|
|
191
|
+
ctx.stroke();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Icône de recherche (à gauche)
|
|
195
|
+
ctx.fillStyle = '#999999';
|
|
196
|
+
ctx.font = `${this.fontSize}px -apple-system, BlinkMacSystemFont, 'Roboto', sans-serif`;
|
|
197
|
+
ctx.textAlign = 'center';
|
|
198
|
+
ctx.textBaseline = 'middle';
|
|
199
|
+
ctx.fillText(this.searchIcon, this.x + 25, this.y + this.height / 2);
|
|
200
|
+
|
|
201
|
+
// Texte
|
|
202
|
+
ctx.fillStyle = this.value ? '#000000' : '#999999';
|
|
203
|
+
ctx.font = `${this.fontSize}px -apple-system, BlinkMacSystemFont, 'Roboto', sans-serif`;
|
|
204
|
+
ctx.textAlign = 'left';
|
|
205
|
+
ctx.textBaseline = 'middle';
|
|
206
|
+
|
|
207
|
+
// Calculer la largeur disponible pour le texte
|
|
208
|
+
const leftPadding = 50; // Pour l'icône de recherche
|
|
209
|
+
const rightPadding = this.showClearButton ? 50 : 20; // Pour le bouton d'effacement
|
|
210
|
+
const maxTextWidth = this.width - leftPadding - rightPadding;
|
|
211
|
+
|
|
212
|
+
const displayText = this.value || this.placeholder;
|
|
213
|
+
|
|
214
|
+
// Tronquer le texte si nécessaire
|
|
215
|
+
let textToShow = displayText;
|
|
216
|
+
let textWidth = ctx.measureText(textToShow).width;
|
|
217
|
+
|
|
218
|
+
if (textWidth > maxTextWidth) {
|
|
219
|
+
// Ajouter "..." à la fin
|
|
220
|
+
while (textWidth > maxTextWidth && textToShow.length > 0) {
|
|
221
|
+
textToShow = textToShow.substring(0, textToShow.length - 1);
|
|
222
|
+
textWidth = ctx.measureText(textToShow + '...').width;
|
|
223
|
+
}
|
|
224
|
+
if (textToShow.length > 0) {
|
|
225
|
+
textToShow += '...';
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Couleur différente pour le placeholder
|
|
230
|
+
if (!this.value) {
|
|
231
|
+
ctx.fillStyle = '#999999';
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
ctx.fillText(textToShow, this.x + leftPadding, this.y + this.height / 2);
|
|
235
|
+
|
|
236
|
+
// Bouton d'effacement (si il y a du texte)
|
|
237
|
+
if (this.showClearButton) {
|
|
238
|
+
const clearButtonSize = this.height * 0.6;
|
|
239
|
+
const clearButtonX = this.x + this.width - clearButtonSize - 15;
|
|
240
|
+
const clearButtonY = this.y + (this.height - clearButtonSize) / 2;
|
|
241
|
+
|
|
242
|
+
// Cercle de fond
|
|
243
|
+
ctx.fillStyle = '#E0E0E0';
|
|
244
|
+
ctx.beginPath();
|
|
245
|
+
ctx.arc(
|
|
246
|
+
clearButtonX + clearButtonSize / 2,
|
|
247
|
+
clearButtonY + clearButtonSize / 2,
|
|
248
|
+
clearButtonSize / 2,
|
|
249
|
+
0,
|
|
250
|
+
Math.PI * 2
|
|
251
|
+
);
|
|
252
|
+
ctx.fill();
|
|
253
|
+
|
|
254
|
+
// Croix
|
|
255
|
+
ctx.fillStyle = '#666666';
|
|
256
|
+
ctx.font = `bold ${clearButtonSize * 0.6}px -apple-system, BlinkMacSystemFont, 'Roboto', sans-serif`;
|
|
257
|
+
ctx.textAlign = 'center';
|
|
258
|
+
ctx.textBaseline = 'middle';
|
|
259
|
+
ctx.fillText(
|
|
260
|
+
this.clearIcon,
|
|
261
|
+
clearButtonX + clearButtonSize / 2,
|
|
262
|
+
clearButtonY + clearButtonSize / 2
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Curseur (si focus et a du texte)
|
|
267
|
+
if (this.focused && this.cursorVisible && this.value) {
|
|
268
|
+
const textWidth = ctx.measureText(this.value).width;
|
|
269
|
+
const cursorX = this.x + leftPadding + Math.min(textWidth, maxTextWidth);
|
|
270
|
+
ctx.fillStyle = '#000000';
|
|
271
|
+
ctx.fillRect(cursorX, this.y + 10, 2, this.height - 20);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
ctx.restore();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Vérifie si un point est dans les limites (surcharge)
|
|
279
|
+
* @param {number} x - Coordonnée X
|
|
280
|
+
* @param {number} y - Coordonnée Y
|
|
281
|
+
* @returns {boolean} True si le point est dans l'input
|
|
282
|
+
*/
|
|
283
|
+
isPointInside(x, y) {
|
|
284
|
+
// Vérifier l'input principal
|
|
285
|
+
const inInput = x >= this.x &&
|
|
286
|
+
x <= this.x + this.width &&
|
|
287
|
+
y >= this.y &&
|
|
288
|
+
y <= this.y + this.height;
|
|
289
|
+
|
|
290
|
+
// Vérifier aussi le bouton d'effacement
|
|
291
|
+
const inClearButton = this.isPointInClearButton(x, y);
|
|
292
|
+
|
|
293
|
+
return inInput || inClearButton;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Nettoie les ressources (surcharge)
|
|
298
|
+
*/
|
|
299
|
+
destroy() {
|
|
300
|
+
// Nettoyer le timeout de recherche
|
|
301
|
+
if (this._searchTimeout) {
|
|
302
|
+
clearTimeout(this._searchTimeout);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Retirer l'écouteur keydown
|
|
306
|
+
if (this.hiddenInput) {
|
|
307
|
+
this.hiddenInput.removeEventListener('keydown', this.handleKeyDown);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Appeler la méthode parent
|
|
311
|
+
super.destroy();
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export default SearchInput;
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import Component from '../core/Component.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Segmented Control (Material + Cupertino)
|
|
5
|
+
* @class
|
|
6
|
+
* @extends Component
|
|
7
|
+
* @param {CanvasFramework} framework - Instance du framework
|
|
8
|
+
* @param {Object} [options={}] - Options de configuration
|
|
9
|
+
* @param {Array<{text: string, onClick?: Function}>} [options.buttons] - Liste des segments
|
|
10
|
+
* @param {number} [options.selectedIndex=0] - Segment sélectionné par défaut
|
|
11
|
+
* @param {number} [options.height=40] - Hauteur du contrôle
|
|
12
|
+
* @param {number} [options.spacing=1] - Espacement entre les segments
|
|
13
|
+
*/
|
|
14
|
+
class SegmentedControl extends Component {
|
|
15
|
+
constructor(framework, options = {}) {
|
|
16
|
+
super(framework, options);
|
|
17
|
+
|
|
18
|
+
this.platform = framework.platform;
|
|
19
|
+
this.buttons = options.buttons || [{ text: 'One' }, { text: 'Two' }, { text: 'Three' }];
|
|
20
|
+
this.selectedIndex = options.selectedIndex || 0;
|
|
21
|
+
this.height = options.height || 40;
|
|
22
|
+
this.spacing = options.spacing || 1;
|
|
23
|
+
|
|
24
|
+
// IMPORTANT: Lier les handlers d'événements
|
|
25
|
+
this.onPress = this.handlePress.bind(this);
|
|
26
|
+
this.onRelease = this.handleRelease.bind(this);
|
|
27
|
+
|
|
28
|
+
// État pour les animations
|
|
29
|
+
this.ripples = [];
|
|
30
|
+
this.pressedIndex = null;
|
|
31
|
+
this._isAnimating = false;
|
|
32
|
+
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Gère la pression sur un segment
|
|
37
|
+
* @param {number} x - Coordonnée X du clic
|
|
38
|
+
* @param {number} y - Coordonnée Y du clic
|
|
39
|
+
* @returns {boolean} True si un segment a été cliqué
|
|
40
|
+
*/
|
|
41
|
+
handlePress(x, y) {
|
|
42
|
+
const index = this.getButtonIndexAt(x, y);
|
|
43
|
+
|
|
44
|
+
if (index !== null) {
|
|
45
|
+
// Sauvegarder l'index pressé pour l'animation
|
|
46
|
+
this.pressedIndex = index;
|
|
47
|
+
|
|
48
|
+
// Pour Material: créer un ripple
|
|
49
|
+
if (this.platform === 'material') {
|
|
50
|
+
const btnWidth = (this.width - this.spacing * (this.buttons.length - 1)) / this.buttons.length;
|
|
51
|
+
const btnX = this.x + index * (btnWidth + this.spacing);
|
|
52
|
+
|
|
53
|
+
this.ripples.push({
|
|
54
|
+
x: x - btnX, // Position relative au bouton
|
|
55
|
+
y: y - this.y, // Position relative au bouton
|
|
56
|
+
index: index,
|
|
57
|
+
radius: 0,
|
|
58
|
+
maxRadius: Math.max(btnWidth, this.height) * 1.5,
|
|
59
|
+
opacity: 0.3,
|
|
60
|
+
startTime: Date.now()
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Démarrer l'animation si pas déjà en cours
|
|
64
|
+
if (!this._isAnimating) {
|
|
65
|
+
this._animate();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Sélectionner le segment
|
|
70
|
+
this.selectedIndex = index;
|
|
71
|
+
|
|
72
|
+
// Forcer le redessin immédiat
|
|
73
|
+
this._requestRedraw();
|
|
74
|
+
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Gère le relâchement
|
|
83
|
+
* @param {number} x - Coordonnée X
|
|
84
|
+
* @param {number} y - Coordonnée Y
|
|
85
|
+
*/
|
|
86
|
+
handleRelease(x, y) {
|
|
87
|
+
const index = this.getButtonIndexAt(x, y);
|
|
88
|
+
|
|
89
|
+
if (index !== null && index === this.pressedIndex) {
|
|
90
|
+
// Appeler le callback si défini
|
|
91
|
+
if (this.buttons[index].onClick) {
|
|
92
|
+
this.buttons[index].onClick(index);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Réinitialiser l'index pressé
|
|
97
|
+
this.pressedIndex = null;
|
|
98
|
+
|
|
99
|
+
// Forcer le redessin
|
|
100
|
+
this._requestRedraw();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Anime les ripples Material
|
|
105
|
+
* @private
|
|
106
|
+
*/
|
|
107
|
+
_animate() {
|
|
108
|
+
this._isAnimating = true;
|
|
109
|
+
|
|
110
|
+
const animateFrame = () => {
|
|
111
|
+
let hasActiveRipples = false;
|
|
112
|
+
const now = Date.now();
|
|
113
|
+
|
|
114
|
+
// Mettre à jour tous les ripples
|
|
115
|
+
for (let i = this.ripples.length - 1; i >= 0; i--) {
|
|
116
|
+
const ripple = this.ripples[i];
|
|
117
|
+
const elapsed = now - ripple.startTime;
|
|
118
|
+
|
|
119
|
+
// Animation sur 600ms
|
|
120
|
+
const progress = Math.min(elapsed / 600, 1);
|
|
121
|
+
|
|
122
|
+
// Équation d'easing
|
|
123
|
+
const easedProgress = 1 - Math.pow(1 - progress, 3);
|
|
124
|
+
|
|
125
|
+
// Mettre à jour le rayon
|
|
126
|
+
ripple.radius = ripple.maxRadius * easedProgress;
|
|
127
|
+
|
|
128
|
+
// Diminuer l'opacité après 50% de progression
|
|
129
|
+
if (progress > 0.5) {
|
|
130
|
+
ripple.opacity = 0.3 * (1 - (progress - 0.5) * 2);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Supprimer les ripples terminés
|
|
134
|
+
if (progress >= 1) {
|
|
135
|
+
this.ripples.splice(i, 1);
|
|
136
|
+
} else {
|
|
137
|
+
hasActiveRipples = true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Redessiner si il y a des ripples actifs
|
|
142
|
+
if (hasActiveRipples) {
|
|
143
|
+
this._requestRedraw();
|
|
144
|
+
requestAnimationFrame(animateFrame);
|
|
145
|
+
} else {
|
|
146
|
+
this._isAnimating = false;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
requestAnimationFrame(animateFrame);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Retourne l'index du bouton sous un point donné
|
|
155
|
+
* @param {number} x - Coordonnée X
|
|
156
|
+
* @param {number} y - Coordonnée Y
|
|
157
|
+
* @returns {number|null} Index du segment ou null
|
|
158
|
+
* @private
|
|
159
|
+
*/
|
|
160
|
+
getButtonIndexAt(x, y) {
|
|
161
|
+
// Vérifier si dans les limites verticales
|
|
162
|
+
if (y < this.y || y > this.y + this.height) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const btnWidth = (this.width - this.spacing * (this.buttons.length - 1)) / this.buttons.length;
|
|
167
|
+
|
|
168
|
+
for (let i = 0; i < this.buttons.length; i++) {
|
|
169
|
+
const btnX = this.x + i * (btnWidth + this.spacing);
|
|
170
|
+
|
|
171
|
+
// Vérifier si dans les limites horizontales du bouton
|
|
172
|
+
if (x >= btnX && x <= btnX + btnWidth) {
|
|
173
|
+
return i;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Force le redessin du composant
|
|
182
|
+
* @private
|
|
183
|
+
*/
|
|
184
|
+
_requestRedraw() {
|
|
185
|
+
if (this.framework && this.framework.markComponentDirty) {
|
|
186
|
+
this.framework.markComponentDirty(this);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Dessine le SegmentedControl
|
|
192
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
193
|
+
*/
|
|
194
|
+
draw(ctx) {
|
|
195
|
+
ctx.save();
|
|
196
|
+
|
|
197
|
+
const btnWidth = (this.width - this.spacing * (this.buttons.length - 1)) / this.buttons.length;
|
|
198
|
+
const radius = this.height / 2;
|
|
199
|
+
|
|
200
|
+
// Dessiner tous les boutons
|
|
201
|
+
for (let i = 0; i < this.buttons.length; i++) {
|
|
202
|
+
const btn = this.buttons[i];
|
|
203
|
+
const btnX = this.x + i * (btnWidth + this.spacing);
|
|
204
|
+
|
|
205
|
+
// Couleurs selon la plateforme et l'état
|
|
206
|
+
let backgroundColor;
|
|
207
|
+
let textColor;
|
|
208
|
+
|
|
209
|
+
if (this.platform === 'material') {
|
|
210
|
+
// Material Design
|
|
211
|
+
if (this.selectedIndex === i) {
|
|
212
|
+
backgroundColor = '#6200EE'; // Violet Material
|
|
213
|
+
textColor = '#FFFFFF';
|
|
214
|
+
} else {
|
|
215
|
+
backgroundColor = '#E0E0E0'; // Gris clair
|
|
216
|
+
textColor = '#000000';
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Si pressé (mais pas encore sélectionné)
|
|
220
|
+
if (this.pressedIndex === i && this.pressedIndex !== this.selectedIndex) {
|
|
221
|
+
backgroundColor = 'rgba(98, 0, 238, 0.12)'; // Violet très transparent
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
// iOS/Cupertino
|
|
225
|
+
if (this.selectedIndex === i) {
|
|
226
|
+
backgroundColor = '#007AFF'; // Bleu iOS
|
|
227
|
+
textColor = '#FFFFFF';
|
|
228
|
+
} else {
|
|
229
|
+
backgroundColor = '#F0F0F0'; // Gris très clair iOS
|
|
230
|
+
textColor = '#000000';
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Si pressé
|
|
234
|
+
if (this.pressedIndex === i) {
|
|
235
|
+
backgroundColor = this.selectedIndex === i ? '#0056CC' : '#D9D9D9';
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Dessiner le fond du bouton
|
|
240
|
+
ctx.fillStyle = backgroundColor;
|
|
241
|
+
|
|
242
|
+
// Coins arrondis selon la position
|
|
243
|
+
ctx.beginPath();
|
|
244
|
+
|
|
245
|
+
if (i === 0 && i === this.buttons.length - 1) {
|
|
246
|
+
// Un seul bouton - tous les coins arrondis
|
|
247
|
+
ctx.moveTo(btnX + radius, this.y);
|
|
248
|
+
ctx.lineTo(btnX + btnWidth - radius, this.y);
|
|
249
|
+
ctx.quadraticCurveTo(btnX + btnWidth, this.y, btnX + btnWidth, this.y + radius);
|
|
250
|
+
ctx.lineTo(btnX + btnWidth, this.y + this.height - radius);
|
|
251
|
+
ctx.quadraticCurveTo(btnX + btnWidth, this.y + this.height, btnX + btnWidth - radius, this.y + this.height);
|
|
252
|
+
ctx.lineTo(btnX + radius, this.y + this.height);
|
|
253
|
+
ctx.quadraticCurveTo(btnX, this.y + this.height, btnX, this.y + this.height - radius);
|
|
254
|
+
ctx.lineTo(btnX, this.y + radius);
|
|
255
|
+
ctx.quadraticCurveTo(btnX, this.y, btnX + radius, this.y);
|
|
256
|
+
} else if (i === 0) {
|
|
257
|
+
// Premier bouton - coins gauche arrondis
|
|
258
|
+
ctx.moveTo(btnX + radius, this.y);
|
|
259
|
+
ctx.lineTo(btnX + btnWidth, this.y);
|
|
260
|
+
ctx.lineTo(btnX + btnWidth, this.y + this.height);
|
|
261
|
+
ctx.lineTo(btnX + radius, this.y + this.height);
|
|
262
|
+
ctx.quadraticCurveTo(btnX, this.y + this.height, btnX, this.y + this.height - radius);
|
|
263
|
+
ctx.lineTo(btnX, this.y + radius);
|
|
264
|
+
ctx.quadraticCurveTo(btnX, this.y, btnX + radius, this.y);
|
|
265
|
+
} else if (i === this.buttons.length - 1) {
|
|
266
|
+
// Dernier bouton - coins droit arrondis
|
|
267
|
+
ctx.moveTo(btnX, this.y);
|
|
268
|
+
ctx.lineTo(btnX + btnWidth - radius, this.y);
|
|
269
|
+
ctx.quadraticCurveTo(btnX + btnWidth, this.y, btnX + btnWidth, this.y + radius);
|
|
270
|
+
ctx.lineTo(btnX + btnWidth, this.y + this.height - radius);
|
|
271
|
+
ctx.quadraticCurveTo(btnX + btnWidth, this.y + this.height, btnX + btnWidth - radius, this.y + this.height);
|
|
272
|
+
ctx.lineTo(btnX, this.y + this.height);
|
|
273
|
+
} else {
|
|
274
|
+
// Bouton du milieu - coins carrés
|
|
275
|
+
ctx.rect(btnX, this.y, btnWidth, this.height);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
ctx.closePath();
|
|
279
|
+
ctx.fill();
|
|
280
|
+
|
|
281
|
+
// Dessiner les ripples Material
|
|
282
|
+
if (this.platform === 'material') {
|
|
283
|
+
for (const ripple of this.ripples) {
|
|
284
|
+
if (ripple.index === i) {
|
|
285
|
+
ctx.save();
|
|
286
|
+
|
|
287
|
+
// Clip sur le bouton pour que le ripple ne dépasse pas
|
|
288
|
+
ctx.beginPath();
|
|
289
|
+
if (i === 0) {
|
|
290
|
+
ctx.moveTo(btnX + radius, this.y);
|
|
291
|
+
ctx.lineTo(btnX + btnWidth, this.y);
|
|
292
|
+
ctx.lineTo(btnX + btnWidth, this.y + this.height);
|
|
293
|
+
ctx.lineTo(btnX + radius, this.y + this.height);
|
|
294
|
+
ctx.quadraticCurveTo(btnX, this.y + this.height, btnX, this.y + this.height - radius);
|
|
295
|
+
ctx.lineTo(btnX, this.y + radius);
|
|
296
|
+
ctx.quadraticCurveTo(btnX, this.y, btnX + radius, this.y);
|
|
297
|
+
} else if (i === this.buttons.length - 1) {
|
|
298
|
+
ctx.moveTo(btnX, this.y);
|
|
299
|
+
ctx.lineTo(btnX + btnWidth - radius, this.y);
|
|
300
|
+
ctx.quadraticCurveTo(btnX + btnWidth, this.y, btnX + btnWidth, this.y + radius);
|
|
301
|
+
ctx.lineTo(btnX + btnWidth, this.y + this.height - radius);
|
|
302
|
+
ctx.quadraticCurveTo(btnX + btnWidth, this.y + this.height, btnX + btnWidth - radius, this.y + this.height);
|
|
303
|
+
ctx.lineTo(btnX, this.y + this.height);
|
|
304
|
+
} else {
|
|
305
|
+
ctx.rect(btnX, this.y, btnWidth, this.height);
|
|
306
|
+
}
|
|
307
|
+
ctx.closePath();
|
|
308
|
+
ctx.clip();
|
|
309
|
+
|
|
310
|
+
// Dessiner le ripple
|
|
311
|
+
ctx.fillStyle = `rgba(255, 255, 255, ${ripple.opacity})`;
|
|
312
|
+
ctx.beginPath();
|
|
313
|
+
ctx.arc(btnX + ripple.x, this.y + ripple.y, ripple.radius, 0, Math.PI * 2);
|
|
314
|
+
ctx.fill();
|
|
315
|
+
|
|
316
|
+
ctx.restore();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Dessiner le texte
|
|
322
|
+
ctx.fillStyle = textColor;
|
|
323
|
+
ctx.font = `500 ${this.height / 2.5}px -apple-system, Roboto, sans-serif`;
|
|
324
|
+
ctx.textAlign = 'center';
|
|
325
|
+
ctx.textBaseline = 'middle';
|
|
326
|
+
ctx.fillText(btn.text || `Button ${i + 1}`, btnX + btnWidth / 2, this.y + this.height / 2);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Dessiner les séparateurs (pour iOS)
|
|
330
|
+
if (this.platform === 'cupertino' && this.buttons.length > 1) {
|
|
331
|
+
ctx.strokeStyle = '#C7C7CC';
|
|
332
|
+
ctx.lineWidth = 1;
|
|
333
|
+
|
|
334
|
+
for (let i = 1; i < this.buttons.length; i++) {
|
|
335
|
+
const separatorX = this.x + i * btnWidth + (i - 1) * this.spacing;
|
|
336
|
+
ctx.beginPath();
|
|
337
|
+
ctx.moveTo(separatorX, this.y + 8);
|
|
338
|
+
ctx.lineTo(separatorX, this.y + this.height - 8);
|
|
339
|
+
ctx.stroke();
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
ctx.restore();
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Vérifie si un point est dans le contrôle
|
|
348
|
+
* @param {number} x - Coordonnée X
|
|
349
|
+
* @param {number} y - Coordonnée Y
|
|
350
|
+
* @returns {boolean} True si un segment est touché
|
|
351
|
+
*/
|
|
352
|
+
isPointInside(x, y) {
|
|
353
|
+
return this.getButtonIndexAt(x, y) !== null;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export default SegmentedControl;
|