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,141 @@
|
|
|
1
|
+
import Component from '../core/Component.js';
|
|
2
|
+
/**
|
|
3
|
+
* Composant texte
|
|
4
|
+
* @class
|
|
5
|
+
* @extends Component
|
|
6
|
+
* @property {string} text - Texte à afficher
|
|
7
|
+
* @property {number} fontSize - Taille de police
|
|
8
|
+
* @property {string} color - Couleur
|
|
9
|
+
* @property {string} align - Alignement ('left', 'center', 'right')
|
|
10
|
+
* @property {boolean} bold - Gras
|
|
11
|
+
* @property {number|null} maxWidth - Largeur maximale
|
|
12
|
+
* @property {boolean} wrap - Retour à la ligne
|
|
13
|
+
* @property {number} lineHeight - Hauteur de ligne
|
|
14
|
+
* @property {string[]|null} wrappedLines - Lignes après wrap
|
|
15
|
+
*/
|
|
16
|
+
class Text extends Component {
|
|
17
|
+
/**
|
|
18
|
+
* Crée une instance de Text
|
|
19
|
+
* @param {CanvasFramework} framework - Framework parent
|
|
20
|
+
* @param {Object} [options={}] - Options de configuration
|
|
21
|
+
* @param {string} [options.text=''] - Texte
|
|
22
|
+
* @param {number} [options.fontSize=16] - Taille de police
|
|
23
|
+
* @param {string} [options.color='#000000'] - Couleur
|
|
24
|
+
* @param {string} [options.align='left'] - Alignement
|
|
25
|
+
* @param {boolean} [options.bold=false] - Gras
|
|
26
|
+
* @param {number} [options.maxWidth] - Largeur maximale
|
|
27
|
+
* @param {boolean} [options.wrap=false] - Retour à la ligne
|
|
28
|
+
* @param {number} [options.lineHeight] - Hauteur de ligne
|
|
29
|
+
*/
|
|
30
|
+
constructor(framework, options = {}) {
|
|
31
|
+
super(framework, options);
|
|
32
|
+
this.text = options.text || '';
|
|
33
|
+
this.fontSize = options.fontSize || 16;
|
|
34
|
+
this.color = options.color || '#000000';
|
|
35
|
+
this.align = options.align || 'left';
|
|
36
|
+
this.bold = options.bold || false;
|
|
37
|
+
this.maxWidth = options.maxWidth || null; // Nouvelle option: largeur maximale
|
|
38
|
+
this.wrap = options.wrap || false; // Nouvelle option: retour à la ligne
|
|
39
|
+
this.lineHeight = options.lineHeight || this.fontSize * 1.2;
|
|
40
|
+
|
|
41
|
+
// Calculer la hauteur en fonction du texte
|
|
42
|
+
if (this.wrap && this.maxWidth && this.text) {
|
|
43
|
+
this.calculateWrappedHeight();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Calcule la hauteur avec wrap
|
|
49
|
+
* @private
|
|
50
|
+
*/
|
|
51
|
+
calculateWrappedHeight() {
|
|
52
|
+
// Cette méthode sera appelée dans draw quand on a le contexte
|
|
53
|
+
// Pour l'instant, on initialise juste
|
|
54
|
+
this.wrappedLines = null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Dessine le texte
|
|
59
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
60
|
+
*/
|
|
61
|
+
draw(ctx) {
|
|
62
|
+
ctx.save();
|
|
63
|
+
ctx.fillStyle = this.color;
|
|
64
|
+
ctx.font = `${this.bold ? 'bold ' : ''}${this.fontSize}px -apple-system, BlinkMacSystemFont, 'Roboto', sans-serif`;
|
|
65
|
+
ctx.textAlign = this.align;
|
|
66
|
+
ctx.textBaseline = 'top';
|
|
67
|
+
|
|
68
|
+
let lines = [this.text];
|
|
69
|
+
|
|
70
|
+
// Si wrap est activé et on a une largeur max, on divise le texte
|
|
71
|
+
if (this.wrap && this.maxWidth && this.text) {
|
|
72
|
+
lines = this.wrapText(ctx, this.text, this.maxWidth);
|
|
73
|
+
} else if (this.maxWidth && this.text) {
|
|
74
|
+
// Sinon, on tronque le texte avec des points de suspension
|
|
75
|
+
const ellipsis = '...';
|
|
76
|
+
let text = this.text;
|
|
77
|
+
while (ctx.measureText(text).width > this.maxWidth && text.length > 3) {
|
|
78
|
+
text = text.substring(0, text.length - 1);
|
|
79
|
+
}
|
|
80
|
+
if (text !== this.text && text.length > 3) {
|
|
81
|
+
text = text.substring(0, text.length - 3) + ellipsis;
|
|
82
|
+
}
|
|
83
|
+
lines = [text];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Calculer la position x en fonction de l'alignement
|
|
87
|
+
const x = this.align === 'center' ? this.x + (this.maxWidth || this.width) / 2 :
|
|
88
|
+
this.align === 'right' ? this.x + (this.maxWidth || this.width) : this.x;
|
|
89
|
+
|
|
90
|
+
// Dessiner chaque ligne
|
|
91
|
+
for (let i = 0; i < lines.length; i++) {
|
|
92
|
+
const line = lines[i];
|
|
93
|
+
const y = this.y + (i * this.lineHeight);
|
|
94
|
+
ctx.fillText(line, x, y);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Ajuster la hauteur si on a plusieurs lignes
|
|
98
|
+
if (lines.length > 1) {
|
|
99
|
+
this.height = lines.length * this.lineHeight;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
ctx.restore();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Divise le texte en plusieurs lignes
|
|
107
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
108
|
+
* @param {string} text - Texte à diviser
|
|
109
|
+
* @param {number} maxWidth - Largeur maximale
|
|
110
|
+
* @returns {string[]} Tableau de lignes
|
|
111
|
+
* @private
|
|
112
|
+
*/
|
|
113
|
+
wrapText(ctx, text, maxWidth) {
|
|
114
|
+
const words = text.split(' ');
|
|
115
|
+
const lines = [];
|
|
116
|
+
let currentLine = words[0];
|
|
117
|
+
|
|
118
|
+
for (let i = 1; i < words.length; i++) {
|
|
119
|
+
const word = words[i];
|
|
120
|
+
const width = ctx.measureText(currentLine + " " + word).width;
|
|
121
|
+
if (width < maxWidth) {
|
|
122
|
+
currentLine += " " + word;
|
|
123
|
+
} else {
|
|
124
|
+
lines.push(currentLine);
|
|
125
|
+
currentLine = word;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
lines.push(currentLine);
|
|
129
|
+
return lines;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Vérifie si un point est dans les limites
|
|
134
|
+
* @returns {boolean} False (non cliquable)
|
|
135
|
+
*/
|
|
136
|
+
isPointInside() {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export default Text;
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import Component from '../core/Component.js';
|
|
2
|
+
/**
|
|
3
|
+
* Champ de texte avancé avec label flottant, validation et messages d'erreur
|
|
4
|
+
* @class
|
|
5
|
+
* @extends Component
|
|
6
|
+
* @param {Framework} framework - Instance du framework
|
|
7
|
+
* @param {Object} [options={}] - Options de configuration
|
|
8
|
+
* @param {string} [options.label=''] - Label du champ
|
|
9
|
+
* @param {string} [options.value=''] - Valeur initiale
|
|
10
|
+
* @param {string} [options.placeholder=''] - Placeholder
|
|
11
|
+
* @param {string} [options.helperText=''] - Texte d'aide
|
|
12
|
+
* @param {string} [options.errorText=''] - Texte d'erreur
|
|
13
|
+
* @param {boolean} [options.error=false] - État d'erreur
|
|
14
|
+
* @param {number} [options.fontSize=16] - Taille de police
|
|
15
|
+
* @param {Function} [options.onChange] - Callback lors du changement
|
|
16
|
+
* @param {number} [options.height=80] - Hauteur totale (inclut label + input + helper)
|
|
17
|
+
* @example
|
|
18
|
+
* const textField = new TextField(framework, {
|
|
19
|
+
* label: 'Email',
|
|
20
|
+
* placeholder: 'Entrez votre email',
|
|
21
|
+
* helperText: 'Nous ne partagerons jamais votre email',
|
|
22
|
+
* onChange: (value) => validateEmail(value)
|
|
23
|
+
* });
|
|
24
|
+
*/
|
|
25
|
+
class TextField extends Component {
|
|
26
|
+
/**
|
|
27
|
+
* @constructs TextField
|
|
28
|
+
*/
|
|
29
|
+
constructor(framework, options = {}) {
|
|
30
|
+
super(framework, options);
|
|
31
|
+
/** @type {string} */
|
|
32
|
+
this.label = options.label || '';
|
|
33
|
+
/** @type {string} */
|
|
34
|
+
this.value = options.value || '';
|
|
35
|
+
/** @type {string} */
|
|
36
|
+
this.placeholder = options.placeholder || '';
|
|
37
|
+
/** @type {string} */
|
|
38
|
+
this.helperText = options.helperText || '';
|
|
39
|
+
/** @type {string} */
|
|
40
|
+
this.errorText = options.errorText || '';
|
|
41
|
+
/** @type {boolean} */
|
|
42
|
+
this.error = options.error || false;
|
|
43
|
+
/** @type {string} */
|
|
44
|
+
this.platform = framework.platform;
|
|
45
|
+
/** @type {boolean} */
|
|
46
|
+
this.focused = false;
|
|
47
|
+
/** @type {number} */
|
|
48
|
+
this.fontSize = options.fontSize || 16;
|
|
49
|
+
/** @type {Function|undefined} */
|
|
50
|
+
this.onChange = options.onChange;
|
|
51
|
+
/** @type {number} */
|
|
52
|
+
this.labelY = this.value ? -10 : 20; // Position du label
|
|
53
|
+
/** @type {number} */
|
|
54
|
+
this.labelFontSize = this.value ? 12 : 16;
|
|
55
|
+
/** @type {boolean} */
|
|
56
|
+
this.cursorVisible = true;
|
|
57
|
+
|
|
58
|
+
// Hauteur pour inclure label + input + helper
|
|
59
|
+
this.height = options.height || 80;
|
|
60
|
+
|
|
61
|
+
this.onFocus = this.handleFocus.bind(this);
|
|
62
|
+
this.onBlur = this.handleBlur.bind(this);
|
|
63
|
+
|
|
64
|
+
this.setupHiddenInput();
|
|
65
|
+
|
|
66
|
+
// Animation du curseur
|
|
67
|
+
/** @type {number} */
|
|
68
|
+
this.cursorInterval = setInterval(() => {
|
|
69
|
+
if (this.focused) this.cursorVisible = !this.cursorVisible;
|
|
70
|
+
}, 500);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Configure l'input caché dans le DOM
|
|
75
|
+
* @private
|
|
76
|
+
*/
|
|
77
|
+
setupHiddenInput() {
|
|
78
|
+
let hiddenInput = document.getElementById('hidden-textfield-input');
|
|
79
|
+
if (!hiddenInput) {
|
|
80
|
+
hiddenInput = document.createElement('input');
|
|
81
|
+
hiddenInput.id = 'hidden-textfield-input';
|
|
82
|
+
hiddenInput.type = 'text';
|
|
83
|
+
hiddenInput.style.position = 'fixed';
|
|
84
|
+
hiddenInput.style.opacity = '0';
|
|
85
|
+
hiddenInput.style.pointerEvents = 'none';
|
|
86
|
+
hiddenInput.style.top = '-100px';
|
|
87
|
+
document.body.appendChild(hiddenInput);
|
|
88
|
+
|
|
89
|
+
hiddenInput.addEventListener('input', (e) => {
|
|
90
|
+
if (this.focused) {
|
|
91
|
+
this.value = e.target.value;
|
|
92
|
+
if (this.onChange) this.onChange(this.value);
|
|
93
|
+
this.animateLabel();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
hiddenInput.addEventListener('blur', () => {
|
|
98
|
+
this.handleBlur();
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/** @type {HTMLInputElement} */
|
|
102
|
+
this.hiddenInput = hiddenInput;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Anime le label (flottant)
|
|
107
|
+
* @private
|
|
108
|
+
*/
|
|
109
|
+
animateLabel() {
|
|
110
|
+
const shouldFloat = this.focused || this.value;
|
|
111
|
+
const targetY = shouldFloat ? -10 : 20;
|
|
112
|
+
const targetSize = shouldFloat ? 12 : 16;
|
|
113
|
+
|
|
114
|
+
const animate = () => {
|
|
115
|
+
const diffY = targetY - this.labelY;
|
|
116
|
+
const diffSize = targetSize - this.labelFontSize;
|
|
117
|
+
|
|
118
|
+
if (Math.abs(diffY) < 0.5 && Math.abs(diffSize) < 0.5) {
|
|
119
|
+
this.labelY = targetY;
|
|
120
|
+
this.labelFontSize = targetSize;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
this.labelY += diffY * 0.2;
|
|
125
|
+
this.labelFontSize += diffSize * 0.2;
|
|
126
|
+
|
|
127
|
+
requestAnimationFrame(animate);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
animate();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Gère le focus sur le champ
|
|
135
|
+
*/
|
|
136
|
+
handleFocus() {
|
|
137
|
+
this.focused = true;
|
|
138
|
+
this.cursorVisible = true;
|
|
139
|
+
if (this.hiddenInput) {
|
|
140
|
+
this.hiddenInput.value = this.value;
|
|
141
|
+
const adjustedY = this.y + this.framework.scrollOffset;
|
|
142
|
+
this.hiddenInput.style.top = `${adjustedY}px`;
|
|
143
|
+
this.hiddenInput.focus();
|
|
144
|
+
}
|
|
145
|
+
this.animateLabel();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Gère la perte de focus
|
|
150
|
+
*/
|
|
151
|
+
handleBlur() {
|
|
152
|
+
this.focused = false;
|
|
153
|
+
this.cursorVisible = false;
|
|
154
|
+
this.animateLabel();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Gère le clic sur le champ
|
|
159
|
+
*/
|
|
160
|
+
onClick() {
|
|
161
|
+
this.handleFocus();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Dessine le champ de texte
|
|
166
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
167
|
+
*/
|
|
168
|
+
draw(ctx) {
|
|
169
|
+
ctx.save();
|
|
170
|
+
|
|
171
|
+
const inputY = this.y + 30;
|
|
172
|
+
const inputHeight = 40;
|
|
173
|
+
|
|
174
|
+
if (this.platform === 'material') {
|
|
175
|
+
// Material Design TextField
|
|
176
|
+
|
|
177
|
+
// Label flottant
|
|
178
|
+
const labelColor = this.error ? '#B00020' :
|
|
179
|
+
(this.focused ? '#6200EE' : '#757575');
|
|
180
|
+
ctx.fillStyle = labelColor;
|
|
181
|
+
ctx.font = `${this.labelFontSize}px Roboto, sans-serif`;
|
|
182
|
+
ctx.textAlign = 'left';
|
|
183
|
+
ctx.textBaseline = 'middle';
|
|
184
|
+
ctx.fillText(this.label, this.x, this.y + 20 + this.labelY);
|
|
185
|
+
|
|
186
|
+
// Ligne de soulignement
|
|
187
|
+
const lineColor = this.error ? '#B00020' :
|
|
188
|
+
(this.focused ? '#6200EE' : '#CCCCCC');
|
|
189
|
+
ctx.strokeStyle = lineColor;
|
|
190
|
+
ctx.lineWidth = this.focused ? 2 : 1;
|
|
191
|
+
ctx.beginPath();
|
|
192
|
+
ctx.moveTo(this.x, inputY + inputHeight);
|
|
193
|
+
ctx.lineTo(this.x + this.width, inputY + inputHeight);
|
|
194
|
+
ctx.stroke();
|
|
195
|
+
|
|
196
|
+
// Valeur ou placeholder
|
|
197
|
+
const displayText = this.value || (this.focused ? '' : this.placeholder);
|
|
198
|
+
ctx.fillStyle = this.value ? '#000000' : '#999999';
|
|
199
|
+
ctx.font = `${this.fontSize}px Roboto, sans-serif`;
|
|
200
|
+
ctx.textBaseline = 'middle';
|
|
201
|
+
ctx.fillText(displayText, this.x, inputY + inputHeight / 2);
|
|
202
|
+
|
|
203
|
+
// Curseur
|
|
204
|
+
if (this.focused && this.cursorVisible) {
|
|
205
|
+
const textWidth = ctx.measureText(this.value).width;
|
|
206
|
+
ctx.fillStyle = '#6200EE';
|
|
207
|
+
ctx.fillRect(this.x + textWidth + 2, inputY + 10, 2, inputHeight - 20);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Helper text ou error text
|
|
211
|
+
const helperColor = this.error ? '#B00020' : '#757575';
|
|
212
|
+
const helperMessage = this.error ? this.errorText : this.helperText;
|
|
213
|
+
|
|
214
|
+
if (helperMessage) {
|
|
215
|
+
ctx.fillStyle = helperColor;
|
|
216
|
+
ctx.font = '12px Roboto, sans-serif';
|
|
217
|
+
ctx.fillText(helperMessage, this.x, inputY + inputHeight + 20);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
} else {
|
|
221
|
+
// Cupertino style (label au-dessus)
|
|
222
|
+
|
|
223
|
+
if (this.label) {
|
|
224
|
+
ctx.fillStyle = '#000000';
|
|
225
|
+
ctx.font = 'bold 14px -apple-system, sans-serif';
|
|
226
|
+
ctx.textAlign = 'left';
|
|
227
|
+
ctx.textBaseline = 'top';
|
|
228
|
+
ctx.fillText(this.label, this.x, this.y);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Input box
|
|
232
|
+
ctx.strokeStyle = this.error ? '#FF3B30' :
|
|
233
|
+
(this.focused ? '#007AFF' : '#C7C7CC');
|
|
234
|
+
ctx.lineWidth = 1;
|
|
235
|
+
ctx.beginPath();
|
|
236
|
+
this.roundRect(ctx, this.x, inputY, this.width, inputHeight, 8);
|
|
237
|
+
ctx.stroke();
|
|
238
|
+
|
|
239
|
+
// Valeur ou placeholder
|
|
240
|
+
const displayText = this.value || this.placeholder;
|
|
241
|
+
ctx.fillStyle = this.value ? '#000000' : '#999999';
|
|
242
|
+
ctx.font = `${this.fontSize}px -apple-system, sans-serif`;
|
|
243
|
+
ctx.textAlign = 'left';
|
|
244
|
+
ctx.textBaseline = 'middle';
|
|
245
|
+
ctx.fillText(displayText, this.x + 10, inputY + inputHeight / 2);
|
|
246
|
+
|
|
247
|
+
// Curseur
|
|
248
|
+
if (this.focused && this.cursorVisible) {
|
|
249
|
+
const textWidth = ctx.measureText(this.value).width;
|
|
250
|
+
ctx.fillStyle = '#007AFF';
|
|
251
|
+
ctx.fillRect(this.x + 10 + textWidth + 2, inputY + 10, 2, inputHeight - 20);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Helper/Error text
|
|
255
|
+
if (this.error && this.errorText) {
|
|
256
|
+
ctx.fillStyle = '#FF3B30';
|
|
257
|
+
ctx.font = '12px -apple-system, sans-serif';
|
|
258
|
+
ctx.fillText(this.errorText, this.x, inputY + inputHeight + 8);
|
|
259
|
+
} else if (this.helperText) {
|
|
260
|
+
ctx.fillStyle = '#8E8E93';
|
|
261
|
+
ctx.font = '12px -apple-system, sans-serif';
|
|
262
|
+
ctx.fillText(this.helperText, this.x, inputY + inputHeight + 8);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
ctx.restore();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Dessine un rectangle avec des coins arrondis
|
|
271
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
272
|
+
* @param {number} x - Position X
|
|
273
|
+
* @param {number} y - Position Y
|
|
274
|
+
* @param {number} width - Largeur
|
|
275
|
+
* @param {number} height - Hauteur
|
|
276
|
+
* @param {number} radius - Rayon des coins
|
|
277
|
+
* @private
|
|
278
|
+
*/
|
|
279
|
+
roundRect(ctx, x, y, width, height, radius) {
|
|
280
|
+
ctx.beginPath();
|
|
281
|
+
ctx.moveTo(x + radius, y);
|
|
282
|
+
ctx.lineTo(x + width - radius, y);
|
|
283
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
284
|
+
ctx.lineTo(x + width, y + height - radius);
|
|
285
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
286
|
+
ctx.lineTo(x + radius, y + height);
|
|
287
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
288
|
+
ctx.lineTo(x, y + radius);
|
|
289
|
+
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
290
|
+
ctx.closePath();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Vérifie si un point est à l'intérieur du composant
|
|
295
|
+
* @param {number} x - Position X
|
|
296
|
+
* @param {number} y - Position Y
|
|
297
|
+
* @returns {boolean} True si le point est à l'intérieur
|
|
298
|
+
*/
|
|
299
|
+
isPointInside(x, y) {
|
|
300
|
+
return x >= this.x && x <= this.x + this.width &&
|
|
301
|
+
y >= this.y && y <= this.y + this.height;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Définit une erreur sur le champ
|
|
306
|
+
* @param {string} errorText - Texte d'erreur à afficher
|
|
307
|
+
*/
|
|
308
|
+
setError(errorText) {
|
|
309
|
+
this.error = true;
|
|
310
|
+
this.errorText = errorText;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Efface l'erreur du champ
|
|
315
|
+
*/
|
|
316
|
+
clearError() {
|
|
317
|
+
this.error = false;
|
|
318
|
+
this.errorText = '';
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Nettoie les ressources (arrête l'animation du curseur)
|
|
323
|
+
*/
|
|
324
|
+
destroy() {
|
|
325
|
+
if (this.cursorInterval) {
|
|
326
|
+
clearInterval(this.cursorInterval);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export default TextField;
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import Component from '../core/Component.js';
|
|
2
|
+
/**
|
|
3
|
+
* Toast (notification temporaire)
|
|
4
|
+
* @class
|
|
5
|
+
* @extends Component
|
|
6
|
+
* @property {string} text - Message
|
|
7
|
+
* @property {number} duration - Durée d'affichage
|
|
8
|
+
* @property {number} fontSize - Taille de police
|
|
9
|
+
* @property {number} padding - Padding interne
|
|
10
|
+
* @property {number} opacity - Opacité
|
|
11
|
+
* @property {string} platform - Plateforme
|
|
12
|
+
* @property {boolean} isVisible - Visibilité
|
|
13
|
+
* @property {number} targetY - Position Y cible
|
|
14
|
+
* @property {number} minWidth - Largeur minimale
|
|
15
|
+
* @property {number} maxWidth - Largeur maximale
|
|
16
|
+
* @property {boolean} animating - En cours d'animation
|
|
17
|
+
*/
|
|
18
|
+
class Toast extends Component {
|
|
19
|
+
/**
|
|
20
|
+
* Crée une instance de Toast
|
|
21
|
+
* @param {CanvasFramework} framework - Framework parent
|
|
22
|
+
* @param {Object} [options={}] - Options de configuration
|
|
23
|
+
* @param {string} [options.text=''] - Message
|
|
24
|
+
* @param {number} [options.duration=3000] - Durée en ms
|
|
25
|
+
* @param {number} [options.x] - Position X (auto-centré)
|
|
26
|
+
* @param {number} [options.y] - Position Y (en bas)
|
|
27
|
+
*/
|
|
28
|
+
constructor(framework, options = {}) {
|
|
29
|
+
super(framework, {
|
|
30
|
+
x: 0,
|
|
31
|
+
y: framework.height, // Commence hors écran en bas
|
|
32
|
+
width: framework.width,
|
|
33
|
+
height: 60, // Hauteur fixe pour le toast
|
|
34
|
+
...options
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
this.text = options.text || '';
|
|
38
|
+
this.duration = options.duration || 3000;
|
|
39
|
+
this.fontSize = 16;
|
|
40
|
+
this.padding = 20;
|
|
41
|
+
this.opacity = 0;
|
|
42
|
+
this.platform = framework.platform;
|
|
43
|
+
this.isVisible = false;
|
|
44
|
+
|
|
45
|
+
// Position cible (en bas, légèrement remonté)
|
|
46
|
+
this.targetY = framework.height - 100;
|
|
47
|
+
|
|
48
|
+
// Calculer la largeur minimale
|
|
49
|
+
this.minWidth = 200;
|
|
50
|
+
this.maxWidth = Math.min(600, framework.width - 40);
|
|
51
|
+
|
|
52
|
+
// Animation
|
|
53
|
+
this.animating = false;
|
|
54
|
+
|
|
55
|
+
// NE PAS appeler show() ici - laissé à l'appelant
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Affiche le toast
|
|
60
|
+
*/
|
|
61
|
+
show() {
|
|
62
|
+
this.isVisible = true;
|
|
63
|
+
this.visible = true;
|
|
64
|
+
this.animateIn();
|
|
65
|
+
|
|
66
|
+
// Auto-dismiss après la durée
|
|
67
|
+
setTimeout(() => {
|
|
68
|
+
if (this.isVisible) {
|
|
69
|
+
this.hide();
|
|
70
|
+
}
|
|
71
|
+
}, this.duration);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Cache le toast
|
|
76
|
+
*/
|
|
77
|
+
hide() {
|
|
78
|
+
this.animateOut();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Anime l'entrée
|
|
83
|
+
* @private
|
|
84
|
+
*/
|
|
85
|
+
animateIn() {
|
|
86
|
+
if (this.animating) return;
|
|
87
|
+
this.animating = true;
|
|
88
|
+
|
|
89
|
+
const animate = () => {
|
|
90
|
+
this.opacity += 0.1;
|
|
91
|
+
this.y -= (this.y - this.targetY) * 0.2;
|
|
92
|
+
|
|
93
|
+
if (this.opacity >= 1 && Math.abs(this.y - this.targetY) < 1) {
|
|
94
|
+
this.opacity = 1;
|
|
95
|
+
this.y = this.targetY;
|
|
96
|
+
this.animating = false;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
requestAnimationFrame(animate);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
animate();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Anime la sortie
|
|
108
|
+
* @private
|
|
109
|
+
*/
|
|
110
|
+
animateOut() {
|
|
111
|
+
if (this.animating) return;
|
|
112
|
+
this.animating = true;
|
|
113
|
+
|
|
114
|
+
const animate = () => {
|
|
115
|
+
this.opacity -= 0.1;
|
|
116
|
+
this.y += 5;
|
|
117
|
+
|
|
118
|
+
if (this.opacity <= 0) {
|
|
119
|
+
this.opacity = 0;
|
|
120
|
+
this.isVisible = false;
|
|
121
|
+
this.visible = false;
|
|
122
|
+
this.animating = false;
|
|
123
|
+
this.framework.remove(this);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
requestAnimationFrame(animate);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
animate();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Dessine le toast
|
|
135
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
136
|
+
*/
|
|
137
|
+
draw(ctx) {
|
|
138
|
+
if (!this.isVisible || this.opacity <= 0) return;
|
|
139
|
+
|
|
140
|
+
ctx.save();
|
|
141
|
+
ctx.globalAlpha = this.opacity;
|
|
142
|
+
|
|
143
|
+
// Calculer la largeur en fonction du texte
|
|
144
|
+
ctx.font = `${this.fontSize}px -apple-system, BlinkMacSystemFont, 'Roboto', sans-serif`;
|
|
145
|
+
const textWidth = ctx.measureText(this.text).width;
|
|
146
|
+
const toastWidth = Math.min(
|
|
147
|
+
this.maxWidth,
|
|
148
|
+
Math.max(this.minWidth, textWidth + this.padding * 2)
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
// Position centrée horizontalement
|
|
152
|
+
const toastX = (this.framework.width - toastWidth) / 2;
|
|
153
|
+
const toastY = this.y;
|
|
154
|
+
|
|
155
|
+
if (this.platform === 'material') {
|
|
156
|
+
// Material Toast
|
|
157
|
+
ctx.fillStyle = '#323232';
|
|
158
|
+
ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
|
|
159
|
+
ctx.shadowBlur = 15;
|
|
160
|
+
ctx.shadowOffsetY = 4;
|
|
161
|
+
ctx.beginPath();
|
|
162
|
+
this.roundRect(ctx, toastX, toastY, toastWidth, this.height, 8);
|
|
163
|
+
ctx.fill();
|
|
164
|
+
} else {
|
|
165
|
+
// Cupertino Toast
|
|
166
|
+
ctx.fillStyle = 'rgba(0, 0, 0, 0.85)';
|
|
167
|
+
ctx.shadowColor = 'rgba(0, 0, 0, 0.2)';
|
|
168
|
+
ctx.shadowBlur = 20;
|
|
169
|
+
ctx.shadowOffsetY = 4;
|
|
170
|
+
ctx.beginPath();
|
|
171
|
+
this.roundRect(ctx, toastX, toastY, toastWidth, this.height, 14);
|
|
172
|
+
ctx.fill();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
ctx.shadowColor = 'transparent';
|
|
176
|
+
ctx.shadowBlur = 0;
|
|
177
|
+
|
|
178
|
+
// Texte
|
|
179
|
+
ctx.fillStyle = '#FFFFFF';
|
|
180
|
+
ctx.font = `${this.fontSize}px -apple-system, BlinkMacSystemFont, 'Roboto', sans-serif`;
|
|
181
|
+
ctx.textAlign = 'center';
|
|
182
|
+
ctx.textBaseline = 'middle';
|
|
183
|
+
|
|
184
|
+
// Tronquer le texte si nécessaire
|
|
185
|
+
let displayText = this.text;
|
|
186
|
+
if (textWidth > toastWidth - this.padding * 2) {
|
|
187
|
+
// Trouver où couper le texte
|
|
188
|
+
let truncated = this.text;
|
|
189
|
+
for (let i = this.text.length; i > 0; i--) {
|
|
190
|
+
truncated = this.text.substring(0, i) + '...';
|
|
191
|
+
if (ctx.measureText(truncated).width <= toastWidth - this.padding * 2) {
|
|
192
|
+
displayText = truncated;
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
ctx.fillText(displayText, toastX + toastWidth / 2, toastY + this.height / 2);
|
|
199
|
+
|
|
200
|
+
ctx.restore();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Dessine un rectangle avec coins arrondis
|
|
205
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
206
|
+
* @param {number} x - Position X
|
|
207
|
+
* @param {number} y - Position Y
|
|
208
|
+
* @param {number} width - Largeur
|
|
209
|
+
* @param {number} height - Hauteur
|
|
210
|
+
* @param {number} radius - Rayon des coins
|
|
211
|
+
* @private
|
|
212
|
+
*/
|
|
213
|
+
roundRect(ctx, x, y, width, height, radius) {
|
|
214
|
+
ctx.beginPath();
|
|
215
|
+
ctx.moveTo(x + radius, y);
|
|
216
|
+
ctx.lineTo(x + width - radius, y);
|
|
217
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
218
|
+
ctx.lineTo(x + width, y + height - radius);
|
|
219
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
220
|
+
ctx.lineTo(x + radius, y + height);
|
|
221
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
222
|
+
ctx.lineTo(x, y + radius);
|
|
223
|
+
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
224
|
+
ctx.closePath();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Vérifie si un point est dans les limites
|
|
229
|
+
* @returns {boolean} False (non cliquable)
|
|
230
|
+
*/
|
|
231
|
+
isPointInside() {
|
|
232
|
+
return false; // Non cliquable
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export default Toast;
|