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,430 @@
|
|
|
1
|
+
import Component from '../core/Component.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sélecteur de date iOS (style roue)
|
|
5
|
+
* @class
|
|
6
|
+
* @extends Component
|
|
7
|
+
* @property {Date} selectedDate - Date sélectionnée
|
|
8
|
+
* @property {Function} onChange - Callback au changement
|
|
9
|
+
* @property {number} monthWheel - Mois sélectionné
|
|
10
|
+
* @property {number} dayWheel - Jour sélectionné
|
|
11
|
+
* @property {number} yearWheel - Année sélectionnée
|
|
12
|
+
* @property {number} wheelHeight - Hauteur de la roue
|
|
13
|
+
* @property {number} itemHeight - Hauteur d'un item
|
|
14
|
+
* @property {number} visibleItems - Nombre d'items visibles
|
|
15
|
+
*/
|
|
16
|
+
class IOSDatePickerWheel extends Component {
|
|
17
|
+
/**
|
|
18
|
+
* Crée une instance de IOSDatePickerWheel
|
|
19
|
+
* @param {CanvasFramework} framework - Framework parent
|
|
20
|
+
* @param {Object} [options={}] - Options de configuration
|
|
21
|
+
* @param {Date} [options.selectedDate=new Date()] - Date initiale
|
|
22
|
+
* @param {Function} [options.onChange] - Callback au changement
|
|
23
|
+
*/
|
|
24
|
+
constructor(framework, options = {}) {
|
|
25
|
+
super(framework, options);
|
|
26
|
+
this.selectedDate = options.selectedDate || new Date();
|
|
27
|
+
this.onChange = options.onChange;
|
|
28
|
+
|
|
29
|
+
// Roues de sélection
|
|
30
|
+
this.monthWheel = this.selectedDate.getMonth();
|
|
31
|
+
this.dayWheel = this.selectedDate.getDate();
|
|
32
|
+
this.yearWheel = this.selectedDate.getFullYear();
|
|
33
|
+
|
|
34
|
+
this.wheelHeight = 200;
|
|
35
|
+
this.itemHeight = 40;
|
|
36
|
+
this.visibleItems = 5;
|
|
37
|
+
|
|
38
|
+
// État interne
|
|
39
|
+
this.dragging = false;
|
|
40
|
+
this.dragStartY = 0;
|
|
41
|
+
this.dragWheel = null;
|
|
42
|
+
this.lastY = 0;
|
|
43
|
+
|
|
44
|
+
// Configuration des limites
|
|
45
|
+
this._setupLimits();
|
|
46
|
+
|
|
47
|
+
// Setup des handlers
|
|
48
|
+
this._setupEventHandlers();
|
|
49
|
+
|
|
50
|
+
// Dimensions
|
|
51
|
+
if (!options.width) {
|
|
52
|
+
this.width = framework.width - 40;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!options.height) {
|
|
56
|
+
this.height = this.wheelHeight;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Configure les limites pour chaque roue
|
|
62
|
+
* @private
|
|
63
|
+
*/
|
|
64
|
+
_setupLimits() {
|
|
65
|
+
// Mois: 0-11 (Janvier à Décembre)
|
|
66
|
+
this.monthMin = 0;
|
|
67
|
+
this.monthMax = 11;
|
|
68
|
+
|
|
69
|
+
// Jour: 1-31 (selon le mois et l'année, on ajustera dynamiquement)
|
|
70
|
+
this.dayMin = 1;
|
|
71
|
+
this.dayMax = 31;
|
|
72
|
+
|
|
73
|
+
// Année: 1900-2100 par défaut
|
|
74
|
+
this.yearMin = 1900;
|
|
75
|
+
this.yearMax = 2100;
|
|
76
|
+
|
|
77
|
+
// Mettre à jour les limites du jour en fonction du mois et de l'année
|
|
78
|
+
this._updateDayLimits();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Met à jour les limites du jour en fonction du mois et de l'année
|
|
83
|
+
* @private
|
|
84
|
+
*/
|
|
85
|
+
_updateDayLimits() {
|
|
86
|
+
// Nombre de jours dans le mois actuel
|
|
87
|
+
const daysInMonth = new Date(this.yearWheel, this.monthWheel + 1, 0).getDate();
|
|
88
|
+
this.dayMax = daysInMonth;
|
|
89
|
+
|
|
90
|
+
// Ajuster le jour sélectionné si nécessaire
|
|
91
|
+
if (this.dayWheel > daysInMonth) {
|
|
92
|
+
this.dayWheel = daysInMonth;
|
|
93
|
+
this._updateSelectedDate();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Configure les gestionnaires d'événements
|
|
99
|
+
* @private
|
|
100
|
+
*/
|
|
101
|
+
_setupEventHandlers() {
|
|
102
|
+
// Handler press
|
|
103
|
+
this.onPress = (x, y) => {
|
|
104
|
+
// Vérifier si dans le composant
|
|
105
|
+
const inside = (x >= this.x && x <= this.x + this.width &&
|
|
106
|
+
y >= this.y && y <= this.y + this.wheelHeight);
|
|
107
|
+
|
|
108
|
+
if (!inside) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Activer le drag
|
|
113
|
+
this.dragging = true;
|
|
114
|
+
this.dragStartY = y;
|
|
115
|
+
this.lastY = y;
|
|
116
|
+
|
|
117
|
+
// Déterminer la roue
|
|
118
|
+
const wheelWidth = this.width / 3;
|
|
119
|
+
if (x < this.x + wheelWidth) {
|
|
120
|
+
this.dragWheel = 0; // Mois
|
|
121
|
+
} else if (x < this.x + wheelWidth * 2) {
|
|
122
|
+
this.dragWheel = 1; // Jour
|
|
123
|
+
} else {
|
|
124
|
+
this.dragWheel = 2; // Année
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Prendre le contrôle
|
|
128
|
+
this.framework.activeComponent = this;
|
|
129
|
+
|
|
130
|
+
// Ajouter l'écouteur global pour les mouvements
|
|
131
|
+
this._addGlobalMoveListener();
|
|
132
|
+
|
|
133
|
+
// Forcer le redessin
|
|
134
|
+
this._requestRedraw();
|
|
135
|
+
|
|
136
|
+
return true;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// Handler release
|
|
140
|
+
this.onRelease = (x, y) => {
|
|
141
|
+
if (this.dragging) {
|
|
142
|
+
this.dragging = false;
|
|
143
|
+
this.dragWheel = null;
|
|
144
|
+
|
|
145
|
+
// Retirer l'écouteur global
|
|
146
|
+
this._removeGlobalMoveListener();
|
|
147
|
+
|
|
148
|
+
// Relâcher le contrôle
|
|
149
|
+
if (this.framework.activeComponent === this) {
|
|
150
|
+
this.framework.activeComponent = null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
this._requestRedraw();
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// Handler move du framework
|
|
158
|
+
this.onMove = (x, y) => {
|
|
159
|
+
// Laissé vide, on utilise l'écouteur global
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Ajoute un écouteur global pour les mouvements
|
|
165
|
+
* @private
|
|
166
|
+
*/
|
|
167
|
+
_addGlobalMoveListener() {
|
|
168
|
+
const canvas = this.framework.canvas;
|
|
169
|
+
|
|
170
|
+
// Sauvegarder les anciens handlers
|
|
171
|
+
this._savedMouseMove = canvas.onmousemove;
|
|
172
|
+
this._savedTouchMove = canvas.ontouchmove;
|
|
173
|
+
|
|
174
|
+
// Overrider les handlers
|
|
175
|
+
canvas.onmousemove = (e) => {
|
|
176
|
+
if (this.dragging) {
|
|
177
|
+
e.preventDefault();
|
|
178
|
+
e.stopPropagation();
|
|
179
|
+
|
|
180
|
+
const rect = canvas.getBoundingClientRect();
|
|
181
|
+
const x = e.clientX - rect.left;
|
|
182
|
+
const y = e.clientY - rect.top;
|
|
183
|
+
|
|
184
|
+
this._handleGlobalMove(x, y);
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Appeler le handler original si on ne drag pas
|
|
189
|
+
if (this._savedMouseMove) {
|
|
190
|
+
return this._savedMouseMove(e);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
canvas.ontouchmove = (e) => {
|
|
195
|
+
if (this.dragging && e.touches.length > 0) {
|
|
196
|
+
e.preventDefault();
|
|
197
|
+
e.stopPropagation();
|
|
198
|
+
|
|
199
|
+
const touch = e.touches[0];
|
|
200
|
+
const rect = canvas.getBoundingClientRect();
|
|
201
|
+
const x = touch.clientX - rect.left;
|
|
202
|
+
const y = touch.clientY - rect.top;
|
|
203
|
+
|
|
204
|
+
this._handleGlobalMove(x, y);
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (this._savedTouchMove) {
|
|
209
|
+
return this._savedTouchMove(e);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Retire l'écouteur global
|
|
216
|
+
* @private
|
|
217
|
+
*/
|
|
218
|
+
_removeGlobalMoveListener() {
|
|
219
|
+
const canvas = this.framework.canvas;
|
|
220
|
+
|
|
221
|
+
if (this._savedMouseMove) {
|
|
222
|
+
canvas.onmousemove = this._savedMouseMove;
|
|
223
|
+
this._savedMouseMove = null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (this._savedTouchMove) {
|
|
227
|
+
canvas.ontouchmove = this._savedTouchMove;
|
|
228
|
+
this._savedTouchMove = null;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Gestionnaire de mouvement global
|
|
234
|
+
* @private
|
|
235
|
+
*/
|
|
236
|
+
_handleGlobalMove(x, y) {
|
|
237
|
+
if (!this.dragging) return;
|
|
238
|
+
|
|
239
|
+
// Calculer le delta
|
|
240
|
+
const deltaY = y - this.lastY;
|
|
241
|
+
this.lastY = y;
|
|
242
|
+
|
|
243
|
+
// Appliquer le scroll si mouvement significatif
|
|
244
|
+
if (Math.abs(deltaY) > 0.5) {
|
|
245
|
+
const direction = deltaY > 0 ? 1 : -1;
|
|
246
|
+
|
|
247
|
+
// Appliquer le déplacement selon la roue avec limites
|
|
248
|
+
if (this.dragWheel === 0) {
|
|
249
|
+
// Mois - avec bouclage
|
|
250
|
+
let newMonth = this.monthWheel - direction;
|
|
251
|
+
if (newMonth < this.monthMin) newMonth = this.monthMax;
|
|
252
|
+
if (newMonth > this.monthMax) newMonth = this.monthMin;
|
|
253
|
+
this.monthWheel = newMonth;
|
|
254
|
+
|
|
255
|
+
// Mettre à jour les limites du jour après changement de mois
|
|
256
|
+
this._updateDayLimits();
|
|
257
|
+
}
|
|
258
|
+
else if (this.dragWheel === 1) {
|
|
259
|
+
// Jour - avec bouclage
|
|
260
|
+
let newDay = this.dayWheel - direction;
|
|
261
|
+
if (newDay < this.dayMin) newDay = this.dayMax;
|
|
262
|
+
if (newDay > this.dayMax) newDay = this.dayMin;
|
|
263
|
+
this.dayWheel = newDay;
|
|
264
|
+
}
|
|
265
|
+
else if (this.dragWheel === 2) {
|
|
266
|
+
// Année - avec limites strictes
|
|
267
|
+
let newYear = this.yearWheel - direction;
|
|
268
|
+
if (newYear < this.yearMin) newYear = this.yearMin;
|
|
269
|
+
if (newYear > this.yearMax) newYear = this.yearMax;
|
|
270
|
+
this.yearWheel = newYear;
|
|
271
|
+
|
|
272
|
+
// Mettre à jour les limites du jour après changement d'année
|
|
273
|
+
this._updateDayLimits();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Mettre à jour la date
|
|
277
|
+
this._updateSelectedDate();
|
|
278
|
+
|
|
279
|
+
// Forcer le redessin
|
|
280
|
+
this._requestRedraw();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Met à jour la date sélectionnée
|
|
286
|
+
* @private
|
|
287
|
+
*/
|
|
288
|
+
_updateSelectedDate() {
|
|
289
|
+
// Créer la nouvelle date
|
|
290
|
+
const newDate = new Date(this.yearWheel, this.monthWheel, this.dayWheel);
|
|
291
|
+
|
|
292
|
+
// Vérifier si la date a changé
|
|
293
|
+
if (newDate.getTime() !== this.selectedDate.getTime()) {
|
|
294
|
+
this.selectedDate = newDate;
|
|
295
|
+
|
|
296
|
+
// Appeler le callback
|
|
297
|
+
if (this.onChange) {
|
|
298
|
+
this.onChange(this.selectedDate);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Force le redessin du composant
|
|
305
|
+
* @private
|
|
306
|
+
*/
|
|
307
|
+
_requestRedraw() {
|
|
308
|
+
if (this.framework.markComponentDirty) {
|
|
309
|
+
this.framework.markComponentDirty(this);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Vérifie si un point est dans les limites du composant
|
|
315
|
+
* @param {number} x - Coordonnée X
|
|
316
|
+
* @param {number} y - Coordonnée Y
|
|
317
|
+
* @returns {boolean} True si le point est dans le composant
|
|
318
|
+
*/
|
|
319
|
+
isPointInside(x, y) {
|
|
320
|
+
// Pas de scrollOffset car le Modal est fixe
|
|
321
|
+
return x >= this.x &&
|
|
322
|
+
x <= this.x + this.width &&
|
|
323
|
+
y >= this.y &&
|
|
324
|
+
y <= this.y + this.wheelHeight;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Dessine le sélecteur de date
|
|
329
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
330
|
+
*/
|
|
331
|
+
draw(ctx) {
|
|
332
|
+
ctx.save();
|
|
333
|
+
|
|
334
|
+
const wheelWidth = this.width / 3;
|
|
335
|
+
|
|
336
|
+
// Fond
|
|
337
|
+
ctx.fillStyle = '#F9F9F9';
|
|
338
|
+
ctx.fillRect(this.x, this.y, this.width, this.wheelHeight);
|
|
339
|
+
|
|
340
|
+
// Bande de sélection
|
|
341
|
+
const selectionY = this.y + this.wheelHeight / 2 - this.itemHeight / 2;
|
|
342
|
+
ctx.fillStyle = 'rgba(0, 0, 0, 0.05)';
|
|
343
|
+
ctx.fillRect(this.x, selectionY, this.width, this.itemHeight);
|
|
344
|
+
|
|
345
|
+
// Lignes de séparation
|
|
346
|
+
ctx.strokeStyle = '#C7C7CC';
|
|
347
|
+
ctx.lineWidth = 0.5;
|
|
348
|
+
ctx.beginPath();
|
|
349
|
+
ctx.moveTo(this.x, selectionY);
|
|
350
|
+
ctx.lineTo(this.x + this.width, selectionY);
|
|
351
|
+
ctx.moveTo(this.x, selectionY + this.itemHeight);
|
|
352
|
+
ctx.lineTo(this.x + this.width, selectionY + this.itemHeight);
|
|
353
|
+
ctx.stroke();
|
|
354
|
+
|
|
355
|
+
// Dividers verticaux
|
|
356
|
+
ctx.beginPath();
|
|
357
|
+
ctx.moveTo(this.x + wheelWidth, this.y);
|
|
358
|
+
ctx.lineTo(this.x + wheelWidth, this.y + this.wheelHeight);
|
|
359
|
+
ctx.moveTo(this.x + wheelWidth * 2, this.y);
|
|
360
|
+
ctx.lineTo(this.x + wheelWidth * 2, this.y + this.wheelHeight);
|
|
361
|
+
ctx.stroke();
|
|
362
|
+
|
|
363
|
+
// Mois (avec bouclage)
|
|
364
|
+
const monthNames = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin',
|
|
365
|
+
'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
|
|
366
|
+
this._drawWheel(ctx, this.x, monthNames, this.monthWheel, this.monthMin, this.monthMax);
|
|
367
|
+
|
|
368
|
+
// Jour (avec ajustement dynamique)
|
|
369
|
+
const daysInMonth = new Date(this.yearWheel, this.monthWheel + 1, 0).getDate();
|
|
370
|
+
const days = Array.from({length: daysInMonth}, (_, i) => (i + 1).toString());
|
|
371
|
+
this._drawWheel(ctx, this.x + wheelWidth, days, this.dayWheel - 1, 0, daysInMonth - 1);
|
|
372
|
+
|
|
373
|
+
// Année (avec limites fixes)
|
|
374
|
+
const years = Array.from({length: this.yearMax - this.yearMin + 1},
|
|
375
|
+
(_, i) => (this.yearMin + i).toString());
|
|
376
|
+
const yearIndex = this.yearWheel - this.yearMin;
|
|
377
|
+
this._drawWheel(ctx, this.x + wheelWidth * 2, years, yearIndex, 0, years.length - 1);
|
|
378
|
+
|
|
379
|
+
ctx.restore();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Dessine une roue de sélection avec limites
|
|
384
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
385
|
+
* @param {number} x - Position X
|
|
386
|
+
* @param {string[]} items - Items à afficher
|
|
387
|
+
* @param {number} selectedIndex - Index sélectionné
|
|
388
|
+
* @param {number} minIndex - Index minimum
|
|
389
|
+
* @param {number} maxIndex - Index maximum
|
|
390
|
+
* @private
|
|
391
|
+
*/
|
|
392
|
+
_drawWheel(ctx, x, items, selectedIndex, minIndex = 0, maxIndex = items.length - 1) {
|
|
393
|
+
const wheelWidth = this.width / 3;
|
|
394
|
+
const centerY = this.y + this.wheelHeight / 2;
|
|
395
|
+
|
|
396
|
+
ctx.save();
|
|
397
|
+
ctx.beginPath();
|
|
398
|
+
ctx.rect(x, this.y, wheelWidth, this.wheelHeight);
|
|
399
|
+
ctx.clip();
|
|
400
|
+
|
|
401
|
+
for (let i = -2; i <= 2; i++) {
|
|
402
|
+
const index = selectedIndex + i;
|
|
403
|
+
if (index >= minIndex && index <= maxIndex) {
|
|
404
|
+
const itemY = centerY + i * this.itemHeight;
|
|
405
|
+
const distance = Math.abs(itemY - centerY);
|
|
406
|
+
const scale = 1 - (distance / this.wheelHeight);
|
|
407
|
+
const opacity = Math.max(0.3, scale);
|
|
408
|
+
|
|
409
|
+
ctx.fillStyle = `rgba(0, 0, 0, ${opacity})`;
|
|
410
|
+
ctx.font = `${i === 0 ? 'bold ' : ''}${18 + scale * 2}px -apple-system, sans-serif`;
|
|
411
|
+
ctx.textAlign = 'center';
|
|
412
|
+
ctx.textBaseline = 'middle';
|
|
413
|
+
ctx.fillText(items[index - minIndex], x + wheelWidth / 2, itemY);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
ctx.restore();
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Nettoie le composant
|
|
422
|
+
* @private
|
|
423
|
+
*/
|
|
424
|
+
_unmount() {
|
|
425
|
+
this._removeGlobalMoveListener();
|
|
426
|
+
super._unmount();
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export default IOSDatePickerWheel;
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import Component from '../core/Component.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Carousel / Slider d'images avec swipe horizontal et lazy load
|
|
5
|
+
* Compatible Material et Cupertino
|
|
6
|
+
* Tout le scroll est géré par le composant
|
|
7
|
+
*/
|
|
8
|
+
class ImageCarousel extends Component {
|
|
9
|
+
constructor(framework, options = {}) {
|
|
10
|
+
super(framework, options);
|
|
11
|
+
|
|
12
|
+
this.images = options.images || [];
|
|
13
|
+
this.currentIndex = 0;
|
|
14
|
+
this.scrollX = 0;
|
|
15
|
+
this.height = options.height || 200;
|
|
16
|
+
this.spacing = options.spacing || 16;
|
|
17
|
+
this.borderRadius = options.borderRadius || 8;
|
|
18
|
+
|
|
19
|
+
this.pageIndicatorSize = options.pageIndicatorSize || 8;
|
|
20
|
+
this.pageIndicatorColor = options.pageIndicatorColor || '#6200EE';
|
|
21
|
+
|
|
22
|
+
this.platform = framework.platform;
|
|
23
|
+
|
|
24
|
+
this.isDragging = false;
|
|
25
|
+
this.lastX = 0;
|
|
26
|
+
this.velocity = 0;
|
|
27
|
+
|
|
28
|
+
this.onSwipeEnd = options.onSwipeEnd || null;
|
|
29
|
+
this.onImageClick = options.onImageClick || null;
|
|
30
|
+
|
|
31
|
+
this.loadedImages = Array(this.images.length).fill(null);
|
|
32
|
+
|
|
33
|
+
this._setupEventHandlers();
|
|
34
|
+
this.animateScroll();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// --------------------------
|
|
38
|
+
// Event handlers
|
|
39
|
+
// --------------------------
|
|
40
|
+
_setupEventHandlers() {
|
|
41
|
+
const canvas = this.framework.canvas;
|
|
42
|
+
|
|
43
|
+
// TOUCH
|
|
44
|
+
canvas.addEventListener('touchstart', (e) => {
|
|
45
|
+
if (e.touches.length === 1 && this.isPointInsideTouch(e.touches[0])) {
|
|
46
|
+
this.isDragging = true;
|
|
47
|
+
this.lastX = e.touches[0].clientX;
|
|
48
|
+
this.velocity = 0;
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
canvas.addEventListener('touchmove', (e) => {
|
|
54
|
+
if (this.isDragging && e.touches.length === 1) {
|
|
55
|
+
const delta = e.touches[0].clientX - this.lastX;
|
|
56
|
+
this.scrollX += delta;
|
|
57
|
+
this.velocity = delta;
|
|
58
|
+
this.lastX = e.touches[0].clientX;
|
|
59
|
+
|
|
60
|
+
this._clampScroll();
|
|
61
|
+
this._requestRedraw();
|
|
62
|
+
e.preventDefault();
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
canvas.addEventListener('touchend', () => this._endDrag());
|
|
67
|
+
|
|
68
|
+
// MOUSE
|
|
69
|
+
canvas.addEventListener('mousedown', (e) => {
|
|
70
|
+
if (this.isPointInside(e)) {
|
|
71
|
+
this.isDragging = true;
|
|
72
|
+
this.lastX = e.clientX;
|
|
73
|
+
this.velocity = 0;
|
|
74
|
+
e.preventDefault();
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
canvas.addEventListener('mousemove', (e) => {
|
|
79
|
+
if (this.isDragging) {
|
|
80
|
+
const delta = e.clientX - this.lastX;
|
|
81
|
+
this.scrollX += delta;
|
|
82
|
+
this.velocity = delta;
|
|
83
|
+
this.lastX = e.clientX;
|
|
84
|
+
|
|
85
|
+
this._clampScroll();
|
|
86
|
+
this._requestRedraw();
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
canvas.addEventListener('mouseup', () => this._endDrag());
|
|
91
|
+
canvas.addEventListener('mouseleave', () => this._endDrag());
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
_endDrag() {
|
|
95
|
+
if (this.isDragging) {
|
|
96
|
+
this.isDragging = false;
|
|
97
|
+
// Snap à la page la plus proche
|
|
98
|
+
const targetIndex = Math.round(-this.scrollX / (this.width + this.spacing));
|
|
99
|
+
this.currentIndex = Math.min(Math.max(targetIndex, 0), this.images.length - 1);
|
|
100
|
+
this.scrollX = -this.currentIndex * (this.width + this.spacing);
|
|
101
|
+
|
|
102
|
+
if (this.onSwipeEnd) this.onSwipeEnd(this.currentIndex);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
_clampScroll() {
|
|
107
|
+
const maxScroll = 0;
|
|
108
|
+
const minScroll = -(this.images.length - 1) * (this.width + this.spacing);
|
|
109
|
+
if (this.scrollX > maxScroll) this.scrollX = maxScroll;
|
|
110
|
+
if (this.scrollX < minScroll) this.scrollX = minScroll;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
isPointInsideTouch(touch) {
|
|
114
|
+
const rect = this.framework.canvas.getBoundingClientRect();
|
|
115
|
+
const x = touch.clientX - rect.left;
|
|
116
|
+
const y = touch.clientY - rect.top;
|
|
117
|
+
return this.isPointInside(x, y);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
isPointInside(x, y) {
|
|
121
|
+
return x >= this.x && x <= this.x + this.width &&
|
|
122
|
+
y >= this.y && y <= this.y + this.height;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
_requestRedraw() {
|
|
126
|
+
if (this.framework.markComponentDirty) this.framework.markComponentDirty(this);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// --------------------------
|
|
130
|
+
// Animation / Inertie
|
|
131
|
+
// --------------------------
|
|
132
|
+
animateScroll() {
|
|
133
|
+
const animate = () => {
|
|
134
|
+
if (!this.isDragging) {
|
|
135
|
+
// inertia
|
|
136
|
+
if (Math.abs(this.velocity) > 0.1) {
|
|
137
|
+
this.scrollX += this.velocity;
|
|
138
|
+
this.velocity *= 0.95;
|
|
139
|
+
|
|
140
|
+
this._clampScroll();
|
|
141
|
+
} else {
|
|
142
|
+
// snap doux vers la page
|
|
143
|
+
const target = -this.currentIndex * (this.width + this.spacing);
|
|
144
|
+
this.scrollX += (target - this.scrollX) * 0.2;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
requestAnimationFrame(animate);
|
|
148
|
+
};
|
|
149
|
+
animate();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// --------------------------
|
|
153
|
+
// Draw
|
|
154
|
+
// --------------------------
|
|
155
|
+
draw(ctx) {
|
|
156
|
+
ctx.save();
|
|
157
|
+
const startX = this.x + this.scrollX + this.spacing / 2;
|
|
158
|
+
|
|
159
|
+
for (let i = 0; i < this.images.length; i++) {
|
|
160
|
+
const imgX = startX + i * (this.width + this.spacing);
|
|
161
|
+
|
|
162
|
+
// lazy load
|
|
163
|
+
if (!this.loadedImages[i]) {
|
|
164
|
+
const img = new Image();
|
|
165
|
+
img.src = this.images[i];
|
|
166
|
+
img.onload = () => { this.loadedImages[i] = img; this._requestRedraw(); };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
ctx.save();
|
|
170
|
+
ctx.beginPath();
|
|
171
|
+
this.roundRect(ctx, imgX, this.y, this.width, this.height, this.borderRadius);
|
|
172
|
+
ctx.clip();
|
|
173
|
+
|
|
174
|
+
if (this.loadedImages[i]) {
|
|
175
|
+
ctx.drawImage(this.loadedImages[i], imgX, this.y, this.width, this.height);
|
|
176
|
+
} else {
|
|
177
|
+
ctx.fillStyle = '#E0E0E0';
|
|
178
|
+
ctx.fillRect(imgX, this.y, this.width, this.height);
|
|
179
|
+
ctx.fillStyle = '#BDBDBD';
|
|
180
|
+
ctx.font = '20px sans-serif';
|
|
181
|
+
ctx.textAlign = 'center';
|
|
182
|
+
ctx.textBaseline = 'middle';
|
|
183
|
+
ctx.fillText('🖼', imgX + this.width / 2, this.y + this.height / 2);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
ctx.restore();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// pagination Material
|
|
190
|
+
if (this.platform === 'material') {
|
|
191
|
+
const dotY = this.y + this.height + 12;
|
|
192
|
+
const totalWidth = this.images.length * this.pageIndicatorSize * 2;
|
|
193
|
+
const startDotX = this.x + (this.width - totalWidth) / 2;
|
|
194
|
+
|
|
195
|
+
for (let i = 0; i < this.images.length; i++) {
|
|
196
|
+
ctx.beginPath();
|
|
197
|
+
ctx.arc(startDotX + i * this.pageIndicatorSize * 2, dotY, this.pageIndicatorSize / 2, 0, Math.PI * 2);
|
|
198
|
+
ctx.fillStyle = i === this.currentIndex ? this.pageIndicatorColor : '#E0E0E0';
|
|
199
|
+
ctx.fill();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
ctx.restore();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
roundRect(ctx, x, y, width, height, radius) {
|
|
207
|
+
ctx.moveTo(x + radius, y);
|
|
208
|
+
ctx.lineTo(x + width - radius, y);
|
|
209
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
210
|
+
ctx.lineTo(x + width, y + height - radius);
|
|
211
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
212
|
+
ctx.lineTo(x + radius, y + height);
|
|
213
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
214
|
+
ctx.lineTo(x, y + radius);
|
|
215
|
+
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export default ImageCarousel;
|