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.
Files changed (85) hide show
  1. package/README.md +554 -0
  2. package/components/Accordion.js +252 -0
  3. package/components/AndroidDatePickerDialog.js +398 -0
  4. package/components/AppBar.js +225 -0
  5. package/components/Avatar.js +202 -0
  6. package/components/BottomNavigationBar.js +205 -0
  7. package/components/BottomSheet.js +374 -0
  8. package/components/Button.js +225 -0
  9. package/components/Card.js +193 -0
  10. package/components/Checkbox.js +180 -0
  11. package/components/Chip.js +212 -0
  12. package/components/CircularProgress.js +143 -0
  13. package/components/ContextMenu.js +116 -0
  14. package/components/DatePicker.js +257 -0
  15. package/components/Dialog.js +367 -0
  16. package/components/Divider.js +125 -0
  17. package/components/Drawer.js +261 -0
  18. package/components/FAB.js +270 -0
  19. package/components/FileUpload.js +315 -0
  20. package/components/IOSDatePickerWheel.js +268 -0
  21. package/components/ImageCarousel.js +193 -0
  22. package/components/ImageComponent.js +223 -0
  23. package/components/Input.js +309 -0
  24. package/components/List.js +94 -0
  25. package/components/ListItem.js +223 -0
  26. package/components/Modal.js +364 -0
  27. package/components/MultiSelectDialog.js +206 -0
  28. package/components/NumberInput.js +271 -0
  29. package/components/ProgressBar.js +88 -0
  30. package/components/RadioButton.js +142 -0
  31. package/components/SearchInput.js +315 -0
  32. package/components/SegmentedControl.js +202 -0
  33. package/components/Select.js +199 -0
  34. package/components/SelectDialog.js +255 -0
  35. package/components/Slider.js +113 -0
  36. package/components/Snackbar.js +243 -0
  37. package/components/Stepper.js +281 -0
  38. package/components/SwipeableListItem.js +179 -0
  39. package/components/Switch.js +147 -0
  40. package/components/Table.js +492 -0
  41. package/components/Tabs.js +125 -0
  42. package/components/Text.js +141 -0
  43. package/components/TextField.js +331 -0
  44. package/components/Toast.js +236 -0
  45. package/components/TreeView.js +420 -0
  46. package/components/Video.js +397 -0
  47. package/components/View.js +140 -0
  48. package/components/VirtualList.js +120 -0
  49. package/core/CanvasFramework.js +1271 -0
  50. package/core/CanvasWork.js +32 -0
  51. package/core/Component.js +153 -0
  52. package/core/LogicWorker.js +25 -0
  53. package/core/WebGLCanvasAdapter.js +1369 -0
  54. package/features/Column.js +43 -0
  55. package/features/Grid.js +47 -0
  56. package/features/LayoutComponent.js +43 -0
  57. package/features/OpenStreetMap.js +310 -0
  58. package/features/Positioned.js +33 -0
  59. package/features/PullToRefresh.js +328 -0
  60. package/features/Row.js +40 -0
  61. package/features/SignaturePad.js +257 -0
  62. package/features/Skeleton.js +84 -0
  63. package/features/Stack.js +21 -0
  64. package/index.js +101 -0
  65. package/manager/AccessibilityManager.js +107 -0
  66. package/manager/ErrorHandler.js +59 -0
  67. package/manager/FeatureFlags.js +60 -0
  68. package/manager/MemoryManager.js +107 -0
  69. package/manager/PerformanceMonitor.js +84 -0
  70. package/manager/SecurityManager.js +54 -0
  71. package/package.json +28 -0
  72. package/utils/AnimationEngine.js +428 -0
  73. package/utils/DataStore.js +403 -0
  74. package/utils/EventBus.js +407 -0
  75. package/utils/FetchClient.js +74 -0
  76. package/utils/FormValidator.js +355 -0
  77. package/utils/GeoLocationService.js +62 -0
  78. package/utils/I18n.js +207 -0
  79. package/utils/IndexedDBManager.js +273 -0
  80. package/utils/OfflineSyncManager.js +342 -0
  81. package/utils/QueryBuilder.js +478 -0
  82. package/utils/SafeArea.js +64 -0
  83. package/utils/SecureStorage.js +289 -0
  84. package/utils/StateManager.js +207 -0
  85. 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;