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,271 @@
|
|
|
1
|
+
import Component from '../core/Component.js';
|
|
2
|
+
/**
|
|
3
|
+
* Champ de saisie numérique avec support multi-plateforme et validation
|
|
4
|
+
* @class
|
|
5
|
+
* @extends Component
|
|
6
|
+
* @param {Framework} framework - Instance du framework
|
|
7
|
+
* @param {Object} [options={}] - Options de configuration
|
|
8
|
+
* @param {number} [options.value=0] - Valeur initiale
|
|
9
|
+
* @param {number} [options.min] - Valeur minimale
|
|
10
|
+
* @param {number} [options.max] - Valeur maximale
|
|
11
|
+
* @param {number} [options.step=1] - Incrément/décrément
|
|
12
|
+
* @param {string} [options.placeholder='0'] - Texte par défaut
|
|
13
|
+
* @param {number} [options.fontSize=16] - Taille de police
|
|
14
|
+
* @param {Function} [options.onChange] - Callback lors du changement de valeur
|
|
15
|
+
* @param {number} [options.height] - Hauteur personnalisée (44 par défaut sur iOS)
|
|
16
|
+
* @example
|
|
17
|
+
* const numberInput = new NumberInput(framework, {
|
|
18
|
+
* value: 10,
|
|
19
|
+
* min: 0,
|
|
20
|
+
* max: 100,
|
|
21
|
+
* step: 5,
|
|
22
|
+
* onChange: (value) => console.log('New value:', value)
|
|
23
|
+
* });
|
|
24
|
+
*/
|
|
25
|
+
class NumberInput extends Component {
|
|
26
|
+
/**
|
|
27
|
+
* @constructs NumberInput
|
|
28
|
+
*/
|
|
29
|
+
constructor(framework, options = {}) {
|
|
30
|
+
super(framework, options);
|
|
31
|
+
/** @type {number|string} */
|
|
32
|
+
this.value = options.value || 0;
|
|
33
|
+
/** @type {number|undefined} */
|
|
34
|
+
this.min = options.min;
|
|
35
|
+
/** @type {number|undefined} */
|
|
36
|
+
this.max = options.max;
|
|
37
|
+
/** @type {number} */
|
|
38
|
+
this.step = options.step || 1;
|
|
39
|
+
/** @type {string} */
|
|
40
|
+
this.placeholder = options.placeholder || '0';
|
|
41
|
+
/** @type {string} */
|
|
42
|
+
this.platform = framework.platform;
|
|
43
|
+
/** @type {boolean} */
|
|
44
|
+
this.focused = false;
|
|
45
|
+
/** @type {number} */
|
|
46
|
+
this.fontSize = options.fontSize || 16;
|
|
47
|
+
/** @type {Function|undefined} */
|
|
48
|
+
this.onChange = options.onChange;
|
|
49
|
+
|
|
50
|
+
// Ajuster la hauteur pour iOS
|
|
51
|
+
if (this.platform === 'cupertino') {
|
|
52
|
+
this.height = options.height || 44;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this.onFocus = this.handleFocus.bind(this);
|
|
56
|
+
this.onBlur = this.handleBlur.bind(this);
|
|
57
|
+
|
|
58
|
+
// CRÉER UN INPUT CACHÉ UNIQUE POUR CETTE INSTANCE
|
|
59
|
+
/** @type {string} */
|
|
60
|
+
this.hiddenInputId = `hidden-number-input-${Math.random().toString(36).substr(2, 9)}`;
|
|
61
|
+
this.setupHiddenInput();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Configure l'input caché dans le DOM pour la saisie clavier
|
|
66
|
+
* @private
|
|
67
|
+
*/
|
|
68
|
+
setupHiddenInput() {
|
|
69
|
+
// Créer un input caché unique pour cette instance
|
|
70
|
+
const hiddenInput = document.createElement('input');
|
|
71
|
+
hiddenInput.id = this.hiddenInputId;
|
|
72
|
+
hiddenInput.type = 'number';
|
|
73
|
+
hiddenInput.style.position = 'absolute';
|
|
74
|
+
hiddenInput.style.opacity = '0';
|
|
75
|
+
hiddenInput.style.width = '1px';
|
|
76
|
+
hiddenInput.style.height = '1px';
|
|
77
|
+
hiddenInput.style.left = '-9999px';
|
|
78
|
+
hiddenInput.style.top = '0px';
|
|
79
|
+
hiddenInput.style.fontSize = '16px'; // Forcer la taille de police
|
|
80
|
+
|
|
81
|
+
// Appliquer les contraintes
|
|
82
|
+
if (this.min !== undefined) hiddenInput.min = this.min;
|
|
83
|
+
if (this.max !== undefined) hiddenInput.max = this.max;
|
|
84
|
+
if (this.step) hiddenInput.step = this.step;
|
|
85
|
+
|
|
86
|
+
document.body.appendChild(hiddenInput);
|
|
87
|
+
|
|
88
|
+
hiddenInput.addEventListener('input', (e) => {
|
|
89
|
+
if (this.focused) {
|
|
90
|
+
let newValue = parseFloat(e.target.value);
|
|
91
|
+
|
|
92
|
+
// Gérer les valeurs vides
|
|
93
|
+
if (isNaN(newValue)) {
|
|
94
|
+
newValue = '';
|
|
95
|
+
} else {
|
|
96
|
+
// Appliquer min/max
|
|
97
|
+
if (this.min !== undefined) newValue = Math.max(this.min, newValue);
|
|
98
|
+
if (this.max !== undefined) newValue = Math.min(this.max, newValue);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
this.value = newValue;
|
|
102
|
+
|
|
103
|
+
if (this.onChange) this.onChange(this.value);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
hiddenInput.addEventListener('blur', () => {
|
|
108
|
+
this.handleBlur();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
hiddenInput.addEventListener('keypress', (e) => {
|
|
112
|
+
if (e.key === 'Enter') {
|
|
113
|
+
this.handleBlur();
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
/** @type {HTMLInputElement} */
|
|
118
|
+
this.hiddenInput = hiddenInput;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Gère le focus sur le champ
|
|
123
|
+
*/
|
|
124
|
+
handleFocus() {
|
|
125
|
+
if (!this.hiddenInput) return;
|
|
126
|
+
|
|
127
|
+
this.focused = true;
|
|
128
|
+
this.cursorVisible = true;
|
|
129
|
+
|
|
130
|
+
// Positionner et configurer l'input
|
|
131
|
+
const adjustedY = this.y + this.framework.scrollOffset;
|
|
132
|
+
|
|
133
|
+
// Sur mobile, on veut forcer le clavier numérique
|
|
134
|
+
this.hiddenInput.type = 'number';
|
|
135
|
+
this.hiddenInput.inputMode = 'decimal';
|
|
136
|
+
this.hiddenInput.pattern = '[0-9]*';
|
|
137
|
+
|
|
138
|
+
// Définir la valeur actuelle
|
|
139
|
+
this.hiddenInput.value = this.value !== null && this.value !== undefined ? this.value : '';
|
|
140
|
+
|
|
141
|
+
// Forcer le focus avec un délai pour éviter les problèmes de rendu
|
|
142
|
+
setTimeout(() => {
|
|
143
|
+
this.hiddenInput.focus();
|
|
144
|
+
}, 50);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Gère la perte de focus
|
|
149
|
+
*/
|
|
150
|
+
handleBlur() {
|
|
151
|
+
this.focused = false;
|
|
152
|
+
this.cursorVisible = false;
|
|
153
|
+
|
|
154
|
+
// S'assurer que la valeur est valide
|
|
155
|
+
if (this.hiddenInput && this.hiddenInput.value !== '') {
|
|
156
|
+
let val = parseFloat(this.hiddenInput.value);
|
|
157
|
+
if (!isNaN(val)) {
|
|
158
|
+
if (this.min !== undefined) val = Math.max(this.min, val);
|
|
159
|
+
if (this.max !== undefined) val = Math.min(this.max, val);
|
|
160
|
+
this.value = val;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Gère le clic sur le champ
|
|
167
|
+
*/
|
|
168
|
+
onClick() {
|
|
169
|
+
this.handleFocus();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Dessine le champ de saisie
|
|
174
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
175
|
+
*/
|
|
176
|
+
draw(ctx) {
|
|
177
|
+
ctx.save();
|
|
178
|
+
|
|
179
|
+
const displayValue = this.value !== null && this.value !== undefined ?
|
|
180
|
+
this.value.toString() : this.placeholder;
|
|
181
|
+
const isPlaceholder = this.value === null || this.value === undefined || this.value === '';
|
|
182
|
+
|
|
183
|
+
if (this.platform === 'material') {
|
|
184
|
+
// Material Design NumberInput
|
|
185
|
+
ctx.strokeStyle = this.focused ? '#6200EE' : '#CCCCCC';
|
|
186
|
+
ctx.lineWidth = this.focused ? 2 : 1;
|
|
187
|
+
ctx.beginPath();
|
|
188
|
+
ctx.moveTo(this.x, this.y + this.height);
|
|
189
|
+
ctx.lineTo(this.x + this.width, this.y + this.height);
|
|
190
|
+
ctx.stroke();
|
|
191
|
+
|
|
192
|
+
// Valeur
|
|
193
|
+
ctx.fillStyle = isPlaceholder ? '#999999' : '#000000';
|
|
194
|
+
ctx.font = `${this.fontSize}px Roboto, sans-serif`;
|
|
195
|
+
ctx.textAlign = 'left';
|
|
196
|
+
ctx.textBaseline = 'middle';
|
|
197
|
+
ctx.fillText(displayValue, this.x + 2, this.y + this.height / 2);
|
|
198
|
+
|
|
199
|
+
} else {
|
|
200
|
+
// Cupertino NumberInput
|
|
201
|
+
ctx.strokeStyle = this.focused ? '#007AFF' : '#C7C7CC';
|
|
202
|
+
ctx.lineWidth = 1;
|
|
203
|
+
ctx.beginPath();
|
|
204
|
+
this.roundRect(ctx, this.x, this.y, this.width, this.height, 8);
|
|
205
|
+
ctx.stroke();
|
|
206
|
+
|
|
207
|
+
// Valeur
|
|
208
|
+
ctx.fillStyle = isPlaceholder ? '#999999' : '#000000';
|
|
209
|
+
ctx.font = `${this.fontSize}px -apple-system, sans-serif`;
|
|
210
|
+
ctx.textAlign = 'left';
|
|
211
|
+
ctx.textBaseline = 'middle';
|
|
212
|
+
ctx.fillText(displayValue, this.x + 10, this.y + this.height / 2);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Curseur
|
|
216
|
+
if (this.focused && this.cursorVisible) {
|
|
217
|
+
const textWidth = ctx.measureText(displayValue).width;
|
|
218
|
+
ctx.fillStyle = this.platform === 'material' ? '#6200EE' : '#007AFF';
|
|
219
|
+
ctx.fillRect(this.x + (this.platform === 'material' ? 2 : 10) + textWidth + 2,
|
|
220
|
+
this.y + 10, 2, this.height - 20);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
ctx.restore();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Dessine un rectangle avec des coins arrondis
|
|
228
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
229
|
+
* @param {number} x - Position X
|
|
230
|
+
* @param {number} y - Position Y
|
|
231
|
+
* @param {number} width - Largeur
|
|
232
|
+
* @param {number} height - Hauteur
|
|
233
|
+
* @param {number} radius - Rayon des coins
|
|
234
|
+
* @private
|
|
235
|
+
*/
|
|
236
|
+
roundRect(ctx, x, y, width, height, radius) {
|
|
237
|
+
ctx.beginPath();
|
|
238
|
+
ctx.moveTo(x + radius, y);
|
|
239
|
+
ctx.lineTo(x + width - radius, y);
|
|
240
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
241
|
+
ctx.lineTo(x + width, y + height - radius);
|
|
242
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
243
|
+
ctx.lineTo(x + radius, y + height);
|
|
244
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
245
|
+
ctx.lineTo(x, y + radius);
|
|
246
|
+
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
247
|
+
ctx.closePath();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Vérifie si un point est à l'intérieur du composant
|
|
252
|
+
* @param {number} x - Position X
|
|
253
|
+
* @param {number} y - Position Y
|
|
254
|
+
* @returns {boolean} True si le point est à l'intérieur
|
|
255
|
+
*/
|
|
256
|
+
isPointInside(x, y) {
|
|
257
|
+
return x >= this.x && x <= this.x + this.width &&
|
|
258
|
+
y >= this.y && y <= this.y + this.height;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Nettoie les ressources (supprime l'input caché du DOM)
|
|
263
|
+
*/
|
|
264
|
+
destroy() {
|
|
265
|
+
if (this.hiddenInput && this.hiddenInput.parentNode) {
|
|
266
|
+
this.hiddenInput.parentNode.removeChild(this.hiddenInput);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export default NumberInput;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import Component from '../core/Component.js';
|
|
2
|
+
/**
|
|
3
|
+
* Barre de progression
|
|
4
|
+
* @class
|
|
5
|
+
* @extends Component
|
|
6
|
+
* @property {number} progress - Progression (0-100)
|
|
7
|
+
* @property {string} platform - Plateforme ('material' ou 'cupertino')
|
|
8
|
+
* @property {number} height - Hauteur de la barre
|
|
9
|
+
*/
|
|
10
|
+
class ProgressBar extends Component {
|
|
11
|
+
/**
|
|
12
|
+
* Crée une instance de ProgressBar
|
|
13
|
+
* @param {CanvasFramework} framework - Framework parent
|
|
14
|
+
* @param {Object} [options={}] - Options de configuration
|
|
15
|
+
* @param {number} [options.progress=0] - Progression (0-100)
|
|
16
|
+
* @param {number} [options.height=4] - Hauteur de la barre
|
|
17
|
+
*/
|
|
18
|
+
constructor(framework, options = {}) {
|
|
19
|
+
super(framework, options);
|
|
20
|
+
this.progress = options.progress || 0; // 0-100
|
|
21
|
+
this.platform = framework.platform;
|
|
22
|
+
this.height = options.height || 4;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Dessine la barre de progression
|
|
27
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
28
|
+
*/
|
|
29
|
+
draw(ctx) {
|
|
30
|
+
ctx.save();
|
|
31
|
+
|
|
32
|
+
if (this.platform === 'material') {
|
|
33
|
+
// Track
|
|
34
|
+
ctx.fillStyle = '#E0E0E0';
|
|
35
|
+
ctx.fillRect(this.x, this.y, this.width, this.height);
|
|
36
|
+
|
|
37
|
+
// Progress
|
|
38
|
+
ctx.fillStyle = '#6200EE';
|
|
39
|
+
ctx.fillRect(this.x, this.y, (this.width * this.progress) / 100, this.height);
|
|
40
|
+
} else {
|
|
41
|
+
// Cupertino
|
|
42
|
+
ctx.fillStyle = '#E5E5EA';
|
|
43
|
+
ctx.beginPath();
|
|
44
|
+
this.roundRect(ctx, this.x, this.y, this.width, this.height, this.height / 2);
|
|
45
|
+
ctx.fill();
|
|
46
|
+
|
|
47
|
+
ctx.fillStyle = '#007AFF';
|
|
48
|
+
ctx.beginPath();
|
|
49
|
+
this.roundRect(ctx, this.x, this.y, (this.width * this.progress) / 100, this.height, this.height / 2);
|
|
50
|
+
ctx.fill();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
ctx.restore();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Dessine un rectangle avec coins arrondis
|
|
58
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
59
|
+
* @param {number} x - Position X
|
|
60
|
+
* @param {number} y - Position Y
|
|
61
|
+
* @param {number} width - Largeur
|
|
62
|
+
* @param {number} height - Hauteur
|
|
63
|
+
* @param {number} radius - Rayon des coins
|
|
64
|
+
* @private
|
|
65
|
+
*/
|
|
66
|
+
roundRect(ctx, x, y, width, height, radius) {
|
|
67
|
+
if (width < radius * 2) width = radius * 2;
|
|
68
|
+
ctx.moveTo(x + radius, y);
|
|
69
|
+
ctx.lineTo(x + width - radius, y);
|
|
70
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
71
|
+
ctx.lineTo(x + width, y + height - radius);
|
|
72
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
73
|
+
ctx.lineTo(x + radius, y + height);
|
|
74
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
75
|
+
ctx.lineTo(x, y + radius);
|
|
76
|
+
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Vérifie si un point est dans les limites (toujours false pour ProgressBar)
|
|
81
|
+
* @returns {boolean} False (non cliquable)
|
|
82
|
+
*/
|
|
83
|
+
isPointInside() {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export default ProgressBar;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import Component from '../core/Component.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Bouton radio pour les sélections uniques
|
|
5
|
+
* @class
|
|
6
|
+
* @extends Component
|
|
7
|
+
* @property {string} group - Groupe de boutons radio
|
|
8
|
+
* @property {boolean} checked - État sélectionné
|
|
9
|
+
* @property {string} label - Texte du label
|
|
10
|
+
* @property {string} platform - Plateforme
|
|
11
|
+
* @property {number} circleSize - Taille du cercle
|
|
12
|
+
* @property {number} circleRadius - Rayon du cercle
|
|
13
|
+
* @property {Function} onChange - Callback au changement
|
|
14
|
+
*/
|
|
15
|
+
class RadioButton extends Component {
|
|
16
|
+
/**
|
|
17
|
+
* Crée une instance de RadioButton
|
|
18
|
+
* @param {CanvasFramework} framework - Framework parent
|
|
19
|
+
* @param {Object} [options={}] - Options de configuration
|
|
20
|
+
* @param {string} [options.group='default'] - Groupe de boutons
|
|
21
|
+
* @param {boolean} [options.checked=false] - État initial
|
|
22
|
+
* @param {string} [options.label=''] - Texte du label
|
|
23
|
+
* @param {Function} [options.onChange] - Callback au changement
|
|
24
|
+
*/
|
|
25
|
+
constructor(framework, options = {}) {
|
|
26
|
+
super(framework, options);
|
|
27
|
+
this.group = options.group || 'default';
|
|
28
|
+
this.checked = options.checked || false;
|
|
29
|
+
this.label = options.label || '';
|
|
30
|
+
this.platform = framework.platform;
|
|
31
|
+
this.circleSize = 24; // Taille du cercle
|
|
32
|
+
this.circleRadius = 10; // Rayon du cercle
|
|
33
|
+
this.onChange = options.onChange;
|
|
34
|
+
|
|
35
|
+
// Calculer la largeur totale incluant le label
|
|
36
|
+
this.totalWidth = this.label ? this.circleSize + 8 + this.getTextWidth(this.label) : this.circleSize;
|
|
37
|
+
this.width = this.totalWidth; // Mettre à jour la largeur totale
|
|
38
|
+
this.height = this.circleSize; // Garder la même hauteur
|
|
39
|
+
|
|
40
|
+
// Définir onClick
|
|
41
|
+
this.onClick = this.handleClick.bind(this);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Calcule la largeur du texte
|
|
46
|
+
* @param {string} text - Texte à mesurer
|
|
47
|
+
* @returns {number} Largeur du texte
|
|
48
|
+
* @private
|
|
49
|
+
*/
|
|
50
|
+
getTextWidth(text) {
|
|
51
|
+
// Utiliser le contexte temporaire pour mesurer le texte
|
|
52
|
+
const ctx = this.framework.ctx;
|
|
53
|
+
ctx.save();
|
|
54
|
+
ctx.font = '16px -apple-system, sans-serif';
|
|
55
|
+
const width = ctx.measureText(text).width;
|
|
56
|
+
ctx.restore();
|
|
57
|
+
return width;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Gère le clic sur le bouton radio
|
|
62
|
+
* @private
|
|
63
|
+
*/
|
|
64
|
+
handleClick() {
|
|
65
|
+
// Décocher les autres du même groupe
|
|
66
|
+
for (let comp of this.framework.components) {
|
|
67
|
+
if (comp instanceof RadioButton && comp.group === this.group && comp !== this) {
|
|
68
|
+
comp.checked = false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
this.checked = true;
|
|
72
|
+
if (this.onChange) this.onChange(this.checked);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Dessine le bouton radio
|
|
77
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
78
|
+
*/
|
|
79
|
+
draw(ctx) {
|
|
80
|
+
ctx.save();
|
|
81
|
+
|
|
82
|
+
const centerX = this.x + this.circleSize / 2;
|
|
83
|
+
const centerY = this.y + this.circleSize / 2;
|
|
84
|
+
|
|
85
|
+
if (this.platform === 'material') {
|
|
86
|
+
// Outer circle
|
|
87
|
+
ctx.strokeStyle = this.checked ? '#6200EE' : '#666666';
|
|
88
|
+
ctx.lineWidth = 2;
|
|
89
|
+
ctx.beginPath();
|
|
90
|
+
ctx.arc(centerX, centerY, this.circleRadius, 0, Math.PI * 2);
|
|
91
|
+
ctx.stroke();
|
|
92
|
+
|
|
93
|
+
// Inner circle
|
|
94
|
+
if (this.checked) {
|
|
95
|
+
ctx.fillStyle = '#6200EE';
|
|
96
|
+
ctx.beginPath();
|
|
97
|
+
ctx.arc(centerX, centerY, 5, 0, Math.PI * 2);
|
|
98
|
+
ctx.fill();
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
// Cupertino
|
|
102
|
+
ctx.strokeStyle = this.checked ? '#007AFF' : '#C7C7CC';
|
|
103
|
+
ctx.lineWidth = 2;
|
|
104
|
+
ctx.beginPath();
|
|
105
|
+
ctx.arc(centerX, centerY, this.circleRadius, 0, Math.PI * 2);
|
|
106
|
+
ctx.stroke();
|
|
107
|
+
|
|
108
|
+
if (this.checked) {
|
|
109
|
+
ctx.fillStyle = '#007AFF';
|
|
110
|
+
ctx.beginPath();
|
|
111
|
+
ctx.arc(centerX, centerY, 5, 0, Math.PI * 2);
|
|
112
|
+
ctx.fill();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Label
|
|
117
|
+
if (this.label) {
|
|
118
|
+
ctx.fillStyle = '#000000';
|
|
119
|
+
ctx.font = '16px -apple-system, sans-serif';
|
|
120
|
+
ctx.textAlign = 'left';
|
|
121
|
+
ctx.textBaseline = 'middle';
|
|
122
|
+
ctx.fillText(this.label, this.x + this.circleSize + 8, centerY);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
ctx.restore();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Vérifie si un point est dans les limites
|
|
130
|
+
* @param {number} x - Coordonnée X
|
|
131
|
+
* @param {number} y - Coordonnée Y
|
|
132
|
+
* @returns {boolean} True si le point est dans le bouton
|
|
133
|
+
*/
|
|
134
|
+
isPointInside(x, y) {
|
|
135
|
+
return x >= this.x &&
|
|
136
|
+
x <= this.x + this.width &&
|
|
137
|
+
y >= this.y &&
|
|
138
|
+
y <= this.y + this.height;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export default RadioButton;
|