canvasframework 0.5.18 → 0.5.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/components/Accordion.js +265 -0
- package/components/AndroidDatePickerDialog.js +406 -0
- package/components/AppBar.js +398 -0
- package/components/AudioPlayer.js +611 -0
- package/components/Avatar.js +202 -0
- package/components/Banner.js +342 -0
- package/components/BottomNavigationBar.js +433 -0
- package/components/BottomSheet.js +234 -0
- package/components/Button.js +358 -0
- package/components/Camera.js +644 -0
- package/components/Card.js +193 -0
- package/components/Chart.js +700 -0
- package/components/Checkbox.js +166 -0
- package/components/Chip.js +212 -0
- package/components/CircularProgress.js +327 -0
- package/components/ContextMenu.js +116 -0
- package/components/DatePicker.js +298 -0
- package/components/Dialog.js +337 -0
- package/components/Divider.js +125 -0
- package/components/Drawer.js +276 -0
- package/components/FAB.js +270 -0
- package/components/FileUpload.js +315 -0
- package/components/FloatedCamera.js +644 -0
- package/components/IOSDatePickerWheel.js +430 -0
- package/components/ImageCarousel.js +219 -0
- package/components/ImageComponent.js +223 -0
- package/components/Input.js +831 -0
- package/components/InputDatalist.js +723 -0
- package/components/InputTags.js +624 -0
- package/components/List.js +95 -0
- package/components/ListItem.js +269 -0
- package/components/Modal.js +364 -0
- package/components/MorphingFAB.js +428 -0
- package/components/MultiSelectDialog.js +206 -0
- package/components/NumberInput.js +271 -0
- package/components/PasswordInput.js +462 -0
- package/components/ProgressBar.js +88 -0
- package/components/QRCodeReader.js +539 -0
- package/components/RadioButton.js +151 -0
- package/components/SearchInput.js +315 -0
- package/components/SegmentedControl.js +357 -0
- package/components/Select.js +199 -0
- package/components/SelectDialog.js +255 -0
- package/components/Slider.js +113 -0
- package/components/SliverAppBar.js +139 -0
- package/components/Snackbar.js +243 -0
- package/components/SpeedDialFAB.js +397 -0
- package/components/Stepper.js +281 -0
- package/components/SwipeableListItem.js +327 -0
- package/components/Switch.js +147 -0
- package/components/Table.js +492 -0
- package/components/Tabs.js +423 -0
- package/components/Text.js +141 -0
- package/components/TextField.js +151 -0
- package/components/TimePicker.js +934 -0
- package/components/Toast.js +236 -0
- package/components/TreeView.js +420 -0
- package/components/Video.js +397 -0
- package/components/View.js +140 -0
- package/components/VirtualList.js +120 -0
- package/core/CanvasFramework.js +3045 -0
- package/core/Component.js +243 -0
- package/core/ThemeManager.js +358 -0
- package/core/UIBuilder.js +267 -0
- package/core/WebGLCanvasAdapter.js +782 -0
- package/features/Column.js +43 -0
- package/features/Grid.js +47 -0
- package/features/LayoutComponent.js +43 -0
- package/features/OpenStreetMap.js +310 -0
- package/features/Positioned.js +33 -0
- package/features/PullToRefresh.js +328 -0
- package/features/Row.js +40 -0
- package/features/SignaturePad.js +257 -0
- package/features/Skeleton.js +193 -0
- package/features/Stack.js +21 -0
- package/index.js +119 -0
- package/manager/AccessibilityManager.js +107 -0
- package/manager/ErrorHandler.js +59 -0
- package/manager/FeatureFlags.js +60 -0
- package/manager/MemoryManager.js +107 -0
- package/manager/PerformanceMonitor.js +84 -0
- package/manager/SecurityManager.js +54 -0
- package/package.json +22 -16
- package/utils/AnimationEngine.js +734 -0
- package/utils/CryptoManager.js +303 -0
- package/utils/DataStore.js +403 -0
- package/utils/DevTools.js +1618 -0
- package/utils/DevToolsConsole.js +201 -0
- package/utils/EventBus.js +407 -0
- package/utils/FetchClient.js +74 -0
- package/utils/FirebaseAuth.js +653 -0
- package/utils/FirebaseCore.js +246 -0
- package/utils/FirebaseFirestore.js +581 -0
- package/utils/FirebaseFunctions.js +97 -0
- package/utils/FirebaseRealtimeDB.js +498 -0
- package/utils/FirebaseStorage.js +612 -0
- package/utils/FormValidator.js +355 -0
- package/utils/GeoLocationService.js +62 -0
- package/utils/I18n.js +207 -0
- package/utils/IndexedDBManager.js +273 -0
- package/utils/InspectionOverlay.js +308 -0
- package/utils/NotificationManager.js +60 -0
- package/utils/OfflineSyncManager.js +342 -0
- package/utils/PayPalPayment.js +678 -0
- package/utils/QueryBuilder.js +478 -0
- package/utils/SafeArea.js +64 -0
- package/utils/SecureStorage.js +289 -0
- package/utils/StateManager.js +207 -0
- package/utils/StripePayment.js +552 -0
- package/utils/WebSocketClient.js +66 -0
- package/dist/canvasframework.js +0 -2
- package/dist/canvasframework.js.LICENSE.txt +0 -1
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import Component from '../core/Component.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Checkbox Material & Cupertino (iOS-like)
|
|
5
|
+
*/
|
|
6
|
+
class Checkbox extends Component {
|
|
7
|
+
constructor(framework, options = {}) {
|
|
8
|
+
super(framework, options);
|
|
9
|
+
|
|
10
|
+
this.checked = !!options.checked;
|
|
11
|
+
this.label = options.label || '';
|
|
12
|
+
this.platform = framework.platform;
|
|
13
|
+
this.onChange = options.onChange;
|
|
14
|
+
|
|
15
|
+
this.boxSize = 22;
|
|
16
|
+
this.padding = 10;
|
|
17
|
+
|
|
18
|
+
this.textWidth = this.label
|
|
19
|
+
? this.getTextWidth(this.label)
|
|
20
|
+
: 0;
|
|
21
|
+
|
|
22
|
+
// Largeur totale
|
|
23
|
+
this.width =
|
|
24
|
+
this.platform === 'material'
|
|
25
|
+
? this.boxSize + this.padding + this.textWidth
|
|
26
|
+
: this.textWidth + 28; // place pour checkmark iOS
|
|
27
|
+
|
|
28
|
+
this.height = 28;
|
|
29
|
+
|
|
30
|
+
this.onClick = () => {
|
|
31
|
+
this.checked = !this.checked;
|
|
32
|
+
this.onChange?.(this.checked);
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
getTextWidth(text) {
|
|
37
|
+
const ctx = this.framework.ctx;
|
|
38
|
+
ctx.save();
|
|
39
|
+
ctx.font = '16px -apple-system, system-ui, sans-serif';
|
|
40
|
+
const w = ctx.measureText(text).width;
|
|
41
|
+
ctx.restore();
|
|
42
|
+
return w;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
draw(ctx) {
|
|
46
|
+
ctx.save();
|
|
47
|
+
ctx.font = '16px -apple-system, system-ui, sans-serif';
|
|
48
|
+
ctx.textBaseline = 'middle';
|
|
49
|
+
|
|
50
|
+
const centerY = this.y + this.height / 2;
|
|
51
|
+
|
|
52
|
+
if (this.platform === 'material') {
|
|
53
|
+
this.drawMaterial(ctx, centerY);
|
|
54
|
+
} else {
|
|
55
|
+
this.drawCupertino(ctx, centerY);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
ctx.restore();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* ---------------- MATERIAL ---------------- */
|
|
62
|
+
|
|
63
|
+
drawMaterial(ctx, centerY) {
|
|
64
|
+
const x = this.x;
|
|
65
|
+
const y = centerY - this.boxSize / 2;
|
|
66
|
+
|
|
67
|
+
// Box
|
|
68
|
+
ctx.lineWidth = 2;
|
|
69
|
+
ctx.strokeStyle = this.checked ? '#6200EE' : '#757575';
|
|
70
|
+
ctx.fillStyle = this.checked ? '#6200EE' : 'transparent';
|
|
71
|
+
|
|
72
|
+
this.roundRect(ctx, x, y, this.boxSize, this.boxSize, 3);
|
|
73
|
+
if (this.checked) ctx.fill();
|
|
74
|
+
ctx.stroke();
|
|
75
|
+
|
|
76
|
+
// Check
|
|
77
|
+
if (this.checked) {
|
|
78
|
+
ctx.strokeStyle = '#FFF';
|
|
79
|
+
ctx.lineWidth = 2.4;
|
|
80
|
+
ctx.beginPath();
|
|
81
|
+
ctx.moveTo(x + 5, y + 12);
|
|
82
|
+
ctx.lineTo(x + 9, y + 16);
|
|
83
|
+
ctx.lineTo(x + 17, y + 7);
|
|
84
|
+
ctx.stroke();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Label
|
|
88
|
+
ctx.fillStyle = '#000';
|
|
89
|
+
ctx.fillText(
|
|
90
|
+
this.label,
|
|
91
|
+
x + this.boxSize + this.padding,
|
|
92
|
+
centerY
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* ---------------- CUPERTINO ---------------- */
|
|
97
|
+
|
|
98
|
+
/* ---------------- CUPERTINO ---------------- */
|
|
99
|
+
|
|
100
|
+
drawCupertino(ctx, centerY) {
|
|
101
|
+
const radius = 10;
|
|
102
|
+
const circleX = this.x + radius;
|
|
103
|
+
const circleY = centerY;
|
|
104
|
+
|
|
105
|
+
// Cercle
|
|
106
|
+
if (this.checked) {
|
|
107
|
+
ctx.fillStyle = '#007AFF'; // Apple blue
|
|
108
|
+
ctx.beginPath();
|
|
109
|
+
ctx.arc(circleX, circleY, radius, 0, Math.PI * 2);
|
|
110
|
+
ctx.fill();
|
|
111
|
+
} else {
|
|
112
|
+
ctx.strokeStyle = '#C7C7CC'; // iOS gray
|
|
113
|
+
ctx.lineWidth = 2;
|
|
114
|
+
ctx.beginPath();
|
|
115
|
+
ctx.arc(circleX, circleY, radius, 0, Math.PI * 2);
|
|
116
|
+
ctx.stroke();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Checkmark
|
|
120
|
+
if (this.checked) {
|
|
121
|
+
ctx.strokeStyle = '#FFFFFF';
|
|
122
|
+
ctx.lineWidth = 2.2;
|
|
123
|
+
ctx.lineCap = 'round';
|
|
124
|
+
ctx.lineJoin = 'round';
|
|
125
|
+
|
|
126
|
+
ctx.beginPath();
|
|
127
|
+
ctx.moveTo(circleX - 4, circleY);
|
|
128
|
+
ctx.lineTo(circleX - 1, circleY + 3);
|
|
129
|
+
ctx.lineTo(circleX + 5, circleY - 4);
|
|
130
|
+
ctx.stroke();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Label
|
|
134
|
+
ctx.fillStyle = '#000';
|
|
135
|
+
ctx.fillText(
|
|
136
|
+
this.label,
|
|
137
|
+
this.x + radius * 2 + this.padding,
|
|
138
|
+
centerY
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
roundRect(ctx, x, y, w, h, r) {
|
|
143
|
+
ctx.beginPath();
|
|
144
|
+
ctx.moveTo(x + r, y);
|
|
145
|
+
ctx.lineTo(x + w - r, y);
|
|
146
|
+
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
|
|
147
|
+
ctx.lineTo(x + w, y + h - r);
|
|
148
|
+
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
|
|
149
|
+
ctx.lineTo(x + r, y + h);
|
|
150
|
+
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
|
|
151
|
+
ctx.lineTo(x, y + r);
|
|
152
|
+
ctx.quadraticCurveTo(x, y, x + r, y);
|
|
153
|
+
ctx.closePath();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
isPointInside(x, y) {
|
|
157
|
+
return (
|
|
158
|
+
x >= this.x &&
|
|
159
|
+
x <= this.x + this.width &&
|
|
160
|
+
y >= this.y &&
|
|
161
|
+
y <= this.y + this.height
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export default Checkbox;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import Component from '../core/Component.js';
|
|
2
|
+
/**
|
|
3
|
+
* Chip (étiquette cliquable)
|
|
4
|
+
* @class
|
|
5
|
+
* @extends Component
|
|
6
|
+
* @property {string} text - Texte
|
|
7
|
+
* @property {string|null} icon - Icône
|
|
8
|
+
* @property {boolean} closable - Peut être fermé
|
|
9
|
+
* @property {string} platform - Plateforme
|
|
10
|
+
* @property {string} bgColor - Couleur de fond
|
|
11
|
+
* @property {string} textColor - Couleur du texte
|
|
12
|
+
* @property {Function} onClose - Callback à la fermeture
|
|
13
|
+
* @property {number} borderRadius - Rayon des coins
|
|
14
|
+
* @property {Object|null} closeButtonRect - Rectangle du bouton fermer
|
|
15
|
+
*/
|
|
16
|
+
class Chip extends Component {
|
|
17
|
+
/**
|
|
18
|
+
* Crée une instance de Chip
|
|
19
|
+
* @param {CanvasFramework} framework - Framework parent
|
|
20
|
+
* @param {Object} [options={}] - Options de configuration
|
|
21
|
+
* @param {string} [options.text=''] - Texte
|
|
22
|
+
* @param {string} [options.icon] - Icône
|
|
23
|
+
* @param {boolean} [options.closable=true] - Peut être fermé
|
|
24
|
+
* @param {string} [options.bgColor] - Couleur de fond (auto selon platform)
|
|
25
|
+
* @param {string} [options.textColor='#000000'] - Couleur du texte
|
|
26
|
+
* @param {Function} [options.onClose] - Callback à la fermeture
|
|
27
|
+
* @param {number} [options.height=32] - Hauteur
|
|
28
|
+
*/
|
|
29
|
+
constructor(framework, options = {}) {
|
|
30
|
+
super(framework, options);
|
|
31
|
+
this.text = options.text || '';
|
|
32
|
+
this.icon = options.icon || null;
|
|
33
|
+
this.closable = options.closable !== false;
|
|
34
|
+
this.platform = framework.platform;
|
|
35
|
+
this.bgColor = options.bgColor || (framework.platform === 'material' ? '#E0E0E0' : '#F0F0F0');
|
|
36
|
+
this.textColor = options.textColor || '#000000';
|
|
37
|
+
this.onClose = options.onClose;
|
|
38
|
+
|
|
39
|
+
// Calculer la largeur en fonction du contenu
|
|
40
|
+
const ctx = framework.ctx;
|
|
41
|
+
ctx.font = '14px -apple-system, sans-serif';
|
|
42
|
+
const textWidth = ctx.measureText(this.text).width;
|
|
43
|
+
const iconWidth = this.icon ? 24 : 0;
|
|
44
|
+
const closeWidth = this.closable ? 24 : 0;
|
|
45
|
+
this.width = iconWidth + textWidth + closeWidth + 24; // padding
|
|
46
|
+
this.height = options.height || 32;
|
|
47
|
+
this.borderRadius = this.height / 2;
|
|
48
|
+
|
|
49
|
+
this.closeButtonRect = null;
|
|
50
|
+
this.onPress = this.handlePress.bind(this);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Dessine le chip
|
|
55
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
56
|
+
*/
|
|
57
|
+
draw(ctx) {
|
|
58
|
+
ctx.save();
|
|
59
|
+
|
|
60
|
+
// Background
|
|
61
|
+
ctx.fillStyle = this.pressed ? this.darkenColor(this.bgColor) : this.bgColor;
|
|
62
|
+
ctx.beginPath();
|
|
63
|
+
this.roundRect(ctx, this.x, this.y, this.width, this.height, this.borderRadius);
|
|
64
|
+
ctx.fill();
|
|
65
|
+
|
|
66
|
+
let currentX = this.x + 12;
|
|
67
|
+
|
|
68
|
+
// Icône
|
|
69
|
+
if (this.icon) {
|
|
70
|
+
ctx.font = '16px sans-serif';
|
|
71
|
+
ctx.textAlign = 'left';
|
|
72
|
+
ctx.textBaseline = 'middle';
|
|
73
|
+
ctx.fillStyle = this.textColor;
|
|
74
|
+
ctx.fillText(this.icon, currentX, this.y + this.height / 2);
|
|
75
|
+
currentX += 20;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Texte
|
|
79
|
+
ctx.font = '14px -apple-system, sans-serif';
|
|
80
|
+
ctx.fillStyle = this.textColor;
|
|
81
|
+
ctx.textAlign = 'left';
|
|
82
|
+
ctx.textBaseline = 'middle';
|
|
83
|
+
ctx.fillText(this.text, currentX, this.y + this.height / 2);
|
|
84
|
+
|
|
85
|
+
// Bouton de fermeture
|
|
86
|
+
if (this.closable) {
|
|
87
|
+
const closeX = this.x + this.width - 20;
|
|
88
|
+
const closeY = this.y + this.height / 2;
|
|
89
|
+
|
|
90
|
+
this.closeButtonRect = {
|
|
91
|
+
x: closeX - 8,
|
|
92
|
+
y: closeY - 8,
|
|
93
|
+
width: 16,
|
|
94
|
+
height: 16
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// Cercle du bouton (optionnel)
|
|
98
|
+
ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
|
|
99
|
+
ctx.beginPath();
|
|
100
|
+
ctx.arc(closeX, closeY, 8, 0, Math.PI * 2);
|
|
101
|
+
ctx.fill();
|
|
102
|
+
|
|
103
|
+
// Croix (X)
|
|
104
|
+
ctx.strokeStyle = this.textColor;
|
|
105
|
+
ctx.lineWidth = 1.5;
|
|
106
|
+
ctx.lineCap = 'round';
|
|
107
|
+
ctx.beginPath();
|
|
108
|
+
ctx.moveTo(closeX - 4, closeY - 4);
|
|
109
|
+
ctx.lineTo(closeX + 4, closeY + 4);
|
|
110
|
+
ctx.stroke();
|
|
111
|
+
|
|
112
|
+
ctx.beginPath();
|
|
113
|
+
ctx.moveTo(closeX + 4, closeY - 4);
|
|
114
|
+
ctx.lineTo(closeX - 4, closeY + 4);
|
|
115
|
+
ctx.stroke();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
ctx.restore();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Gère la pression (clic)
|
|
123
|
+
* @param {number} x - Coordonnée X
|
|
124
|
+
* @param {number} y - Coordonnée Y
|
|
125
|
+
* @private
|
|
126
|
+
*/
|
|
127
|
+
handlePress(x, y) {
|
|
128
|
+
const adjustedY = y - this.framework.scrollOffset;
|
|
129
|
+
|
|
130
|
+
// Vérifier si on clique sur le bouton de fermeture
|
|
131
|
+
if (this.closable && this.closeButtonRect) {
|
|
132
|
+
if (x >= this.closeButtonRect.x &&
|
|
133
|
+
x <= this.closeButtonRect.x + this.closeButtonRect.width &&
|
|
134
|
+
adjustedY >= this.closeButtonRect.y &&
|
|
135
|
+
adjustedY <= this.closeButtonRect.y + this.closeButtonRect.height) {
|
|
136
|
+
if (this.onClose) this.onClose();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Sinon, déclencher onClick normal
|
|
142
|
+
if (this.onClick) this.onClick();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Dessine un rectangle avec coins arrondis
|
|
147
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
148
|
+
* @param {number} x - Position X
|
|
149
|
+
* @param {number} y - Position Y
|
|
150
|
+
* @param {number} width - Largeur
|
|
151
|
+
* @param {number} height - Hauteur
|
|
152
|
+
* @param {number} radius - Rayon des coins
|
|
153
|
+
* @private
|
|
154
|
+
*/
|
|
155
|
+
roundRect(ctx, x, y, width, height, radius) {
|
|
156
|
+
ctx.beginPath();
|
|
157
|
+
ctx.moveTo(x + radius, y);
|
|
158
|
+
ctx.lineTo(x + width - radius, y);
|
|
159
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
160
|
+
ctx.lineTo(x + width, y + height - radius);
|
|
161
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
162
|
+
ctx.lineTo(x + radius, y + height);
|
|
163
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
164
|
+
ctx.lineTo(x, y + radius);
|
|
165
|
+
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
166
|
+
ctx.closePath();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Assombrit une couleur
|
|
171
|
+
* @param {string} color - Couleur
|
|
172
|
+
* @returns {string} Couleur assombrie
|
|
173
|
+
* @private
|
|
174
|
+
*/
|
|
175
|
+
darkenColor(color) {
|
|
176
|
+
// Utiliser la même méthode que Button
|
|
177
|
+
if (color.startsWith('#')) {
|
|
178
|
+
const rgb = this.hexToRgb(color);
|
|
179
|
+
return `rgb(${Math.max(0, rgb.r - 20)}, ${Math.max(0, rgb.g - 20)}, ${Math.max(0, rgb.b - 20)})`;
|
|
180
|
+
}
|
|
181
|
+
return color;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Convertit une couleur hex en RGB
|
|
186
|
+
* @param {string} hex - Couleur hexadécimale
|
|
187
|
+
* @returns {{r: number, g: number, b: number}} Objet RGB
|
|
188
|
+
* @private
|
|
189
|
+
*/
|
|
190
|
+
hexToRgb(hex) {
|
|
191
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
192
|
+
return result ? {
|
|
193
|
+
r: parseInt(result[1], 16),
|
|
194
|
+
g: parseInt(result[2], 16),
|
|
195
|
+
b: parseInt(result[3], 16)
|
|
196
|
+
} : { r: 0, g: 0, b: 0 };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Vérifie si un point est dans les limites
|
|
201
|
+
* @param {number} x - Coordonnée X
|
|
202
|
+
* @param {number} y - Coordonnée Y
|
|
203
|
+
* @returns {boolean} True si le point est dans le chip
|
|
204
|
+
*/
|
|
205
|
+
isPointInside(x, y) {
|
|
206
|
+
const adjustedY = y - this.framework.scrollOffset;
|
|
207
|
+
return x >= this.x && x <= this.x + this.width &&
|
|
208
|
+
adjustedY >= this.y && adjustedY <= this.y + this.height;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export default Chip;
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import Component from '../core/Component.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Spinner de chargement circulaire avec support Material et Cupertino
|
|
5
|
+
* @class
|
|
6
|
+
* @extends Component
|
|
7
|
+
* @property {number} size - Taille du spinner
|
|
8
|
+
* @property {boolean} indeterminate - Mode indéterminé
|
|
9
|
+
* @property {number} progress - Progression (0-100)
|
|
10
|
+
* @property {string} platform - Plateforme
|
|
11
|
+
* @property {string} color - Couleur
|
|
12
|
+
* @property {number} lineWidth - Épaisseur de la ligne
|
|
13
|
+
* @property {number} rotation - Rotation actuelle
|
|
14
|
+
* @property {number} animationSpeed - Vitesse d'animation
|
|
15
|
+
*/
|
|
16
|
+
class CircularProgress extends Component {
|
|
17
|
+
/**
|
|
18
|
+
* Crée une instance de CircularProgress
|
|
19
|
+
* @param {CanvasFramework} framework - Framework parent
|
|
20
|
+
* @param {Object} [options={}] - Options de configuration
|
|
21
|
+
* @param {number} [options.size=40] - Taille
|
|
22
|
+
* @param {boolean} [options.indeterminate=true] - Mode indéterminé
|
|
23
|
+
* @param {number} [options.progress=0] - Progression (0-100)
|
|
24
|
+
* @param {string} [options.color] - Couleur (auto selon platform)
|
|
25
|
+
* @param {number} [options.lineWidth] - Épaisseur (auto selon platform)
|
|
26
|
+
* @param {number} [options.animationSpeed] - Vitesse d'animation (auto selon platform)
|
|
27
|
+
*/
|
|
28
|
+
constructor(framework, options = {}) {
|
|
29
|
+
super(framework, options);
|
|
30
|
+
this.size = options.size || 40;
|
|
31
|
+
this.indeterminate = options.indeterminate !== false;
|
|
32
|
+
this.progress = options.progress || 0; // 0-100
|
|
33
|
+
this.platform = framework.platform;
|
|
34
|
+
|
|
35
|
+
// Couleurs selon la plateforme
|
|
36
|
+
this.color = options.color || (
|
|
37
|
+
this.platform === 'material' ? '#6200EE' : '#8E8E93'
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// Épaisseur selon la plateforme
|
|
41
|
+
this.lineWidth = options.lineWidth || (
|
|
42
|
+
this.platform === 'material' ? 4 : 2.5
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// Vitesse d'animation selon la plateforme
|
|
46
|
+
this.animationSpeed = options.animationSpeed || (
|
|
47
|
+
this.platform === 'material' ? 0.05 : 0.08
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
this.rotation = 0;
|
|
51
|
+
|
|
52
|
+
// Pour l'animation Material (arc qui s'agrandit/rétrécit)
|
|
53
|
+
this.arcStart = 0;
|
|
54
|
+
this.arcEnd = 0;
|
|
55
|
+
this.arcGrowing = true;
|
|
56
|
+
|
|
57
|
+
// Pour l'animation Cupertino (12 traits qui tournent)
|
|
58
|
+
this.cupertinoLines = 12;
|
|
59
|
+
this.cupertinoOpacity = Array(this.cupertinoLines).fill(0);
|
|
60
|
+
|
|
61
|
+
this.width = this.size;
|
|
62
|
+
this.height = this.size;
|
|
63
|
+
|
|
64
|
+
// Démarrer l'animation pour indeterminate
|
|
65
|
+
if (this.indeterminate) {
|
|
66
|
+
this.startAnimation();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Démarre l'animation du spinner
|
|
72
|
+
* @private
|
|
73
|
+
*/
|
|
74
|
+
startAnimation() {
|
|
75
|
+
let lastTime = performance.now();
|
|
76
|
+
|
|
77
|
+
const animate = (currentTime) => {
|
|
78
|
+
if (!this.visible || !this.indeterminate) return;
|
|
79
|
+
|
|
80
|
+
const deltaTime = currentTime - lastTime;
|
|
81
|
+
lastTime = currentTime;
|
|
82
|
+
|
|
83
|
+
if (this.platform === 'material') {
|
|
84
|
+
this.animateMaterial(deltaTime);
|
|
85
|
+
} else {
|
|
86
|
+
this.animateCupertino(deltaTime);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this.markDirty();
|
|
90
|
+
requestAnimationFrame(animate);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
requestAnimationFrame(animate);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Animation Material (arc qui tourne et change de taille)
|
|
98
|
+
* @private
|
|
99
|
+
*/
|
|
100
|
+
animateMaterial(deltaTime) {
|
|
101
|
+
// Rotation globale
|
|
102
|
+
this.rotation += this.animationSpeed;
|
|
103
|
+
if (this.rotation > Math.PI * 2) {
|
|
104
|
+
this.rotation -= Math.PI * 2;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Animation de l'arc (grossit puis rétrécit)
|
|
108
|
+
const arcSpeed = 0.03;
|
|
109
|
+
|
|
110
|
+
if (this.arcGrowing) {
|
|
111
|
+
this.arcEnd += arcSpeed;
|
|
112
|
+
if (this.arcEnd > Math.PI * 1.5) {
|
|
113
|
+
this.arcGrowing = false;
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
this.arcStart += arcSpeed;
|
|
117
|
+
if (this.arcStart >= this.arcEnd) {
|
|
118
|
+
this.arcGrowing = true;
|
|
119
|
+
this.arcStart = 0;
|
|
120
|
+
this.arcEnd = 0;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Animation Cupertino (traits qui s'estompent)
|
|
127
|
+
* @private
|
|
128
|
+
*/
|
|
129
|
+
animateCupertino(deltaTime) {
|
|
130
|
+
this.rotation += this.animationSpeed;
|
|
131
|
+
if (this.rotation > Math.PI * 2) {
|
|
132
|
+
this.rotation -= Math.PI * 2;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Calculer l'opacité de chaque trait (fade progressif)
|
|
136
|
+
const activeIndex = Math.floor((this.rotation / (Math.PI * 2)) * this.cupertinoLines);
|
|
137
|
+
|
|
138
|
+
for (let i = 0; i < this.cupertinoLines; i++) {
|
|
139
|
+
const distance = Math.abs(i - activeIndex);
|
|
140
|
+
const minDistance = Math.min(distance, this.cupertinoLines - distance);
|
|
141
|
+
this.cupertinoOpacity[i] = 1 - (minDistance / this.cupertinoLines) * 0.8;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Dessine le spinner Material
|
|
147
|
+
* @private
|
|
148
|
+
*/
|
|
149
|
+
drawMaterial(ctx, centerX, centerY, radius) {
|
|
150
|
+
if (this.indeterminate) {
|
|
151
|
+
// Track (cercle de base - très léger)
|
|
152
|
+
ctx.strokeStyle = 'rgba(98, 0, 238, 0.1)';
|
|
153
|
+
ctx.lineWidth = this.lineWidth;
|
|
154
|
+
ctx.beginPath();
|
|
155
|
+
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
|
|
156
|
+
ctx.stroke();
|
|
157
|
+
|
|
158
|
+
// Arc animé qui tourne
|
|
159
|
+
ctx.save();
|
|
160
|
+
ctx.translate(centerX, centerY);
|
|
161
|
+
ctx.rotate(this.rotation);
|
|
162
|
+
|
|
163
|
+
ctx.strokeStyle = this.color;
|
|
164
|
+
ctx.lineCap = 'round';
|
|
165
|
+
ctx.lineWidth = this.lineWidth;
|
|
166
|
+
ctx.beginPath();
|
|
167
|
+
ctx.arc(0, 0, radius, this.arcStart, this.arcEnd);
|
|
168
|
+
ctx.stroke();
|
|
169
|
+
|
|
170
|
+
ctx.restore();
|
|
171
|
+
} else {
|
|
172
|
+
// Progress circulaire déterminé
|
|
173
|
+
// Track
|
|
174
|
+
ctx.strokeStyle = '#E0E0E0';
|
|
175
|
+
ctx.lineWidth = this.lineWidth;
|
|
176
|
+
ctx.beginPath();
|
|
177
|
+
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
|
|
178
|
+
ctx.stroke();
|
|
179
|
+
|
|
180
|
+
// Progress
|
|
181
|
+
const angle = (this.progress / 100) * Math.PI * 2;
|
|
182
|
+
ctx.strokeStyle = this.color;
|
|
183
|
+
ctx.lineCap = 'round';
|
|
184
|
+
ctx.beginPath();
|
|
185
|
+
ctx.arc(centerX, centerY, radius, -Math.PI / 2, -Math.PI / 2 + angle);
|
|
186
|
+
ctx.stroke();
|
|
187
|
+
|
|
188
|
+
// Pourcentage au centre
|
|
189
|
+
if (this.progress > 0) {
|
|
190
|
+
ctx.fillStyle = this.color;
|
|
191
|
+
ctx.font = `bold ${this.size / 3}px -apple-system, sans-serif`;
|
|
192
|
+
ctx.textAlign = 'center';
|
|
193
|
+
ctx.textBaseline = 'middle';
|
|
194
|
+
ctx.fillText(`${Math.round(this.progress)}%`, centerX, centerY);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Dessine le spinner Cupertino (style iOS)
|
|
201
|
+
* @private
|
|
202
|
+
*/
|
|
203
|
+
drawCupertino(ctx, centerX, centerY, radius) {
|
|
204
|
+
if (this.indeterminate) {
|
|
205
|
+
// Spinner iOS avec 12 traits
|
|
206
|
+
ctx.lineCap = 'round';
|
|
207
|
+
ctx.lineWidth = this.lineWidth;
|
|
208
|
+
|
|
209
|
+
for (let i = 0; i < this.cupertinoLines; i++) {
|
|
210
|
+
const angle = (i / this.cupertinoLines) * Math.PI * 2;
|
|
211
|
+
const opacity = this.cupertinoOpacity[i];
|
|
212
|
+
|
|
213
|
+
ctx.save();
|
|
214
|
+
ctx.translate(centerX, centerY);
|
|
215
|
+
ctx.rotate(angle);
|
|
216
|
+
|
|
217
|
+
// Trait avec opacité variable
|
|
218
|
+
const startRadius = radius * 0.6;
|
|
219
|
+
const endRadius = radius;
|
|
220
|
+
|
|
221
|
+
ctx.strokeStyle = this.hexToRgba(this.color, opacity);
|
|
222
|
+
ctx.beginPath();
|
|
223
|
+
ctx.moveTo(0, -startRadius);
|
|
224
|
+
ctx.lineTo(0, -endRadius);
|
|
225
|
+
ctx.stroke();
|
|
226
|
+
|
|
227
|
+
ctx.restore();
|
|
228
|
+
}
|
|
229
|
+
} else {
|
|
230
|
+
// Progress circulaire iOS (plus fin et élégant)
|
|
231
|
+
// Track
|
|
232
|
+
ctx.strokeStyle = '#E5E5EA';
|
|
233
|
+
ctx.lineWidth = this.lineWidth;
|
|
234
|
+
ctx.beginPath();
|
|
235
|
+
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
|
|
236
|
+
ctx.stroke();
|
|
237
|
+
|
|
238
|
+
// Progress
|
|
239
|
+
const angle = (this.progress / 100) * Math.PI * 2;
|
|
240
|
+
ctx.strokeStyle = this.color;
|
|
241
|
+
ctx.lineCap = 'round';
|
|
242
|
+
ctx.beginPath();
|
|
243
|
+
ctx.arc(centerX, centerY, radius, -Math.PI / 2, -Math.PI / 2 + angle);
|
|
244
|
+
ctx.stroke();
|
|
245
|
+
|
|
246
|
+
// Pas de texte au centre pour iOS (plus minimaliste)
|
|
247
|
+
// Mais si tu veux, décommente :
|
|
248
|
+
/*
|
|
249
|
+
if (this.progress > 0) {
|
|
250
|
+
ctx.fillStyle = this.color;
|
|
251
|
+
ctx.font = `${this.size / 4}px -apple-system, sans-serif`;
|
|
252
|
+
ctx.textAlign = 'center';
|
|
253
|
+
ctx.textBaseline = 'middle';
|
|
254
|
+
ctx.fillText(`${Math.round(this.progress)}%`, centerX, centerY);
|
|
255
|
+
}
|
|
256
|
+
*/
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Dessine le spinner
|
|
262
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
263
|
+
*/
|
|
264
|
+
draw(ctx) {
|
|
265
|
+
ctx.save();
|
|
266
|
+
|
|
267
|
+
const centerX = this.x + this.size / 2;
|
|
268
|
+
const centerY = this.y + this.size / 2;
|
|
269
|
+
const radius = (this.size - this.lineWidth * 2) / 2;
|
|
270
|
+
|
|
271
|
+
if (this.platform === 'material') {
|
|
272
|
+
this.drawMaterial(ctx, centerX, centerY, radius);
|
|
273
|
+
} else {
|
|
274
|
+
this.drawCupertino(ctx, centerX, centerY, radius);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
ctx.restore();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Convertit une couleur hex en rgba avec opacité
|
|
282
|
+
* @private
|
|
283
|
+
*/
|
|
284
|
+
hexToRgba(hex, alpha) {
|
|
285
|
+
// Si c'est déjà rgba, le retourner
|
|
286
|
+
if (hex.startsWith('rgba')) return hex;
|
|
287
|
+
if (hex.startsWith('rgb')) {
|
|
288
|
+
return hex.replace('rgb', 'rgba').replace(')', `, ${alpha})`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Convertir hex en rgba
|
|
292
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
293
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
294
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
295
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Définit la progression
|
|
300
|
+
* @param {number} value - Valeur de progression (0-100)
|
|
301
|
+
*/
|
|
302
|
+
setProgress(value) {
|
|
303
|
+
this.progress = Math.max(0, Math.min(100, value));
|
|
304
|
+
this.indeterminate = false;
|
|
305
|
+
this.markDirty();
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Active le mode indéterminé
|
|
310
|
+
*/
|
|
311
|
+
setIndeterminate() {
|
|
312
|
+
this.indeterminate = true;
|
|
313
|
+
if (!this._animating) {
|
|
314
|
+
this.startAnimation();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Vérifie si un point est dans les limites
|
|
320
|
+
* @returns {boolean} False (non cliquable)
|
|
321
|
+
*/
|
|
322
|
+
isPointInside() {
|
|
323
|
+
return false; // Non cliquable
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export default CircularProgress;
|