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,252 @@
|
|
|
1
|
+
import Component from '../core/Component.js';
|
|
2
|
+
/**
|
|
3
|
+
* Accordéon (section extensible)
|
|
4
|
+
* @class
|
|
5
|
+
* @extends Component
|
|
6
|
+
* @property {string} title - Titre
|
|
7
|
+
* @property {string} content - Contenu
|
|
8
|
+
* @property {string|null} icon - Icône
|
|
9
|
+
* @property {boolean} expanded - Déplié
|
|
10
|
+
* @property {string} platform - Plateforme
|
|
11
|
+
* @property {number} headerHeight - Hauteur de l'en-tête
|
|
12
|
+
* @property {number} contentPadding - Padding du contenu
|
|
13
|
+
* @property {string} bgColor - Couleur de fond
|
|
14
|
+
* @property {string} borderColor - Couleur de la bordure
|
|
15
|
+
* @property {Function} onToggle - Callback au toggle
|
|
16
|
+
* @property {boolean} animating - En cours d'animation
|
|
17
|
+
* @property {number} animProgress - Progression de l'animation
|
|
18
|
+
* @property {number} contentHeight - Hauteur du contenu
|
|
19
|
+
*/
|
|
20
|
+
class Accordion extends Component {
|
|
21
|
+
/**
|
|
22
|
+
* Crée une instance de Accordion
|
|
23
|
+
* @param {CanvasFramework} framework - Framework parent
|
|
24
|
+
* @param {Object} [options={}] - Options de configuration
|
|
25
|
+
* @param {string} [options.title=''] - Titre
|
|
26
|
+
* @param {string} [options.content=''] - Contenu
|
|
27
|
+
* @param {string} [options.icon] - Icône
|
|
28
|
+
* @param {boolean} [options.expanded=false] - Déplié initialement
|
|
29
|
+
* @param {Function} [options.onToggle] - Callback au toggle
|
|
30
|
+
* @param {string} [options.bgColor='#FFFFFF'] - Couleur de fond
|
|
31
|
+
* @param {string} [options.borderColor='#E0E0E0'] - Couleur de bordure
|
|
32
|
+
*/
|
|
33
|
+
constructor(framework, options = {}) {
|
|
34
|
+
super(framework, options);
|
|
35
|
+
this.title = options.title || '';
|
|
36
|
+
this.content = options.content || '';
|
|
37
|
+
this.icon = options.icon || null;
|
|
38
|
+
this.expanded = options.expanded || false;
|
|
39
|
+
this.platform = framework.platform;
|
|
40
|
+
this.headerHeight = 56;
|
|
41
|
+
this.contentPadding = 16;
|
|
42
|
+
this.bgColor = options.bgColor || '#FFFFFF';
|
|
43
|
+
this.borderColor = options.borderColor || '#E0E0E0';
|
|
44
|
+
this.onToggle = options.onToggle;
|
|
45
|
+
this.animating = false;
|
|
46
|
+
this.animProgress = this.expanded ? 1 : 0;
|
|
47
|
+
|
|
48
|
+
// Calculer la hauteur du contenu
|
|
49
|
+
this.calculateContentHeight();
|
|
50
|
+
this.height = this.headerHeight + (this.expanded ? this.contentHeight : 0);
|
|
51
|
+
|
|
52
|
+
// CORRECTION: Bloquer les clics pendant l'animation
|
|
53
|
+
this.onClick = () => {
|
|
54
|
+
// Ignorer les clics pendant l'animation
|
|
55
|
+
if (this.animating) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
this.toggle();
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Calcule la hauteur du contenu
|
|
64
|
+
* @private
|
|
65
|
+
*/
|
|
66
|
+
calculateContentHeight() {
|
|
67
|
+
const ctx = this.framework.ctx;
|
|
68
|
+
ctx.save();
|
|
69
|
+
ctx.font = '14px -apple-system, sans-serif';
|
|
70
|
+
|
|
71
|
+
// Diviser le contenu en lignes
|
|
72
|
+
const maxWidth = this.width - (this.contentPadding * 2);
|
|
73
|
+
const lines = this.wrapText(ctx, this.content, maxWidth);
|
|
74
|
+
const lineHeight = 20;
|
|
75
|
+
|
|
76
|
+
ctx.restore();
|
|
77
|
+
this.contentHeight = lines.length * lineHeight + (this.contentPadding * 2);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Divise le texte en lignes
|
|
82
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
83
|
+
* @param {string} text - Texte
|
|
84
|
+
* @param {number} maxWidth - Largeur maximale
|
|
85
|
+
* @returns {string[]} Tableau de lignes
|
|
86
|
+
* @private
|
|
87
|
+
*/
|
|
88
|
+
wrapText(ctx, text, maxWidth) {
|
|
89
|
+
const words = text.split(' ');
|
|
90
|
+
const lines = [];
|
|
91
|
+
let currentLine = words[0] || '';
|
|
92
|
+
|
|
93
|
+
for (let i = 1; i < words.length; i++) {
|
|
94
|
+
const word = words[i];
|
|
95
|
+
const width = ctx.measureText(currentLine + " " + word).width;
|
|
96
|
+
if (width < maxWidth) {
|
|
97
|
+
currentLine += " " + word;
|
|
98
|
+
} else {
|
|
99
|
+
lines.push(currentLine);
|
|
100
|
+
currentLine = word;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
lines.push(currentLine);
|
|
104
|
+
return lines;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Alterne l'état déplié/replié
|
|
109
|
+
*/
|
|
110
|
+
toggle() {
|
|
111
|
+
// Empêcher les toggles multiples pendant l'animation
|
|
112
|
+
if (this.animating) return;
|
|
113
|
+
|
|
114
|
+
this.expanded = !this.expanded;
|
|
115
|
+
if (this.onToggle) this.onToggle(this.expanded);
|
|
116
|
+
this.animate();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Anime le toggle
|
|
121
|
+
* @private
|
|
122
|
+
*/
|
|
123
|
+
animate() {
|
|
124
|
+
if (this.animating) return;
|
|
125
|
+
this.animating = true;
|
|
126
|
+
|
|
127
|
+
const target = this.expanded ? 1 : 0;
|
|
128
|
+
const step = 0.1;
|
|
129
|
+
|
|
130
|
+
const doAnimate = () => {
|
|
131
|
+
if (Math.abs(this.animProgress - target) < 0.01) {
|
|
132
|
+
this.animProgress = target;
|
|
133
|
+
this.height = this.headerHeight + (this.contentHeight * this.animProgress);
|
|
134
|
+
this.animating = false;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
this.animProgress += this.animProgress < target ? step : -step;
|
|
139
|
+
this.height = this.headerHeight + (this.contentHeight * this.animProgress);
|
|
140
|
+
requestAnimationFrame(doAnimate);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
doAnimate();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Dessine l'accordéon
|
|
148
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
149
|
+
*/
|
|
150
|
+
draw(ctx) {
|
|
151
|
+
ctx.save();
|
|
152
|
+
|
|
153
|
+
// Background
|
|
154
|
+
ctx.fillStyle = this.bgColor;
|
|
155
|
+
ctx.fillRect(this.x, this.y, this.width, this.height);
|
|
156
|
+
|
|
157
|
+
// Bordure
|
|
158
|
+
ctx.strokeStyle = this.borderColor;
|
|
159
|
+
ctx.lineWidth = 1;
|
|
160
|
+
ctx.strokeRect(this.x, this.y, this.width, this.height);
|
|
161
|
+
|
|
162
|
+
// Header
|
|
163
|
+
// Icône (si présente)
|
|
164
|
+
if (this.icon) {
|
|
165
|
+
ctx.font = '20px sans-serif';
|
|
166
|
+
ctx.fillStyle = '#666666';
|
|
167
|
+
ctx.textAlign = 'left';
|
|
168
|
+
ctx.textBaseline = 'middle';
|
|
169
|
+
ctx.fillText(this.icon, this.x + 16, this.y + this.headerHeight / 2);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Titre
|
|
173
|
+
ctx.fillStyle = '#000000';
|
|
174
|
+
ctx.font = 'bold 16px -apple-system, sans-serif';
|
|
175
|
+
ctx.textAlign = 'left';
|
|
176
|
+
ctx.textBaseline = 'middle';
|
|
177
|
+
const titleX = this.x + (this.icon ? 56 : 16);
|
|
178
|
+
ctx.fillText(this.title, titleX, this.y + this.headerHeight / 2);
|
|
179
|
+
|
|
180
|
+
// Chevron (flèche)
|
|
181
|
+
const chevronX = this.x + this.width - 30;
|
|
182
|
+
const chevronY = this.y + this.headerHeight / 2;
|
|
183
|
+
const chevronRotation = this.animProgress * Math.PI;
|
|
184
|
+
|
|
185
|
+
ctx.save();
|
|
186
|
+
ctx.translate(chevronX, chevronY);
|
|
187
|
+
ctx.rotate(chevronRotation);
|
|
188
|
+
|
|
189
|
+
ctx.strokeStyle = '#666666';
|
|
190
|
+
ctx.lineWidth = 2;
|
|
191
|
+
ctx.lineCap = 'round';
|
|
192
|
+
ctx.lineJoin = 'round';
|
|
193
|
+
ctx.beginPath();
|
|
194
|
+
ctx.moveTo(-6, -3);
|
|
195
|
+
ctx.lineTo(0, 3);
|
|
196
|
+
ctx.lineTo(6, -3);
|
|
197
|
+
ctx.stroke();
|
|
198
|
+
|
|
199
|
+
ctx.restore();
|
|
200
|
+
|
|
201
|
+
// Contenu (si expanded ou en train d'animer)
|
|
202
|
+
if (this.animProgress > 0) {
|
|
203
|
+
ctx.save();
|
|
204
|
+
|
|
205
|
+
// Clipping pour l'animation
|
|
206
|
+
ctx.beginPath();
|
|
207
|
+
ctx.rect(this.x, this.y + this.headerHeight, this.width, this.contentHeight * this.animProgress);
|
|
208
|
+
ctx.clip();
|
|
209
|
+
|
|
210
|
+
// Divider
|
|
211
|
+
ctx.strokeStyle = this.borderColor;
|
|
212
|
+
ctx.lineWidth = 1;
|
|
213
|
+
ctx.beginPath();
|
|
214
|
+
ctx.moveTo(this.x, this.y + this.headerHeight);
|
|
215
|
+
ctx.lineTo(this.x + this.width, this.y + this.headerHeight);
|
|
216
|
+
ctx.stroke();
|
|
217
|
+
|
|
218
|
+
// Texte du contenu
|
|
219
|
+
ctx.fillStyle = '#666666';
|
|
220
|
+
ctx.font = '14px -apple-system, sans-serif';
|
|
221
|
+
ctx.textAlign = 'left';
|
|
222
|
+
ctx.textBaseline = 'top';
|
|
223
|
+
|
|
224
|
+
const contentX = this.x + this.contentPadding;
|
|
225
|
+
const contentY = this.y + this.headerHeight + this.contentPadding;
|
|
226
|
+
const maxWidth = this.width - (this.contentPadding * 2);
|
|
227
|
+
const lines = this.wrapText(ctx, this.content, maxWidth);
|
|
228
|
+
const lineHeight = 20;
|
|
229
|
+
|
|
230
|
+
lines.forEach((line, index) => {
|
|
231
|
+
ctx.fillText(line, contentX, contentY + (index * lineHeight));
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
ctx.restore();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
ctx.restore();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Vérifie si un point est dans les limites
|
|
242
|
+
* @param {number} x - Coordonnée X
|
|
243
|
+
* @param {number} y - Coordonnée Y
|
|
244
|
+
* @returns {boolean} True si le point est dans l'en-tête
|
|
245
|
+
*/
|
|
246
|
+
isPointInside(x, y) {
|
|
247
|
+
return x >= this.x && x <= this.x + this.width &&
|
|
248
|
+
y >= this.y && y <= this.y + this.headerHeight;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export default Accordion;
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import Component from '../core/Component.js';
|
|
2
|
+
/**
|
|
3
|
+
* Dialog de sélection de date Android
|
|
4
|
+
* @class
|
|
5
|
+
* @extends Component
|
|
6
|
+
* @property {Date} selectedDate - Date sélectionnée
|
|
7
|
+
* @property {Function} onChange - Callback au changement
|
|
8
|
+
* @property {number} currentMonth - Mois courant
|
|
9
|
+
* @property {number} currentYear - Année courante
|
|
10
|
+
* @property {number|null} hoveredDay - Jour survolé
|
|
11
|
+
* @property {number} dialogWidth - Largeur du dialog
|
|
12
|
+
* @property {number} dialogHeight - Hauteur du dialog
|
|
13
|
+
* @property {number} headerHeight - Hauteur de l'en-tête
|
|
14
|
+
* @property {number} daySize - Taille d'un jour
|
|
15
|
+
* @property {number} opacity - Opacité
|
|
16
|
+
* @property {boolean} isVisible - Visibilité
|
|
17
|
+
*/
|
|
18
|
+
class AndroidDatePickerDialog extends Component {
|
|
19
|
+
/**
|
|
20
|
+
* Crée une instance de AndroidDatePickerDialog
|
|
21
|
+
* @param {CanvasFramework} framework - Framework parent
|
|
22
|
+
* @param {Object} [options={}] - Options de configuration
|
|
23
|
+
* @param {Date} [options.selectedDate=new Date()] - Date initiale
|
|
24
|
+
* @param {Function} [options.onChange] - Callback au changement
|
|
25
|
+
*/
|
|
26
|
+
constructor(framework, options = {}) {
|
|
27
|
+
super(framework, {
|
|
28
|
+
x: 0,
|
|
29
|
+
y: 0,
|
|
30
|
+
width: framework.width,
|
|
31
|
+
height: framework.height,
|
|
32
|
+
visible: false
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
this.selectedDate = options.selectedDate || new Date();
|
|
36
|
+
this.onChange = options.onChange;
|
|
37
|
+
this.currentMonth = this.selectedDate.getMonth();
|
|
38
|
+
this.currentYear = this.selectedDate.getFullYear();
|
|
39
|
+
this.hoveredDay = null;
|
|
40
|
+
|
|
41
|
+
this.dialogWidth = Math.min(320, framework.width - 40);
|
|
42
|
+
this.dialogHeight = 420;
|
|
43
|
+
this.headerHeight = 100;
|
|
44
|
+
this.daySize = (this.dialogWidth - 40) / 7;
|
|
45
|
+
|
|
46
|
+
this.opacity = 0;
|
|
47
|
+
this.isVisible = false;
|
|
48
|
+
|
|
49
|
+
this.onPress = this.handlePress.bind(this);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Affiche le dialog
|
|
54
|
+
*/
|
|
55
|
+
show() {
|
|
56
|
+
this.isVisible = true;
|
|
57
|
+
this.visible = true;
|
|
58
|
+
const fadeIn = () => {
|
|
59
|
+
this.opacity += 0.1;
|
|
60
|
+
if (this.opacity < 1) requestAnimationFrame(fadeIn);
|
|
61
|
+
};
|
|
62
|
+
fadeIn();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Cache le dialog
|
|
67
|
+
*/
|
|
68
|
+
hide() {
|
|
69
|
+
const fadeOut = () => {
|
|
70
|
+
this.opacity -= 0.1;
|
|
71
|
+
if (this.opacity > 0) {
|
|
72
|
+
requestAnimationFrame(fadeOut);
|
|
73
|
+
} else {
|
|
74
|
+
this.isVisible = false;
|
|
75
|
+
this.visible = false;
|
|
76
|
+
this.framework.remove(this);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
fadeOut();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Obtient le nombre de jours dans un mois
|
|
84
|
+
* @param {number} month - Mois (0-11)
|
|
85
|
+
* @param {number} year - Année
|
|
86
|
+
* @returns {number} Nombre de jours
|
|
87
|
+
* @private
|
|
88
|
+
*/
|
|
89
|
+
getDaysInMonth(month, year) {
|
|
90
|
+
return new Date(year, month + 1, 0).getDate();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Obtient le premier jour de la semaine d'un mois
|
|
95
|
+
* @param {number} month - Mois (0-11)
|
|
96
|
+
* @param {number} year - Année
|
|
97
|
+
* @returns {number} Jour de la semaine (0-6)
|
|
98
|
+
* @private
|
|
99
|
+
*/
|
|
100
|
+
getFirstDayOfMonth(month, year) {
|
|
101
|
+
return new Date(year, month, 1).getDay();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Passe au mois précédent
|
|
106
|
+
* @private
|
|
107
|
+
*/
|
|
108
|
+
previousMonth() {
|
|
109
|
+
this.currentMonth--;
|
|
110
|
+
if (this.currentMonth < 0) {
|
|
111
|
+
this.currentMonth = 11;
|
|
112
|
+
this.currentYear--;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Passe au mois suivant
|
|
118
|
+
* @private
|
|
119
|
+
*/
|
|
120
|
+
nextMonth() {
|
|
121
|
+
this.currentMonth++;
|
|
122
|
+
if (this.currentMonth > 11) {
|
|
123
|
+
this.currentMonth = 0;
|
|
124
|
+
this.currentYear++;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Sélectionne un jour
|
|
130
|
+
* @param {number} day - Jour à sélectionner
|
|
131
|
+
* @private
|
|
132
|
+
*/
|
|
133
|
+
selectDate(day) {
|
|
134
|
+
this.selectedDate = new Date(this.currentYear, this.currentMonth, day);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Dessine le dialog
|
|
139
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
140
|
+
*/
|
|
141
|
+
draw(ctx) {
|
|
142
|
+
if (this.opacity <= 0 || !this.isVisible) return;
|
|
143
|
+
|
|
144
|
+
ctx.save();
|
|
145
|
+
ctx.globalAlpha = this.opacity;
|
|
146
|
+
|
|
147
|
+
// Overlay
|
|
148
|
+
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
|
|
149
|
+
ctx.fillRect(0, 0, this.framework.width, this.framework.height);
|
|
150
|
+
|
|
151
|
+
const dialogX = (this.framework.width - this.dialogWidth) / 2;
|
|
152
|
+
const dialogY = (this.framework.height - this.dialogHeight) / 2;
|
|
153
|
+
|
|
154
|
+
// Dialog background
|
|
155
|
+
ctx.fillStyle = '#FFFFFF';
|
|
156
|
+
ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
|
|
157
|
+
ctx.shadowBlur = 20;
|
|
158
|
+
ctx.beginPath();
|
|
159
|
+
this.roundRect(ctx, dialogX, dialogY, this.dialogWidth, this.dialogHeight, 4);
|
|
160
|
+
ctx.fill();
|
|
161
|
+
|
|
162
|
+
ctx.shadowColor = 'transparent';
|
|
163
|
+
|
|
164
|
+
// Header coloré
|
|
165
|
+
ctx.fillStyle = '#6200EE';
|
|
166
|
+
ctx.beginPath();
|
|
167
|
+
this.roundRect(ctx, dialogX, dialogY, this.dialogWidth, this.headerHeight, 4);
|
|
168
|
+
ctx.rect(dialogX, dialogY + this.headerHeight - 4, this.dialogWidth, 4);
|
|
169
|
+
ctx.fill();
|
|
170
|
+
|
|
171
|
+
// Année (petit)
|
|
172
|
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
|
|
173
|
+
ctx.font = '16px Roboto, sans-serif';
|
|
174
|
+
ctx.textAlign = 'left';
|
|
175
|
+
ctx.textBaseline = 'top';
|
|
176
|
+
ctx.fillText(this.currentYear.toString(), dialogX + 20, dialogY + 20);
|
|
177
|
+
|
|
178
|
+
// Date sélectionnée (grand)
|
|
179
|
+
const monthNames = ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Juin',
|
|
180
|
+
'Juil', 'Aoû', 'Sep', 'Oct', 'Nov', 'Déc'];
|
|
181
|
+
const dayNames = ['Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam'];
|
|
182
|
+
const selectedDay = dayNames[this.selectedDate.getDay()];
|
|
183
|
+
const selectedMonth = monthNames[this.selectedDate.getMonth()];
|
|
184
|
+
const selectedDayNum = this.selectedDate.getDate();
|
|
185
|
+
|
|
186
|
+
ctx.fillStyle = '#FFFFFF';
|
|
187
|
+
ctx.font = 'bold 32px Roboto, sans-serif';
|
|
188
|
+
ctx.textBaseline = 'middle';
|
|
189
|
+
ctx.fillText(`${selectedDay}, ${selectedMonth} ${selectedDayNum}`,
|
|
190
|
+
dialogX + 20, dialogY + this.headerHeight / 2 + 15);
|
|
191
|
+
|
|
192
|
+
// Navigation mois
|
|
193
|
+
const navY = dialogY + this.headerHeight + 20;
|
|
194
|
+
|
|
195
|
+
// Bouton mois précédent
|
|
196
|
+
ctx.fillStyle = '#666666';
|
|
197
|
+
ctx.font = '20px sans-serif';
|
|
198
|
+
ctx.textAlign = 'center';
|
|
199
|
+
ctx.fillText('◀', dialogX + 30, navY);
|
|
200
|
+
|
|
201
|
+
// Mois et année actuel
|
|
202
|
+
const monthNamesLong = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin',
|
|
203
|
+
'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
|
|
204
|
+
ctx.font = 'bold 16px Roboto, sans-serif';
|
|
205
|
+
ctx.fillText(`${monthNamesLong[this.currentMonth]} ${this.currentYear}`,
|
|
206
|
+
dialogX + this.dialogWidth / 2, navY);
|
|
207
|
+
|
|
208
|
+
// Bouton mois suivant
|
|
209
|
+
ctx.fillText('▶', dialogX + this.dialogWidth - 30, navY);
|
|
210
|
+
|
|
211
|
+
// Jours de la semaine
|
|
212
|
+
const dayNamesShort = ['D', 'L', 'M', 'M', 'J', 'V', 'S'];
|
|
213
|
+
ctx.fillStyle = '#666666';
|
|
214
|
+
ctx.font = 'bold 12px Roboto, sans-serif';
|
|
215
|
+
|
|
216
|
+
for (let i = 0; i < 7; i++) {
|
|
217
|
+
const dayX = dialogX + 20 + i * this.daySize + this.daySize / 2;
|
|
218
|
+
const dayY = navY + 40;
|
|
219
|
+
ctx.fillText(dayNamesShort[i], dayX, dayY);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Grille de jours
|
|
223
|
+
const daysInMonth = this.getDaysInMonth(this.currentMonth, this.currentYear);
|
|
224
|
+
const firstDay = this.getFirstDayOfMonth(this.currentMonth, this.currentYear);
|
|
225
|
+
|
|
226
|
+
let dayNumber = 1;
|
|
227
|
+
const startY = navY + 60;
|
|
228
|
+
|
|
229
|
+
for (let week = 0; week < 6; week++) {
|
|
230
|
+
for (let day = 0; day < 7; day++) {
|
|
231
|
+
const index = week * 7 + day;
|
|
232
|
+
|
|
233
|
+
if (index >= firstDay && dayNumber <= daysInMonth) {
|
|
234
|
+
const dayX = dialogX + 20 + day * this.daySize;
|
|
235
|
+
const dayY = startY + week * this.daySize;
|
|
236
|
+
|
|
237
|
+
const isSelected = this.selectedDate.getDate() === dayNumber &&
|
|
238
|
+
this.selectedDate.getMonth() === this.currentMonth &&
|
|
239
|
+
this.selectedDate.getFullYear() === this.currentYear;
|
|
240
|
+
|
|
241
|
+
const today = new Date();
|
|
242
|
+
const isToday = dayNumber === today.getDate() &&
|
|
243
|
+
this.currentMonth === today.getMonth() &&
|
|
244
|
+
this.currentYear === today.getFullYear();
|
|
245
|
+
|
|
246
|
+
// Cercle sélectionné
|
|
247
|
+
if (isSelected) {
|
|
248
|
+
ctx.fillStyle = '#6200EE';
|
|
249
|
+
ctx.beginPath();
|
|
250
|
+
ctx.arc(dayX + this.daySize / 2, dayY + this.daySize / 2,
|
|
251
|
+
this.daySize / 2 - 2, 0, Math.PI * 2);
|
|
252
|
+
ctx.fill();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Cercle aujourd'hui
|
|
256
|
+
if (isToday && !isSelected) {
|
|
257
|
+
ctx.strokeStyle = '#6200EE';
|
|
258
|
+
ctx.lineWidth = 2;
|
|
259
|
+
ctx.beginPath();
|
|
260
|
+
ctx.arc(dayX + this.daySize / 2, dayY + this.daySize / 2,
|
|
261
|
+
this.daySize / 2 - 2, 0, Math.PI * 2);
|
|
262
|
+
ctx.stroke();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Numéro
|
|
266
|
+
ctx.fillStyle = isSelected ? '#FFFFFF' : '#000000';
|
|
267
|
+
ctx.font = '14px Roboto, sans-serif';
|
|
268
|
+
ctx.textAlign = 'center';
|
|
269
|
+
ctx.textBaseline = 'middle';
|
|
270
|
+
ctx.fillText(dayNumber.toString(), dayX + this.daySize / 2, dayY + this.daySize / 2);
|
|
271
|
+
|
|
272
|
+
dayNumber++;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (dayNumber > daysInMonth) break;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Boutons d'action
|
|
280
|
+
const btnY = dialogY + this.dialogHeight - 30;
|
|
281
|
+
|
|
282
|
+
// Annuler
|
|
283
|
+
ctx.fillStyle = '#6200EE';
|
|
284
|
+
ctx.font = 'bold 14px Roboto, sans-serif';
|
|
285
|
+
ctx.textAlign = 'right';
|
|
286
|
+
ctx.fillText('ANNULER', dialogX + this.dialogWidth - 120, btnY);
|
|
287
|
+
|
|
288
|
+
// OK
|
|
289
|
+
ctx.fillText('OK', dialogX + this.dialogWidth - 20, btnY);
|
|
290
|
+
|
|
291
|
+
ctx.restore();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Gère la pression (clic)
|
|
296
|
+
* @param {number} x - Coordonnée X
|
|
297
|
+
* @param {number} y - Coordonnée Y
|
|
298
|
+
* @private
|
|
299
|
+
*/
|
|
300
|
+
handlePress(x, y) {
|
|
301
|
+
const dialogX = (this.framework.width - this.dialogWidth) / 2;
|
|
302
|
+
const dialogY = (this.framework.height - this.dialogHeight) / 2;
|
|
303
|
+
|
|
304
|
+
// Boutons navigation
|
|
305
|
+
const navY = dialogY + this.headerHeight + 20;
|
|
306
|
+
|
|
307
|
+
if (y >= navY - 15 && y <= navY + 15) {
|
|
308
|
+
if (x >= dialogX + 10 && x <= dialogX + 50) {
|
|
309
|
+
this.previousMonth();
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (x >= dialogX + this.dialogWidth - 50 && x <= dialogX + this.dialogWidth - 10) {
|
|
313
|
+
this.nextMonth();
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Sélection d'un jour
|
|
319
|
+
const startY = navY + 60;
|
|
320
|
+
const daysInMonth = this.getDaysInMonth(this.currentMonth, this.currentYear);
|
|
321
|
+
const firstDay = this.getFirstDayOfMonth(this.currentMonth, this.currentYear);
|
|
322
|
+
|
|
323
|
+
let dayNumber = 1;
|
|
324
|
+
for (let week = 0; week < 6; week++) {
|
|
325
|
+
for (let day = 0; day < 7; day++) {
|
|
326
|
+
const index = week * 7 + day;
|
|
327
|
+
if (index >= firstDay && dayNumber <= daysInMonth) {
|
|
328
|
+
const dayX = dialogX + 20 + day * this.daySize;
|
|
329
|
+
const dayY = startY + week * this.daySize;
|
|
330
|
+
|
|
331
|
+
if (x >= dayX && x <= dayX + this.daySize &&
|
|
332
|
+
y >= dayY && y <= dayY + this.daySize) {
|
|
333
|
+
this.selectDate(dayNumber);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
dayNumber++;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (dayNumber > daysInMonth) break;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Boutons d'action
|
|
343
|
+
const btnY = dialogY + this.dialogHeight - 30;
|
|
344
|
+
if (y >= btnY - 20 && y <= btnY + 20) {
|
|
345
|
+
// Annuler
|
|
346
|
+
if (x >= dialogX + this.dialogWidth - 180 && x <= dialogX + this.dialogWidth - 80) {
|
|
347
|
+
this.hide();
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
// OK
|
|
351
|
+
if (x >= dialogX + this.dialogWidth - 70 && x <= dialogX + this.dialogWidth) {
|
|
352
|
+
if (this.onChange) this.onChange(this.selectedDate);
|
|
353
|
+
this.hide();
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Clic sur overlay pour fermer
|
|
359
|
+
if (x < dialogX || x > dialogX + this.dialogWidth ||
|
|
360
|
+
y < dialogY || y > dialogY + this.dialogHeight) {
|
|
361
|
+
this.hide();
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Dessine un rectangle avec coins arrondis
|
|
367
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
368
|
+
* @param {number} x - Position X
|
|
369
|
+
* @param {number} y - Position Y
|
|
370
|
+
* @param {number} width - Largeur
|
|
371
|
+
* @param {number} height - Hauteur
|
|
372
|
+
* @param {number} radius - Rayon des coins
|
|
373
|
+
* @private
|
|
374
|
+
*/
|
|
375
|
+
roundRect(ctx, x, y, width, height, radius) {
|
|
376
|
+
ctx.beginPath();
|
|
377
|
+
ctx.moveTo(x + radius, y);
|
|
378
|
+
ctx.lineTo(x + width - radius, y);
|
|
379
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
380
|
+
ctx.lineTo(x + width, y + height - radius);
|
|
381
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
382
|
+
ctx.lineTo(x + radius, y + height);
|
|
383
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
384
|
+
ctx.lineTo(x, y + radius);
|
|
385
|
+
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
386
|
+
ctx.closePath();
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Vérifie si un point est dans les limites
|
|
391
|
+
* @returns {boolean} True si visible
|
|
392
|
+
*/
|
|
393
|
+
isPointInside() {
|
|
394
|
+
return this.isVisible;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export default AndroidDatePickerDialog;
|