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,309 @@
|
|
|
1
|
+
import Component from '../core/Component.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Champ de saisie texte
|
|
5
|
+
* @class
|
|
6
|
+
* @extends Component
|
|
7
|
+
* @property {string} placeholder - Texte d'indication
|
|
8
|
+
* @property {string} value - Valeur
|
|
9
|
+
* @property {number} fontSize - Taille de police
|
|
10
|
+
* @property {boolean} focused - Focus actif
|
|
11
|
+
* @property {string} platform - Plateforme
|
|
12
|
+
* @property {boolean} cursorVisible - Curseur visible
|
|
13
|
+
* @property {number} cursorPosition - Position du curseur
|
|
14
|
+
* @property {HTMLInputElement} hiddenInput - Input HTML caché
|
|
15
|
+
*/
|
|
16
|
+
class Input extends Component {
|
|
17
|
+
static activeInput = null;
|
|
18
|
+
static allInputs = new Set();
|
|
19
|
+
static globalClickHandler = null;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Crée une instance de Input
|
|
23
|
+
* @param {CanvasFramework} framework - Framework parent
|
|
24
|
+
* @param {Object} [options={}] - Options de configuration
|
|
25
|
+
* @param {string} [options.placeholder=''] - Texte d'indication
|
|
26
|
+
* @param {string} [options.value=''] - Valeur initiale
|
|
27
|
+
* @param {number} [options.fontSize=16] - Taille de police
|
|
28
|
+
* @param {Function} [options.onFocus] - Callback au focus
|
|
29
|
+
* @param {Function} [options.onBlur] - Callback au blur
|
|
30
|
+
*/
|
|
31
|
+
constructor(framework, options = {}) {
|
|
32
|
+
super(framework, options);
|
|
33
|
+
this.placeholder = options.placeholder || '';
|
|
34
|
+
this.value = options.value || '';
|
|
35
|
+
this.fontSize = options.fontSize || 16;
|
|
36
|
+
this.focused = false;
|
|
37
|
+
this.platform = framework.platform;
|
|
38
|
+
this.cursorVisible = true;
|
|
39
|
+
this.cursorPosition = this.value.length;
|
|
40
|
+
|
|
41
|
+
// Gestion du focus
|
|
42
|
+
this.onFocus = this.onFocus.bind(this);
|
|
43
|
+
this.onBlur = this.onBlur.bind(this);
|
|
44
|
+
|
|
45
|
+
// Enregistrer cet input
|
|
46
|
+
Input.allInputs.add(this);
|
|
47
|
+
|
|
48
|
+
// Animation du curseur
|
|
49
|
+
this.cursorInterval = setInterval(() => {
|
|
50
|
+
if (this.focused) this.cursorVisible = !this.cursorVisible;
|
|
51
|
+
}, 500);
|
|
52
|
+
|
|
53
|
+
// Écouter les clics partout pour détecter quand on clique ailleurs
|
|
54
|
+
this.setupGlobalClickHandler();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Écoute les clics globaux pour détecter les clics hors input
|
|
59
|
+
*/
|
|
60
|
+
setupGlobalClickHandler() {
|
|
61
|
+
// On crée un gestionnaire unique pour tous les inputs
|
|
62
|
+
if (!Input.globalClickHandler) {
|
|
63
|
+
Input.globalClickHandler = (e) => {
|
|
64
|
+
// Vérifier si on a cliqué en dehors de TOUS les inputs
|
|
65
|
+
let clickedOnInput = false;
|
|
66
|
+
|
|
67
|
+
for (let input of Input.allInputs) {
|
|
68
|
+
if (input.hiddenInput && e.target === input.hiddenInput) {
|
|
69
|
+
clickedOnInput = true;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Si on n'a pas cliqué sur un input, détruire tous les inputs HTML
|
|
75
|
+
if (!clickedOnInput) {
|
|
76
|
+
Input.removeAllHiddenInputs();
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Attacher l'écouteur avec capture pour qu'il se déclenche tôt
|
|
81
|
+
document.addEventListener('click', Input.globalClickHandler, true);
|
|
82
|
+
document.addEventListener('touchstart', Input.globalClickHandler, true);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Configure l'input HTML caché
|
|
88
|
+
* @private
|
|
89
|
+
*/
|
|
90
|
+
setupHiddenInput() {
|
|
91
|
+
if (this.hiddenInput) return;
|
|
92
|
+
|
|
93
|
+
// Créer un input HTML caché unique pour cette instance
|
|
94
|
+
this.hiddenInput = document.createElement('input');
|
|
95
|
+
this.hiddenInput.style.position = 'fixed';
|
|
96
|
+
this.hiddenInput.style.opacity = '0';
|
|
97
|
+
this.hiddenInput.style.pointerEvents = 'none';
|
|
98
|
+
this.hiddenInput.style.top = '-100px';
|
|
99
|
+
this.hiddenInput.style.zIndex = '9999';
|
|
100
|
+
document.body.appendChild(this.hiddenInput);
|
|
101
|
+
|
|
102
|
+
this.hiddenInput.addEventListener('input', (e) => {
|
|
103
|
+
if (this.focused) {
|
|
104
|
+
this.value = e.target.value;
|
|
105
|
+
this.cursorPosition = this.value.length;
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
this.hiddenInput.addEventListener('blur', () => {
|
|
110
|
+
this.focused = false;
|
|
111
|
+
this.cursorVisible = false;
|
|
112
|
+
|
|
113
|
+
// Détruire l'input HTML après un court délai
|
|
114
|
+
setTimeout(() => {
|
|
115
|
+
this.destroyHiddenInput();
|
|
116
|
+
}, 100);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Gère le focus
|
|
122
|
+
*/
|
|
123
|
+
onFocus() {
|
|
124
|
+
// Si c'est déjà l'input actif, ne rien faire
|
|
125
|
+
if (Input.activeInput === this) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// D'abord, détruire TOUS les autres inputs HTML
|
|
130
|
+
Input.removeAllHiddenInputs();
|
|
131
|
+
|
|
132
|
+
// Désactiver tous les autres inputs visuellement
|
|
133
|
+
for (let input of Input.allInputs) {
|
|
134
|
+
if (input !== this) {
|
|
135
|
+
input.focused = false;
|
|
136
|
+
input.cursorVisible = false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Activer celui-ci
|
|
141
|
+
this.focused = true;
|
|
142
|
+
this.cursorVisible = true;
|
|
143
|
+
Input.activeInput = this;
|
|
144
|
+
|
|
145
|
+
// Créer l'input HTML si nécessaire
|
|
146
|
+
this.setupHiddenInput();
|
|
147
|
+
|
|
148
|
+
if (this.hiddenInput) {
|
|
149
|
+
this.hiddenInput.value = this.value;
|
|
150
|
+
// Positionner l'input au bon endroit pour le scroll du clavier
|
|
151
|
+
const adjustedY = this.y + this.framework.scrollOffset;
|
|
152
|
+
this.hiddenInput.style.top = `${adjustedY}px`;
|
|
153
|
+
|
|
154
|
+
// Focus avec un petit délai
|
|
155
|
+
setTimeout(() => {
|
|
156
|
+
if (this.hiddenInput && this.focused) {
|
|
157
|
+
this.hiddenInput.focus();
|
|
158
|
+
// Positionner le curseur à la fin
|
|
159
|
+
this.hiddenInput.setSelectionRange(this.value.length, this.value.length);
|
|
160
|
+
}
|
|
161
|
+
}, 50);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Gère le blur
|
|
167
|
+
*/
|
|
168
|
+
onBlur() {
|
|
169
|
+
this.focused = false;
|
|
170
|
+
this.cursorVisible = false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Détruit l'input HTML
|
|
175
|
+
*/
|
|
176
|
+
destroyHiddenInput() {
|
|
177
|
+
if (this.hiddenInput && this.hiddenInput.parentNode) {
|
|
178
|
+
this.hiddenInput.parentNode.removeChild(this.hiddenInput);
|
|
179
|
+
this.hiddenInput = null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Gère le clic
|
|
185
|
+
*/
|
|
186
|
+
onClick() {
|
|
187
|
+
this.onFocus();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Méthode statique pour détruire tous les inputs HTML
|
|
192
|
+
*/
|
|
193
|
+
static removeAllHiddenInputs() {
|
|
194
|
+
// Désactiver tous les inputs visuels
|
|
195
|
+
for (let input of Input.allInputs) {
|
|
196
|
+
input.focused = false;
|
|
197
|
+
input.cursorVisible = false;
|
|
198
|
+
|
|
199
|
+
// Détruire l'input HTML
|
|
200
|
+
if (input.hiddenInput && input.hiddenInput.parentNode) {
|
|
201
|
+
input.hiddenInput.parentNode.removeChild(input.hiddenInput);
|
|
202
|
+
input.hiddenInput = null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
Input.activeInput = null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Vérifie si un point est dans les limites
|
|
211
|
+
* @param {number} x - Coordonnée X
|
|
212
|
+
* @param {number} y - Coordonnée Y
|
|
213
|
+
* @returns {boolean} True si le point est dans l'input
|
|
214
|
+
*/
|
|
215
|
+
isPointInside(x, y) {
|
|
216
|
+
return x >= this.x &&
|
|
217
|
+
x <= this.x + this.width &&
|
|
218
|
+
y >= this.y &&
|
|
219
|
+
y <= this.y + this.height;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Dessine l'input
|
|
224
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
225
|
+
*/
|
|
226
|
+
draw(ctx) {
|
|
227
|
+
ctx.save();
|
|
228
|
+
|
|
229
|
+
if (this.platform === 'material') {
|
|
230
|
+
// Material Design Input
|
|
231
|
+
ctx.strokeStyle = this.focused ? '#6200EE' : '#CCCCCC';
|
|
232
|
+
ctx.lineWidth = this.focused ? 2 : 1;
|
|
233
|
+
ctx.beginPath();
|
|
234
|
+
ctx.moveTo(this.x, this.y + this.height);
|
|
235
|
+
ctx.lineTo(this.x + this.width, this.y + this.height);
|
|
236
|
+
ctx.stroke();
|
|
237
|
+
} else {
|
|
238
|
+
// Cupertino Input
|
|
239
|
+
ctx.strokeStyle = this.focused ? '#007AFF' : '#C7C7CC';
|
|
240
|
+
ctx.lineWidth = 1;
|
|
241
|
+
ctx.beginPath();
|
|
242
|
+
this.roundRect(ctx, this.x, this.y, this.width, this.height, 8);
|
|
243
|
+
ctx.stroke();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Texte
|
|
247
|
+
ctx.fillStyle = this.value ? '#000000' : '#999999';
|
|
248
|
+
ctx.font = `${this.fontSize}px -apple-system, BlinkMacSystemFont, 'Roboto', sans-serif`;
|
|
249
|
+
ctx.textAlign = 'left';
|
|
250
|
+
ctx.textBaseline = 'middle';
|
|
251
|
+
const displayText = this.value || this.placeholder;
|
|
252
|
+
ctx.fillText(displayText, this.x + 10, this.y + this.height / 2);
|
|
253
|
+
|
|
254
|
+
// Curseur
|
|
255
|
+
if (this.focused && this.cursorVisible && this.value) {
|
|
256
|
+
const textWidth = ctx.measureText(this.value).width;
|
|
257
|
+
ctx.fillStyle = '#000000';
|
|
258
|
+
ctx.fillRect(this.x + 10 + textWidth, this.y + 10, 2, this.height - 20);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
ctx.restore();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Dessine un rectangle avec coins arrondis
|
|
266
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
267
|
+
* @param {number} x - Position X
|
|
268
|
+
* @param {number} y - Position Y
|
|
269
|
+
* @param {number} width - Largeur
|
|
270
|
+
* @param {number} height - Hauteur
|
|
271
|
+
* @param {number} radius - Rayon des coins
|
|
272
|
+
* @private
|
|
273
|
+
*/
|
|
274
|
+
roundRect(ctx, x, y, width, height, radius) {
|
|
275
|
+
ctx.moveTo(x + radius, y);
|
|
276
|
+
ctx.lineTo(x + width - radius, y);
|
|
277
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
278
|
+
ctx.lineTo(x + width, y + height - radius);
|
|
279
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
280
|
+
ctx.lineTo(x + radius, y + height);
|
|
281
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
282
|
+
ctx.lineTo(x, y + radius);
|
|
283
|
+
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Nettoie les ressources
|
|
288
|
+
*/
|
|
289
|
+
destroy() {
|
|
290
|
+
this.destroyHiddenInput();
|
|
291
|
+
|
|
292
|
+
if (this.cursorInterval) {
|
|
293
|
+
clearInterval(this.cursorInterval);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
Input.allInputs.delete(this);
|
|
297
|
+
|
|
298
|
+
// Si c'était le dernier input, retirer le gestionnaire global
|
|
299
|
+
if (Input.allInputs.size === 0 && Input.globalClickHandler) {
|
|
300
|
+
document.removeEventListener('click', Input.globalClickHandler, true);
|
|
301
|
+
document.removeEventListener('touchstart', Input.globalClickHandler, true);
|
|
302
|
+
Input.globalClickHandler = null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
super.destroy && super.destroy();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export default Input;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import Component from '../core/Component.js';
|
|
2
|
+
import ListItem from '../components/ListItem.js';
|
|
3
|
+
/**
|
|
4
|
+
* Conteneur pour les éléments de liste (ListItems) avec défilement automatique
|
|
5
|
+
* @class
|
|
6
|
+
* @extends Component
|
|
7
|
+
* @param {Framework} framework - Instance du framework
|
|
8
|
+
* @param {Object} [options={}] - Options de configuration
|
|
9
|
+
* @param {number} [options.itemHeight=56] - Hauteur de chaque item en pixels
|
|
10
|
+
* @param {Function} [options.onItemClick] - Callback appelé lors du clic sur un item
|
|
11
|
+
* @param {number} [options.y=0] - Position Y de départ
|
|
12
|
+
* @example
|
|
13
|
+
* const list = new List(framework, {
|
|
14
|
+
* itemHeight: 64,
|
|
15
|
+
* onItemClick: (index, itemOptions) => console.log('Item clicked:', index)
|
|
16
|
+
* });
|
|
17
|
+
*/
|
|
18
|
+
class List extends Component {
|
|
19
|
+
/**
|
|
20
|
+
* @constructs List
|
|
21
|
+
*/
|
|
22
|
+
constructor(framework, options = {}) {
|
|
23
|
+
super(framework, options);
|
|
24
|
+
/** @type {ListItem[]} */
|
|
25
|
+
this.items = [];
|
|
26
|
+
/** @type {number} */
|
|
27
|
+
this.itemHeight = options.itemHeight || 56;
|
|
28
|
+
/** @type {Function|undefined} */
|
|
29
|
+
this.onItemClick = options.onItemClick;
|
|
30
|
+
/** @type {number} */
|
|
31
|
+
this.y = options.y || 0; // Position Y de départ
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Ajoute un item à la liste
|
|
36
|
+
* @param {Object} itemOptions - Options pour l'item
|
|
37
|
+
* @param {string} itemOptions.text - Texte à afficher
|
|
38
|
+
* @param {Function} [itemOptions.onClick] - Callback spécifique à l'item
|
|
39
|
+
* @param {Object} [itemOptions.style] - Style optionnel pour l'item
|
|
40
|
+
* @returns {ListItem} L'item créé
|
|
41
|
+
*/
|
|
42
|
+
addItem(itemOptions) {
|
|
43
|
+
const item = new ListItem(this.framework, {
|
|
44
|
+
...itemOptions,
|
|
45
|
+
x: this.x,
|
|
46
|
+
y: this.y + (this.items.length * this.itemHeight),
|
|
47
|
+
width: this.width,
|
|
48
|
+
height: this.itemHeight, // IMPORTANT: définir la hauteur
|
|
49
|
+
onClick: () => {
|
|
50
|
+
if (this.onItemClick) {
|
|
51
|
+
this.onItemClick(this.items.length, itemOptions);
|
|
52
|
+
}
|
|
53
|
+
if (itemOptions.onClick) {
|
|
54
|
+
itemOptions.onClick();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
this.items.push(item);
|
|
60
|
+
this.framework.add(item); // Ajouter chaque item au framework
|
|
61
|
+
this.height = this.items.length * this.itemHeight; // Mettre à jour la hauteur totale
|
|
62
|
+
|
|
63
|
+
return item;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Vide la liste et supprime tous les items du framework
|
|
68
|
+
*/
|
|
69
|
+
clear() {
|
|
70
|
+
for (let item of this.items) {
|
|
71
|
+
this.framework.remove(item);
|
|
72
|
+
}
|
|
73
|
+
this.items = [];
|
|
74
|
+
this.height = 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Dessine le composant (les items se dessinent eux-mêmes)
|
|
79
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
80
|
+
*/
|
|
81
|
+
draw(ctx) {
|
|
82
|
+
// Les items se dessinent eux-mêmes
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Vérifie si un point est à l'intérieur du composant
|
|
87
|
+
* @returns {boolean} Toujours false (les ListItems gèrent leurs propres clics)
|
|
88
|
+
*/
|
|
89
|
+
isPointInside() {
|
|
90
|
+
return false; // Les ListItems gèrent leurs propres clics
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export default List;
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import Component from '../core/Component.js';
|
|
2
|
+
/**
|
|
3
|
+
* Élément de liste
|
|
4
|
+
* @class
|
|
5
|
+
* @extends Component
|
|
6
|
+
* @property {string} title - Titre
|
|
7
|
+
* @property {string} subtitle - Sous-titre
|
|
8
|
+
* @property {string|null} leftIcon - Icône gauche
|
|
9
|
+
* @property {string|null} leftImage - Image gauche (URL)
|
|
10
|
+
* @property {string|null} rightIcon - Icône droite
|
|
11
|
+
* @property {string|null} rightText - Texte droite
|
|
12
|
+
* @property {boolean} divider - Afficher un diviseur
|
|
13
|
+
* @property {string} platform - Plateforme
|
|
14
|
+
* @property {string} bgColor - Couleur de fond
|
|
15
|
+
* @property {Array} ripples - Effets ripple (Material)
|
|
16
|
+
*/
|
|
17
|
+
class ListItem extends Component {
|
|
18
|
+
/**
|
|
19
|
+
* Crée une instance de ListItem
|
|
20
|
+
* @param {CanvasFramework} framework - Framework parent
|
|
21
|
+
* @param {Object} [options={}] - Options de configuration
|
|
22
|
+
* @param {string} [options.title=''] - Titre
|
|
23
|
+
* @param {string} [options.subtitle=''] - Sous-titre
|
|
24
|
+
* @param {string} [options.leftIcon] - Icône gauche
|
|
25
|
+
* @param {string} [options.leftImage] - URL image gauche
|
|
26
|
+
* @param {string} [options.rightIcon] - Icône droite
|
|
27
|
+
* @param {string} [options.rightText] - Texte droite
|
|
28
|
+
* @param {boolean} [options.divider=true] - Diviseur
|
|
29
|
+
* @param {string} [options.bgColor='#FFFFFF'] - Couleur de fond
|
|
30
|
+
* @param {number} [options.height] - Hauteur (auto selon contenu)
|
|
31
|
+
*/
|
|
32
|
+
constructor(framework, options = {}) {
|
|
33
|
+
super(framework, options);
|
|
34
|
+
this.title = options.title || '';
|
|
35
|
+
this.subtitle = options.subtitle || '';
|
|
36
|
+
this.leftIcon = options.leftIcon || null;
|
|
37
|
+
this.leftImage = options.leftImage || null; // URL d'image
|
|
38
|
+
this.rightIcon = options.rightIcon || null;
|
|
39
|
+
this.rightText = options.rightText || '';
|
|
40
|
+
this.divider = options.divider !== false;
|
|
41
|
+
this.platform = framework.platform;
|
|
42
|
+
this.height = options.height || (this.subtitle ? 72 : 56);
|
|
43
|
+
this.width = options.width || framework.width;
|
|
44
|
+
this.bgColor = options.bgColor || '#FFFFFF';
|
|
45
|
+
this.ripples = []; // Pour l'effet ripple Material
|
|
46
|
+
|
|
47
|
+
this.onPress = this.handlePress.bind(this);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Gère la pression (clic)
|
|
52
|
+
* @param {number} x - Coordonnée X
|
|
53
|
+
* @param {number} y - Coordonnée Y
|
|
54
|
+
* @private
|
|
55
|
+
*/
|
|
56
|
+
handlePress(x, y) {
|
|
57
|
+
if (this.platform === 'material') {
|
|
58
|
+
const adjustedY = y - this.framework.scrollOffset;
|
|
59
|
+
this.ripples.push({
|
|
60
|
+
x: x - this.x,
|
|
61
|
+
y: adjustedY - this.y,
|
|
62
|
+
radius: 0,
|
|
63
|
+
maxRadius: Math.max(this.width, this.height) * 1.5,
|
|
64
|
+
opacity: 1
|
|
65
|
+
});
|
|
66
|
+
this.animateRipple();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Anime les effets ripple
|
|
72
|
+
* @private
|
|
73
|
+
*/
|
|
74
|
+
animateRipple() {
|
|
75
|
+
const animate = () => {
|
|
76
|
+
let hasActiveRipples = false;
|
|
77
|
+
|
|
78
|
+
for (let ripple of this.ripples) {
|
|
79
|
+
if (ripple.radius < ripple.maxRadius) {
|
|
80
|
+
ripple.radius += ripple.maxRadius / 15;
|
|
81
|
+
hasActiveRipples = true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (ripple.radius >= ripple.maxRadius * 0.5) {
|
|
85
|
+
ripple.opacity -= 0.05;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this.ripples = this.ripples.filter(r => r.opacity > 0);
|
|
90
|
+
|
|
91
|
+
if (hasActiveRipples) {
|
|
92
|
+
requestAnimationFrame(animate);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
animate();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Dessine l'élément de liste
|
|
101
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
102
|
+
*/
|
|
103
|
+
draw(ctx) {
|
|
104
|
+
ctx.save();
|
|
105
|
+
|
|
106
|
+
// Background
|
|
107
|
+
ctx.fillStyle = this.pressed ? '#F5F5F5' : this.bgColor;
|
|
108
|
+
ctx.fillRect(this.x, this.y, this.width, this.height);
|
|
109
|
+
|
|
110
|
+
// Ripple effect (Material)
|
|
111
|
+
if (this.platform === 'material' && this.ripples.length > 0) {
|
|
112
|
+
ctx.save();
|
|
113
|
+
ctx.beginPath();
|
|
114
|
+
ctx.rect(this.x, this.y, this.width, this.height);
|
|
115
|
+
ctx.clip();
|
|
116
|
+
|
|
117
|
+
for (let ripple of this.ripples) {
|
|
118
|
+
ctx.globalAlpha = ripple.opacity;
|
|
119
|
+
ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
|
|
120
|
+
ctx.beginPath();
|
|
121
|
+
ctx.arc(this.x + ripple.x, this.y + ripple.y, ripple.radius, 0, Math.PI * 2);
|
|
122
|
+
ctx.fill();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
ctx.restore();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let leftOffset = 16;
|
|
129
|
+
|
|
130
|
+
// Left Icon ou Image
|
|
131
|
+
if (this.leftIcon) {
|
|
132
|
+
ctx.fillStyle = '#757575';
|
|
133
|
+
ctx.font = '24px sans-serif';
|
|
134
|
+
ctx.textAlign = 'left';
|
|
135
|
+
ctx.textBaseline = 'middle';
|
|
136
|
+
ctx.fillText(this.leftIcon, this.x + leftOffset, this.y + this.height / 2);
|
|
137
|
+
leftOffset += 48;
|
|
138
|
+
} else if (this.leftImage) {
|
|
139
|
+
// Circle pour l'avatar
|
|
140
|
+
ctx.fillStyle = '#E0E0E0';
|
|
141
|
+
ctx.beginPath();
|
|
142
|
+
ctx.arc(this.x + leftOffset + 20, this.y + this.height / 2, 20, 0, Math.PI * 2);
|
|
143
|
+
ctx.fill();
|
|
144
|
+
|
|
145
|
+
// TODO: Charger vraie image
|
|
146
|
+
ctx.fillStyle = '#757575';
|
|
147
|
+
ctx.font = '14px sans-serif';
|
|
148
|
+
ctx.textAlign = 'center';
|
|
149
|
+
ctx.textBaseline = 'middle';
|
|
150
|
+
ctx.fillText('👤', this.x + leftOffset + 20, this.y + this.height / 2);
|
|
151
|
+
|
|
152
|
+
leftOffset += 56;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Title et Subtitle
|
|
156
|
+
const textX = this.x + leftOffset;
|
|
157
|
+
const centerY = this.y + this.height / 2;
|
|
158
|
+
|
|
159
|
+
if (this.subtitle) {
|
|
160
|
+
// Title
|
|
161
|
+
ctx.fillStyle = '#000000';
|
|
162
|
+
ctx.font = '16px -apple-system, Roboto, sans-serif';
|
|
163
|
+
ctx.textAlign = 'left';
|
|
164
|
+
ctx.textBaseline = 'bottom';
|
|
165
|
+
ctx.fillText(this.title, textX, centerY - 2);
|
|
166
|
+
|
|
167
|
+
// Subtitle
|
|
168
|
+
ctx.fillStyle = '#757575';
|
|
169
|
+
ctx.font = '14px -apple-system, Roboto, sans-serif';
|
|
170
|
+
ctx.textBaseline = 'top';
|
|
171
|
+
ctx.fillText(this.subtitle, textX, centerY + 2);
|
|
172
|
+
} else {
|
|
173
|
+
// Title seul (centré verticalement)
|
|
174
|
+
ctx.fillStyle = '#000000';
|
|
175
|
+
ctx.font = '16px -apple-system, Roboto, sans-serif';
|
|
176
|
+
ctx.textAlign = 'left';
|
|
177
|
+
ctx.textBaseline = 'middle';
|
|
178
|
+
ctx.fillText(this.title, textX, centerY);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Right Text ou Icon
|
|
182
|
+
if (this.rightText) {
|
|
183
|
+
ctx.fillStyle = '#757575';
|
|
184
|
+
ctx.font = '14px -apple-system, Roboto, sans-serif';
|
|
185
|
+
ctx.textAlign = 'right';
|
|
186
|
+
ctx.textBaseline = 'middle';
|
|
187
|
+
ctx.fillText(this.rightText, this.x + this.width - 16, centerY);
|
|
188
|
+
} else if (this.rightIcon) {
|
|
189
|
+
ctx.fillStyle = '#757575';
|
|
190
|
+
ctx.font = '20px sans-serif';
|
|
191
|
+
ctx.textAlign = 'right';
|
|
192
|
+
ctx.textBaseline = 'middle';
|
|
193
|
+
ctx.fillText(this.rightIcon, this.x + this.width - 16, centerY);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Divider
|
|
197
|
+
if (this.divider) {
|
|
198
|
+
ctx.strokeStyle = '#E0E0E0';
|
|
199
|
+
ctx.lineWidth = 1;
|
|
200
|
+
ctx.beginPath();
|
|
201
|
+
ctx.moveTo(this.x + leftOffset, this.y + this.height);
|
|
202
|
+
ctx.lineTo(this.x + this.width, this.y + this.height);
|
|
203
|
+
ctx.stroke();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
ctx.restore();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Vérifie si un point est dans les limites
|
|
211
|
+
* @param {number} x - Coordonnée X
|
|
212
|
+
* @param {number} y - Coordonnée Y
|
|
213
|
+
* @returns {boolean} True si le point est dans l'élément
|
|
214
|
+
*/
|
|
215
|
+
isPointInside(x, y) {
|
|
216
|
+
return x >= this.x &&
|
|
217
|
+
x <= this.x + this.width &&
|
|
218
|
+
y >= this.y &&
|
|
219
|
+
y <= this.y + this.height;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export default ListItem;
|