@blackcube/aurelia2-bleet 1.0.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/blackcube-aurelia2-bleet-1.0.0.tgz +0 -0
- package/dist/index.es.js +4514 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.js +4549 -0
- package/dist/index.js.map +1 -0
- package/dist/types/attributes/ajaxify-trigger.d.ts +36 -0
- package/dist/types/attributes/ajaxify-trigger.d.ts.map +1 -0
- package/dist/types/attributes/alert.d.ts +15 -0
- package/dist/types/attributes/alert.d.ts.map +1 -0
- package/dist/types/attributes/badge.d.ts +13 -0
- package/dist/types/attributes/badge.d.ts.map +1 -0
- package/dist/types/attributes/burger.d.ts +11 -0
- package/dist/types/attributes/burger.d.ts.map +1 -0
- package/dist/types/attributes/drawer-trigger.d.ts +16 -0
- package/dist/types/attributes/drawer-trigger.d.ts.map +1 -0
- package/dist/types/attributes/dropdown.d.ts +38 -0
- package/dist/types/attributes/dropdown.d.ts.map +1 -0
- package/dist/types/attributes/index.d.ts +16 -0
- package/dist/types/attributes/index.d.ts.map +1 -0
- package/dist/types/attributes/menu.d.ts +32 -0
- package/dist/types/attributes/menu.d.ts.map +1 -0
- package/dist/types/attributes/modal-trigger.d.ts +16 -0
- package/dist/types/attributes/modal-trigger.d.ts.map +1 -0
- package/dist/types/attributes/pager.d.ts +13 -0
- package/dist/types/attributes/pager.d.ts.map +1 -0
- package/dist/types/attributes/password.d.ts +15 -0
- package/dist/types/attributes/password.d.ts.map +1 -0
- package/dist/types/attributes/profile.d.ts +24 -0
- package/dist/types/attributes/profile.d.ts.map +1 -0
- package/dist/types/attributes/select.d.ts +24 -0
- package/dist/types/attributes/select.d.ts.map +1 -0
- package/dist/types/attributes/tabs.d.ts +16 -0
- package/dist/types/attributes/tabs.d.ts.map +1 -0
- package/dist/types/attributes/toaster-trigger.d.ts +19 -0
- package/dist/types/attributes/toaster-trigger.d.ts.map +1 -0
- package/dist/types/attributes/upload.d.ts +57 -0
- package/dist/types/attributes/upload.d.ts.map +1 -0
- package/dist/types/codecs/ajaxify-codec.d.ts +5 -0
- package/dist/types/codecs/ajaxify-codec.d.ts.map +1 -0
- package/dist/types/codecs/csrf-codec.d.ts +7 -0
- package/dist/types/codecs/csrf-codec.d.ts.map +1 -0
- package/dist/types/codecs/request-codec.d.ts +5 -0
- package/dist/types/codecs/request-codec.d.ts.map +1 -0
- package/dist/types/components/bleet-ajaxify.d.ts +17 -0
- package/dist/types/components/bleet-ajaxify.d.ts.map +1 -0
- package/dist/types/components/bleet-ajaxify.html.d.ts +3 -0
- package/dist/types/components/bleet-ajaxify.html.d.ts.map +1 -0
- package/dist/types/components/bleet-drawer.d.ts +40 -0
- package/dist/types/components/bleet-drawer.d.ts.map +1 -0
- package/dist/types/components/bleet-drawer.html.d.ts +3 -0
- package/dist/types/components/bleet-drawer.html.d.ts.map +1 -0
- package/dist/types/components/bleet-modal.d.ts +46 -0
- package/dist/types/components/bleet-modal.d.ts.map +1 -0
- package/dist/types/components/bleet-modal.html.d.ts +3 -0
- package/dist/types/components/bleet-modal.html.d.ts.map +1 -0
- package/dist/types/components/bleet-overlay.d.ts +21 -0
- package/dist/types/components/bleet-overlay.d.ts.map +1 -0
- package/dist/types/components/bleet-quilljs.d.ts +19 -0
- package/dist/types/components/bleet-quilljs.d.ts.map +1 -0
- package/dist/types/components/bleet-quilljs.html.d.ts +3 -0
- package/dist/types/components/bleet-quilljs.html.d.ts.map +1 -0
- package/dist/types/components/bleet-toast.d.ts +26 -0
- package/dist/types/components/bleet-toast.d.ts.map +1 -0
- package/dist/types/components/bleet-toast.html.d.ts +3 -0
- package/dist/types/components/bleet-toast.html.d.ts.map +1 -0
- package/dist/types/components/bleet-toaster-trigger.d.ts +20 -0
- package/dist/types/components/bleet-toaster-trigger.d.ts.map +1 -0
- package/dist/types/components/bleet-toaster.d.ts +15 -0
- package/dist/types/components/bleet-toaster.d.ts.map +1 -0
- package/dist/types/components/bleet-toaster.html.d.ts +3 -0
- package/dist/types/components/bleet-toaster.html.d.ts.map +1 -0
- package/dist/types/components/index.d.ts +9 -0
- package/dist/types/components/index.d.ts.map +1 -0
- package/dist/types/configure.d.ts +35 -0
- package/dist/types/configure.d.ts.map +1 -0
- package/dist/types/enums/api.d.ts +11 -0
- package/dist/types/enums/api.d.ts.map +1 -0
- package/dist/types/enums/event-aggregator.d.ts +123 -0
- package/dist/types/enums/event-aggregator.d.ts.map +1 -0
- package/dist/types/index.d.ts +26 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/interfaces/api.d.ts +56 -0
- package/dist/types/interfaces/api.d.ts.map +1 -0
- package/dist/types/interfaces/dialog.d.ts +18 -0
- package/dist/types/interfaces/dialog.d.ts.map +1 -0
- package/dist/types/interfaces/event-aggregator.d.ts +75 -0
- package/dist/types/interfaces/event-aggregator.d.ts.map +1 -0
- package/dist/types/services/api-service.d.ts +64 -0
- package/dist/types/services/api-service.d.ts.map +1 -0
- package/dist/types/services/http-service.d.ts +22 -0
- package/dist/types/services/http-service.d.ts.map +1 -0
- package/dist/types/services/socketio-service.d.ts +23 -0
- package/dist/types/services/socketio-service.d.ts.map +1 -0
- package/dist/types/services/storage-service.d.ts +13 -0
- package/dist/types/services/storage-service.d.ts.map +1 -0
- package/dist/types/services/svg-service.d.ts +17 -0
- package/dist/types/services/svg-service.d.ts.map +1 -0
- package/dist/types/services/transition-service.d.ts +13 -0
- package/dist/types/services/transition-service.d.ts.map +1 -0
- package/dist/types/services/trap-focus-service.d.ts +28 -0
- package/dist/types/services/trap-focus-service.d.ts.map +1 -0
- package/doc/bleet-api-reference.md +1333 -0
- package/doc/bleet-model-api-reference.md +379 -0
- package/doc/bleet-typescript-api-reference.md +1037 -0
- package/package.json +43 -0
- package/resource.d.ts +22 -0
- package/src/attributes/ajaxify-trigger.ts +218 -0
- package/src/attributes/alert.ts +55 -0
- package/src/attributes/badge.ts +39 -0
- package/src/attributes/burger.ts +36 -0
- package/src/attributes/drawer-trigger.ts +53 -0
- package/src/attributes/dropdown.ts +377 -0
- package/src/attributes/index.ts +15 -0
- package/src/attributes/menu.ts +179 -0
- package/src/attributes/modal-trigger.ts +53 -0
- package/src/attributes/pager.ts +43 -0
- package/src/attributes/password.ts +47 -0
- package/src/attributes/profile.ts +112 -0
- package/src/attributes/select.ts +214 -0
- package/src/attributes/tabs.ts +99 -0
- package/src/attributes/toaster-trigger.ts +54 -0
- package/src/attributes/upload.ts +380 -0
- package/src/codecs/ajaxify-codec.ts +16 -0
- package/src/codecs/csrf-codec.ts +41 -0
- package/src/codecs/request-codec.ts +16 -0
- package/src/components/bleet-ajaxify.html.ts +4 -0
- package/src/components/bleet-ajaxify.ts +62 -0
- package/src/components/bleet-drawer.html.ts +36 -0
- package/src/components/bleet-drawer.ts +236 -0
- package/src/components/bleet-modal.html.ts +30 -0
- package/src/components/bleet-modal.ts +274 -0
- package/src/components/bleet-overlay.ts +111 -0
- package/src/components/bleet-quilljs.html.ts +4 -0
- package/src/components/bleet-quilljs.ts +73 -0
- package/src/components/bleet-toast.html.ts +44 -0
- package/src/components/bleet-toast.ts +133 -0
- package/src/components/bleet-toaster-trigger.ts +66 -0
- package/src/components/bleet-toaster.html.ts +11 -0
- package/src/components/bleet-toaster.ts +72 -0
- package/src/components/index.ts +8 -0
- package/src/configure.ts +121 -0
- package/src/enums/api.ts +12 -0
- package/src/enums/event-aggregator.ts +131 -0
- package/src/index.ts +220 -0
- package/src/interfaces/api.ts +64 -0
- package/src/interfaces/dialog.ts +25 -0
- package/src/interfaces/event-aggregator.ts +88 -0
- package/src/services/api-service.ts +387 -0
- package/src/services/http-service.ts +166 -0
- package/src/services/socketio-service.ts +138 -0
- package/src/services/storage-service.ts +36 -0
- package/src/services/svg-service.ts +35 -0
- package/src/services/transition-service.ts +39 -0
- package/src/services/trap-focus-service.ts +213 -0
- package/src/types/css.d.ts +4 -0
- package/src/types/html.d.ts +12 -0
- package/src/types/svg.d.ts +4 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import {customAttribute, bindable, ILogger, INode, IEventAggregator, resolve} from "aurelia";
|
|
2
|
+
import Resumable from "resumablejs";
|
|
3
|
+
import {Channels, ToasterAction, UiColor, UiToastIcon} from '../enums/event-aggregator';
|
|
4
|
+
import {IToaster} from '../interfaces/event-aggregator';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Fichier geré par l'attribut
|
|
8
|
+
*/
|
|
9
|
+
interface UploadedFile {
|
|
10
|
+
name: string;
|
|
11
|
+
shortname: string | undefined;
|
|
12
|
+
previewUrl: string;
|
|
13
|
+
deleteUrl: string;
|
|
14
|
+
file?: Resumable.ResumableFile | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@customAttribute({ name: 'bleet-upload', defaultProperty: 'endpoint' })
|
|
18
|
+
export class BleetUploadCustomAttribute {
|
|
19
|
+
@bindable endpoint: string = '';
|
|
20
|
+
@bindable() previewEndpoint: string = '';
|
|
21
|
+
@bindable() deleteEndpoint: string = '';
|
|
22
|
+
@bindable() accept: string = '';
|
|
23
|
+
@bindable() maxFiles: number = 1;
|
|
24
|
+
@bindable() multiple: boolean = false;
|
|
25
|
+
@bindable() chunkSize: number = 512 * 1024;
|
|
26
|
+
|
|
27
|
+
private resumable: Resumable | null = null;
|
|
28
|
+
private dropzone: HTMLElement | null = null;
|
|
29
|
+
private browseButton: HTMLElement | null = null;
|
|
30
|
+
private fileList: HTMLElement | null = null;
|
|
31
|
+
private hiddenInput: HTMLInputElement | null = null;
|
|
32
|
+
private previewTemplate: HTMLTemplateElement | null = null;
|
|
33
|
+
private handledFiles: UploadedFile[] = [];
|
|
34
|
+
private parentForm: HTMLFormElement | null = null;
|
|
35
|
+
private csrfToken: { name: string; value: string } | null = null;
|
|
36
|
+
|
|
37
|
+
public constructor(
|
|
38
|
+
private readonly logger: ILogger = resolve(ILogger).scopeTo('bleet-upload'),
|
|
39
|
+
private readonly element: HTMLElement = resolve(INode) as HTMLElement,
|
|
40
|
+
private readonly ea: IEventAggregator = resolve(IEventAggregator),
|
|
41
|
+
) {
|
|
42
|
+
this.logger.trace('constructor');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public attaching(): void {
|
|
46
|
+
this.dropzone = this.element.querySelector('[data-upload=dropzone]');
|
|
47
|
+
this.browseButton = this.element.querySelector('[data-upload=browse]');
|
|
48
|
+
this.fileList = this.element.querySelector('[data-upload=list]');
|
|
49
|
+
this.hiddenInput = this.element.querySelector('[data-upload=value]') as HTMLInputElement;
|
|
50
|
+
this.previewTemplate = this.element.querySelector('[data-upload=preview-template]') as HTMLTemplateElement;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public attached(): void {
|
|
54
|
+
if (!this.endpoint || !this.dropzone) {
|
|
55
|
+
this.logger.warn('Missing endpoint or dropzone');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (this.element.hasAttribute('data-disabled')) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.parentForm = this.element.closest('form');
|
|
64
|
+
this.extractCsrfToken();
|
|
65
|
+
this.initResumable();
|
|
66
|
+
this.setFiles(this.hiddenInput?.value || '');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public detaching(): void {
|
|
70
|
+
if (this.resumable && this.dropzone) {
|
|
71
|
+
this.dropzone.removeEventListener('dragover', this.onDragEnter);
|
|
72
|
+
this.dropzone.removeEventListener('dragenter', this.onDragEnter);
|
|
73
|
+
this.dropzone.removeEventListener('dragleave', this.onDragLeave);
|
|
74
|
+
this.dropzone.removeEventListener('drop', this.onDragLeave);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private extractCsrfToken(): void {
|
|
79
|
+
if (!this.parentForm) return;
|
|
80
|
+
|
|
81
|
+
const csrfInput = this.parentForm.querySelector('input[name=_csrf]') as HTMLInputElement;
|
|
82
|
+
if (csrfInput) {
|
|
83
|
+
this.csrfToken = {
|
|
84
|
+
name: csrfInput.name,
|
|
85
|
+
value: csrfInput.value
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private initResumable(): void {
|
|
91
|
+
const resumableConfig: Resumable.ConfigurationHash = {
|
|
92
|
+
target: this.endpoint,
|
|
93
|
+
chunkSize: this.chunkSize,
|
|
94
|
+
simultaneousUploads: 3,
|
|
95
|
+
permanentErrors: [400, 404, 415, 422, 500, 501],
|
|
96
|
+
maxChunkRetries: 0
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
if (this.accept) {
|
|
100
|
+
const fileTypes = this.accept.split(/\s*,\s*/).filter(v => v.trim() !== '');
|
|
101
|
+
resumableConfig.fileType = fileTypes;
|
|
102
|
+
resumableConfig.fileTypeErrorCallback = (file: Resumable.ResumableFile) => {
|
|
103
|
+
this.showErrorToast(`Le fichier "${file.fileName}" n'est pas un type autorise (${fileTypes.map(t => t.toUpperCase()).join(', ')})`);
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (this.csrfToken) {
|
|
108
|
+
resumableConfig.headers = {
|
|
109
|
+
'X-CSRF-Token': this.csrfToken.value
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
this.resumable = new Resumable(resumableConfig);
|
|
114
|
+
|
|
115
|
+
if (!this.resumable.support) {
|
|
116
|
+
this.logger.warn('Resumable.js not supported');
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (this.browseButton) {
|
|
121
|
+
this.resumable.assignBrowse(this.browseButton, false);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (this.dropzone) {
|
|
125
|
+
this.resumable.assignDrop(this.dropzone);
|
|
126
|
+
this.dropzone.addEventListener('dragover', this.onDragEnter);
|
|
127
|
+
this.dropzone.addEventListener('dragenter', this.onDragEnter);
|
|
128
|
+
this.dropzone.addEventListener('dragleave', this.onDragLeave);
|
|
129
|
+
this.dropzone.addEventListener('drop', this.onDragLeave);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
this.resumable.on('fileAdded', this.onFileAdded);
|
|
133
|
+
this.resumable.on('fileSuccess', this.onFileSuccess);
|
|
134
|
+
this.resumable.on('fileError', this.onFileError);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Charge les fichiers depuis une valeur (initialisation)
|
|
139
|
+
*/
|
|
140
|
+
private setFiles(value: string): void {
|
|
141
|
+
const files = value.split(/\s*,\s*/).filter(v => v.trim() !== '');
|
|
142
|
+
this.handledFiles = files.map(name => ({
|
|
143
|
+
name,
|
|
144
|
+
shortname: name.split(/.*[\/|\\]/).pop(),
|
|
145
|
+
previewUrl: this.generatePreviewUrl(name),
|
|
146
|
+
deleteUrl: this.generateDeleteUrl(name)
|
|
147
|
+
}));
|
|
148
|
+
this.renderFileList();
|
|
149
|
+
this.updateHiddenInput();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Remplace tous les fichiers par un seul (mode single)
|
|
154
|
+
*/
|
|
155
|
+
private setFile(name: string, file: Resumable.ResumableFile | null = null): void {
|
|
156
|
+
// Supprimer les anciens fichiers temporaires
|
|
157
|
+
this.handledFiles.forEach(f => {
|
|
158
|
+
if (f.file && this.resumable) {
|
|
159
|
+
this.resumable.removeFile(f.file);
|
|
160
|
+
}
|
|
161
|
+
this.deleteFileOnServer(f.name);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
this.handledFiles = [{
|
|
165
|
+
name,
|
|
166
|
+
shortname: name.split(/.*[\/|\\]/).pop(),
|
|
167
|
+
previewUrl: this.generatePreviewUrl(name),
|
|
168
|
+
deleteUrl: this.generateDeleteUrl(name),
|
|
169
|
+
file
|
|
170
|
+
}];
|
|
171
|
+
this.renderFileList();
|
|
172
|
+
this.updateHiddenInput();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Ajoute un fichier (mode multiple)
|
|
177
|
+
*/
|
|
178
|
+
private appendFile(name: string, file: Resumable.ResumableFile | null = null): void {
|
|
179
|
+
this.handledFiles.push({
|
|
180
|
+
name,
|
|
181
|
+
shortname: name.split(/.*[\/|\\]/).pop(),
|
|
182
|
+
previewUrl: this.generatePreviewUrl(name),
|
|
183
|
+
deleteUrl: this.generateDeleteUrl(name),
|
|
184
|
+
file
|
|
185
|
+
});
|
|
186
|
+
this.renderFileList();
|
|
187
|
+
this.updateHiddenInput();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Supprime un fichier
|
|
192
|
+
*/
|
|
193
|
+
private onRemove(handledFile: UploadedFile, evt: Event): void {
|
|
194
|
+
evt.stopPropagation();
|
|
195
|
+
evt.preventDefault();
|
|
196
|
+
|
|
197
|
+
const index = this.handledFiles.findIndex(f => f.name === handledFile.name);
|
|
198
|
+
if (index === -1) return;
|
|
199
|
+
|
|
200
|
+
if (handledFile.file && this.resumable) {
|
|
201
|
+
this.resumable.removeFile(handledFile.file);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
this.deleteFileOnServer(handledFile.name);
|
|
205
|
+
this.handledFiles.splice(index, 1);
|
|
206
|
+
this.renderFileList();
|
|
207
|
+
this.updateHiddenInput();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private deleteFileOnServer(name: string): void {
|
|
211
|
+
// Ne supprimer que les fichiers temporaires
|
|
212
|
+
if (!name || !name.startsWith('@bltmp/')) return;
|
|
213
|
+
|
|
214
|
+
const deleteUrl = this.generateDeleteUrl(name);
|
|
215
|
+
if (!deleteUrl) return;
|
|
216
|
+
|
|
217
|
+
fetch(deleteUrl, {
|
|
218
|
+
method: 'DELETE',
|
|
219
|
+
headers: this.csrfToken ? {
|
|
220
|
+
'X-CSRF-Token': this.csrfToken.value
|
|
221
|
+
} : {}
|
|
222
|
+
}).catch(e => this.logger.error('Delete failed', e));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private generatePreviewUrl(name: string): string {
|
|
226
|
+
if (!this.previewEndpoint) return '';
|
|
227
|
+
return this.previewEndpoint.replace('__name__', encodeURIComponent(name));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private generateDeleteUrl(name: string): string {
|
|
231
|
+
if (!this.deleteEndpoint) return '';
|
|
232
|
+
return this.deleteEndpoint.replace('__name__', encodeURIComponent(name));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private updateHiddenInput(): void {
|
|
236
|
+
if (!this.hiddenInput) return;
|
|
237
|
+
this.hiddenInput.value = this.handledFiles.map(f => f.name).join(', ');
|
|
238
|
+
this.hiddenInput.dispatchEvent(new Event('change', { bubbles: true }));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private renderFileList(): void {
|
|
242
|
+
if (!this.fileList || !this.previewTemplate) return;
|
|
243
|
+
|
|
244
|
+
this.fileList.innerHTML = '';
|
|
245
|
+
|
|
246
|
+
this.handledFiles.forEach(handledFile => {
|
|
247
|
+
const fragment = this.previewTemplate!.content.cloneNode(true) as DocumentFragment;
|
|
248
|
+
const item = fragment.firstElementChild as HTMLElement;
|
|
249
|
+
|
|
250
|
+
// Preview link
|
|
251
|
+
const previewLink = item.querySelector('[data-upload=preview-link]') as HTMLAnchorElement;
|
|
252
|
+
if (previewLink) {
|
|
253
|
+
previewLink.href = handledFile.previewUrl ? `${handledFile.previewUrl}&original=1` : '#';
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Preview image et icon
|
|
257
|
+
const previewImage = item.querySelector('[data-upload=preview-image]') as HTMLImageElement;
|
|
258
|
+
const previewIcon = item.querySelector('[data-upload=preview-icon]') as HTMLElement;
|
|
259
|
+
|
|
260
|
+
if (handledFile.previewUrl) {
|
|
261
|
+
this.loadPreview(previewImage, previewIcon, handledFile);
|
|
262
|
+
}
|
|
263
|
+
// Si pas de previewUrl, l'icône reste visible (hidden est sur l'image par défaut)
|
|
264
|
+
|
|
265
|
+
// Nom du fichier
|
|
266
|
+
const nameEl = item.querySelector('[data-upload=preview-name]') as HTMLElement;
|
|
267
|
+
if (nameEl) {
|
|
268
|
+
nameEl.textContent = handledFile.shortname || '';
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Bouton supprimer
|
|
272
|
+
const removeBtn = item.querySelector('[data-upload=preview-remove]') as HTMLButtonElement;
|
|
273
|
+
if (removeBtn) {
|
|
274
|
+
removeBtn.addEventListener('click', (e) => this.onRemove(handledFile, e));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
this.fileList!.appendChild(fragment);
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private loadPreview(previewImage: HTMLImageElement, previewIcon: HTMLElement, handledFile: UploadedFile): void {
|
|
282
|
+
const shortname = handledFile.shortname || '';
|
|
283
|
+
|
|
284
|
+
if (shortname.toLowerCase().endsWith('.svg')) {
|
|
285
|
+
// SVG : fetch et inline dans le container parent (le lien)
|
|
286
|
+
fetch(handledFile.previewUrl)
|
|
287
|
+
.then(response => {
|
|
288
|
+
if (!response.ok) throw new Error('Failed to load SVG');
|
|
289
|
+
return response.text();
|
|
290
|
+
})
|
|
291
|
+
.then(svgContent => {
|
|
292
|
+
// Cacher l'icône par défaut
|
|
293
|
+
previewIcon.classList.add('hidden');
|
|
294
|
+
// Insérer le SVG à la place de l'image
|
|
295
|
+
previewImage.insertAdjacentHTML('afterend', svgContent);
|
|
296
|
+
const svg = previewImage.parentElement?.querySelector('svg:not([data-upload])');
|
|
297
|
+
if (svg) {
|
|
298
|
+
svg.classList.add('size-full');
|
|
299
|
+
svg.removeAttribute('width');
|
|
300
|
+
svg.removeAttribute('height');
|
|
301
|
+
}
|
|
302
|
+
})
|
|
303
|
+
.catch(() => {
|
|
304
|
+
// Garder l'icône visible en cas d'erreur
|
|
305
|
+
});
|
|
306
|
+
} else {
|
|
307
|
+
// Autres fichiers : utiliser l'image du template
|
|
308
|
+
previewImage.src = handledFile.previewUrl;
|
|
309
|
+
previewImage.alt = shortname;
|
|
310
|
+
previewImage.onload = () => {
|
|
311
|
+
previewImage.classList.remove('hidden');
|
|
312
|
+
previewIcon.classList.add('hidden');
|
|
313
|
+
};
|
|
314
|
+
previewImage.onerror = () => {
|
|
315
|
+
// Garder l'icône visible en cas d'erreur
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private showErrorToast(message: string): void {
|
|
321
|
+
this.ea.publish(Channels.Toaster, <IToaster>{
|
|
322
|
+
action: ToasterAction.Add,
|
|
323
|
+
toast: {
|
|
324
|
+
id: `upload-error-${Date.now()}`,
|
|
325
|
+
duration: 5000,
|
|
326
|
+
color: UiColor.Danger,
|
|
327
|
+
icon: UiToastIcon.Danger,
|
|
328
|
+
title: 'Erreur',
|
|
329
|
+
content: message
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Resumable.js event handlers
|
|
335
|
+
private onDragEnter = (evt: DragEvent): void => {
|
|
336
|
+
evt.preventDefault();
|
|
337
|
+
const dt = evt.dataTransfer;
|
|
338
|
+
if (dt && dt.types.indexOf('Files') >= 0) {
|
|
339
|
+
evt.stopPropagation();
|
|
340
|
+
dt.dropEffect = 'copy';
|
|
341
|
+
this.dropzone?.classList.add('border-primary-600', 'bg-primary-50');
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
private onDragLeave = (evt: Event): void => {
|
|
346
|
+
this.dropzone?.classList.remove('border-primary-600', 'bg-primary-50');
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
private onFileAdded = (file: Resumable.ResumableFile, event: DragEvent): void => {
|
|
350
|
+
this.logger.debug('onFileAdded', file.fileName);
|
|
351
|
+
this.resumable?.upload();
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
private onFileSuccess = (file: Resumable.ResumableFile, serverMessage: string): void => {
|
|
355
|
+
this.logger.debug('onFileSuccess', file.fileName, serverMessage);
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
const response = JSON.parse(serverMessage);
|
|
359
|
+
if (!response.finalFilename) {
|
|
360
|
+
throw new Error('Missing finalFilename in response');
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const finalName = `@bltmp/${response.finalFilename}`;
|
|
364
|
+
|
|
365
|
+
if (!this.multiple) {
|
|
366
|
+
this.setFile(finalName, file);
|
|
367
|
+
} else {
|
|
368
|
+
this.appendFile(finalName, file);
|
|
369
|
+
}
|
|
370
|
+
} catch (e) {
|
|
371
|
+
this.logger.error('Failed to parse server response', e);
|
|
372
|
+
this.showErrorToast('Reponse serveur invalide');
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
private onFileError = (file: Resumable.ResumableFile, message: string): void => {
|
|
377
|
+
this.logger.error('onFileError', file.fileName, message);
|
|
378
|
+
this.showErrorToast(`Echec de l'upload de "${file.fileName}"`);
|
|
379
|
+
};
|
|
380
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {ICodec, IHttpRequest} from '../interfaces/api';
|
|
2
|
+
|
|
3
|
+
export class AjaxifyCodec {
|
|
4
|
+
|
|
5
|
+
public static codec: ICodec = {
|
|
6
|
+
encode: (ctx: IHttpRequest) => {
|
|
7
|
+
return Promise.resolve({
|
|
8
|
+
...ctx,
|
|
9
|
+
headers: {
|
|
10
|
+
...ctx.headers,
|
|
11
|
+
'X-Requested-For': 'Ajaxify',
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ICodec, IHttpRequest } from '../interfaces/api';
|
|
2
|
+
import { CsrfConfig } from '../configure';
|
|
3
|
+
|
|
4
|
+
export class CsrfCodec {
|
|
5
|
+
|
|
6
|
+
public static codec: ICodec = {
|
|
7
|
+
encode: (ctx: IHttpRequest) => {
|
|
8
|
+
const meta = document.querySelector('meta[name="csrf"]');
|
|
9
|
+
const token = meta?.getAttribute('content');
|
|
10
|
+
if (!token) {
|
|
11
|
+
return Promise.resolve(ctx);
|
|
12
|
+
}
|
|
13
|
+
return Promise.resolve({
|
|
14
|
+
...ctx,
|
|
15
|
+
headers: {
|
|
16
|
+
...ctx.headers,
|
|
17
|
+
'X-CSRF-Token': token,
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
public static fromConfig(config: CsrfConfig): ICodec {
|
|
24
|
+
return {
|
|
25
|
+
encode: (ctx: IHttpRequest) => {
|
|
26
|
+
const meta = document.querySelector(`meta[name="${config.metaName}"]`);
|
|
27
|
+
const token = meta?.getAttribute('content');
|
|
28
|
+
if (!token) {
|
|
29
|
+
return Promise.resolve(ctx);
|
|
30
|
+
}
|
|
31
|
+
return Promise.resolve({
|
|
32
|
+
...ctx,
|
|
33
|
+
headers: {
|
|
34
|
+
...ctx.headers,
|
|
35
|
+
[config.headerName]: token,
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {ICodec, IHttpRequest} from '../interfaces/api';
|
|
2
|
+
|
|
3
|
+
export class RequestCodec {
|
|
4
|
+
|
|
5
|
+
public static codec: ICodec = {
|
|
6
|
+
encode: (ctx: IHttpRequest) => {
|
|
7
|
+
return Promise.resolve({
|
|
8
|
+
...ctx,
|
|
9
|
+
headers: {
|
|
10
|
+
...ctx.headers,
|
|
11
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {customElement, bindable, IEventAggregator, resolve, ILogger, IDisposable} from 'aurelia';
|
|
2
|
+
import template from './bleet-ajaxify.html';
|
|
3
|
+
import {IApiService} from '../services/api-service';
|
|
4
|
+
import {IAjaxify} from '../interfaces/event-aggregator';
|
|
5
|
+
import {AjaxifyAction, Channels} from '../enums/event-aggregator';
|
|
6
|
+
import {AjaxifyCodec} from '../codecs/ajaxify-codec';
|
|
7
|
+
|
|
8
|
+
@customElement({name: 'bleet-ajaxify', template})
|
|
9
|
+
export class BleetAjaxify {
|
|
10
|
+
@bindable() id: string = '';
|
|
11
|
+
@bindable() url?: string;
|
|
12
|
+
|
|
13
|
+
private ajaxedView: string | null = null;
|
|
14
|
+
private disposable?: IDisposable;
|
|
15
|
+
|
|
16
|
+
public constructor(
|
|
17
|
+
private ea: IEventAggregator = resolve(IEventAggregator),
|
|
18
|
+
private logger: ILogger = resolve(ILogger).scopeTo('<bleet-ajaxify>'),
|
|
19
|
+
private apiService: IApiService = resolve(IApiService),
|
|
20
|
+
) {
|
|
21
|
+
this.logger.trace('constructor');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public attaching() {
|
|
25
|
+
this.logger.trace('attaching')
|
|
26
|
+
this.disposable = this.ea.subscribe(Channels.Ajaxify, this.onEvent);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public detaching() {
|
|
30
|
+
this.logger.trace('detaching');
|
|
31
|
+
this.disposable?.dispose();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public dispose() {
|
|
35
|
+
this.logger.trace('dispose');
|
|
36
|
+
this.disposable?.dispose();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private onEvent = (data: IAjaxify) => {
|
|
40
|
+
this.logger.trace('onEvent', data);
|
|
41
|
+
if (data.action === AjaxifyAction.Refresh) {
|
|
42
|
+
if (data.id && data.id == this.id) {
|
|
43
|
+
this.logger.debug(`Refreshing ajaxify id=${this.id} from url=${this.url}`);
|
|
44
|
+
const url = data.url ? data.url : this.url;
|
|
45
|
+
if (url.length >1) {
|
|
46
|
+
this.apiService
|
|
47
|
+
.url(url)
|
|
48
|
+
.withInputCodec(AjaxifyCodec.codec)
|
|
49
|
+
.toText()
|
|
50
|
+
.get<string>()
|
|
51
|
+
.then((response) => {
|
|
52
|
+
this.logger.debug(`Received for id=${this.id}`);
|
|
53
|
+
this.ajaxedView = response.body;
|
|
54
|
+
})
|
|
55
|
+
.catch((error) => {
|
|
56
|
+
this.logger.error(`Error for id=${this.id}: `, error);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export default `<template>
|
|
2
|
+
<dialog ref="dialogElement"
|
|
3
|
+
class="fixed inset-0 z-50 size-auto max-h-none max-w-none overflow-hidden backdrop:bg-transparent bg-transparent transform translate-x-full transition ease-in-out duration-300">
|
|
4
|
+
<div class="absolute inset-0 pl-10 sm:pl-16 overflow-hidden">
|
|
5
|
+
<div class="ml-auto flex flex-col h-full w-full sm:w-2/3 sm:min-w-md transform bg-white shadow-xl">
|
|
6
|
+
|
|
7
|
+
<!-- Loader -->
|
|
8
|
+
<div if.bind="loading" class="flex items-center justify-center h-full">
|
|
9
|
+
<svg class="animate-spin size-8 text-primary-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
10
|
+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
11
|
+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
12
|
+
</svg>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<!-- Contenu -->
|
|
16
|
+
<template else>
|
|
17
|
+
<!-- Header (fixed) -->
|
|
18
|
+
<div class="shrink-0">
|
|
19
|
+
<au-compose if.bind="headerView" template.bind="headerView"></au-compose>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<!-- Content (scrollable) -->
|
|
23
|
+
<div class="flex-1 overflow-y-auto">
|
|
24
|
+
<au-compose if.bind="contentView" template.bind="contentView"></au-compose>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<!-- Footer (fixed) -->
|
|
28
|
+
<div class="shrink-0">
|
|
29
|
+
<au-compose if.bind="footerView" template.bind="footerView"></au-compose>
|
|
30
|
+
</div>
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</dialog>
|
|
36
|
+
</template>`
|