@ticatec/uniface-element 0.2.3 → 0.3.0
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/dist/accordion/README.md +169 -0
- package/dist/accordion/README_CN.md +176 -0
- package/dist/action-bar/README.md +275 -0
- package/dist/action-bar/README_CN.md +285 -0
- package/dist/app-layout/README.md +374 -0
- package/dist/app-layout/README_CN.md +381 -0
- package/dist/app-top-bar/README.md +357 -0
- package/dist/app-top-bar/README_CN.md +365 -0
- package/dist/attachment-files/AttachmentFilesField.svelte +67 -1
- package/dist/attachment-files/AttachmentFilesField.svelte.d.ts +8 -1
- package/dist/base-calendar/CalendarBase.svelte +0 -3
- package/dist/box/README.md +385 -0
- package/dist/box/README_CN.md +393 -0
- package/dist/breadcrumbs/README.md +442 -0
- package/dist/breadcrumbs/README_CN.md +442 -0
- package/dist/button/Button.svelte +27 -7
- package/dist/button/Button.svelte.d.ts +3 -1
- package/dist/button/IconButton.svelte +26 -6
- package/dist/button/IconButton.svelte.d.ts +3 -1
- package/dist/button/IconButton_README.md +513 -0
- package/dist/button/IconButton_README_CN.md +513 -0
- package/dist/button/README.md +428 -0
- package/dist/button/README_CN.md +428 -0
- package/dist/button/TextButton.svelte +26 -6
- package/dist/button/TextButton.svelte.d.ts +3 -1
- package/dist/button/TextButton_README.md +571 -0
- package/dist/button/TextButton_README_CN.md +571 -0
- package/dist/card/README.md +666 -0
- package/dist/card/README_CN.md +666 -0
- package/dist/cascade-options-select/CascadeOptionSelect.svelte +43 -7
- package/dist/cascade-options-select/CascadeOptionSelect.svelte.d.ts +3 -4
- package/dist/cascade-options-select/README.md +663 -2
- package/dist/cascade-options-select/README_CN.md +664 -0
- package/dist/checkbox/CheckBox.svelte +20 -6
- package/dist/checkbox/CheckBox.svelte.d.ts +4 -2
- package/dist/checkbox/README.md +562 -0
- package/dist/checkbox/README_CN.md +727 -0
- package/dist/common-editor/NumberInput.svelte +39 -26
- package/dist/common-editor/NumberInput.svelte.d.ts +5 -1
- package/dist/concise-data-table/ConciseListTable.svelte +133 -36
- package/dist/concise-data-table/ConciseListTable.svelte.d.ts +8 -1
- package/dist/concise-data-table/DataCell.svelte +5 -6
- package/dist/concise-data-table/DataRow.svelte +22 -2
- package/dist/concise-data-table/DataRow.svelte.d.ts +1 -0
- package/dist/concise-data-table/README.md +606 -0
- package/dist/concise-data-table/README_CN.md +611 -0
- package/dist/context-menu/ContextMenu.svelte +138 -21
- package/dist/context-menu/ContextMenu.svelte.d.ts +5 -0
- package/dist/context-menu/ContextMenuItem.d.ts +20 -4
- package/dist/context-menu/README.md +551 -0
- package/dist/context-menu/README_CN.md +551 -0
- package/dist/criteria-field/CriteriaField.svelte +3 -5
- package/dist/criteria-field/README.md +532 -0
- package/dist/criteria-field/README_CN.md +532 -0
- package/dist/data-table/README.md +712 -0
- package/dist/data-table/README_CN.md +712 -0
- package/dist/date-picker/DatePicker.svelte +44 -7
- package/dist/date-picker/DatePicker.svelte.d.ts +3 -4
- package/dist/date-picker/DateTimePicker.svelte +32 -8
- package/dist/date-picker/DateTimePicker.svelte.d.ts +3 -4
- package/dist/date-picker/README.md +326 -0
- package/dist/date-picker/README_CN.md +316 -0
- package/dist/date-picker/ScrollBar.svelte +18 -8
- package/dist/date-picker/ScrollBar.svelte.d.ts +1 -1
- package/dist/date-picker/TimePanel.svelte +5 -5
- package/dist/date-picker/TimePanel.svelte.d.ts +1 -1
- package/dist/date-range/README.md +374 -0
- package/dist/date-range/README_CN.md +374 -0
- package/dist/dialog/DialogBoard.svelte +1 -1
- package/dist/dialog/README.md +596 -0
- package/dist/dialog/README_CN.md +596 -0
- package/dist/drawer/README.md +679 -1
- package/dist/drawer/README_CN.md +679 -0
- package/dist/group-box/group-check-box/GroupCheckBox.svelte +62 -5
- package/dist/group-box/group-check-box/GroupCheckBox.svelte.d.ts +7 -2
- package/dist/group-box/group-check-box/README.md +749 -0
- package/dist/group-box/group-check-box/README_CN.md +750 -0
- package/dist/group-box/group-radio-box/GroupRadioBox.svelte +69 -4
- package/dist/group-box/group-radio-box/GroupRadioBox.svelte.d.ts +8 -2
- package/dist/group-box/group-radio-box/README.md +920 -0
- package/dist/group-box/group-radio-box/README_CN.md +920 -0
- package/dist/image-files/README.md +1346 -0
- package/dist/image-files/README_CN.md +1346 -0
- package/dist/input-options-select/InputOptionsSelect.svelte +161 -65
- package/dist/input-options-select/InputOptionsSelect.svelte.d.ts +4 -1
- package/dist/lib/forwardAllEvents.d.ts +3 -0
- package/dist/lib/forwardAllEvents.js +24 -0
- package/dist/list-box/README.md +579 -0
- package/dist/list-box/README_CN.md +682 -0
- package/dist/lookup-editor/LookupEditor.svelte +15 -3
- package/dist/lookup-editor/LookupEditor.svelte.d.ts +3 -4
- package/dist/lookup-editor/README.md +684 -0
- package/dist/lookup-editor/README_CN.md +684 -0
- package/dist/memo-editor/MemoEditor.svelte +77 -27
- package/dist/memo-editor/MemoEditor.svelte.d.ts +6 -3
- package/dist/memo-editor/README.md +819 -0
- package/dist/memo-editor/README_CN.md +819 -0
- package/dist/message-box/README.md +398 -0
- package/dist/message-box/README_CN.md +398 -0
- package/dist/number-editor/NumberEditor.svelte +2 -2
- package/dist/number-editor/NumberEditor.svelte.d.ts +1 -1
- package/dist/number-editor/README.md +353 -22
- package/dist/number-editor/README_CN.md +357 -0
- package/dist/number-range/README.md +443 -0
- package/dist/number-range/README_CN.md +443 -0
- package/dist/options-multi-select/OptionsMultiSelect.svelte +87 -12
- package/dist/options-multi-select/OptionsMultiSelect.svelte.d.ts +3 -1
- package/dist/options-multi-select/README.md +480 -0
- package/dist/options-multi-select/README_CN.md +480 -0
- package/dist/options-select/OptionsSelect.svelte +36 -5
- package/dist/options-select/OptionsSelect.svelte.d.ts +3 -4
- package/dist/options-select/README.md +632 -0
- package/dist/options-select/README_CN.md +632 -0
- package/dist/page/README.md +605 -0
- package/dist/page/README_CN.md +605 -0
- package/dist/pagination/README.md +946 -0
- package/dist/pagination/README_CN.md +946 -0
- package/dist/pagination-panel/README.md +1075 -0
- package/dist/pagination-panel/README_CN.md +1075 -0
- package/dist/popup-hint/README.md +841 -0
- package/dist/popup-hint/README_CN.md +840 -0
- package/dist/progress-bar/README.md +242 -0
- package/dist/progress-bar/README_CN.md +242 -0
- package/dist/progress-step-bar/README.md +316 -0
- package/dist/progress-step-bar/README_CN.md +316 -0
- package/dist/prompts-text-editor/PromptsTextEditor.svelte +6 -3
- package/dist/prompts-text-editor/PromptsTextEditor.svelte.d.ts +3 -1
- package/dist/prompts-text-editor/README.md +329 -0
- package/dist/prompts-text-editor/README_CN.md +329 -0
- package/dist/property-editor/README.md +969 -0
- package/dist/property-editor/README_CN.md +969 -0
- package/dist/radio-button/README.md +372 -0
- package/dist/radio-button/README_CN.md +372 -0
- package/dist/radio-button/RadioButton.svelte +32 -3
- package/dist/radio-button/RadioButton.svelte.d.ts +12 -1
- package/dist/search-box/README.md +425 -0
- package/dist/search-box/README_CN.md +424 -0
- package/dist/search-box/SearchBox.svelte +22 -6
- package/dist/search-box/SearchBox.svelte.d.ts +4 -1
- package/dist/tabs/README.md +919 -0
- package/dist/tabs/README_CN.md +920 -0
- package/dist/tag/README.md +922 -0
- package/dist/tag/README_CN.md +922 -0
- package/dist/text-editor/PasswordEditor.svelte +17 -2
- package/dist/text-editor/PasswordEditor.svelte.d.ts +3 -3
- package/dist/text-editor/PasswordEditor_README.md +551 -0
- package/dist/text-editor/PasswordEditor_README_CN.md +551 -0
- package/dist/text-editor/README.md +466 -0
- package/dist/text-editor/README_CN.md +466 -0
- package/dist/text-editor/TextEditor.svelte +51 -19
- package/dist/text-editor/TextEditor.svelte.d.ts +0 -1
- package/dist/ticatec-uniface-web.css +1 -0
- package/dist/time-editor/README.md +498 -24
- package/dist/time-editor/README_CN.md +498 -0
- package/dist/time-editor/TimeEditor.svelte +7 -3
- package/dist/time-editor/TimeEditor.svelte.d.ts +3 -1
- package/dist/toast/README.md +667 -0
- package/dist/toast/README_CN.md +667 -0
- package/dist/transfer/README.md +795 -0
- package/dist/transfer/Transfer.svelte +6 -4
- package/dist/transfer/Transfer.svelte.d.ts +3 -1
- package/dist/tree-view/README.md +761 -0
- package/dist/tree-view/README_CN.md +761 -0
- package/dist/tree-view/TreeView.svelte +5 -3
- package/dist/tree-view/TreeView.svelte.d.ts +3 -1
- package/dist/unit-number-editor/README.md +546 -20
- package/dist/unit-number-editor/README_CN.md +525 -0
- package/dist/unit-number-editor/UnitNumberEditor.svelte +62 -8
- package/dist/unit-number-editor/UnitNumberEditor.svelte.d.ts +5 -1
- package/docs/form-field/cn/README_CN.md +364 -0
- package/docs/form-field/en/README.md +364 -0
- package/docs/form-panel/flex-form/cn/README_CN.md +586 -0
- package/docs/form-panel/flex-form/en/README.md +586 -0
- package/docs/form-panel/flex-row-form/cn/README_CN.md +787 -0
- package/docs/form-panel/flex-row-form/en/README.md +787 -0
- package/docs/form-panel/grid-form/en/README.md +744 -0
- package/package.json +1 -1
- package/dist/drawer/README-CN.md +0 -1
|
@@ -0,0 +1,1346 @@
|
|
|
1
|
+
# ImageFilesField
|
|
2
|
+
|
|
3
|
+
A comprehensive image upload and management component that provides drag-and-drop upload functionality, image preview, thumbnail generation, and file management capabilities. Perfect for handling image galleries, avatar uploads, and document attachments with visual previews.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @ticatec/uniface-element
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Import
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import ImageFilesField from "@ticatec/uniface-element/ImageFilesField";
|
|
15
|
+
import type { ImageFile, RemoveConfirm } from "@ticatec/uniface-element/ImageFilesField";
|
|
16
|
+
import type { FileUpload } from "@ticatec/uniface-element";
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Basic Usage
|
|
20
|
+
|
|
21
|
+
```svelte
|
|
22
|
+
<script>
|
|
23
|
+
import ImageFilesField from "@ticatec/uniface-element/ImageFilesField";
|
|
24
|
+
|
|
25
|
+
let files = [];
|
|
26
|
+
|
|
27
|
+
// Simple upload function
|
|
28
|
+
const uploadImage = (file, progressUpdate, onUploaded, errorHandler) => {
|
|
29
|
+
// Simulate upload progress
|
|
30
|
+
let progress = 0;
|
|
31
|
+
const interval = setInterval(() => {
|
|
32
|
+
progress += 10;
|
|
33
|
+
progressUpdate(Math.floor(file.size / 100 * progress));
|
|
34
|
+
|
|
35
|
+
if (progress >= 100) {
|
|
36
|
+
clearInterval(interval);
|
|
37
|
+
// Call onUploaded with the uploaded file URL and thumbnail URL
|
|
38
|
+
onUploaded(`/uploads/${file.name}`, `/thumbnails/${file.name}`);
|
|
39
|
+
}
|
|
40
|
+
}, 100);
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
cancel: async () => {
|
|
44
|
+
clearInterval(interval);
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<ImageFilesField
|
|
52
|
+
bind:files={files}
|
|
53
|
+
uploadFile={uploadImage}
|
|
54
|
+
maxFiles={5}
|
|
55
|
+
/>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Props
|
|
59
|
+
|
|
60
|
+
| Prop | Type | Default | Description |
|
|
61
|
+
|------|------|---------|-------------|
|
|
62
|
+
| `files` | `Array<ImageFile>` | `[]` | Array of uploaded image files |
|
|
63
|
+
| `uploadFile` | `FileUpload` | required | Upload function handler |
|
|
64
|
+
| `maxFiles` | `number` | `1` | Maximum number of files allowed |
|
|
65
|
+
| `variant` | `"" \| "plain" \| "outlined" \| "filled"` | `""` | Visual variant |
|
|
66
|
+
| `disabled` | `boolean` | `false` | Whether the component is disabled |
|
|
67
|
+
| `readonly` | `boolean` | `false` | Whether the component is read-only |
|
|
68
|
+
| `displayMode` | `DisplayMode` | `DisplayMode.Edit` | Display mode (Edit or View) |
|
|
69
|
+
| `removeFileConfirm` | `RemoveConfirm \| null` | `null` | Confirmation function for file removal |
|
|
70
|
+
| `style` | `string` | `""` | Additional CSS styles |
|
|
71
|
+
| `text` | `string \| null` | `null` | Text representation of files (auto-generated) |
|
|
72
|
+
|
|
73
|
+
## Types
|
|
74
|
+
|
|
75
|
+
### ImageFile
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
interface ImageFile {
|
|
79
|
+
name: string; // File name
|
|
80
|
+
uri: string; // Full-size image URL
|
|
81
|
+
thumbnail?: string; // Thumbnail URL (optional)
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### RemoveConfirm
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
type RemoveConfirm = (file: ImageFile) => Promise<boolean>;
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### FileUpload
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
type FileUpload = (
|
|
95
|
+
file: File,
|
|
96
|
+
progressUpdate: ProgressUpdate,
|
|
97
|
+
onUploaded: OnUploaded,
|
|
98
|
+
errorHandler: ErrorHandler
|
|
99
|
+
) => UploadProgress;
|
|
100
|
+
|
|
101
|
+
type ProgressUpdate = (bytesUploaded: number) => void;
|
|
102
|
+
type OnUploaded = (url: string, thumbnail?: string) => void;
|
|
103
|
+
type ErrorHandler = (error: Error) => void;
|
|
104
|
+
|
|
105
|
+
interface UploadProgress {
|
|
106
|
+
cancel: () => Promise<boolean>;
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Examples
|
|
111
|
+
|
|
112
|
+
### Basic Image Upload with Preview
|
|
113
|
+
|
|
114
|
+
```svelte
|
|
115
|
+
<script>
|
|
116
|
+
import ImageFilesField from "@ticatec/uniface-element/ImageFilesField";
|
|
117
|
+
import FormField from "@ticatec/uniface-element/FormField";
|
|
118
|
+
|
|
119
|
+
let profileImages = [];
|
|
120
|
+
|
|
121
|
+
const uploadProfileImage = (file, progressUpdate, onUploaded, errorHandler) => {
|
|
122
|
+
const formData = new FormData();
|
|
123
|
+
formData.append('image', file);
|
|
124
|
+
|
|
125
|
+
const xhr = new XMLHttpRequest();
|
|
126
|
+
|
|
127
|
+
xhr.upload.addEventListener('progress', (e) => {
|
|
128
|
+
if (e.lengthComputable) {
|
|
129
|
+
progressUpdate(e.loaded);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
xhr.onload = () => {
|
|
134
|
+
if (xhr.status === 200) {
|
|
135
|
+
const response = JSON.parse(xhr.responseText);
|
|
136
|
+
onUploaded(response.url, response.thumbnail);
|
|
137
|
+
} else {
|
|
138
|
+
errorHandler(new Error(`Upload failed: ${xhr.statusText}`));
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
xhr.onerror = () => {
|
|
143
|
+
errorHandler(new Error('Network error during upload'));
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
xhr.open('POST', '/api/upload/profile-image');
|
|
147
|
+
xhr.send(formData);
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
cancel: async () => {
|
|
151
|
+
xhr.abort();
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const confirmRemoveImage = async (file) => {
|
|
158
|
+
return confirm(`Are you sure you want to remove ${file.name}?`);
|
|
159
|
+
};
|
|
160
|
+
</script>
|
|
161
|
+
|
|
162
|
+
<div class="profile-section">
|
|
163
|
+
<FormField label="Profile Pictures">
|
|
164
|
+
<ImageFilesField
|
|
165
|
+
bind:files={profileImages}
|
|
166
|
+
uploadFile={uploadProfileImage}
|
|
167
|
+
removeFileConfirm={confirmRemoveImage}
|
|
168
|
+
maxFiles={3}
|
|
169
|
+
variant="outlined"
|
|
170
|
+
/>
|
|
171
|
+
</FormField>
|
|
172
|
+
|
|
173
|
+
<div class="image-info">
|
|
174
|
+
<p>Uploaded {profileImages.length} image(s)</p>
|
|
175
|
+
{#each profileImages as image}
|
|
176
|
+
<p>• {image.name}</p>
|
|
177
|
+
{/each}
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<style>
|
|
182
|
+
.profile-section {
|
|
183
|
+
max-width: 600px;
|
|
184
|
+
margin: 20px;
|
|
185
|
+
padding: 20px;
|
|
186
|
+
border: 1px solid #e2e8f0;
|
|
187
|
+
border-radius: 8px;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.image-info {
|
|
191
|
+
margin-top: 16px;
|
|
192
|
+
padding: 12px;
|
|
193
|
+
background: #f8fafc;
|
|
194
|
+
border-radius: 6px;
|
|
195
|
+
}
|
|
196
|
+
</style>
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Product Gallery Management
|
|
200
|
+
|
|
201
|
+
```svelte
|
|
202
|
+
<script>
|
|
203
|
+
import ImageFilesField from "@ticatec/uniface-element/ImageFilesField";
|
|
204
|
+
import FormField from "@ticatec/uniface-element/FormField";
|
|
205
|
+
|
|
206
|
+
let productImages = [
|
|
207
|
+
{
|
|
208
|
+
name: "product-main.jpg",
|
|
209
|
+
uri: "/images/products/product-main.jpg",
|
|
210
|
+
thumbnail: "/images/products/thumbs/product-main.jpg"
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
name: "product-side.jpg",
|
|
214
|
+
uri: "/images/products/product-side.jpg",
|
|
215
|
+
thumbnail: "/images/products/thumbs/product-side.jpg"
|
|
216
|
+
}
|
|
217
|
+
];
|
|
218
|
+
|
|
219
|
+
let isUploading = false;
|
|
220
|
+
let uploadProgress = {};
|
|
221
|
+
|
|
222
|
+
const uploadProductImage = (file, progressUpdate, onUploaded, errorHandler) => {
|
|
223
|
+
isUploading = true;
|
|
224
|
+
const fileId = `${file.name}-${Date.now()}`;
|
|
225
|
+
uploadProgress[fileId] = 0;
|
|
226
|
+
|
|
227
|
+
// Simulate image processing and upload
|
|
228
|
+
const totalSteps = 100;
|
|
229
|
+
let currentStep = 0;
|
|
230
|
+
|
|
231
|
+
const interval = setInterval(() => {
|
|
232
|
+
currentStep += Math.random() * 5;
|
|
233
|
+
|
|
234
|
+
if (currentStep >= totalSteps) {
|
|
235
|
+
currentStep = totalSteps;
|
|
236
|
+
clearInterval(interval);
|
|
237
|
+
|
|
238
|
+
// Simulate successful upload
|
|
239
|
+
const baseUrl = '/api/products/images';
|
|
240
|
+
const imageUrl = `${baseUrl}/${fileId}.jpg`;
|
|
241
|
+
const thumbnailUrl = `${baseUrl}/thumbs/${fileId}.jpg`;
|
|
242
|
+
|
|
243
|
+
onUploaded(imageUrl, thumbnailUrl);
|
|
244
|
+
delete uploadProgress[fileId];
|
|
245
|
+
isUploading = false;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const bytesUploaded = Math.floor(file.size * (currentStep / totalSteps));
|
|
249
|
+
progressUpdate(bytesUploaded);
|
|
250
|
+
uploadProgress[fileId] = currentStep;
|
|
251
|
+
uploadProgress = { ...uploadProgress };
|
|
252
|
+
}, 50);
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
cancel: async () => {
|
|
256
|
+
clearInterval(interval);
|
|
257
|
+
delete uploadProgress[fileId];
|
|
258
|
+
uploadProgress = { ...uploadProgress };
|
|
259
|
+
isUploading = false;
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const confirmRemoveProductImage = async (file) => {
|
|
266
|
+
const result = await new Promise((resolve) => {
|
|
267
|
+
const modal = document.createElement('div');
|
|
268
|
+
modal.innerHTML = `
|
|
269
|
+
<div style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000;">
|
|
270
|
+
<div style="background: white; padding: 24px; border-radius: 8px; max-width: 400px;">
|
|
271
|
+
<h3>Remove Image</h3>
|
|
272
|
+
<p>Are you sure you want to remove <strong>${file.name}</strong>?</p>
|
|
273
|
+
<p>This action cannot be undone.</p>
|
|
274
|
+
<div style="display: flex; gap: 12px; justify-content: flex-end; margin-top: 20px;">
|
|
275
|
+
<button id="cancel">Cancel</button>
|
|
276
|
+
<button id="confirm" style="background: #dc2626; color: white;">Remove</button>
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
`;
|
|
281
|
+
|
|
282
|
+
document.body.appendChild(modal);
|
|
283
|
+
|
|
284
|
+
modal.querySelector('#cancel').onclick = () => {
|
|
285
|
+
document.body.removeChild(modal);
|
|
286
|
+
resolve(false);
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
modal.querySelector('#confirm').onclick = () => {
|
|
290
|
+
document.body.removeChild(modal);
|
|
291
|
+
resolve(true);
|
|
292
|
+
};
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
return result;
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
$: totalImages = productImages.length;
|
|
299
|
+
$: canAddMore = totalImages < 10;
|
|
300
|
+
</script>
|
|
301
|
+
|
|
302
|
+
<div class="product-gallery">
|
|
303
|
+
<h2>Product Image Gallery</h2>
|
|
304
|
+
|
|
305
|
+
<FormField label="Product Images (Max 10)" error={!canAddMore ? "Maximum 10 images allowed" : ""}>
|
|
306
|
+
<ImageFilesField
|
|
307
|
+
bind:files={productImages}
|
|
308
|
+
uploadFile={uploadProductImage}
|
|
309
|
+
removeFileConfirm={confirmRemoveProductImage}
|
|
310
|
+
maxFiles={10}
|
|
311
|
+
variant="filled"
|
|
312
|
+
disabled={!canAddMore}
|
|
313
|
+
/>
|
|
314
|
+
</FormField>
|
|
315
|
+
|
|
316
|
+
<div class="gallery-stats">
|
|
317
|
+
<div class="stat-item">
|
|
318
|
+
<span class="stat-label">Total Images:</span>
|
|
319
|
+
<span class="stat-value">{totalImages}/10</span>
|
|
320
|
+
</div>
|
|
321
|
+
|
|
322
|
+
{#if isUploading}
|
|
323
|
+
<div class="upload-status">
|
|
324
|
+
<span>Uploading images...</span>
|
|
325
|
+
<div class="progress-indicators">
|
|
326
|
+
{#each Object.entries(uploadProgress) as [fileId, progress]}
|
|
327
|
+
<div class="progress-bar">
|
|
328
|
+
<div class="progress-fill" style="width: {progress}%"></div>
|
|
329
|
+
</div>
|
|
330
|
+
{/each}
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
{/if}
|
|
334
|
+
</div>
|
|
335
|
+
|
|
336
|
+
<div class="image-list">
|
|
337
|
+
<h3>Current Images</h3>
|
|
338
|
+
{#each productImages as image, index}
|
|
339
|
+
<div class="image-item">
|
|
340
|
+
<img src={image.thumbnail || image.uri} alt={image.name} width="50" height="50" />
|
|
341
|
+
<div class="image-details">
|
|
342
|
+
<span class="image-name">{image.name}</span>
|
|
343
|
+
<span class="image-position">Position {index + 1}</span>
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
{/each}
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
|
|
350
|
+
<style>
|
|
351
|
+
.product-gallery {
|
|
352
|
+
max-width: 800px;
|
|
353
|
+
margin: 20px;
|
|
354
|
+
padding: 24px;
|
|
355
|
+
border: 1px solid #e2e8f0;
|
|
356
|
+
border-radius: 8px;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.gallery-stats {
|
|
360
|
+
margin: 16px 0;
|
|
361
|
+
padding: 12px;
|
|
362
|
+
background: #f8fafc;
|
|
363
|
+
border-radius: 6px;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
.stat-item {
|
|
367
|
+
display: flex;
|
|
368
|
+
justify-content: space-between;
|
|
369
|
+
margin-bottom: 8px;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
.stat-label {
|
|
373
|
+
font-weight: 500;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.stat-value {
|
|
377
|
+
color: #3b82f6;
|
|
378
|
+
font-weight: 600;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.upload-status {
|
|
382
|
+
margin-top: 12px;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.progress-indicators {
|
|
386
|
+
margin-top: 8px;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.progress-bar {
|
|
390
|
+
height: 4px;
|
|
391
|
+
background: #e5e7eb;
|
|
392
|
+
border-radius: 2px;
|
|
393
|
+
overflow: hidden;
|
|
394
|
+
margin: 4px 0;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.progress-fill {
|
|
398
|
+
height: 100%;
|
|
399
|
+
background: #3b82f6;
|
|
400
|
+
transition: width 0.3s ease;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.image-list {
|
|
404
|
+
margin-top: 20px;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.image-item {
|
|
408
|
+
display: flex;
|
|
409
|
+
align-items: center;
|
|
410
|
+
gap: 12px;
|
|
411
|
+
padding: 8px;
|
|
412
|
+
border-bottom: 1px solid #f3f4f6;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
.image-item img {
|
|
416
|
+
border-radius: 4px;
|
|
417
|
+
object-fit: cover;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
.image-details {
|
|
421
|
+
display: flex;
|
|
422
|
+
flex-direction: column;
|
|
423
|
+
gap: 2px;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.image-name {
|
|
427
|
+
font-weight: 500;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
.image-position {
|
|
431
|
+
font-size: 0.875rem;
|
|
432
|
+
color: #6b7280;
|
|
433
|
+
}
|
|
434
|
+
</style>
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### Avatar Upload with Cropping
|
|
438
|
+
|
|
439
|
+
```svelte
|
|
440
|
+
<script>
|
|
441
|
+
import ImageFilesField from "@ticatec/uniface-element/ImageFilesField";
|
|
442
|
+
import FormField from "@ticatec/uniface-element/FormField";
|
|
443
|
+
import { DisplayMode } from "@ticatec/uniface-element";
|
|
444
|
+
|
|
445
|
+
let userAvatar = [];
|
|
446
|
+
let displayMode = DisplayMode.Edit;
|
|
447
|
+
|
|
448
|
+
const uploadAvatar = (file, progressUpdate, onUploaded, errorHandler) => {
|
|
449
|
+
// Validate file type
|
|
450
|
+
if (!file.type.startsWith('image/')) {
|
|
451
|
+
errorHandler(new Error('Please select an image file'));
|
|
452
|
+
return { cancel: async () => true };
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Validate file size (max 5MB)
|
|
456
|
+
if (file.size > 5 * 1024 * 1024) {
|
|
457
|
+
errorHandler(new Error('Image must be smaller than 5MB'));
|
|
458
|
+
return { cancel: async () => true };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Create image for validation
|
|
462
|
+
const img = new Image();
|
|
463
|
+
const canvas = document.createElement('canvas');
|
|
464
|
+
const ctx = canvas.getContext('2d');
|
|
465
|
+
|
|
466
|
+
return new Promise((resolve) => {
|
|
467
|
+
img.onload = () => {
|
|
468
|
+
// Set canvas size for square thumbnail
|
|
469
|
+
const size = 200;
|
|
470
|
+
canvas.width = size;
|
|
471
|
+
canvas.height = size;
|
|
472
|
+
|
|
473
|
+
// Calculate crop dimensions (center crop to square)
|
|
474
|
+
const minDimension = Math.min(img.width, img.height);
|
|
475
|
+
const x = (img.width - minDimension) / 2;
|
|
476
|
+
const y = (img.height - minDimension) / 2;
|
|
477
|
+
|
|
478
|
+
// Draw cropped and resized image
|
|
479
|
+
ctx.drawImage(img, x, y, minDimension, minDimension, 0, 0, size, size);
|
|
480
|
+
|
|
481
|
+
// Simulate upload progress
|
|
482
|
+
let progress = 0;
|
|
483
|
+
const interval = setInterval(() => {
|
|
484
|
+
progress += 10;
|
|
485
|
+
progressUpdate(Math.floor(file.size * (progress / 100)));
|
|
486
|
+
|
|
487
|
+
if (progress >= 100) {
|
|
488
|
+
clearInterval(interval);
|
|
489
|
+
|
|
490
|
+
// Convert canvas to blob and create URLs
|
|
491
|
+
canvas.toBlob((blob) => {
|
|
492
|
+
const thumbnailUrl = URL.createObjectURL(blob);
|
|
493
|
+
const fullUrl = URL.createObjectURL(file);
|
|
494
|
+
|
|
495
|
+
onUploaded(fullUrl, thumbnailUrl);
|
|
496
|
+
}, 'image/jpeg', 0.8);
|
|
497
|
+
}
|
|
498
|
+
}, 100);
|
|
499
|
+
|
|
500
|
+
resolve({
|
|
501
|
+
cancel: async () => {
|
|
502
|
+
clearInterval(interval);
|
|
503
|
+
return true;
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
img.onerror = () => {
|
|
509
|
+
errorHandler(new Error('Invalid image file'));
|
|
510
|
+
resolve({ cancel: async () => true });
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
img.src = URL.createObjectURL(file);
|
|
514
|
+
});
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
const confirmRemoveAvatar = async (file) => {
|
|
518
|
+
return confirm('Remove your profile picture?');
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
const toggleMode = () => {
|
|
522
|
+
displayMode = displayMode === DisplayMode.Edit ? DisplayMode.View : DisplayMode.Edit;
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
$: hasAvatar = userAvatar.length > 0;
|
|
526
|
+
</script>
|
|
527
|
+
|
|
528
|
+
<div class="avatar-section">
|
|
529
|
+
<div class="avatar-header">
|
|
530
|
+
<h2>Profile Picture</h2>
|
|
531
|
+
<button on:click={toggleMode} class="mode-toggle">
|
|
532
|
+
{displayMode === DisplayMode.Edit ? 'Preview Mode' : 'Edit Mode'}
|
|
533
|
+
</button>
|
|
534
|
+
</div>
|
|
535
|
+
|
|
536
|
+
<div class="avatar-container">
|
|
537
|
+
<FormField label="Upload Avatar">
|
|
538
|
+
<ImageFilesField
|
|
539
|
+
bind:files={userAvatar}
|
|
540
|
+
uploadFile={uploadAvatar}
|
|
541
|
+
removeFileConfirm={confirmRemoveAvatar}
|
|
542
|
+
maxFiles={1}
|
|
543
|
+
variant="outlined"
|
|
544
|
+
{displayMode}
|
|
545
|
+
style="min-height: 200px; display: flex; align-items: center; justify-content: center;"
|
|
546
|
+
/>
|
|
547
|
+
</FormField>
|
|
548
|
+
|
|
549
|
+
{#if !hasAvatar && displayMode === DisplayMode.Edit}
|
|
550
|
+
<div class="avatar-placeholder">
|
|
551
|
+
<div class="placeholder-icon">👤</div>
|
|
552
|
+
<p>Click to upload your profile picture</p>
|
|
553
|
+
<p class="placeholder-hint">Recommended: Square image, max 5MB</p>
|
|
554
|
+
</div>
|
|
555
|
+
{/if}
|
|
556
|
+
</div>
|
|
557
|
+
|
|
558
|
+
{#if hasAvatar}
|
|
559
|
+
<div class="avatar-info">
|
|
560
|
+
<h3>Current Avatar</h3>
|
|
561
|
+
<div class="avatar-details">
|
|
562
|
+
<img
|
|
563
|
+
src={userAvatar[0].thumbnail || userAvatar[0].uri}
|
|
564
|
+
alt="Avatar preview"
|
|
565
|
+
class="avatar-preview"
|
|
566
|
+
/>
|
|
567
|
+
<div class="avatar-metadata">
|
|
568
|
+
<p><strong>File:</strong> {userAvatar[0].name}</p>
|
|
569
|
+
<p><strong>Status:</strong> ✅ Uploaded successfully</p>
|
|
570
|
+
</div>
|
|
571
|
+
</div>
|
|
572
|
+
</div>
|
|
573
|
+
{/if}
|
|
574
|
+
</div>
|
|
575
|
+
|
|
576
|
+
<style>
|
|
577
|
+
.avatar-section {
|
|
578
|
+
max-width: 500px;
|
|
579
|
+
margin: 20px;
|
|
580
|
+
padding: 24px;
|
|
581
|
+
border: 1px solid #e2e8f0;
|
|
582
|
+
border-radius: 8px;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
.avatar-header {
|
|
586
|
+
display: flex;
|
|
587
|
+
justify-content: space-between;
|
|
588
|
+
align-items: center;
|
|
589
|
+
margin-bottom: 20px;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
.mode-toggle {
|
|
593
|
+
padding: 6px 12px;
|
|
594
|
+
background: #f3f4f6;
|
|
595
|
+
border: 1px solid #d1d5db;
|
|
596
|
+
border-radius: 6px;
|
|
597
|
+
cursor: pointer;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
.avatar-container {
|
|
601
|
+
position: relative;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
.avatar-placeholder {
|
|
605
|
+
position: absolute;
|
|
606
|
+
top: 50%;
|
|
607
|
+
left: 50%;
|
|
608
|
+
transform: translate(-50%, -50%);
|
|
609
|
+
text-align: center;
|
|
610
|
+
pointer-events: none;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
.placeholder-icon {
|
|
614
|
+
font-size: 48px;
|
|
615
|
+
margin-bottom: 12px;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
.placeholder-hint {
|
|
619
|
+
font-size: 0.875rem;
|
|
620
|
+
color: #6b7280;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
.avatar-info {
|
|
624
|
+
margin-top: 20px;
|
|
625
|
+
padding: 16px;
|
|
626
|
+
background: #f8fafc;
|
|
627
|
+
border-radius: 8px;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
.avatar-details {
|
|
631
|
+
display: flex;
|
|
632
|
+
align-items: center;
|
|
633
|
+
gap: 16px;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
.avatar-preview {
|
|
637
|
+
width: 80px;
|
|
638
|
+
height: 80px;
|
|
639
|
+
border-radius: 50%;
|
|
640
|
+
object-fit: cover;
|
|
641
|
+
border: 3px solid white;
|
|
642
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
.avatar-metadata p {
|
|
646
|
+
margin: 4px 0;
|
|
647
|
+
}
|
|
648
|
+
</style>
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
### Document Attachments with Thumbnails
|
|
652
|
+
|
|
653
|
+
```svelte
|
|
654
|
+
<script>
|
|
655
|
+
import ImageFilesField from "@ticatec/uniface-element/ImageFilesField";
|
|
656
|
+
import FormField from "@ticatec/uniface-element/FormField";
|
|
657
|
+
|
|
658
|
+
let documentImages = [];
|
|
659
|
+
let uploadErrors = [];
|
|
660
|
+
|
|
661
|
+
const uploadDocument = (file, progressUpdate, onUploaded, errorHandler) => {
|
|
662
|
+
// Clear previous errors
|
|
663
|
+
uploadErrors = [];
|
|
664
|
+
|
|
665
|
+
// Validate file type (images only for this example)
|
|
666
|
+
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
|
667
|
+
if (!allowedTypes.includes(file.type)) {
|
|
668
|
+
const error = new Error(`Unsupported file type: ${file.type}. Please upload JPEG, PNG, GIF, or WebP images.`);
|
|
669
|
+
errorHandler(error);
|
|
670
|
+
uploadErrors = [...uploadErrors, error.message];
|
|
671
|
+
return { cancel: async () => true };
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Simulate document processing (OCR, thumbnail generation, etc.)
|
|
675
|
+
let progress = 0;
|
|
676
|
+
const stages = [
|
|
677
|
+
'Uploading file...',
|
|
678
|
+
'Generating thumbnail...',
|
|
679
|
+
'Processing document...',
|
|
680
|
+
'Saving to database...',
|
|
681
|
+
'Complete!'
|
|
682
|
+
];
|
|
683
|
+
let currentStage = 0;
|
|
684
|
+
|
|
685
|
+
const interval = setInterval(() => {
|
|
686
|
+
progress += Math.random() * 15;
|
|
687
|
+
|
|
688
|
+
if (progress > (currentStage + 1) * 20 && currentStage < stages.length - 1) {
|
|
689
|
+
currentStage++;
|
|
690
|
+
console.log(stages[currentStage]);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (progress >= 100) {
|
|
694
|
+
progress = 100;
|
|
695
|
+
clearInterval(interval);
|
|
696
|
+
|
|
697
|
+
// Simulate successful upload
|
|
698
|
+
const documentId = Date.now().toString();
|
|
699
|
+
const documentUrl = `/api/documents/${documentId}`;
|
|
700
|
+
const thumbnailUrl = `/api/documents/${documentId}/thumbnail`;
|
|
701
|
+
|
|
702
|
+
onUploaded(documentUrl, thumbnailUrl);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
progressUpdate(Math.floor(file.size * (progress / 100)));
|
|
706
|
+
}, 200);
|
|
707
|
+
|
|
708
|
+
return {
|
|
709
|
+
cancel: async () => {
|
|
710
|
+
clearInterval(interval);
|
|
711
|
+
console.log('Upload cancelled');
|
|
712
|
+
return true;
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
const confirmRemoveDocument = async (file) => {
|
|
718
|
+
const userConfirmed = confirm(`Remove document "${file.name}"?\n\nThis will permanently delete the document and its thumbnail.`);
|
|
719
|
+
|
|
720
|
+
if (userConfirmed) {
|
|
721
|
+
// Simulate API call to delete document
|
|
722
|
+
console.log('Deleting document:', file.name);
|
|
723
|
+
// You could add actual deletion logic here
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
return userConfirmed;
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
$: documentCount = documentImages.length;
|
|
730
|
+
$: hasErrors = uploadErrors.length > 0;
|
|
731
|
+
</script>
|
|
732
|
+
|
|
733
|
+
<div class="document-section">
|
|
734
|
+
<h2>Document Attachments</h2>
|
|
735
|
+
|
|
736
|
+
<FormField label="Upload Documents" error={hasErrors ? uploadErrors.join(', ') : ''}>
|
|
737
|
+
<ImageFilesField
|
|
738
|
+
bind:files={documentImages}
|
|
739
|
+
uploadFile={uploadDocument}
|
|
740
|
+
removeFileConfirm={confirmRemoveDocument}
|
|
741
|
+
maxFiles={20}
|
|
742
|
+
variant="filled"
|
|
743
|
+
/>
|
|
744
|
+
</FormField>
|
|
745
|
+
|
|
746
|
+
<div class="document-summary">
|
|
747
|
+
<div class="summary-stats">
|
|
748
|
+
<div class="stat">
|
|
749
|
+
<span class="stat-number">{documentCount}</span>
|
|
750
|
+
<span class="stat-label">Documents</span>
|
|
751
|
+
</div>
|
|
752
|
+
<div class="stat">
|
|
753
|
+
<span class="stat-number">{20 - documentCount}</span>
|
|
754
|
+
<span class="stat-label">Remaining</span>
|
|
755
|
+
</div>
|
|
756
|
+
</div>
|
|
757
|
+
|
|
758
|
+
{#if hasErrors}
|
|
759
|
+
<div class="error-panel">
|
|
760
|
+
<h4>Upload Errors</h4>
|
|
761
|
+
{#each uploadErrors as error}
|
|
762
|
+
<p class="error-message">❌ {error}</p>
|
|
763
|
+
{/each}
|
|
764
|
+
</div>
|
|
765
|
+
{/if}
|
|
766
|
+
</div>
|
|
767
|
+
|
|
768
|
+
{#if documentCount > 0}
|
|
769
|
+
<div class="document-list">
|
|
770
|
+
<h3>Uploaded Documents</h3>
|
|
771
|
+
<div class="document-grid">
|
|
772
|
+
{#each documentImages as doc, index}
|
|
773
|
+
<div class="document-card">
|
|
774
|
+
<div class="document-thumbnail">
|
|
775
|
+
<img src={doc.thumbnail || doc.uri} alt={doc.name} />
|
|
776
|
+
<div class="document-index">{index + 1}</div>
|
|
777
|
+
</div>
|
|
778
|
+
<div class="document-info">
|
|
779
|
+
<p class="document-name" title={doc.name}>{doc.name}</p>
|
|
780
|
+
<p class="document-status">✅ Processed</p>
|
|
781
|
+
</div>
|
|
782
|
+
</div>
|
|
783
|
+
{/each}
|
|
784
|
+
</div>
|
|
785
|
+
</div>
|
|
786
|
+
{/if}
|
|
787
|
+
</div>
|
|
788
|
+
|
|
789
|
+
<style>
|
|
790
|
+
.document-section {
|
|
791
|
+
max-width: 800px;
|
|
792
|
+
margin: 20px;
|
|
793
|
+
padding: 24px;
|
|
794
|
+
border: 1px solid #e2e8f0;
|
|
795
|
+
border-radius: 8px;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
.document-summary {
|
|
799
|
+
margin: 16px 0;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
.summary-stats {
|
|
803
|
+
display: flex;
|
|
804
|
+
gap: 24px;
|
|
805
|
+
margin-bottom: 16px;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
.stat {
|
|
809
|
+
display: flex;
|
|
810
|
+
flex-direction: column;
|
|
811
|
+
align-items: center;
|
|
812
|
+
padding: 12px;
|
|
813
|
+
background: #f8fafc;
|
|
814
|
+
border-radius: 8px;
|
|
815
|
+
min-width: 80px;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
.stat-number {
|
|
819
|
+
font-size: 1.5rem;
|
|
820
|
+
font-weight: 600;
|
|
821
|
+
color: #3b82f6;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
.stat-label {
|
|
825
|
+
font-size: 0.875rem;
|
|
826
|
+
color: #6b7280;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
.error-panel {
|
|
830
|
+
padding: 12px;
|
|
831
|
+
background: #fef2f2;
|
|
832
|
+
border: 1px solid #fecaca;
|
|
833
|
+
border-radius: 6px;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
.error-message {
|
|
837
|
+
margin: 4px 0;
|
|
838
|
+
color: #dc2626;
|
|
839
|
+
font-size: 0.875rem;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
.document-list {
|
|
843
|
+
margin-top: 24px;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
.document-grid {
|
|
847
|
+
display: grid;
|
|
848
|
+
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
849
|
+
gap: 16px;
|
|
850
|
+
margin-top: 12px;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
.document-card {
|
|
854
|
+
border: 1px solid #e5e7eb;
|
|
855
|
+
border-radius: 8px;
|
|
856
|
+
overflow: hidden;
|
|
857
|
+
background: white;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
.document-thumbnail {
|
|
861
|
+
position: relative;
|
|
862
|
+
height: 120px;
|
|
863
|
+
background: #f9fafb;
|
|
864
|
+
display: flex;
|
|
865
|
+
align-items: center;
|
|
866
|
+
justify-content: center;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
.document-thumbnail img {
|
|
870
|
+
max-width: 100%;
|
|
871
|
+
max-height: 100%;
|
|
872
|
+
object-fit: cover;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
.document-index {
|
|
876
|
+
position: absolute;
|
|
877
|
+
top: 8px;
|
|
878
|
+
right: 8px;
|
|
879
|
+
background: #3b82f6;
|
|
880
|
+
color: white;
|
|
881
|
+
border-radius: 50%;
|
|
882
|
+
width: 24px;
|
|
883
|
+
height: 24px;
|
|
884
|
+
display: flex;
|
|
885
|
+
align-items: center;
|
|
886
|
+
justify-content: center;
|
|
887
|
+
font-size: 0.75rem;
|
|
888
|
+
font-weight: 600;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
.document-info {
|
|
892
|
+
padding: 12px;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
.document-name {
|
|
896
|
+
margin: 0 0 4px 0;
|
|
897
|
+
font-weight: 500;
|
|
898
|
+
font-size: 0.875rem;
|
|
899
|
+
overflow: hidden;
|
|
900
|
+
text-overflow: ellipsis;
|
|
901
|
+
white-space: nowrap;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
.document-status {
|
|
905
|
+
margin: 0;
|
|
906
|
+
font-size: 0.75rem;
|
|
907
|
+
color: #16a34a;
|
|
908
|
+
}
|
|
909
|
+
</style>
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
### Custom Upload Progress and Error Handling
|
|
913
|
+
|
|
914
|
+
```svelte
|
|
915
|
+
<script>
|
|
916
|
+
import ImageFilesField from "@ticatec/uniface-element/ImageFilesField";
|
|
917
|
+
import FormField from "@ticatec/uniface-element/FormField";
|
|
918
|
+
|
|
919
|
+
let galleryImages = [];
|
|
920
|
+
let activeUploads = new Map();
|
|
921
|
+
let uploadHistory = [];
|
|
922
|
+
|
|
923
|
+
const uploadWithErrorHandling = (file, progressUpdate, onUploaded, errorHandler) => {
|
|
924
|
+
const uploadId = Date.now() + Math.random();
|
|
925
|
+
|
|
926
|
+
// Add to active uploads tracking
|
|
927
|
+
activeUploads.set(uploadId, {
|
|
928
|
+
file: file.name,
|
|
929
|
+
startTime: new Date(),
|
|
930
|
+
progress: 0
|
|
931
|
+
});
|
|
932
|
+
activeUploads = new Map(activeUploads);
|
|
933
|
+
|
|
934
|
+
// Simulate various upload scenarios
|
|
935
|
+
const scenarios = [
|
|
936
|
+
{ success: true, delay: 2000 }, // Success after 2s
|
|
937
|
+
{ success: false, error: 'Network timeout', delay: 3000 }, // Network error
|
|
938
|
+
{ success: false, error: 'Server error 500', delay: 1500 }, // Server error
|
|
939
|
+
{ success: true, delay: 4000 }, // Slow success
|
|
940
|
+
];
|
|
941
|
+
|
|
942
|
+
const scenario = scenarios[Math.floor(Math.random() * scenarios.length)];
|
|
943
|
+
let progress = 0;
|
|
944
|
+
let cancelled = false;
|
|
945
|
+
|
|
946
|
+
const interval = setInterval(() => {
|
|
947
|
+
if (cancelled) {
|
|
948
|
+
clearInterval(interval);
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
progress += Math.random() * 20;
|
|
953
|
+
|
|
954
|
+
// Update tracking
|
|
955
|
+
if (activeUploads.has(uploadId)) {
|
|
956
|
+
activeUploads.get(uploadId).progress = Math.min(progress, 95);
|
|
957
|
+
activeUploads = new Map(activeUploads);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
progressUpdate(Math.floor(file.size * (Math.min(progress, 95) / 100)));
|
|
961
|
+
|
|
962
|
+
if (progress >= 100) {
|
|
963
|
+
clearInterval(interval);
|
|
964
|
+
|
|
965
|
+
setTimeout(() => {
|
|
966
|
+
if (cancelled) return;
|
|
967
|
+
|
|
968
|
+
// Remove from active uploads
|
|
969
|
+
activeUploads.delete(uploadId);
|
|
970
|
+
activeUploads = new Map(activeUploads);
|
|
971
|
+
|
|
972
|
+
if (scenario.success) {
|
|
973
|
+
// Successful upload
|
|
974
|
+
const imageUrl = `/uploads/${uploadId}_${file.name}`;
|
|
975
|
+
const thumbnailUrl = `/uploads/thumbs/${uploadId}_${file.name}`;
|
|
976
|
+
|
|
977
|
+
onUploaded(imageUrl, thumbnailUrl);
|
|
978
|
+
|
|
979
|
+
// Add to history
|
|
980
|
+
uploadHistory = [{
|
|
981
|
+
file: file.name,
|
|
982
|
+
status: 'success',
|
|
983
|
+
timestamp: new Date(),
|
|
984
|
+
message: 'Upload completed successfully'
|
|
985
|
+
}, ...uploadHistory];
|
|
986
|
+
} else {
|
|
987
|
+
// Failed upload
|
|
988
|
+
const error = new Error(scenario.error);
|
|
989
|
+
errorHandler(error);
|
|
990
|
+
|
|
991
|
+
// Add to history
|
|
992
|
+
uploadHistory = [{
|
|
993
|
+
file: file.name,
|
|
994
|
+
status: 'error',
|
|
995
|
+
timestamp: new Date(),
|
|
996
|
+
message: scenario.error
|
|
997
|
+
}, ...uploadHistory];
|
|
998
|
+
}
|
|
999
|
+
}, scenario.delay - (progress > 95 ? (progress - 95) * 50 : 0));
|
|
1000
|
+
}
|
|
1001
|
+
}, 100);
|
|
1002
|
+
|
|
1003
|
+
return {
|
|
1004
|
+
cancel: async () => {
|
|
1005
|
+
cancelled = true;
|
|
1006
|
+
clearInterval(interval);
|
|
1007
|
+
|
|
1008
|
+
// Remove from active uploads
|
|
1009
|
+
activeUploads.delete(uploadId);
|
|
1010
|
+
activeUploads = new Map(activeUploads);
|
|
1011
|
+
|
|
1012
|
+
// Add to history
|
|
1013
|
+
uploadHistory = [{
|
|
1014
|
+
file: file.name,
|
|
1015
|
+
status: 'cancelled',
|
|
1016
|
+
timestamp: new Date(),
|
|
1017
|
+
message: 'Upload cancelled by user'
|
|
1018
|
+
}, ...uploadHistory];
|
|
1019
|
+
|
|
1020
|
+
return true;
|
|
1021
|
+
}
|
|
1022
|
+
};
|
|
1023
|
+
};
|
|
1024
|
+
|
|
1025
|
+
const confirmRemoveGalleryImage = async (file) => {
|
|
1026
|
+
const shouldRemove = confirm(`Remove "${file.name}" from gallery?`);
|
|
1027
|
+
|
|
1028
|
+
if (shouldRemove) {
|
|
1029
|
+
// Add to history
|
|
1030
|
+
uploadHistory = [{
|
|
1031
|
+
file: file.name,
|
|
1032
|
+
status: 'removed',
|
|
1033
|
+
timestamp: new Date(),
|
|
1034
|
+
message: 'Image removed from gallery'
|
|
1035
|
+
}, ...uploadHistory];
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
return shouldRemove;
|
|
1039
|
+
};
|
|
1040
|
+
|
|
1041
|
+
const clearHistory = () => {
|
|
1042
|
+
uploadHistory = [];
|
|
1043
|
+
};
|
|
1044
|
+
|
|
1045
|
+
const formatTime = (date) => {
|
|
1046
|
+
return date.toLocaleTimeString();
|
|
1047
|
+
};
|
|
1048
|
+
|
|
1049
|
+
const getStatusIcon = (status) => {
|
|
1050
|
+
const icons = {
|
|
1051
|
+
success: '✅',
|
|
1052
|
+
error: '❌',
|
|
1053
|
+
cancelled: '⏹️',
|
|
1054
|
+
removed: '🗑️'
|
|
1055
|
+
};
|
|
1056
|
+
return icons[status] || '📄';
|
|
1057
|
+
};
|
|
1058
|
+
|
|
1059
|
+
$: hasActiveUploads = activeUploads.size > 0;
|
|
1060
|
+
$: totalImages = galleryImages.length;
|
|
1061
|
+
</script>
|
|
1062
|
+
|
|
1063
|
+
<div class="advanced-upload">
|
|
1064
|
+
<h2>Advanced Image Upload</h2>
|
|
1065
|
+
|
|
1066
|
+
<FormField label="Gallery Images">
|
|
1067
|
+
<ImageFilesField
|
|
1068
|
+
bind:files={galleryImages}
|
|
1069
|
+
uploadFile={uploadWithErrorHandling}
|
|
1070
|
+
removeFileConfirm={confirmRemoveGalleryImage}
|
|
1071
|
+
maxFiles={15}
|
|
1072
|
+
variant="outlined"
|
|
1073
|
+
/>
|
|
1074
|
+
</FormField>
|
|
1075
|
+
|
|
1076
|
+
{#if hasActiveUploads}
|
|
1077
|
+
<div class="active-uploads">
|
|
1078
|
+
<h3>Active Uploads</h3>
|
|
1079
|
+
{#each Array.from(activeUploads.entries()) as [id, upload]}
|
|
1080
|
+
<div class="upload-item">
|
|
1081
|
+
<div class="upload-info">
|
|
1082
|
+
<span class="upload-filename">{upload.file}</span>
|
|
1083
|
+
<span class="upload-time">Started: {formatTime(upload.startTime)}</span>
|
|
1084
|
+
</div>
|
|
1085
|
+
<div class="upload-progress">
|
|
1086
|
+
<div class="progress-bar">
|
|
1087
|
+
<div class="progress-fill" style="width: {upload.progress}%"></div>
|
|
1088
|
+
</div>
|
|
1089
|
+
<span class="progress-text">{Math.round(upload.progress)}%</span>
|
|
1090
|
+
</div>
|
|
1091
|
+
</div>
|
|
1092
|
+
{/each}
|
|
1093
|
+
</div>
|
|
1094
|
+
{/if}
|
|
1095
|
+
|
|
1096
|
+
<div class="gallery-stats">
|
|
1097
|
+
<div class="stat-row">
|
|
1098
|
+
<span>Total Images: {totalImages}/15</span>
|
|
1099
|
+
<span>Active Uploads: {activeUploads.size}</span>
|
|
1100
|
+
</div>
|
|
1101
|
+
</div>
|
|
1102
|
+
|
|
1103
|
+
{#if uploadHistory.length > 0}
|
|
1104
|
+
<div class="upload-history">
|
|
1105
|
+
<div class="history-header">
|
|
1106
|
+
<h3>Upload History</h3>
|
|
1107
|
+
<button on:click={clearHistory} class="clear-history">Clear</button>
|
|
1108
|
+
</div>
|
|
1109
|
+
|
|
1110
|
+
<div class="history-list">
|
|
1111
|
+
{#each uploadHistory.slice(0, 10) as entry}
|
|
1112
|
+
<div class="history-item" class:error={entry.status === 'error'}>
|
|
1113
|
+
<span class="history-icon">{getStatusIcon(entry.status)}</span>
|
|
1114
|
+
<div class="history-details">
|
|
1115
|
+
<span class="history-file">{entry.file}</span>
|
|
1116
|
+
<span class="history-message">{entry.message}</span>
|
|
1117
|
+
</div>
|
|
1118
|
+
<span class="history-time">{formatTime(entry.timestamp)}</span>
|
|
1119
|
+
</div>
|
|
1120
|
+
{/each}
|
|
1121
|
+
</div>
|
|
1122
|
+
</div>
|
|
1123
|
+
{/if}
|
|
1124
|
+
</div>
|
|
1125
|
+
|
|
1126
|
+
<style>
|
|
1127
|
+
.advanced-upload {
|
|
1128
|
+
max-width: 800px;
|
|
1129
|
+
margin: 20px;
|
|
1130
|
+
padding: 24px;
|
|
1131
|
+
border: 1px solid #e2e8f0;
|
|
1132
|
+
border-radius: 8px;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
.active-uploads {
|
|
1136
|
+
margin: 20px 0;
|
|
1137
|
+
padding: 16px;
|
|
1138
|
+
background: #fef3c7;
|
|
1139
|
+
border-radius: 8px;
|
|
1140
|
+
border-left: 4px solid #f59e0b;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
.upload-item {
|
|
1144
|
+
display: flex;
|
|
1145
|
+
align-items: center;
|
|
1146
|
+
gap: 16px;
|
|
1147
|
+
margin: 8px 0;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
.upload-info {
|
|
1151
|
+
flex: 1;
|
|
1152
|
+
display: flex;
|
|
1153
|
+
flex-direction: column;
|
|
1154
|
+
gap: 2px;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
.upload-filename {
|
|
1158
|
+
font-weight: 500;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
.upload-time {
|
|
1162
|
+
font-size: 0.75rem;
|
|
1163
|
+
color: #6b7280;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
.upload-progress {
|
|
1167
|
+
display: flex;
|
|
1168
|
+
align-items: center;
|
|
1169
|
+
gap: 8px;
|
|
1170
|
+
min-width: 120px;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
.progress-bar {
|
|
1174
|
+
flex: 1;
|
|
1175
|
+
height: 6px;
|
|
1176
|
+
background: #e5e7eb;
|
|
1177
|
+
border-radius: 3px;
|
|
1178
|
+
overflow: hidden;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
.progress-fill {
|
|
1182
|
+
height: 100%;
|
|
1183
|
+
background: #3b82f6;
|
|
1184
|
+
transition: width 0.3s ease;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
.progress-text {
|
|
1188
|
+
font-size: 0.875rem;
|
|
1189
|
+
font-weight: 500;
|
|
1190
|
+
min-width: 40px;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
.gallery-stats {
|
|
1194
|
+
margin: 16px 0;
|
|
1195
|
+
padding: 12px;
|
|
1196
|
+
background: #f8fafc;
|
|
1197
|
+
border-radius: 6px;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
.stat-row {
|
|
1201
|
+
display: flex;
|
|
1202
|
+
justify-content: space-between;
|
|
1203
|
+
font-weight: 500;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
.upload-history {
|
|
1207
|
+
margin-top: 24px;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
.history-header {
|
|
1211
|
+
display: flex;
|
|
1212
|
+
justify-content: space-between;
|
|
1213
|
+
align-items: center;
|
|
1214
|
+
margin-bottom: 12px;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
.clear-history {
|
|
1218
|
+
padding: 4px 8px;
|
|
1219
|
+
background: #f3f4f6;
|
|
1220
|
+
border: 1px solid #d1d5db;
|
|
1221
|
+
border-radius: 4px;
|
|
1222
|
+
cursor: pointer;
|
|
1223
|
+
font-size: 0.875rem;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
.history-list {
|
|
1227
|
+
border: 1px solid #e5e7eb;
|
|
1228
|
+
border-radius: 6px;
|
|
1229
|
+
overflow: hidden;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
.history-item {
|
|
1233
|
+
display: flex;
|
|
1234
|
+
align-items: center;
|
|
1235
|
+
gap: 12px;
|
|
1236
|
+
padding: 12px;
|
|
1237
|
+
border-bottom: 1px solid #f3f4f6;
|
|
1238
|
+
background: white;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
.history-item:last-child {
|
|
1242
|
+
border-bottom: none;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
.history-item.error {
|
|
1246
|
+
background: #fef2f2;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
.history-icon {
|
|
1250
|
+
font-size: 1.25rem;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
.history-details {
|
|
1254
|
+
flex: 1;
|
|
1255
|
+
display: flex;
|
|
1256
|
+
flex-direction: column;
|
|
1257
|
+
gap: 2px;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
.history-file {
|
|
1261
|
+
font-weight: 500;
|
|
1262
|
+
font-size: 0.875rem;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
.history-message {
|
|
1266
|
+
font-size: 0.75rem;
|
|
1267
|
+
color: #6b7280;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
.history-time {
|
|
1271
|
+
font-size: 0.75rem;
|
|
1272
|
+
color: #9ca3af;
|
|
1273
|
+
min-width: 80px;
|
|
1274
|
+
text-align: right;
|
|
1275
|
+
}
|
|
1276
|
+
</style>
|
|
1277
|
+
```
|
|
1278
|
+
|
|
1279
|
+
## Styling
|
|
1280
|
+
|
|
1281
|
+
The ImageFilesField component can be styled using CSS custom properties:
|
|
1282
|
+
|
|
1283
|
+
```css
|
|
1284
|
+
.uniface-images-field {
|
|
1285
|
+
--border-color: #e2e8f0;
|
|
1286
|
+
--background-color: #ffffff;
|
|
1287
|
+
--hover-background: #f9fafb;
|
|
1288
|
+
--upload-icon-color: #6b7280;
|
|
1289
|
+
--preview-overlay: rgba(0, 0, 0, 0.7);
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
/* Variant-specific styling */
|
|
1293
|
+
.uniface-common-editor.outlined {
|
|
1294
|
+
border: 2px dashed var(--border-color);
|
|
1295
|
+
border-radius: 8px;
|
|
1296
|
+
padding: 20px;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
.uniface-common-editor.filled {
|
|
1300
|
+
background-color: #f8fafc;
|
|
1301
|
+
border: 1px solid var(--border-color);
|
|
1302
|
+
border-radius: 8px;
|
|
1303
|
+
padding: 20px;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
/* Upload area styling */
|
|
1307
|
+
.editor-action-icon {
|
|
1308
|
+
display: flex;
|
|
1309
|
+
align-items: center;
|
|
1310
|
+
justify-content: center;
|
|
1311
|
+
padding: 20px;
|
|
1312
|
+
border: 2px dashed #d1d5db;
|
|
1313
|
+
border-radius: 8px;
|
|
1314
|
+
cursor: pointer;
|
|
1315
|
+
transition: all 0.2s;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
.editor-action-icon:hover {
|
|
1319
|
+
border-color: #3b82f6;
|
|
1320
|
+
background-color: #f0f9ff;
|
|
1321
|
+
}
|
|
1322
|
+
```
|
|
1323
|
+
|
|
1324
|
+
## Accessibility
|
|
1325
|
+
|
|
1326
|
+
The ImageFilesField component includes several accessibility features:
|
|
1327
|
+
|
|
1328
|
+
- Keyboard navigation support
|
|
1329
|
+
- ARIA labels for screen readers
|
|
1330
|
+
- Focus management for interactive elements
|
|
1331
|
+
- Alt text support for images
|
|
1332
|
+
- Screen reader announcements for upload progress
|
|
1333
|
+
|
|
1334
|
+
## Best Practices
|
|
1335
|
+
|
|
1336
|
+
1. **File Validation**: Always validate file types and sizes before upload
|
|
1337
|
+
2. **Error Handling**: Provide clear error messages and retry mechanisms
|
|
1338
|
+
3. **Progress Feedback**: Show upload progress and allow cancellation
|
|
1339
|
+
4. **Thumbnail Generation**: Generate appropriate thumbnails for performance
|
|
1340
|
+
5. **Security**: Validate files on both client and server side
|
|
1341
|
+
6. **Storage**: Implement proper file storage and cleanup strategies
|
|
1342
|
+
7. **Performance**: Consider lazy loading for large image galleries
|
|
1343
|
+
|
|
1344
|
+
## License
|
|
1345
|
+
|
|
1346
|
+
MIT
|