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,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,202 @@
|
|
|
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
|
+
* Plateforme : "material" ou "cupertino"
|
|
19
|
+
* @type {string}
|
|
20
|
+
*/
|
|
21
|
+
this.platform = framework.platform;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Liste des boutons
|
|
25
|
+
* @type {Array<{text: string, onClick?: Function}>}
|
|
26
|
+
*/
|
|
27
|
+
this.buttons = options.buttons || [{ text: 'One' }, { text: 'Two' }, { text: 'Three' }];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Index du segment sélectionné
|
|
31
|
+
* @type {number}
|
|
32
|
+
*/
|
|
33
|
+
this.selectedIndex = options.selectedIndex || 0;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Hauteur du contrôle
|
|
37
|
+
* @type {number}
|
|
38
|
+
*/
|
|
39
|
+
this.height = options.height || 40;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Espacement entre segments
|
|
43
|
+
* @type {number}
|
|
44
|
+
*/
|
|
45
|
+
this.spacing = options.spacing || 1;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Ripples Material
|
|
49
|
+
* @type {Array<{x: number, y: number, index: number, radius: number, maxRadius: number, opacity: number}>}
|
|
50
|
+
*/
|
|
51
|
+
this.ripples = [];
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Index temporaire pressé pour Cupertino
|
|
55
|
+
* @type {number|null}
|
|
56
|
+
*/
|
|
57
|
+
this.pressedIndex = null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Gère la pression sur un segment
|
|
62
|
+
* @param {number} x - Coordonnée X du clic
|
|
63
|
+
* @param {number} y - Coordonnée Y du clic
|
|
64
|
+
*/
|
|
65
|
+
handlePress(x, y) {
|
|
66
|
+
const index = this.getButtonIndexAt(x, y);
|
|
67
|
+
if (index !== null) {
|
|
68
|
+
if (this.platform === 'material') {
|
|
69
|
+
const btnWidth = (this.width - this.spacing * (this.buttons.length - 1)) / this.buttons.length;
|
|
70
|
+
const btnX = this.x + index * (btnWidth + this.spacing);
|
|
71
|
+
this.ripples.push({
|
|
72
|
+
x: x - btnX,
|
|
73
|
+
y: y - this.y,
|
|
74
|
+
index: index,
|
|
75
|
+
radius: 0,
|
|
76
|
+
maxRadius: Math.max(btnWidth, this.height) * 1.5,
|
|
77
|
+
opacity: 1
|
|
78
|
+
});
|
|
79
|
+
this.animateRipple();
|
|
80
|
+
} else {
|
|
81
|
+
this.pressedIndex = index;
|
|
82
|
+
setTimeout(() => this.pressedIndex = null, 150);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.selectedIndex = index;
|
|
86
|
+
if (this.buttons[index].onClick) this.buttons[index].onClick(index);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Anime les ripples Material
|
|
92
|
+
* @private
|
|
93
|
+
*/
|
|
94
|
+
animateRipple() {
|
|
95
|
+
const animate = () => {
|
|
96
|
+
let active = false;
|
|
97
|
+
for (let ripple of this.ripples) {
|
|
98
|
+
if (ripple.radius < ripple.maxRadius) {
|
|
99
|
+
ripple.radius += ripple.maxRadius / 15;
|
|
100
|
+
active = true;
|
|
101
|
+
}
|
|
102
|
+
if (ripple.radius >= ripple.maxRadius * 0.5) ripple.opacity -= 0.05;
|
|
103
|
+
}
|
|
104
|
+
this.ripples = this.ripples.filter(r => r.opacity > 0);
|
|
105
|
+
if (active) requestAnimationFrame(animate);
|
|
106
|
+
};
|
|
107
|
+
animate();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Retourne l'index du bouton sous un point donné
|
|
112
|
+
* @param {number} x - Coordonnée X
|
|
113
|
+
* @param {number} y - Coordonnée Y
|
|
114
|
+
* @returns {number|null} Index du segment ou null
|
|
115
|
+
* @private
|
|
116
|
+
*/
|
|
117
|
+
getButtonIndexAt(x, y) {
|
|
118
|
+
const btnWidth = (this.width - this.spacing * (this.buttons.length - 1)) / this.buttons.length;
|
|
119
|
+
if (y < this.y || y > this.y + this.height) return null;
|
|
120
|
+
for (let i = 0; i < this.buttons.length; i++) {
|
|
121
|
+
const btnX = this.x + i * (btnWidth + this.spacing);
|
|
122
|
+
if (x >= btnX && x <= btnX + btnWidth) return i;
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Dessine le SegmentedControl
|
|
129
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
130
|
+
*/
|
|
131
|
+
draw(ctx) {
|
|
132
|
+
const btnWidth = (this.width - this.spacing * (this.buttons.length - 1)) / this.buttons.length;
|
|
133
|
+
|
|
134
|
+
this.buttons.forEach((btn, i) => {
|
|
135
|
+
const btnX = this.x + i * (btnWidth + this.spacing);
|
|
136
|
+
|
|
137
|
+
// Background
|
|
138
|
+
if (this.platform === 'material') {
|
|
139
|
+
ctx.fillStyle = this.selectedIndex === i ? '#6200EE' : '#E0E0E0';
|
|
140
|
+
} else {
|
|
141
|
+
ctx.fillStyle = this.selectedIndex === i ? '#007AFF' : '#F0F0F0';
|
|
142
|
+
if (this.pressedIndex === i) ctx.fillStyle = '#D9D9D9';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const radius = this.height / 2;
|
|
146
|
+
ctx.beginPath();
|
|
147
|
+
if (i === 0) {
|
|
148
|
+
ctx.moveTo(btnX + radius, this.y);
|
|
149
|
+
ctx.lineTo(btnX + btnWidth, this.y);
|
|
150
|
+
ctx.lineTo(btnX + btnWidth, this.y + this.height);
|
|
151
|
+
ctx.lineTo(btnX + radius, this.y + this.height);
|
|
152
|
+
ctx.quadraticCurveTo(btnX, this.y + this.height, btnX, this.y + this.height - radius);
|
|
153
|
+
ctx.lineTo(btnX, this.y + radius);
|
|
154
|
+
ctx.quadraticCurveTo(btnX, this.y, btnX + radius, this.y);
|
|
155
|
+
} else if (i === this.buttons.length - 1) {
|
|
156
|
+
ctx.moveTo(btnX, this.y);
|
|
157
|
+
ctx.lineTo(btnX + btnWidth - radius, this.y);
|
|
158
|
+
ctx.quadraticCurveTo(btnX + btnWidth, this.y, btnX + btnWidth, this.y + radius);
|
|
159
|
+
ctx.lineTo(btnX + btnWidth, this.y + this.height - radius);
|
|
160
|
+
ctx.quadraticCurveTo(btnX + btnWidth, this.y + this.height, btnX + btnWidth - radius, this.y + this.height);
|
|
161
|
+
ctx.lineTo(btnX, this.y + this.height);
|
|
162
|
+
} else {
|
|
163
|
+
ctx.rect(btnX, this.y, btnWidth, this.height);
|
|
164
|
+
}
|
|
165
|
+
ctx.fill();
|
|
166
|
+
|
|
167
|
+
// Texte
|
|
168
|
+
ctx.fillStyle = this.platform === 'material'
|
|
169
|
+
? (this.selectedIndex === i ? '#FFF' : '#000')
|
|
170
|
+
: (this.selectedIndex === i ? '#FFF' : '#000');
|
|
171
|
+
ctx.font = `${this.height / 2}px -apple-system, Roboto, sans-serif`;
|
|
172
|
+
ctx.textAlign = 'center';
|
|
173
|
+
ctx.textBaseline = 'middle';
|
|
174
|
+
ctx.fillText(btn.text || `Button ${i + 1}`, btnX + btnWidth / 2, this.y + this.height / 2);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Ripples Material
|
|
178
|
+
if (this.platform === 'material' && this.ripples.length) {
|
|
179
|
+
ctx.save();
|
|
180
|
+
this.ripples.forEach(r => {
|
|
181
|
+
const btnX = this.x + r.index * (btnWidth + this.spacing);
|
|
182
|
+
ctx.beginPath();
|
|
183
|
+
ctx.arc(btnX + r.x, this.y + r.y, r.radius, 0, Math.PI * 2);
|
|
184
|
+
ctx.fillStyle = `rgba(255,255,255,${r.opacity})`;
|
|
185
|
+
ctx.fill();
|
|
186
|
+
});
|
|
187
|
+
ctx.restore();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Vérifie si un point est dans le contrôle
|
|
193
|
+
* @param {number} x - Coordonnée X
|
|
194
|
+
* @param {number} y - Coordonnée Y
|
|
195
|
+
* @returns {boolean} True si un segment est touché
|
|
196
|
+
*/
|
|
197
|
+
isPointInside(x, y) {
|
|
198
|
+
return this.getButtonIndexAt(x, y) !== null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export default SegmentedControl;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import Component from '../core/Component.js';
|
|
2
|
+
import SelectDialog from '../components/SelectDialog.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Composant de sélection déroulante (dropdown)
|
|
6
|
+
* @class
|
|
7
|
+
* @extends Component
|
|
8
|
+
* @param {Framework} framework - Instance du framework
|
|
9
|
+
* @param {Object} [options={}] - Options de configuration
|
|
10
|
+
* @param {string[]} [options.options=[]] - Liste des options
|
|
11
|
+
* @param {number} [options.selectedIndex=0] - Index de l'option sélectionnée
|
|
12
|
+
* @param {string} [options.placeholder='Select...'] - Texte par défaut
|
|
13
|
+
* @param {number} [options.fontSize=16] - Taille de police
|
|
14
|
+
* @param {Function} [options.onChange] - Callback lors du changement de sélection
|
|
15
|
+
* @example
|
|
16
|
+
* const select = new Select(framework, {
|
|
17
|
+
* options: ['Option 1', 'Option 2', 'Option 3'],
|
|
18
|
+
* placeholder: 'Choisissez une option',
|
|
19
|
+
* onChange: (value, index) => console.log('Selected:', value)
|
|
20
|
+
* });
|
|
21
|
+
*/
|
|
22
|
+
class Select extends Component {
|
|
23
|
+
/**
|
|
24
|
+
* @constructs Select
|
|
25
|
+
*/
|
|
26
|
+
constructor(framework, options = {}) {
|
|
27
|
+
super(framework, options);
|
|
28
|
+
/** @type {string[]} */
|
|
29
|
+
this.options = options.options || [];
|
|
30
|
+
/** @type {number} */
|
|
31
|
+
this.selectedIndex = options.selectedIndex || 0;
|
|
32
|
+
/** @type {string} */
|
|
33
|
+
this.placeholder = options.placeholder || 'Select...';
|
|
34
|
+
/** @type {string} */
|
|
35
|
+
this.platform = framework.platform;
|
|
36
|
+
/** @type {number} */
|
|
37
|
+
this.fontSize = options.fontSize || 16;
|
|
38
|
+
/** @type {Function|undefined} */
|
|
39
|
+
this.onChange = options.onChange;
|
|
40
|
+
/** @type {boolean} */
|
|
41
|
+
this.isOpen = false;
|
|
42
|
+
/** @type {SelectDialog|null} */
|
|
43
|
+
this.dialog = null;
|
|
44
|
+
|
|
45
|
+
// Définir onClick pour le Select
|
|
46
|
+
this.onClick = this.toggleMenu.bind(this);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Ouvre ou ferme le menu de sélection
|
|
51
|
+
*/
|
|
52
|
+
toggleMenu() {
|
|
53
|
+
if (this.isOpen && this.dialog) {
|
|
54
|
+
this.closeMenu();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.openMenu();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Ouvre le menu de sélection (affiche le modal)
|
|
63
|
+
*/
|
|
64
|
+
openMenu() {
|
|
65
|
+
if (this.isOpen) return;
|
|
66
|
+
|
|
67
|
+
this.dialog = new SelectDialog(this.framework, {
|
|
68
|
+
title: this.placeholder,
|
|
69
|
+
options: this.options,
|
|
70
|
+
selectedIndex: this.selectedIndex,
|
|
71
|
+
onSelect: (index, value) => {
|
|
72
|
+
this.selectedIndex = index;
|
|
73
|
+
if (this.onChange) {
|
|
74
|
+
this.onChange(value, index);
|
|
75
|
+
}
|
|
76
|
+
this.closeMenu();
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
this.framework.add(this.dialog);
|
|
81
|
+
this.isOpen = true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Ferme le menu de sélection
|
|
86
|
+
*/
|
|
87
|
+
closeMenu() {
|
|
88
|
+
if (this.dialog) {
|
|
89
|
+
this.dialog.hide();
|
|
90
|
+
this.dialog = null;
|
|
91
|
+
}
|
|
92
|
+
this.isOpen = false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Dessine le composant Select
|
|
97
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
98
|
+
*/
|
|
99
|
+
draw(ctx) {
|
|
100
|
+
ctx.save();
|
|
101
|
+
|
|
102
|
+
const selectedValue = this.options[this.selectedIndex] || this.placeholder;
|
|
103
|
+
|
|
104
|
+
if (this.platform === 'material') {
|
|
105
|
+
// Material Design Select
|
|
106
|
+
ctx.fillStyle = this.pressed ? '#F5F5F5' : '#FFFFFF';
|
|
107
|
+
ctx.fillRect(this.x, this.y, this.width, this.height);
|
|
108
|
+
|
|
109
|
+
ctx.strokeStyle = this.isOpen ? '#6200EE' : '#CCCCCC';
|
|
110
|
+
ctx.lineWidth = this.isOpen ? 2 : 1;
|
|
111
|
+
ctx.strokeRect(this.x, this.y, this.width, this.height);
|
|
112
|
+
|
|
113
|
+
// Texte
|
|
114
|
+
ctx.fillStyle = selectedValue === this.placeholder ? '#999999' : '#000000';
|
|
115
|
+
ctx.font = `${this.fontSize}px Roboto, sans-serif`;
|
|
116
|
+
ctx.textAlign = 'left';
|
|
117
|
+
ctx.textBaseline = 'middle';
|
|
118
|
+
ctx.fillText(selectedValue, this.x + 15, this.y + this.height / 2);
|
|
119
|
+
|
|
120
|
+
// Flèche
|
|
121
|
+
ctx.fillStyle = '#666666';
|
|
122
|
+
const arrowX = this.x + this.width - 20;
|
|
123
|
+
const arrowY = this.y + this.height / 2;
|
|
124
|
+
ctx.beginPath();
|
|
125
|
+
ctx.moveTo(arrowX - 5, arrowY - 3);
|
|
126
|
+
ctx.lineTo(arrowX + 5, arrowY - 3);
|
|
127
|
+
ctx.lineTo(arrowX, arrowY + 3);
|
|
128
|
+
ctx.closePath();
|
|
129
|
+
ctx.fill();
|
|
130
|
+
} else {
|
|
131
|
+
// Cupertino Select
|
|
132
|
+
ctx.fillStyle = '#FFFFFF';
|
|
133
|
+
ctx.beginPath();
|
|
134
|
+
this.roundRect(ctx, this.x, this.y, this.width, this.height, 8);
|
|
135
|
+
ctx.fill();
|
|
136
|
+
|
|
137
|
+
ctx.strokeStyle = this.isOpen ? '#007AFF' : '#C7C7CC';
|
|
138
|
+
ctx.lineWidth = 1;
|
|
139
|
+
ctx.stroke();
|
|
140
|
+
|
|
141
|
+
// Texte
|
|
142
|
+
ctx.fillStyle = selectedValue === this.placeholder ? '#999999' : '#000000';
|
|
143
|
+
ctx.font = `${this.fontSize}px -apple-system, sans-serif`;
|
|
144
|
+
ctx.textAlign = 'left';
|
|
145
|
+
ctx.textBaseline = 'middle';
|
|
146
|
+
ctx.fillText(selectedValue, this.x + 15, this.y + this.height / 2);
|
|
147
|
+
|
|
148
|
+
// Chevron
|
|
149
|
+
ctx.strokeStyle = '#007AFF';
|
|
150
|
+
ctx.lineWidth = 2;
|
|
151
|
+
const chevronX = this.x + this.width - 20;
|
|
152
|
+
const chevronY = this.y + this.height / 2;
|
|
153
|
+
ctx.beginPath();
|
|
154
|
+
ctx.moveTo(chevronX - 5, chevronY - 3);
|
|
155
|
+
ctx.lineTo(chevronX, chevronY + 2);
|
|
156
|
+
ctx.lineTo(chevronX + 5, chevronY - 3);
|
|
157
|
+
ctx.stroke();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
ctx.restore();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Dessine un rectangle avec des 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
|
+
* Vérifie si un point est à l'intérieur du composant
|
|
187
|
+
* @param {number} x - Position X
|
|
188
|
+
* @param {number} y - Position Y
|
|
189
|
+
* @returns {boolean} True si le point est à l'intérieur
|
|
190
|
+
*/
|
|
191
|
+
isPointInside(x, y) {
|
|
192
|
+
return x >= this.x &&
|
|
193
|
+
x <= this.x + this.width &&
|
|
194
|
+
y >= this.y &&
|
|
195
|
+
y <= this.y + this.height;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export default Select;
|