@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.
Files changed (157) hide show
  1. package/blackcube-aurelia2-bleet-1.0.0.tgz +0 -0
  2. package/dist/index.es.js +4514 -0
  3. package/dist/index.es.js.map +1 -0
  4. package/dist/index.js +4549 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/types/attributes/ajaxify-trigger.d.ts +36 -0
  7. package/dist/types/attributes/ajaxify-trigger.d.ts.map +1 -0
  8. package/dist/types/attributes/alert.d.ts +15 -0
  9. package/dist/types/attributes/alert.d.ts.map +1 -0
  10. package/dist/types/attributes/badge.d.ts +13 -0
  11. package/dist/types/attributes/badge.d.ts.map +1 -0
  12. package/dist/types/attributes/burger.d.ts +11 -0
  13. package/dist/types/attributes/burger.d.ts.map +1 -0
  14. package/dist/types/attributes/drawer-trigger.d.ts +16 -0
  15. package/dist/types/attributes/drawer-trigger.d.ts.map +1 -0
  16. package/dist/types/attributes/dropdown.d.ts +38 -0
  17. package/dist/types/attributes/dropdown.d.ts.map +1 -0
  18. package/dist/types/attributes/index.d.ts +16 -0
  19. package/dist/types/attributes/index.d.ts.map +1 -0
  20. package/dist/types/attributes/menu.d.ts +32 -0
  21. package/dist/types/attributes/menu.d.ts.map +1 -0
  22. package/dist/types/attributes/modal-trigger.d.ts +16 -0
  23. package/dist/types/attributes/modal-trigger.d.ts.map +1 -0
  24. package/dist/types/attributes/pager.d.ts +13 -0
  25. package/dist/types/attributes/pager.d.ts.map +1 -0
  26. package/dist/types/attributes/password.d.ts +15 -0
  27. package/dist/types/attributes/password.d.ts.map +1 -0
  28. package/dist/types/attributes/profile.d.ts +24 -0
  29. package/dist/types/attributes/profile.d.ts.map +1 -0
  30. package/dist/types/attributes/select.d.ts +24 -0
  31. package/dist/types/attributes/select.d.ts.map +1 -0
  32. package/dist/types/attributes/tabs.d.ts +16 -0
  33. package/dist/types/attributes/tabs.d.ts.map +1 -0
  34. package/dist/types/attributes/toaster-trigger.d.ts +19 -0
  35. package/dist/types/attributes/toaster-trigger.d.ts.map +1 -0
  36. package/dist/types/attributes/upload.d.ts +57 -0
  37. package/dist/types/attributes/upload.d.ts.map +1 -0
  38. package/dist/types/codecs/ajaxify-codec.d.ts +5 -0
  39. package/dist/types/codecs/ajaxify-codec.d.ts.map +1 -0
  40. package/dist/types/codecs/csrf-codec.d.ts +7 -0
  41. package/dist/types/codecs/csrf-codec.d.ts.map +1 -0
  42. package/dist/types/codecs/request-codec.d.ts +5 -0
  43. package/dist/types/codecs/request-codec.d.ts.map +1 -0
  44. package/dist/types/components/bleet-ajaxify.d.ts +17 -0
  45. package/dist/types/components/bleet-ajaxify.d.ts.map +1 -0
  46. package/dist/types/components/bleet-ajaxify.html.d.ts +3 -0
  47. package/dist/types/components/bleet-ajaxify.html.d.ts.map +1 -0
  48. package/dist/types/components/bleet-drawer.d.ts +40 -0
  49. package/dist/types/components/bleet-drawer.d.ts.map +1 -0
  50. package/dist/types/components/bleet-drawer.html.d.ts +3 -0
  51. package/dist/types/components/bleet-drawer.html.d.ts.map +1 -0
  52. package/dist/types/components/bleet-modal.d.ts +46 -0
  53. package/dist/types/components/bleet-modal.d.ts.map +1 -0
  54. package/dist/types/components/bleet-modal.html.d.ts +3 -0
  55. package/dist/types/components/bleet-modal.html.d.ts.map +1 -0
  56. package/dist/types/components/bleet-overlay.d.ts +21 -0
  57. package/dist/types/components/bleet-overlay.d.ts.map +1 -0
  58. package/dist/types/components/bleet-quilljs.d.ts +19 -0
  59. package/dist/types/components/bleet-quilljs.d.ts.map +1 -0
  60. package/dist/types/components/bleet-quilljs.html.d.ts +3 -0
  61. package/dist/types/components/bleet-quilljs.html.d.ts.map +1 -0
  62. package/dist/types/components/bleet-toast.d.ts +26 -0
  63. package/dist/types/components/bleet-toast.d.ts.map +1 -0
  64. package/dist/types/components/bleet-toast.html.d.ts +3 -0
  65. package/dist/types/components/bleet-toast.html.d.ts.map +1 -0
  66. package/dist/types/components/bleet-toaster-trigger.d.ts +20 -0
  67. package/dist/types/components/bleet-toaster-trigger.d.ts.map +1 -0
  68. package/dist/types/components/bleet-toaster.d.ts +15 -0
  69. package/dist/types/components/bleet-toaster.d.ts.map +1 -0
  70. package/dist/types/components/bleet-toaster.html.d.ts +3 -0
  71. package/dist/types/components/bleet-toaster.html.d.ts.map +1 -0
  72. package/dist/types/components/index.d.ts +9 -0
  73. package/dist/types/components/index.d.ts.map +1 -0
  74. package/dist/types/configure.d.ts +35 -0
  75. package/dist/types/configure.d.ts.map +1 -0
  76. package/dist/types/enums/api.d.ts +11 -0
  77. package/dist/types/enums/api.d.ts.map +1 -0
  78. package/dist/types/enums/event-aggregator.d.ts +123 -0
  79. package/dist/types/enums/event-aggregator.d.ts.map +1 -0
  80. package/dist/types/index.d.ts +26 -0
  81. package/dist/types/index.d.ts.map +1 -0
  82. package/dist/types/interfaces/api.d.ts +56 -0
  83. package/dist/types/interfaces/api.d.ts.map +1 -0
  84. package/dist/types/interfaces/dialog.d.ts +18 -0
  85. package/dist/types/interfaces/dialog.d.ts.map +1 -0
  86. package/dist/types/interfaces/event-aggregator.d.ts +75 -0
  87. package/dist/types/interfaces/event-aggregator.d.ts.map +1 -0
  88. package/dist/types/services/api-service.d.ts +64 -0
  89. package/dist/types/services/api-service.d.ts.map +1 -0
  90. package/dist/types/services/http-service.d.ts +22 -0
  91. package/dist/types/services/http-service.d.ts.map +1 -0
  92. package/dist/types/services/socketio-service.d.ts +23 -0
  93. package/dist/types/services/socketio-service.d.ts.map +1 -0
  94. package/dist/types/services/storage-service.d.ts +13 -0
  95. package/dist/types/services/storage-service.d.ts.map +1 -0
  96. package/dist/types/services/svg-service.d.ts +17 -0
  97. package/dist/types/services/svg-service.d.ts.map +1 -0
  98. package/dist/types/services/transition-service.d.ts +13 -0
  99. package/dist/types/services/transition-service.d.ts.map +1 -0
  100. package/dist/types/services/trap-focus-service.d.ts +28 -0
  101. package/dist/types/services/trap-focus-service.d.ts.map +1 -0
  102. package/doc/bleet-api-reference.md +1333 -0
  103. package/doc/bleet-model-api-reference.md +379 -0
  104. package/doc/bleet-typescript-api-reference.md +1037 -0
  105. package/package.json +43 -0
  106. package/resource.d.ts +22 -0
  107. package/src/attributes/ajaxify-trigger.ts +218 -0
  108. package/src/attributes/alert.ts +55 -0
  109. package/src/attributes/badge.ts +39 -0
  110. package/src/attributes/burger.ts +36 -0
  111. package/src/attributes/drawer-trigger.ts +53 -0
  112. package/src/attributes/dropdown.ts +377 -0
  113. package/src/attributes/index.ts +15 -0
  114. package/src/attributes/menu.ts +179 -0
  115. package/src/attributes/modal-trigger.ts +53 -0
  116. package/src/attributes/pager.ts +43 -0
  117. package/src/attributes/password.ts +47 -0
  118. package/src/attributes/profile.ts +112 -0
  119. package/src/attributes/select.ts +214 -0
  120. package/src/attributes/tabs.ts +99 -0
  121. package/src/attributes/toaster-trigger.ts +54 -0
  122. package/src/attributes/upload.ts +380 -0
  123. package/src/codecs/ajaxify-codec.ts +16 -0
  124. package/src/codecs/csrf-codec.ts +41 -0
  125. package/src/codecs/request-codec.ts +16 -0
  126. package/src/components/bleet-ajaxify.html.ts +4 -0
  127. package/src/components/bleet-ajaxify.ts +62 -0
  128. package/src/components/bleet-drawer.html.ts +36 -0
  129. package/src/components/bleet-drawer.ts +236 -0
  130. package/src/components/bleet-modal.html.ts +30 -0
  131. package/src/components/bleet-modal.ts +274 -0
  132. package/src/components/bleet-overlay.ts +111 -0
  133. package/src/components/bleet-quilljs.html.ts +4 -0
  134. package/src/components/bleet-quilljs.ts +73 -0
  135. package/src/components/bleet-toast.html.ts +44 -0
  136. package/src/components/bleet-toast.ts +133 -0
  137. package/src/components/bleet-toaster-trigger.ts +66 -0
  138. package/src/components/bleet-toaster.html.ts +11 -0
  139. package/src/components/bleet-toaster.ts +72 -0
  140. package/src/components/index.ts +8 -0
  141. package/src/configure.ts +121 -0
  142. package/src/enums/api.ts +12 -0
  143. package/src/enums/event-aggregator.ts +131 -0
  144. package/src/index.ts +220 -0
  145. package/src/interfaces/api.ts +64 -0
  146. package/src/interfaces/dialog.ts +25 -0
  147. package/src/interfaces/event-aggregator.ts +88 -0
  148. package/src/services/api-service.ts +387 -0
  149. package/src/services/http-service.ts +166 -0
  150. package/src/services/socketio-service.ts +138 -0
  151. package/src/services/storage-service.ts +36 -0
  152. package/src/services/svg-service.ts +35 -0
  153. package/src/services/transition-service.ts +39 -0
  154. package/src/services/trap-focus-service.ts +213 -0
  155. package/src/types/css.d.ts +4 -0
  156. package/src/types/html.d.ts +12 -0
  157. 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,4 @@
1
+ export default `<template>
2
+ <au-slot if.bind="!ajaxedView"></au-slot>
3
+ <au-compose if.bind="ajaxedView" template.bind="ajaxedView"></au-compose>
4
+ </template>`;
@@ -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>`