canvasframework 0.3.15 → 0.3.16
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/components/Accordion.js +135 -122
- package/components/BottomSheet.js +104 -244
- package/components/Checkbox.js +135 -149
- package/components/DatePicker.js +7 -0
- package/components/Dialog.js +252 -282
- package/components/IOSDatePickerWheel.js +257 -95
- package/components/SegmentedControl.js +240 -85
- package/components/TextField.js +109 -289
- package/core/CanvasFramework.js +2 -7
- package/index.js +1 -1
- package/package.json +1 -1
|
@@ -14,97 +14,140 @@ import Component from '../core/Component.js';
|
|
|
14
14
|
class SegmentedControl extends Component {
|
|
15
15
|
constructor(framework, options = {}) {
|
|
16
16
|
super(framework, options);
|
|
17
|
-
|
|
18
|
-
* Plateforme : "material" ou "cupertino"
|
|
19
|
-
* @type {string}
|
|
20
|
-
*/
|
|
17
|
+
|
|
21
18
|
this.platform = framework.platform;
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Liste des boutons
|
|
25
|
-
* @type {Array<{text: string, onClick?: Function}>}
|
|
26
|
-
*/
|
|
27
19
|
this.buttons = options.buttons || [{ text: 'One' }, { text: 'Two' }, { text: 'Three' }];
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Index du segment sélectionné
|
|
31
|
-
* @type {number}
|
|
32
|
-
*/
|
|
33
20
|
this.selectedIndex = options.selectedIndex || 0;
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Hauteur du contrôle
|
|
37
|
-
* @type {number}
|
|
38
|
-
*/
|
|
39
21
|
this.height = options.height || 40;
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Espacement entre segments
|
|
43
|
-
* @type {number}
|
|
44
|
-
*/
|
|
45
22
|
this.spacing = options.spacing || 1;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
23
|
+
|
|
24
|
+
// IMPORTANT: Lier les handlers d'événements
|
|
25
|
+
this.onPress = this.handlePress.bind(this);
|
|
26
|
+
this.onRelease = this.handleRelease.bind(this);
|
|
27
|
+
|
|
28
|
+
// État pour les animations
|
|
51
29
|
this.ripples = [];
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Index temporaire pressé pour Cupertino
|
|
55
|
-
* @type {number|null}
|
|
56
|
-
*/
|
|
57
30
|
this.pressedIndex = null;
|
|
31
|
+
this._isAnimating = false;
|
|
32
|
+
|
|
58
33
|
}
|
|
59
34
|
|
|
60
35
|
/**
|
|
61
36
|
* Gère la pression sur un segment
|
|
62
37
|
* @param {number} x - Coordonnée X du clic
|
|
63
38
|
* @param {number} y - Coordonnée Y du clic
|
|
39
|
+
* @returns {boolean} True si un segment a été cliqué
|
|
64
40
|
*/
|
|
65
41
|
handlePress(x, y) {
|
|
66
42
|
const index = this.getButtonIndexAt(x, y);
|
|
43
|
+
|
|
67
44
|
if (index !== null) {
|
|
45
|
+
// Sauvegarder l'index pressé pour l'animation
|
|
46
|
+
this.pressedIndex = index;
|
|
47
|
+
|
|
48
|
+
// Pour Material: créer un ripple
|
|
68
49
|
if (this.platform === 'material') {
|
|
69
50
|
const btnWidth = (this.width - this.spacing * (this.buttons.length - 1)) / this.buttons.length;
|
|
70
51
|
const btnX = this.x + index * (btnWidth + this.spacing);
|
|
52
|
+
|
|
71
53
|
this.ripples.push({
|
|
72
|
-
x: x - btnX,
|
|
73
|
-
y: y - this.y,
|
|
54
|
+
x: x - btnX, // Position relative au bouton
|
|
55
|
+
y: y - this.y, // Position relative au bouton
|
|
74
56
|
index: index,
|
|
75
57
|
radius: 0,
|
|
76
58
|
maxRadius: Math.max(btnWidth, this.height) * 1.5,
|
|
77
|
-
opacity:
|
|
59
|
+
opacity: 0.3,
|
|
60
|
+
startTime: Date.now()
|
|
78
61
|
});
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
this.
|
|
82
|
-
|
|
62
|
+
|
|
63
|
+
// Démarrer l'animation si pas déjà en cours
|
|
64
|
+
if (!this._isAnimating) {
|
|
65
|
+
this._animate();
|
|
66
|
+
}
|
|
83
67
|
}
|
|
84
|
-
|
|
68
|
+
|
|
69
|
+
// Sélectionner le segment
|
|
85
70
|
this.selectedIndex = index;
|
|
86
|
-
|
|
71
|
+
|
|
72
|
+
// Forcer le redessin immédiat
|
|
73
|
+
this._requestRedraw();
|
|
74
|
+
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Gère le relâchement
|
|
83
|
+
* @param {number} x - Coordonnée X
|
|
84
|
+
* @param {number} y - Coordonnée Y
|
|
85
|
+
*/
|
|
86
|
+
handleRelease(x, y) {
|
|
87
|
+
const index = this.getButtonIndexAt(x, y);
|
|
88
|
+
|
|
89
|
+
if (index !== null && index === this.pressedIndex) {
|
|
90
|
+
// Appeler le callback si défini
|
|
91
|
+
if (this.buttons[index].onClick) {
|
|
92
|
+
this.buttons[index].onClick(index);
|
|
93
|
+
}
|
|
87
94
|
}
|
|
95
|
+
|
|
96
|
+
// Réinitialiser l'index pressé
|
|
97
|
+
this.pressedIndex = null;
|
|
98
|
+
|
|
99
|
+
// Forcer le redessin
|
|
100
|
+
this._requestRedraw();
|
|
88
101
|
}
|
|
89
102
|
|
|
90
103
|
/**
|
|
91
104
|
* Anime les ripples Material
|
|
92
105
|
* @private
|
|
93
106
|
*/
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
107
|
+
_animate() {
|
|
108
|
+
this._isAnimating = true;
|
|
109
|
+
|
|
110
|
+
const animateFrame = () => {
|
|
111
|
+
let hasActiveRipples = false;
|
|
112
|
+
const now = Date.now();
|
|
113
|
+
|
|
114
|
+
// Mettre à jour tous les ripples
|
|
115
|
+
for (let i = this.ripples.length - 1; i >= 0; i--) {
|
|
116
|
+
const ripple = this.ripples[i];
|
|
117
|
+
const elapsed = now - ripple.startTime;
|
|
118
|
+
|
|
119
|
+
// Animation sur 600ms
|
|
120
|
+
const progress = Math.min(elapsed / 600, 1);
|
|
121
|
+
|
|
122
|
+
// Équation d'easing
|
|
123
|
+
const easedProgress = 1 - Math.pow(1 - progress, 3);
|
|
124
|
+
|
|
125
|
+
// Mettre à jour le rayon
|
|
126
|
+
ripple.radius = ripple.maxRadius * easedProgress;
|
|
127
|
+
|
|
128
|
+
// Diminuer l'opacité après 50% de progression
|
|
129
|
+
if (progress > 0.5) {
|
|
130
|
+
ripple.opacity = 0.3 * (1 - (progress - 0.5) * 2);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Supprimer les ripples terminés
|
|
134
|
+
if (progress >= 1) {
|
|
135
|
+
this.ripples.splice(i, 1);
|
|
136
|
+
} else {
|
|
137
|
+
hasActiveRipples = true;
|
|
101
138
|
}
|
|
102
|
-
if (ripple.radius >= ripple.maxRadius * 0.5) ripple.opacity -= 0.05;
|
|
103
139
|
}
|
|
104
|
-
|
|
105
|
-
|
|
140
|
+
|
|
141
|
+
// Redessiner si il y a des ripples actifs
|
|
142
|
+
if (hasActiveRipples) {
|
|
143
|
+
this._requestRedraw();
|
|
144
|
+
requestAnimationFrame(animateFrame);
|
|
145
|
+
} else {
|
|
146
|
+
this._isAnimating = false;
|
|
147
|
+
}
|
|
106
148
|
};
|
|
107
|
-
|
|
149
|
+
|
|
150
|
+
requestAnimationFrame(animateFrame);
|
|
108
151
|
}
|
|
109
152
|
|
|
110
153
|
/**
|
|
@@ -115,36 +158,103 @@ class SegmentedControl extends Component {
|
|
|
115
158
|
* @private
|
|
116
159
|
*/
|
|
117
160
|
getButtonIndexAt(x, y) {
|
|
161
|
+
// Vérifier si dans les limites verticales
|
|
162
|
+
if (y < this.y || y > this.y + this.height) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
118
166
|
const btnWidth = (this.width - this.spacing * (this.buttons.length - 1)) / this.buttons.length;
|
|
119
|
-
|
|
167
|
+
|
|
120
168
|
for (let i = 0; i < this.buttons.length; i++) {
|
|
121
169
|
const btnX = this.x + i * (btnWidth + this.spacing);
|
|
122
|
-
|
|
170
|
+
|
|
171
|
+
// Vérifier si dans les limites horizontales du bouton
|
|
172
|
+
if (x >= btnX && x <= btnX + btnWidth) {
|
|
173
|
+
return i;
|
|
174
|
+
}
|
|
123
175
|
}
|
|
176
|
+
|
|
124
177
|
return null;
|
|
125
178
|
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Force le redessin du composant
|
|
182
|
+
* @private
|
|
183
|
+
*/
|
|
184
|
+
_requestRedraw() {
|
|
185
|
+
if (this.framework && this.framework.markComponentDirty) {
|
|
186
|
+
this.framework.markComponentDirty(this);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
126
189
|
|
|
127
190
|
/**
|
|
128
191
|
* Dessine le SegmentedControl
|
|
129
192
|
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
130
193
|
*/
|
|
131
194
|
draw(ctx) {
|
|
195
|
+
ctx.save();
|
|
196
|
+
|
|
132
197
|
const btnWidth = (this.width - this.spacing * (this.buttons.length - 1)) / this.buttons.length;
|
|
133
|
-
|
|
134
|
-
|
|
198
|
+
const radius = this.height / 2;
|
|
199
|
+
|
|
200
|
+
// Dessiner tous les boutons
|
|
201
|
+
for (let i = 0; i < this.buttons.length; i++) {
|
|
202
|
+
const btn = this.buttons[i];
|
|
135
203
|
const btnX = this.x + i * (btnWidth + this.spacing);
|
|
136
|
-
|
|
137
|
-
//
|
|
204
|
+
|
|
205
|
+
// Couleurs selon la plateforme et l'état
|
|
206
|
+
let backgroundColor;
|
|
207
|
+
let textColor;
|
|
208
|
+
|
|
138
209
|
if (this.platform === 'material') {
|
|
139
|
-
|
|
210
|
+
// Material Design
|
|
211
|
+
if (this.selectedIndex === i) {
|
|
212
|
+
backgroundColor = '#6200EE'; // Violet Material
|
|
213
|
+
textColor = '#FFFFFF';
|
|
214
|
+
} else {
|
|
215
|
+
backgroundColor = '#E0E0E0'; // Gris clair
|
|
216
|
+
textColor = '#000000';
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Si pressé (mais pas encore sélectionné)
|
|
220
|
+
if (this.pressedIndex === i && this.pressedIndex !== this.selectedIndex) {
|
|
221
|
+
backgroundColor = 'rgba(98, 0, 238, 0.12)'; // Violet très transparent
|
|
222
|
+
}
|
|
140
223
|
} else {
|
|
141
|
-
|
|
142
|
-
if (this.
|
|
224
|
+
// iOS/Cupertino
|
|
225
|
+
if (this.selectedIndex === i) {
|
|
226
|
+
backgroundColor = '#007AFF'; // Bleu iOS
|
|
227
|
+
textColor = '#FFFFFF';
|
|
228
|
+
} else {
|
|
229
|
+
backgroundColor = '#F0F0F0'; // Gris très clair iOS
|
|
230
|
+
textColor = '#000000';
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Si pressé
|
|
234
|
+
if (this.pressedIndex === i) {
|
|
235
|
+
backgroundColor = this.selectedIndex === i ? '#0056CC' : '#D9D9D9';
|
|
236
|
+
}
|
|
143
237
|
}
|
|
144
|
-
|
|
145
|
-
|
|
238
|
+
|
|
239
|
+
// Dessiner le fond du bouton
|
|
240
|
+
ctx.fillStyle = backgroundColor;
|
|
241
|
+
|
|
242
|
+
// Coins arrondis selon la position
|
|
146
243
|
ctx.beginPath();
|
|
147
|
-
|
|
244
|
+
|
|
245
|
+
if (i === 0 && i === this.buttons.length - 1) {
|
|
246
|
+
// Un seul bouton - tous les coins arrondis
|
|
247
|
+
ctx.moveTo(btnX + radius, this.y);
|
|
248
|
+
ctx.lineTo(btnX + btnWidth - radius, this.y);
|
|
249
|
+
ctx.quadraticCurveTo(btnX + btnWidth, this.y, btnX + btnWidth, this.y + radius);
|
|
250
|
+
ctx.lineTo(btnX + btnWidth, this.y + this.height - radius);
|
|
251
|
+
ctx.quadraticCurveTo(btnX + btnWidth, this.y + this.height, btnX + btnWidth - radius, this.y + this.height);
|
|
252
|
+
ctx.lineTo(btnX + radius, this.y + this.height);
|
|
253
|
+
ctx.quadraticCurveTo(btnX, this.y + this.height, btnX, this.y + this.height - radius);
|
|
254
|
+
ctx.lineTo(btnX, this.y + radius);
|
|
255
|
+
ctx.quadraticCurveTo(btnX, this.y, btnX + radius, this.y);
|
|
256
|
+
} else if (i === 0) {
|
|
257
|
+
// Premier bouton - coins gauche arrondis
|
|
148
258
|
ctx.moveTo(btnX + radius, this.y);
|
|
149
259
|
ctx.lineTo(btnX + btnWidth, this.y);
|
|
150
260
|
ctx.lineTo(btnX + btnWidth, this.y + this.height);
|
|
@@ -153,6 +263,7 @@ class SegmentedControl extends Component {
|
|
|
153
263
|
ctx.lineTo(btnX, this.y + radius);
|
|
154
264
|
ctx.quadraticCurveTo(btnX, this.y, btnX + radius, this.y);
|
|
155
265
|
} else if (i === this.buttons.length - 1) {
|
|
266
|
+
// Dernier bouton - coins droit arrondis
|
|
156
267
|
ctx.moveTo(btnX, this.y);
|
|
157
268
|
ctx.lineTo(btnX + btnWidth - radius, this.y);
|
|
158
269
|
ctx.quadraticCurveTo(btnX + btnWidth, this.y, btnX + btnWidth, this.y + radius);
|
|
@@ -160,32 +271,76 @@ class SegmentedControl extends Component {
|
|
|
160
271
|
ctx.quadraticCurveTo(btnX + btnWidth, this.y + this.height, btnX + btnWidth - radius, this.y + this.height);
|
|
161
272
|
ctx.lineTo(btnX, this.y + this.height);
|
|
162
273
|
} else {
|
|
274
|
+
// Bouton du milieu - coins carrés
|
|
163
275
|
ctx.rect(btnX, this.y, btnWidth, this.height);
|
|
164
276
|
}
|
|
277
|
+
|
|
278
|
+
ctx.closePath();
|
|
165
279
|
ctx.fill();
|
|
166
|
-
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
280
|
+
|
|
281
|
+
// Dessiner les ripples Material
|
|
282
|
+
if (this.platform === 'material') {
|
|
283
|
+
for (const ripple of this.ripples) {
|
|
284
|
+
if (ripple.index === i) {
|
|
285
|
+
ctx.save();
|
|
286
|
+
|
|
287
|
+
// Clip sur le bouton pour que le ripple ne dépasse pas
|
|
288
|
+
ctx.beginPath();
|
|
289
|
+
if (i === 0) {
|
|
290
|
+
ctx.moveTo(btnX + radius, this.y);
|
|
291
|
+
ctx.lineTo(btnX + btnWidth, this.y);
|
|
292
|
+
ctx.lineTo(btnX + btnWidth, this.y + this.height);
|
|
293
|
+
ctx.lineTo(btnX + radius, this.y + this.height);
|
|
294
|
+
ctx.quadraticCurveTo(btnX, this.y + this.height, btnX, this.y + this.height - radius);
|
|
295
|
+
ctx.lineTo(btnX, this.y + radius);
|
|
296
|
+
ctx.quadraticCurveTo(btnX, this.y, btnX + radius, this.y);
|
|
297
|
+
} else if (i === this.buttons.length - 1) {
|
|
298
|
+
ctx.moveTo(btnX, this.y);
|
|
299
|
+
ctx.lineTo(btnX + btnWidth - radius, this.y);
|
|
300
|
+
ctx.quadraticCurveTo(btnX + btnWidth, this.y, btnX + btnWidth, this.y + radius);
|
|
301
|
+
ctx.lineTo(btnX + btnWidth, this.y + this.height - radius);
|
|
302
|
+
ctx.quadraticCurveTo(btnX + btnWidth, this.y + this.height, btnX + btnWidth - radius, this.y + this.height);
|
|
303
|
+
ctx.lineTo(btnX, this.y + this.height);
|
|
304
|
+
} else {
|
|
305
|
+
ctx.rect(btnX, this.y, btnWidth, this.height);
|
|
306
|
+
}
|
|
307
|
+
ctx.closePath();
|
|
308
|
+
ctx.clip();
|
|
309
|
+
|
|
310
|
+
// Dessiner le ripple
|
|
311
|
+
ctx.fillStyle = `rgba(255, 255, 255, ${ripple.opacity})`;
|
|
312
|
+
ctx.beginPath();
|
|
313
|
+
ctx.arc(btnX + ripple.x, this.y + ripple.y, ripple.radius, 0, Math.PI * 2);
|
|
314
|
+
ctx.fill();
|
|
315
|
+
|
|
316
|
+
ctx.restore();
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Dessiner le texte
|
|
322
|
+
ctx.fillStyle = textColor;
|
|
323
|
+
ctx.font = `500 ${this.height / 2.5}px -apple-system, Roboto, sans-serif`;
|
|
172
324
|
ctx.textAlign = 'center';
|
|
173
325
|
ctx.textBaseline = 'middle';
|
|
174
326
|
ctx.fillText(btn.text || `Button ${i + 1}`, btnX + btnWidth / 2, this.y + this.height / 2);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
//
|
|
178
|
-
if (this.platform === '
|
|
179
|
-
ctx.
|
|
180
|
-
|
|
181
|
-
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Dessiner les séparateurs (pour iOS)
|
|
330
|
+
if (this.platform === 'cupertino' && this.buttons.length > 1) {
|
|
331
|
+
ctx.strokeStyle = '#C7C7CC';
|
|
332
|
+
ctx.lineWidth = 1;
|
|
333
|
+
|
|
334
|
+
for (let i = 1; i < this.buttons.length; i++) {
|
|
335
|
+
const separatorX = this.x + i * btnWidth + (i - 1) * this.spacing;
|
|
182
336
|
ctx.beginPath();
|
|
183
|
-
ctx.
|
|
184
|
-
ctx.
|
|
185
|
-
ctx.
|
|
186
|
-
}
|
|
187
|
-
ctx.restore();
|
|
337
|
+
ctx.moveTo(separatorX, this.y + 8);
|
|
338
|
+
ctx.lineTo(separatorX, this.y + this.height - 8);
|
|
339
|
+
ctx.stroke();
|
|
340
|
+
}
|
|
188
341
|
}
|
|
342
|
+
|
|
343
|
+
ctx.restore();
|
|
189
344
|
}
|
|
190
345
|
|
|
191
346
|
/**
|
|
@@ -199,4 +354,4 @@ class SegmentedControl extends Component {
|
|
|
199
354
|
}
|
|
200
355
|
}
|
|
201
356
|
|
|
202
|
-
export default SegmentedControl;
|
|
357
|
+
export default SegmentedControl;
|