canvasframework 0.5.18 → 0.5.19
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 +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,199 @@
|
|
|
1
|
+
import Component from '../core/Component.js';
|
|
2
|
+
import SelectDialog from '../components/SelectDialog.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Composant de sélection déroulante (dropdown)
|
|
6
|
+
* @class
|
|
7
|
+
* @extends Component
|
|
8
|
+
* @param {Framework} framework - Instance du framework
|
|
9
|
+
* @param {Object} [options={}] - Options de configuration
|
|
10
|
+
* @param {string[]} [options.options=[]] - Liste des options
|
|
11
|
+
* @param {number} [options.selectedIndex=0] - Index de l'option sélectionnée
|
|
12
|
+
* @param {string} [options.placeholder='Select...'] - Texte par défaut
|
|
13
|
+
* @param {number} [options.fontSize=16] - Taille de police
|
|
14
|
+
* @param {Function} [options.onChange] - Callback lors du changement de sélection
|
|
15
|
+
* @example
|
|
16
|
+
* const select = new Select(framework, {
|
|
17
|
+
* options: ['Option 1', 'Option 2', 'Option 3'],
|
|
18
|
+
* placeholder: 'Choisissez une option',
|
|
19
|
+
* onChange: (value, index) => console.log('Selected:', value)
|
|
20
|
+
* });
|
|
21
|
+
*/
|
|
22
|
+
class Select extends Component {
|
|
23
|
+
/**
|
|
24
|
+
* @constructs Select
|
|
25
|
+
*/
|
|
26
|
+
constructor(framework, options = {}) {
|
|
27
|
+
super(framework, options);
|
|
28
|
+
/** @type {string[]} */
|
|
29
|
+
this.options = options.options || [];
|
|
30
|
+
/** @type {number} */
|
|
31
|
+
this.selectedIndex = options.selectedIndex || 0;
|
|
32
|
+
/** @type {string} */
|
|
33
|
+
this.placeholder = options.placeholder || 'Select...';
|
|
34
|
+
/** @type {string} */
|
|
35
|
+
this.platform = framework.platform;
|
|
36
|
+
/** @type {number} */
|
|
37
|
+
this.fontSize = options.fontSize || 16;
|
|
38
|
+
/** @type {Function|undefined} */
|
|
39
|
+
this.onChange = options.onChange;
|
|
40
|
+
/** @type {boolean} */
|
|
41
|
+
this.isOpen = false;
|
|
42
|
+
/** @type {SelectDialog|null} */
|
|
43
|
+
this.dialog = null;
|
|
44
|
+
|
|
45
|
+
// Définir onClick pour le Select
|
|
46
|
+
this.onClick = this.toggleMenu.bind(this);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Ouvre ou ferme le menu de sélection
|
|
51
|
+
*/
|
|
52
|
+
toggleMenu() {
|
|
53
|
+
if (this.isOpen && this.dialog) {
|
|
54
|
+
this.closeMenu();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.openMenu();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Ouvre le menu de sélection (affiche le modal)
|
|
63
|
+
*/
|
|
64
|
+
openMenu() {
|
|
65
|
+
if (this.isOpen) return;
|
|
66
|
+
|
|
67
|
+
this.dialog = new SelectDialog(this.framework, {
|
|
68
|
+
title: this.placeholder,
|
|
69
|
+
options: this.options,
|
|
70
|
+
selectedIndex: this.selectedIndex,
|
|
71
|
+
onSelect: (index, value) => {
|
|
72
|
+
this.selectedIndex = index;
|
|
73
|
+
if (this.onChange) {
|
|
74
|
+
this.onChange(value, index);
|
|
75
|
+
}
|
|
76
|
+
this.closeMenu();
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
this.framework.add(this.dialog);
|
|
81
|
+
this.isOpen = true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Ferme le menu de sélection
|
|
86
|
+
*/
|
|
87
|
+
closeMenu() {
|
|
88
|
+
if (this.dialog) {
|
|
89
|
+
this.dialog.hide();
|
|
90
|
+
this.dialog = null;
|
|
91
|
+
}
|
|
92
|
+
this.isOpen = false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Dessine le composant Select
|
|
97
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
98
|
+
*/
|
|
99
|
+
draw(ctx) {
|
|
100
|
+
ctx.save();
|
|
101
|
+
|
|
102
|
+
const selectedValue = this.options[this.selectedIndex] || this.placeholder;
|
|
103
|
+
|
|
104
|
+
if (this.platform === 'material') {
|
|
105
|
+
// Material Design Select
|
|
106
|
+
ctx.fillStyle = this.pressed ? '#F5F5F5' : '#FFFFFF';
|
|
107
|
+
ctx.fillRect(this.x, this.y, this.width, this.height);
|
|
108
|
+
|
|
109
|
+
ctx.strokeStyle = this.isOpen ? '#6200EE' : '#CCCCCC';
|
|
110
|
+
ctx.lineWidth = this.isOpen ? 2 : 1;
|
|
111
|
+
ctx.strokeRect(this.x, this.y, this.width, this.height);
|
|
112
|
+
|
|
113
|
+
// Texte
|
|
114
|
+
ctx.fillStyle = selectedValue === this.placeholder ? '#999999' : '#000000';
|
|
115
|
+
ctx.font = `${this.fontSize}px Roboto, sans-serif`;
|
|
116
|
+
ctx.textAlign = 'left';
|
|
117
|
+
ctx.textBaseline = 'middle';
|
|
118
|
+
ctx.fillText(selectedValue, this.x + 15, this.y + this.height / 2);
|
|
119
|
+
|
|
120
|
+
// Flèche
|
|
121
|
+
ctx.fillStyle = '#666666';
|
|
122
|
+
const arrowX = this.x + this.width - 20;
|
|
123
|
+
const arrowY = this.y + this.height / 2;
|
|
124
|
+
ctx.beginPath();
|
|
125
|
+
ctx.moveTo(arrowX - 5, arrowY - 3);
|
|
126
|
+
ctx.lineTo(arrowX + 5, arrowY - 3);
|
|
127
|
+
ctx.lineTo(arrowX, arrowY + 3);
|
|
128
|
+
ctx.closePath();
|
|
129
|
+
ctx.fill();
|
|
130
|
+
} else {
|
|
131
|
+
// Cupertino Select
|
|
132
|
+
ctx.fillStyle = '#FFFFFF';
|
|
133
|
+
ctx.beginPath();
|
|
134
|
+
this.roundRect(ctx, this.x, this.y, this.width, this.height, 8);
|
|
135
|
+
ctx.fill();
|
|
136
|
+
|
|
137
|
+
ctx.strokeStyle = this.isOpen ? '#007AFF' : '#C7C7CC';
|
|
138
|
+
ctx.lineWidth = 1;
|
|
139
|
+
ctx.stroke();
|
|
140
|
+
|
|
141
|
+
// Texte
|
|
142
|
+
ctx.fillStyle = selectedValue === this.placeholder ? '#999999' : '#000000';
|
|
143
|
+
ctx.font = `${this.fontSize}px -apple-system, sans-serif`;
|
|
144
|
+
ctx.textAlign = 'left';
|
|
145
|
+
ctx.textBaseline = 'middle';
|
|
146
|
+
ctx.fillText(selectedValue, this.x + 15, this.y + this.height / 2);
|
|
147
|
+
|
|
148
|
+
// Chevron
|
|
149
|
+
ctx.strokeStyle = '#007AFF';
|
|
150
|
+
ctx.lineWidth = 2;
|
|
151
|
+
const chevronX = this.x + this.width - 20;
|
|
152
|
+
const chevronY = this.y + this.height / 2;
|
|
153
|
+
ctx.beginPath();
|
|
154
|
+
ctx.moveTo(chevronX - 5, chevronY - 3);
|
|
155
|
+
ctx.lineTo(chevronX, chevronY + 2);
|
|
156
|
+
ctx.lineTo(chevronX + 5, chevronY - 3);
|
|
157
|
+
ctx.stroke();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
ctx.restore();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Dessine un rectangle avec des coins arrondis
|
|
165
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
166
|
+
* @param {number} x - Position X
|
|
167
|
+
* @param {number} y - Position Y
|
|
168
|
+
* @param {number} width - Largeur
|
|
169
|
+
* @param {number} height - Hauteur
|
|
170
|
+
* @param {number} radius - Rayon des coins
|
|
171
|
+
* @private
|
|
172
|
+
*/
|
|
173
|
+
roundRect(ctx, x, y, width, height, radius) {
|
|
174
|
+
ctx.moveTo(x + radius, y);
|
|
175
|
+
ctx.lineTo(x + width - radius, y);
|
|
176
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
177
|
+
ctx.lineTo(x + width, y + height - radius);
|
|
178
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
179
|
+
ctx.lineTo(x + radius, y + height);
|
|
180
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
181
|
+
ctx.lineTo(x, y + radius);
|
|
182
|
+
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Vérifie si un point est à l'intérieur du composant
|
|
187
|
+
* @param {number} x - Position X
|
|
188
|
+
* @param {number} y - Position Y
|
|
189
|
+
* @returns {boolean} True si le point est à l'intérieur
|
|
190
|
+
*/
|
|
191
|
+
isPointInside(x, y) {
|
|
192
|
+
return x >= this.x &&
|
|
193
|
+
x <= this.x + this.width &&
|
|
194
|
+
y >= this.y &&
|
|
195
|
+
y <= this.y + this.height;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export default Select;
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import Modal from '../components/Modal.js';
|
|
2
|
+
/**
|
|
3
|
+
* Modal pour la sélection d'une option parmi une liste
|
|
4
|
+
* @class
|
|
5
|
+
* @extends Modal
|
|
6
|
+
* @param {Framework} framework - Instance du framework
|
|
7
|
+
* @param {Object} [options={}] - Options de configuration
|
|
8
|
+
* @param {string} [options.title='Sélectionner une option'] - Titre du modal
|
|
9
|
+
* @param {string[]} [options.options=[]] - Liste des options
|
|
10
|
+
* @param {number} [options.selectedIndex=0] - Index de l'option sélectionnée par défaut
|
|
11
|
+
* @param {Function} [options.onSelect] - Callback lors de la sélection
|
|
12
|
+
* @example
|
|
13
|
+
* const dialog = new SelectDialog(framework, {
|
|
14
|
+
* title: 'Choisir une couleur',
|
|
15
|
+
* options: ['Rouge', 'Vert', 'Bleu'],
|
|
16
|
+
* selectedIndex: 1,
|
|
17
|
+
* onSelect: (index, value) => console.log('Selected:', value)
|
|
18
|
+
* });
|
|
19
|
+
*/
|
|
20
|
+
class SelectDialog extends Modal {
|
|
21
|
+
/**
|
|
22
|
+
* @constructs SelectDialog
|
|
23
|
+
*/
|
|
24
|
+
constructor(framework, options = {}) {
|
|
25
|
+
// Calculer la hauteur en fonction du nombre d'options
|
|
26
|
+
const optionsCount = options.options?.length || 0;
|
|
27
|
+
const itemHeight = 50;
|
|
28
|
+
const dialogHeight = Math.min(
|
|
29
|
+
400, // Hauteur max
|
|
30
|
+
Math.max(200, optionsCount * itemHeight + 100) // Hauteur min + espace pour titre
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
// Appeler le constructeur parent
|
|
34
|
+
super(framework, {
|
|
35
|
+
title: options.title || 'Sélectionner une option',
|
|
36
|
+
width: Math.min(350, framework.width - 40),
|
|
37
|
+
height: dialogHeight,
|
|
38
|
+
showCloseButton: true,
|
|
39
|
+
closeOnOverlayClick: true,
|
|
40
|
+
padding: 0, // Pas de padding, on gère nous-même
|
|
41
|
+
...options
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/** @type {string[]} */
|
|
45
|
+
this.options = options.options || [];
|
|
46
|
+
/** @type {number} */
|
|
47
|
+
this.selectedIndex = options.selectedIndex || 0;
|
|
48
|
+
/** @type {Function|undefined} */
|
|
49
|
+
this.onSelect = options.onSelect;
|
|
50
|
+
/** @type {number} */
|
|
51
|
+
this.itemHeight = itemHeight;
|
|
52
|
+
/** @type {number} */
|
|
53
|
+
this.hoveredIndex = -1;
|
|
54
|
+
|
|
55
|
+
// Désactiver les animations
|
|
56
|
+
this.opacity = 1;
|
|
57
|
+
this.scale = 1;
|
|
58
|
+
this.isVisible = true;
|
|
59
|
+
this.visible = true;
|
|
60
|
+
|
|
61
|
+
// AJOUTER: Définir onPress et onMove pour que le framework les appelle
|
|
62
|
+
this.onPress = this.handlePress.bind(this);
|
|
63
|
+
this.onMove = this.handleMove.bind(this);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Dessine le modal de sélection
|
|
68
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
69
|
+
*/
|
|
70
|
+
draw(ctx) {
|
|
71
|
+
if (!this.isVisible) return;
|
|
72
|
+
|
|
73
|
+
ctx.save();
|
|
74
|
+
ctx.globalAlpha = this.opacity;
|
|
75
|
+
|
|
76
|
+
// Overlay sombre
|
|
77
|
+
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
|
|
78
|
+
ctx.fillRect(0, 0, this.framework.width, this.framework.height);
|
|
79
|
+
|
|
80
|
+
// Calculer la position du modal (centré)
|
|
81
|
+
const modalX = (this.framework.width - this.modalWidth) / 2;
|
|
82
|
+
const modalY = (this.framework.height - this.modalHeight) / 2;
|
|
83
|
+
|
|
84
|
+
// Fond du modal
|
|
85
|
+
ctx.fillStyle = '#FFFFFF';
|
|
86
|
+
ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
|
|
87
|
+
ctx.shadowBlur = 20;
|
|
88
|
+
ctx.shadowOffsetY = 10;
|
|
89
|
+
|
|
90
|
+
ctx.beginPath();
|
|
91
|
+
this.roundRect(ctx, modalX, modalY, this.modalWidth, this.modalHeight, 12);
|
|
92
|
+
ctx.fill();
|
|
93
|
+
|
|
94
|
+
ctx.shadowColor = 'transparent';
|
|
95
|
+
|
|
96
|
+
// Titre
|
|
97
|
+
if (this.title) {
|
|
98
|
+
ctx.fillStyle = '#000000';
|
|
99
|
+
ctx.font = 'bold 18px -apple-system, sans-serif';
|
|
100
|
+
ctx.textAlign = 'center';
|
|
101
|
+
ctx.textBaseline = 'middle';
|
|
102
|
+
ctx.fillText(this.title, modalX + this.modalWidth / 2, modalY + 30);
|
|
103
|
+
|
|
104
|
+
// Ligne de séparation sous le titre
|
|
105
|
+
ctx.strokeStyle = '#E0E0E0';
|
|
106
|
+
ctx.lineWidth = 1;
|
|
107
|
+
ctx.beginPath();
|
|
108
|
+
ctx.moveTo(modalX, modalY + 50);
|
|
109
|
+
ctx.lineTo(modalX + this.modalWidth, modalY + 50);
|
|
110
|
+
ctx.stroke();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Zone de contenu (avec scroll si nécessaire)
|
|
114
|
+
const contentX = modalX;
|
|
115
|
+
const contentY = modalY + 55; // Après le titre et la ligne
|
|
116
|
+
const contentHeight = this.modalHeight - 55;
|
|
117
|
+
|
|
118
|
+
ctx.save();
|
|
119
|
+
ctx.beginPath();
|
|
120
|
+
ctx.rect(contentX, contentY, this.modalWidth, contentHeight);
|
|
121
|
+
ctx.clip();
|
|
122
|
+
|
|
123
|
+
// Options
|
|
124
|
+
for (let i = 0; i < this.options.length; i++) {
|
|
125
|
+
const optionY = contentY + i * this.itemHeight;
|
|
126
|
+
|
|
127
|
+
// Si l'option est en dehors de la zone visible, passer à la suivante
|
|
128
|
+
if (optionY + this.itemHeight < contentY || optionY > contentY + contentHeight) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Option sélectionnée
|
|
133
|
+
if (i === this.selectedIndex) {
|
|
134
|
+
ctx.fillStyle = this.framework.platform === 'material' ? 'rgba(98, 0, 238, 0.1)' : 'rgba(0, 122, 255, 0.1)';
|
|
135
|
+
ctx.fillRect(contentX, optionY, this.modalWidth, this.itemHeight);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Effet hover
|
|
139
|
+
if (this.hoveredIndex === i) {
|
|
140
|
+
ctx.fillStyle = '#F5F5F5';
|
|
141
|
+
ctx.fillRect(contentX, optionY, this.modalWidth, this.itemHeight);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Texte de l'option
|
|
145
|
+
ctx.fillStyle = i === this.selectedIndex ?
|
|
146
|
+
(this.framework.platform === 'material' ? '#6200EE' : '#007AFF') :
|
|
147
|
+
'#000000';
|
|
148
|
+
ctx.font = i === this.selectedIndex ? 'bold 16px -apple-system, sans-serif' : '16px -apple-system, sans-serif';
|
|
149
|
+
ctx.textAlign = 'left';
|
|
150
|
+
ctx.textBaseline = 'middle';
|
|
151
|
+
ctx.fillText(this.options[i], contentX + 20, optionY + this.itemHeight / 2);
|
|
152
|
+
|
|
153
|
+
// Divider entre les options
|
|
154
|
+
if (i < this.options.length - 1) {
|
|
155
|
+
ctx.strokeStyle = '#E0E0E0';
|
|
156
|
+
ctx.lineWidth = 1;
|
|
157
|
+
ctx.beginPath();
|
|
158
|
+
ctx.moveTo(contentX + 20, optionY + this.itemHeight);
|
|
159
|
+
ctx.lineTo(contentX + this.modalWidth - 20, optionY + this.itemHeight);
|
|
160
|
+
ctx.stroke();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
ctx.restore();
|
|
165
|
+
ctx.restore();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Gère le clic dans le modal
|
|
170
|
+
* @param {number} x - Position X du clic
|
|
171
|
+
* @param {number} y - Position Y du clic
|
|
172
|
+
*/
|
|
173
|
+
handlePress(x, y) {
|
|
174
|
+
// POUR LES MODALS: utiliser les coordonnées brutes car le modal est un composant FIXE
|
|
175
|
+
// Les modals ne sont pas affectés par le scroll
|
|
176
|
+
|
|
177
|
+
const modalX = (this.framework.width - this.modalWidth) / 2;
|
|
178
|
+
const modalY = (this.framework.height - this.modalHeight) / 2;
|
|
179
|
+
const contentY = modalY + 55;
|
|
180
|
+
|
|
181
|
+
// Vérifier si une option a été cliquée
|
|
182
|
+
for (let i = 0; i < this.options.length; i++) {
|
|
183
|
+
const optionY = contentY + i * this.itemHeight;
|
|
184
|
+
|
|
185
|
+
// IMPORTANT: Pas d'ajustement de scroll pour les modals
|
|
186
|
+
if (y >= optionY && y <= optionY + this.itemHeight &&
|
|
187
|
+
x >= modalX && x <= modalX + this.modalWidth) {
|
|
188
|
+
|
|
189
|
+
this.selectedIndex = i;
|
|
190
|
+
if (this.onSelect) {
|
|
191
|
+
this.onSelect(i, this.options[i]);
|
|
192
|
+
}
|
|
193
|
+
this.hide();
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Sinon, laisser le parent gérer (bouton de fermeture, overlay)
|
|
199
|
+
super.handlePress(x, y);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Gère le survol dans le modal
|
|
204
|
+
* @param {number} x - Position X actuelle
|
|
205
|
+
* @param {number} y - Position Y actuelle
|
|
206
|
+
*/
|
|
207
|
+
handleMove(x, y) {
|
|
208
|
+
const modalX = (this.framework.width - this.modalWidth) / 2;
|
|
209
|
+
const modalY = (this.framework.height - this.modalHeight) / 2;
|
|
210
|
+
const contentY = modalY + 55;
|
|
211
|
+
|
|
212
|
+
this.hoveredIndex = -1;
|
|
213
|
+
|
|
214
|
+
// Vérifier si on survole une option
|
|
215
|
+
for (let i = 0; i < this.options.length; i++) {
|
|
216
|
+
const optionY = contentY + i * this.itemHeight;
|
|
217
|
+
|
|
218
|
+
// IMPORTANT: Pas d'ajustement de scroll pour les modals
|
|
219
|
+
if (y >= optionY && y <= optionY + this.itemHeight &&
|
|
220
|
+
x >= modalX && x <= modalX + this.modalWidth) {
|
|
221
|
+
this.hoveredIndex = i;
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Vérifie si un point est à l'intérieur du modal
|
|
229
|
+
* @param {number} x - Position X
|
|
230
|
+
* @param {number} y - Position Y
|
|
231
|
+
* @returns {boolean} True si le point est à l'intérieur du modal
|
|
232
|
+
*/
|
|
233
|
+
isPointInside(x, y) {
|
|
234
|
+
const modalX = (this.framework.width - this.modalWidth) / 2;
|
|
235
|
+
const modalY = (this.framework.height - this.modalHeight) / 2;
|
|
236
|
+
|
|
237
|
+
// Les modals sont des composants fixes, donc pas d'ajustement de scroll
|
|
238
|
+
return x >= modalX &&
|
|
239
|
+
x <= modalX + this.modalWidth &&
|
|
240
|
+
y >= modalY &&
|
|
241
|
+
y <= modalY + this.modalHeight;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Affiche le modal
|
|
246
|
+
*/
|
|
247
|
+
show() {
|
|
248
|
+
this.isVisible = true;
|
|
249
|
+
this.visible = true;
|
|
250
|
+
this.opacity = 1;
|
|
251
|
+
this.scale = 1;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export default SelectDialog;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import Component from '../core/Component.js';
|
|
2
|
+
|
|
3
|
+
class Slider extends Component {
|
|
4
|
+
constructor(framework, options = {}) {
|
|
5
|
+
super(framework, options);
|
|
6
|
+
this.min = options.min || 0;
|
|
7
|
+
this.max = options.max || 100;
|
|
8
|
+
this.value = options.value || 50;
|
|
9
|
+
this.platform = framework.platform;
|
|
10
|
+
this.onChange = options.onChange;
|
|
11
|
+
this.dragging = false;
|
|
12
|
+
|
|
13
|
+
// Animation state
|
|
14
|
+
this.thumbScale = 1.0;
|
|
15
|
+
this.targetScale = 1.0;
|
|
16
|
+
|
|
17
|
+
this.onPress = this.handlePress.bind(this);
|
|
18
|
+
this.onMove = this.handleMove.bind(this);
|
|
19
|
+
this.onClick = this.handleClick.bind(this);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
handlePress(x, y) {
|
|
23
|
+
this.dragging = true;
|
|
24
|
+
this.targetScale = 1.5; // Agrandissement de 50%
|
|
25
|
+
this.updateValue(x);
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
handleMove(x, y) {
|
|
30
|
+
if (this.dragging) {
|
|
31
|
+
this.updateValue(x);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
handleClick() {
|
|
36
|
+
this.dragging = false;
|
|
37
|
+
this.targetScale = 1.0; // Retour à la taille normale
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
updateValue(x) {
|
|
41
|
+
const relativeX = Math.max(0, Math.min(this.width, x - this.x));
|
|
42
|
+
const newValue = this.min + (relativeX / this.width) * (this.max - this.min);
|
|
43
|
+
|
|
44
|
+
if (newValue !== this.value) {
|
|
45
|
+
this.value = newValue;
|
|
46
|
+
if (this.onChange) this.onChange(this.value);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
draw(ctx) {
|
|
51
|
+
ctx.save();
|
|
52
|
+
|
|
53
|
+
// Animation du scale
|
|
54
|
+
this.thumbScale += (this.targetScale - this.thumbScale) * 0.2;
|
|
55
|
+
|
|
56
|
+
const progress = (this.value - this.min) / (this.max - this.min);
|
|
57
|
+
const thumbX = this.x + progress * this.width;
|
|
58
|
+
|
|
59
|
+
// Track
|
|
60
|
+
ctx.strokeStyle = this.platform === 'material' ? '#E0E0E0' : '#C7C7CC';
|
|
61
|
+
ctx.lineWidth = 2;
|
|
62
|
+
ctx.beginPath();
|
|
63
|
+
ctx.moveTo(this.x, this.y + this.height / 2);
|
|
64
|
+
ctx.lineTo(this.x + this.width, this.y + this.height / 2);
|
|
65
|
+
ctx.stroke();
|
|
66
|
+
|
|
67
|
+
// Track rempli
|
|
68
|
+
const trackColor = this.platform === 'material' ? '#6200EE' : '#007AFF';
|
|
69
|
+
ctx.strokeStyle = trackColor;
|
|
70
|
+
ctx.lineWidth = 2;
|
|
71
|
+
ctx.beginPath();
|
|
72
|
+
ctx.moveTo(this.x, this.y + this.height / 2);
|
|
73
|
+
ctx.lineTo(thumbX, this.y + this.height / 2);
|
|
74
|
+
ctx.stroke();
|
|
75
|
+
|
|
76
|
+
// Curseur avec animation
|
|
77
|
+
const baseRadius = 8;
|
|
78
|
+
const currentRadius = baseRadius * this.thumbScale;
|
|
79
|
+
|
|
80
|
+
// Effet d'ombre pendant le drag
|
|
81
|
+
if (this.dragging) {
|
|
82
|
+
ctx.shadowColor = trackColor;
|
|
83
|
+
ctx.shadowBlur = 10;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
ctx.fillStyle = trackColor;
|
|
87
|
+
ctx.beginPath();
|
|
88
|
+
ctx.arc(thumbX, this.y + this.height / 2, currentRadius, 0, Math.PI * 2);
|
|
89
|
+
ctx.fill();
|
|
90
|
+
|
|
91
|
+
if (this.dragging) {
|
|
92
|
+
ctx.shadowColor = 'transparent';
|
|
93
|
+
ctx.shadowBlur = 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
ctx.restore();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
isPointInside(x, y) {
|
|
100
|
+
const progress = (this.value - this.min) / (this.max - this.min);
|
|
101
|
+
const thumbX = this.x + progress * this.width;
|
|
102
|
+
const thumbY = this.y + this.height / 2;
|
|
103
|
+
const maxRadius = 8 * 1.5 + 5; // Rayon max + marge
|
|
104
|
+
|
|
105
|
+
// Zone de clic plus large pour faciliter l'utilisation
|
|
106
|
+
const distance = Math.sqrt((x - thumbX) ** 2 + (y - thumbY) ** 2);
|
|
107
|
+
return distance <= maxRadius ||
|
|
108
|
+
(x >= this.x && x <= this.x + this.width &&
|
|
109
|
+
y >= this.y && y <= this.y + this.height);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export default Slider;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import AppBar from './AppBar.js';
|
|
2
|
+
/**
|
|
3
|
+
* SliverAppBar - CanvasFramework
|
|
4
|
+
* Collapse / Stretch / Parallax
|
|
5
|
+
* Compatible Material & Cupertino
|
|
6
|
+
* ⚠️ Ne modifie PAS AppBar
|
|
7
|
+
*/
|
|
8
|
+
class SliverAppBar extends AppBar {
|
|
9
|
+
constructor(framework, options = {}) {
|
|
10
|
+
super(framework, {
|
|
11
|
+
...options,
|
|
12
|
+
y: 0
|
|
13
|
+
});
|
|
14
|
+
// Heights
|
|
15
|
+
this.expandedHeight = options.expandedHeight || 240;
|
|
16
|
+
this.collapsedHeight = options.collapsedHeight ?? 56;
|
|
17
|
+
// Effects
|
|
18
|
+
this.stretch = options.stretch ?? true;
|
|
19
|
+
this.parallax = options.parallax ?? true;
|
|
20
|
+
this.parallaxFactor = options.parallaxFactor ?? 0.4;
|
|
21
|
+
// Background
|
|
22
|
+
this.backgroundImage = options.backgroundImage || null;
|
|
23
|
+
this.backgroundColor = options.backgroundColor || this.bgColor || '#FFFFFF';
|
|
24
|
+
this.backgroundOpacity = this._extractOpacity(this.backgroundColor);
|
|
25
|
+
// Overlay (au-dessus de l'image)
|
|
26
|
+
this.overlayColor = options.overlayColor || '#000000';
|
|
27
|
+
this.overlayOpacity = options.overlayOpacity ?? 0;
|
|
28
|
+
// Foreground (titre + icônes)
|
|
29
|
+
this.foregroundColor = options.foregroundColor || options.textColor || this.textColor || '#FFFFFF';
|
|
30
|
+
// SliverAppBar participe au scroll
|
|
31
|
+
this.fixed = false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Extrait l'opacité d'une couleur rgba() ou retourne 1
|
|
36
|
+
*/
|
|
37
|
+
_extractOpacity(color) {
|
|
38
|
+
if (typeof color === 'string' && color.startsWith('rgba')) {
|
|
39
|
+
const match = color.match(/rgba?\([^,]+,[^,]+,[^,]+,\s*([\d.]+)\)/);
|
|
40
|
+
return match ? parseFloat(match[1]) : 1;
|
|
41
|
+
}
|
|
42
|
+
return 1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Mise à jour dynamique selon le scroll
|
|
47
|
+
*/
|
|
48
|
+
updateWithScroll(scrollOffset) {
|
|
49
|
+
const scrollY = -scrollOffset;
|
|
50
|
+
const collapseRange = this.expandedHeight - this.collapsedHeight;
|
|
51
|
+
const collapse = Math.min(scrollY, collapseRange);
|
|
52
|
+
let newHeight = this.expandedHeight - collapse;
|
|
53
|
+
// Stretch (overscroll)
|
|
54
|
+
if (scrollY < 0 && this.stretch) {
|
|
55
|
+
newHeight = this.expandedHeight - scrollY;
|
|
56
|
+
}
|
|
57
|
+
this.height = Math.max(newHeight, this.collapsedHeight);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
draw(ctx) {
|
|
61
|
+
const scrollOffset = this.framework.scrollOffset || 0;
|
|
62
|
+
this.updateWithScroll(scrollOffset);
|
|
63
|
+
ctx.save();
|
|
64
|
+
// ===== Collapse progress (0 → 1)
|
|
65
|
+
const collapseProgress = Math.min(
|
|
66
|
+
1,
|
|
67
|
+
Math.max(
|
|
68
|
+
0,
|
|
69
|
+
(this.expandedHeight - this.height) /
|
|
70
|
+
(this.expandedHeight - this.collapsedHeight)
|
|
71
|
+
)
|
|
72
|
+
);
|
|
73
|
+
// ===== BACKGROUND IMAGE (en premier) =====
|
|
74
|
+
if (this.backgroundImage && this.backgroundImage.complete) {
|
|
75
|
+
let bgOffset = 0;
|
|
76
|
+
if (this.parallax) {
|
|
77
|
+
bgOffset = Math.max(0, -scrollOffset * this.parallaxFactor);
|
|
78
|
+
}
|
|
79
|
+
ctx.drawImage(
|
|
80
|
+
this.backgroundImage,
|
|
81
|
+
this.x,
|
|
82
|
+
this.y - bgOffset,
|
|
83
|
+
this.width,
|
|
84
|
+
this.height + bgOffset
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ===== BACKGROUND COLOR (overlay coloré au-dessus de l'image) =====
|
|
89
|
+
ctx.globalAlpha = this.backgroundOpacity;
|
|
90
|
+
ctx.fillStyle = this.backgroundColor;
|
|
91
|
+
ctx.fillRect(this.x, this.y, this.width, this.height);
|
|
92
|
+
ctx.globalAlpha = 1;
|
|
93
|
+
|
|
94
|
+
// ===== OVERLAY SUPPLÉMENTAIRE (si besoin) =====
|
|
95
|
+
if (this.overlayOpacity > 0) {
|
|
96
|
+
ctx.globalAlpha = this.overlayOpacity;
|
|
97
|
+
ctx.fillStyle = this.overlayColor;
|
|
98
|
+
ctx.fillRect(this.x, this.y, this.width, this.height);
|
|
99
|
+
ctx.globalAlpha = 1;
|
|
100
|
+
}
|
|
101
|
+
// ===== TITLE (fade with collapse) =====
|
|
102
|
+
ctx.globalAlpha = 1 - collapseProgress;
|
|
103
|
+
ctx.fillStyle = this.foregroundColor;
|
|
104
|
+
ctx.font = `bold 20px -apple-system, Roboto, sans-serif`;
|
|
105
|
+
ctx.textAlign = 'left';
|
|
106
|
+
ctx.textBaseline = 'bottom';
|
|
107
|
+
ctx.fillText(
|
|
108
|
+
this.title,
|
|
109
|
+
this.x + 16,
|
|
110
|
+
this.y + this.height - 16
|
|
111
|
+
);
|
|
112
|
+
ctx.globalAlpha = 1;
|
|
113
|
+
|
|
114
|
+
// ===== PREVENT APPBAR BACKGROUND OVERWRITE =====
|
|
115
|
+
const originalBg = this.bgColor;
|
|
116
|
+
const originalTextColor = this.textColor;
|
|
117
|
+
const originalIconColor = this.iconColor;
|
|
118
|
+
const originalPlatform = this.platform;
|
|
119
|
+
|
|
120
|
+
// Force transparent pour éviter que super.draw() dessine un fond
|
|
121
|
+
this.bgColor = 'rgba(0,0,0,0)';
|
|
122
|
+
// Force la couleur du texte et des icônes
|
|
123
|
+
this.textColor = this.foregroundColor;
|
|
124
|
+
this.iconColor = this.foregroundColor;
|
|
125
|
+
// Force Material pour uniformiser le comportement des couleurs
|
|
126
|
+
this.platform = 'material';
|
|
127
|
+
|
|
128
|
+
// Icons, ripple, elevation, etc.
|
|
129
|
+
super.draw(ctx);
|
|
130
|
+
|
|
131
|
+
// Restore
|
|
132
|
+
this.bgColor = originalBg;
|
|
133
|
+
this.textColor = originalTextColor;
|
|
134
|
+
this.iconColor = originalIconColor;
|
|
135
|
+
this.platform = originalPlatform;
|
|
136
|
+
ctx.restore();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
export default SliverAppBar;
|