@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,236 @@
1
+ import { bindable, customElement, IDisposable, IEventAggregator, ILogger, IPlatform, resolve } from 'aurelia';
2
+ import { Channels, DialogAction, DrawerAction, DrawerStatus, OverlayAction, ToasterAction, UiColor } from '../enums/event-aggregator';
3
+ import { IDrawer, IDrawerStatus, IOverlay, IToast, IToaster } from '../interfaces/event-aggregator';
4
+ import { IDialogResponse } from '../interfaces/dialog';
5
+ import { ITransitionService } from '../services/transition-service';
6
+ import { ISvgService } from '../services/svg-service';
7
+ import { IApiService } from '../services/api-service';
8
+ import template from './bleet-drawer.html';
9
+
10
+ @customElement({ name: 'bleet-drawer', template })
11
+ export class BleetDrawer {
12
+ @bindable id: string = '';
13
+
14
+ private dialogElement: HTMLDialogElement;
15
+ private disposable?: IDisposable;
16
+
17
+ // State
18
+ private loading: boolean = false;
19
+ private color: UiColor = UiColor.Primary;
20
+ private headerView: string | null = null;
21
+ private contentView: string | null = null;
22
+ private footerView: string | null = null;
23
+
24
+ // Color classes for header
25
+ private static readonly HEADER_BG_CLASSES: Record<UiColor, string> = {
26
+ [UiColor.Primary]: 'bg-primary-700',
27
+ [UiColor.Secondary]: 'bg-secondary-700',
28
+ [UiColor.Success]: 'bg-success-700',
29
+ [UiColor.Danger]: 'bg-danger-700',
30
+ [UiColor.Warning]: 'bg-warning-700',
31
+ [UiColor.Info]: 'bg-info-700',
32
+ [UiColor.Accent]: 'bg-accent-700',
33
+ };
34
+
35
+ private static readonly CLOSE_BUTTON_TEXT_CLASSES: Record<UiColor, string> = {
36
+ [UiColor.Primary]: 'text-primary-200',
37
+ [UiColor.Secondary]: 'text-secondary-200',
38
+ [UiColor.Success]: 'text-success-200',
39
+ [UiColor.Danger]: 'text-danger-200',
40
+ [UiColor.Warning]: 'text-warning-200',
41
+ [UiColor.Info]: 'text-info-200',
42
+ [UiColor.Accent]: 'text-accent-200',
43
+ };
44
+
45
+ public constructor(
46
+ private readonly logger: ILogger = resolve(ILogger).scopeTo('<bleet-drawer>'),
47
+ private readonly ea: IEventAggregator = resolve(IEventAggregator),
48
+ private readonly platform: IPlatform = resolve(IPlatform),
49
+ private readonly transitionService: ITransitionService = resolve(ITransitionService),
50
+ private readonly svgService: ISvgService = resolve(ISvgService),
51
+ private readonly apiService: IApiService = resolve(IApiService),
52
+ ) {}
53
+
54
+ // Getters
55
+ private get headerBgClass(): string {
56
+ return BleetDrawer.HEADER_BG_CLASSES[this.color] ?? BleetDrawer.HEADER_BG_CLASSES[UiColor.Primary];
57
+ }
58
+
59
+ private get closeButtonTextClass(): string {
60
+ return BleetDrawer.CLOSE_BUTTON_TEXT_CLASSES[this.color] ?? BleetDrawer.CLOSE_BUTTON_TEXT_CLASSES[UiColor.Primary];
61
+ }
62
+
63
+ // Lifecycle
64
+ public attached(): void {
65
+ this.disposable = this.ea.subscribe(Channels.Drawer, this.onDrawerEvent);
66
+ this.dialogElement.addEventListener('close', this.onCloseEvent);
67
+ this.dialogElement.addEventListener('cancel', this.onCancelEvent);
68
+ }
69
+
70
+ public detached(): void {
71
+ this.dialogElement.removeEventListener('close', this.onCloseEvent);
72
+ this.dialogElement.removeEventListener('cancel', this.onCancelEvent);
73
+ this.disposable?.dispose();
74
+ }
75
+
76
+ private bindDialogEvents(): void {
77
+ this.dialogElement.addEventListener('submit', this.onFormSubmit);
78
+ this.dialogElement.addEventListener('click', this.onDialogClick);
79
+ }
80
+
81
+ private unbindDialogEvents(): void {
82
+ this.dialogElement.removeEventListener('submit', this.onFormSubmit);
83
+ this.dialogElement.removeEventListener('click', this.onDialogClick);
84
+ }
85
+
86
+ // Handlers
87
+ private onDrawerEvent = (data: IDrawer): void => {
88
+ if (this.id && this.id !== '' && data.id === this.id) {
89
+ if (data.action === DrawerAction.Open && this.dialogElement.open !== true) {
90
+ if (data.url) {
91
+ this.loadFromUrl(data.url);
92
+ }
93
+ } else if (data.action === DrawerAction.Close && this.dialogElement.open === true) {
94
+ this.close();
95
+ } else if (data.action === DrawerAction.Toggle) {
96
+ if (this.dialogElement.open === true) {
97
+ this.close();
98
+ } else if (data.url) {
99
+ this.loadFromUrl(data.url);
100
+ }
101
+ }
102
+ }
103
+ }
104
+
105
+ private onCloseEvent = (event: Event): void => {
106
+ this.ea.publish(Channels.Overlay, <IOverlay>{ action: OverlayAction.Close });
107
+ }
108
+
109
+ private onCancelEvent = (event: Event): void => {
110
+ this.close();
111
+ }
112
+
113
+ private onDialogClick = (event: MouseEvent): void => {
114
+ const target = event.target as HTMLElement;
115
+ if (target.closest('[data-drawer="close"]')) {
116
+ event.preventDefault();
117
+ this.close();
118
+ }
119
+ }
120
+
121
+ private onFormSubmit = (event: SubmitEvent): void => {
122
+ const form = (event.target as HTMLElement).closest('form');
123
+ if (!form) return;
124
+
125
+ event.preventDefault();
126
+
127
+ const formData = new FormData(form);
128
+
129
+ // Capture submitter button name/value (not included by default in FormData)
130
+ if (event.submitter instanceof HTMLButtonElement && event.submitter.name) {
131
+ formData.append(event.submitter.name, event.submitter.value || '');
132
+ }
133
+
134
+ const method = (formData.get('_method') as string) || form.getAttribute('method') || 'POST';
135
+
136
+ this.apiService
137
+ .url(form.action)
138
+ .fromMultipart(formData)
139
+ .request<IDialogResponse>(method)
140
+ .then((response) => {
141
+ this.applyResponse(response.body);
142
+ })
143
+ .catch((error) => {
144
+ this.logger.error('form submit failed', error);
145
+ this.ea.publish(Channels.Toaster, <IToaster>{
146
+ action: ToasterAction.Add,
147
+ toast: { color: UiColor.Danger, content: 'Erreur lors de l\'envoi' } as IToast
148
+ });
149
+ });
150
+ }
151
+
152
+ private loadFromUrl(url: string): void {
153
+ this.loading = true;
154
+ this.open();
155
+
156
+ this.apiService.url(url).get<IDialogResponse>()
157
+ .then((response) => {
158
+ this.applyResponse(response.body);
159
+ })
160
+ .catch((error) => {
161
+ this.logger.error('loadFromUrl failed', error);
162
+ this.close();
163
+ this.ea.publish(Channels.Toaster, <IToaster>{
164
+ action: ToasterAction.Add,
165
+ toast: { color: UiColor.Danger, content: 'Erreur de chargement' } as IToast
166
+ });
167
+ })
168
+ .finally(() => {
169
+ this.loading = false;
170
+ });
171
+ }
172
+
173
+ private applyResponse(response: IDialogResponse): void {
174
+ this.color = response.color ?? UiColor.Primary;
175
+ if (response.header) this.headerView = response.header;
176
+ if (response.content) this.contentView = response.content;
177
+ if (response.footer) this.footerView = response.footer;
178
+ this.executeActions(response);
179
+ }
180
+
181
+ private executeActions(response: IDialogResponse): void {
182
+ // 1. Primary action
183
+ if (response.action === DialogAction.Close) {
184
+ this.close();
185
+ }
186
+
187
+ // 2. Secondary actions (combinable)
188
+ if (response.toast) {
189
+ this.ea.publish(Channels.Toaster, <IToaster>{
190
+ action: ToasterAction.Add,
191
+ toast: response.toast
192
+ });
193
+ }
194
+ if (response.ajaxify) {
195
+ this.ea.publish(Channels.Ajaxify, response.ajaxify);
196
+ } else if (response.redirect) {
197
+ this.platform.window.location.href = response.redirect;
198
+ } else if (response.refresh) {
199
+ this.platform.window.location.reload();
200
+ }
201
+ }
202
+
203
+ // Open / Close — translate-x
204
+ private open(): void {
205
+ this.bindDialogEvents();
206
+ this.transitionService.run(this.dialogElement, (element) => {
207
+ this.ea.publish(Channels.Overlay, <IOverlay>{ action: OverlayAction.Open });
208
+ this.ea.publish(Channels.DrawerStatus, <IDrawerStatus>{ status: DrawerStatus.Opening, id: this.id });
209
+ (element as HTMLDialogElement).showModal();
210
+ this.platform.requestAnimationFrame(() => {
211
+ element.classList.add('translate-x-0');
212
+ element.classList.remove('translate-x-full');
213
+ });
214
+ }, () => {
215
+ this.ea.publish(Channels.DrawerStatus, <IDrawerStatus>{ status: DrawerStatus.Opened, id: this.id });
216
+ });
217
+ }
218
+
219
+ private close(): void {
220
+ this.transitionService.run(this.dialogElement, (element) => {
221
+ this.ea.publish(Channels.DrawerStatus, <IDrawerStatus>{ status: DrawerStatus.Closing, id: this.id });
222
+ this.ea.publish(Channels.Overlay, <IOverlay>{ action: OverlayAction.Close });
223
+ element.classList.add('translate-x-full');
224
+ element.classList.remove('translate-x-0');
225
+ }, (element) => {
226
+ (element as HTMLDialogElement).close();
227
+ this.unbindDialogEvents();
228
+ this.headerView = null;
229
+ this.contentView = null;
230
+ this.footerView = null;
231
+ this.color = UiColor.Primary;
232
+ this.loading = false;
233
+ this.ea.publish(Channels.DrawerStatus, <IDrawerStatus>{ status: DrawerStatus.Closed, id: this.id });
234
+ });
235
+ }
236
+ }
@@ -0,0 +1,30 @@
1
+ export default `<template>
2
+ <dialog ref="dialogElement"
3
+ class="fixed inset-0 z-50 size-auto max-h-none max-w-none overflow-y-auto transition ease-in-out duration-300 bg-transparent backdrop:bg-transparent opacity-0">
4
+ <div class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
5
+ <div class="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl sm:my-8 sm:w-full sm:max-w-lg">
6
+
7
+ <!-- Loader -->
8
+ <div if.bind="loading" class="flex items-center justify-center py-12">
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 -->
18
+ <au-compose if.bind="headerView" template.bind="headerView"></au-compose>
19
+
20
+ <!-- Content -->
21
+ <au-compose if.bind="contentView" template.bind="contentView"></au-compose>
22
+
23
+ <!-- Footer -->
24
+ <au-compose if.bind="footerView" template.bind="footerView"></au-compose>
25
+ </template>
26
+
27
+ </div>
28
+ </div>
29
+ </dialog>
30
+ </template>`
@@ -0,0 +1,274 @@
1
+ import { bindable, customElement, IDisposable, IEventAggregator, ILogger, IPlatform, resolve } from 'aurelia';
2
+ import { Channels, DialogAction, ModalAction, ModalStatus, OverlayAction, ToasterAction, UiColor } from '../enums/event-aggregator';
3
+ import { IModal, IModalStatus, IOverlay, IToast, IToaster } from '../interfaces/event-aggregator';
4
+ import { IDialogResponse } from '../interfaces/dialog';
5
+ import { ITransitionService } from '../services/transition-service';
6
+ import { ISvgService } from '../services/svg-service';
7
+ import { IApiService } from '../services/api-service';
8
+ import template from './bleet-modal.html';
9
+
10
+ @customElement({ name: 'bleet-modal', template })
11
+ export class BleetModal {
12
+ @bindable id: string = '';
13
+
14
+ private dialogElement: HTMLDialogElement;
15
+ private disposable?: IDisposable;
16
+
17
+ // State
18
+ private loading: boolean = false;
19
+ private color: UiColor = UiColor.Primary;
20
+ private icon: string | null = null;
21
+ private headerView: string | null = null;
22
+ private contentView: string | null = null;
23
+ private footerView: string | null = null;
24
+
25
+ // Color classes — no Tailwind interpolation
26
+ private static readonly ICON_BG_CLASSES: Record<UiColor, string> = {
27
+ [UiColor.Primary]: 'bg-primary-100',
28
+ [UiColor.Secondary]: 'bg-secondary-100',
29
+ [UiColor.Success]: 'bg-success-100',
30
+ [UiColor.Danger]: 'bg-danger-100',
31
+ [UiColor.Warning]: 'bg-warning-100',
32
+ [UiColor.Info]: 'bg-info-100',
33
+ [UiColor.Accent]: 'bg-accent-100',
34
+ };
35
+
36
+ private static readonly ICON_TEXT_CLASSES: Record<UiColor, string> = {
37
+ [UiColor.Primary]: 'text-primary-600',
38
+ [UiColor.Secondary]: 'text-secondary-600',
39
+ [UiColor.Success]: 'text-success-600',
40
+ [UiColor.Danger]: 'text-danger-600',
41
+ [UiColor.Warning]: 'text-warning-600',
42
+ [UiColor.Info]: 'text-info-600',
43
+ [UiColor.Accent]: 'text-accent-600',
44
+ };
45
+
46
+ private static readonly HEADER_BG_CLASSES: Record<UiColor, string> = {
47
+ [UiColor.Primary]: 'bg-primary-600',
48
+ [UiColor.Secondary]: 'bg-secondary-600',
49
+ [UiColor.Success]: 'bg-success-600',
50
+ [UiColor.Danger]: 'bg-danger-600',
51
+ [UiColor.Warning]: 'bg-warning-600',
52
+ [UiColor.Info]: 'bg-info-600',
53
+ [UiColor.Accent]: 'bg-accent-600',
54
+ };
55
+
56
+ private static readonly CLOSE_BUTTON_TEXT_CLASSES: Record<UiColor, string> = {
57
+ [UiColor.Primary]: 'text-primary-200',
58
+ [UiColor.Secondary]: 'text-secondary-200',
59
+ [UiColor.Success]: 'text-success-200',
60
+ [UiColor.Danger]: 'text-danger-200',
61
+ [UiColor.Warning]: 'text-warning-200',
62
+ [UiColor.Info]: 'text-info-200',
63
+ [UiColor.Accent]: 'text-accent-200',
64
+ };
65
+
66
+ public constructor(
67
+ private readonly logger: ILogger = resolve(ILogger).scopeTo('<bleet-modal>'),
68
+ private readonly ea: IEventAggregator = resolve(IEventAggregator),
69
+ private readonly platform: IPlatform = resolve(IPlatform),
70
+ private readonly transitionService: ITransitionService = resolve(ITransitionService),
71
+ private readonly svgService: ISvgService = resolve(ISvgService),
72
+ private readonly apiService: IApiService = resolve(IApiService),
73
+ ) {}
74
+
75
+ // Getters
76
+ private get iconSvg(): string | null {
77
+ if (!this.icon) return null;
78
+ return this.svgService.get(this.icon);
79
+ }
80
+
81
+ private get iconBgClass(): string {
82
+ return BleetModal.ICON_BG_CLASSES[this.color] ?? BleetModal.ICON_BG_CLASSES[UiColor.Primary];
83
+ }
84
+
85
+ private get iconTextClass(): string {
86
+ return BleetModal.ICON_TEXT_CLASSES[this.color] ?? BleetModal.ICON_TEXT_CLASSES[UiColor.Primary];
87
+ }
88
+
89
+ private get headerBgClass(): string {
90
+ return BleetModal.HEADER_BG_CLASSES[this.color] ?? BleetModal.HEADER_BG_CLASSES[UiColor.Primary];
91
+ }
92
+
93
+ private get closeButtonTextClass(): string {
94
+ return BleetModal.CLOSE_BUTTON_TEXT_CLASSES[this.color] ?? BleetModal.CLOSE_BUTTON_TEXT_CLASSES[UiColor.Primary];
95
+ }
96
+
97
+ // Lifecycle
98
+ public attached(): void {
99
+ this.disposable = this.ea.subscribe(Channels.Modal, this.onModalEvent);
100
+ this.dialogElement.addEventListener('close', this.onCloseEvent);
101
+ this.dialogElement.addEventListener('cancel', this.onCancelEvent);
102
+ }
103
+
104
+ public detached(): void {
105
+ this.dialogElement.removeEventListener('close', this.onCloseEvent);
106
+ this.dialogElement.removeEventListener('cancel', this.onCancelEvent);
107
+ this.disposable?.dispose();
108
+ }
109
+
110
+ private bindDialogEvents(): void {
111
+ this.dialogElement.addEventListener('submit', this.onFormSubmit);
112
+ this.dialogElement.addEventListener('click', this.onDialogClick);
113
+ }
114
+
115
+ private unbindDialogEvents(): void {
116
+ this.dialogElement.removeEventListener('submit', this.onFormSubmit);
117
+ this.dialogElement.removeEventListener('click', this.onDialogClick);
118
+ }
119
+
120
+ // Handlers
121
+ private onModalEvent = (data: IModal): void => {
122
+ if (this.id && this.id !== '' && data.id === this.id) {
123
+ if (data.action === ModalAction.Open && this.dialogElement.open !== true) {
124
+ if (data.url) {
125
+ this.loadFromUrl(data.url);
126
+ }
127
+ } else if (data.action === ModalAction.Close && this.dialogElement.open === true) {
128
+ this.close();
129
+ } else if (data.action === ModalAction.Toggle) {
130
+ if (this.dialogElement.open === true) {
131
+ this.close();
132
+ } else if (data.url) {
133
+ this.loadFromUrl(data.url);
134
+ }
135
+ }
136
+ }
137
+ }
138
+
139
+ private onCloseEvent = (event: Event): void => {
140
+ this.ea.publish(Channels.Overlay, <IOverlay>{ action: OverlayAction.Close });
141
+ }
142
+
143
+ private onCancelEvent = (event: Event): void => {
144
+ this.close();
145
+ }
146
+
147
+ private onDialogClick = (event: MouseEvent): void => {
148
+ const target = event.target as HTMLElement;
149
+ if (target.closest('[data-modal="close"]')) {
150
+ event.preventDefault();
151
+ this.close();
152
+ }
153
+ }
154
+
155
+ private onFormSubmit = (event: SubmitEvent): void => {
156
+ const form = (event.target as HTMLElement).closest('form');
157
+ if (!form) return;
158
+
159
+ event.preventDefault();
160
+
161
+ const formData = new FormData(form);
162
+ const method = (formData.get('_method') as string) || form.getAttribute('method') || 'POST';
163
+
164
+ this.apiService
165
+ .url(form.action)
166
+ .fromMultipart(formData)
167
+ .request<IDialogResponse>(method)
168
+ .then((response) => {
169
+ this.applyResponse(response.body);
170
+ })
171
+ .catch((error) => {
172
+ this.logger.error('form submit failed', error);
173
+ this.ea.publish(Channels.Toaster, <IToaster>{
174
+ action: ToasterAction.Add,
175
+ toast: { color: UiColor.Danger, content: 'Erreur lors de l\'envoi' } as IToast
176
+ });
177
+ });
178
+ }
179
+
180
+ // AJAX
181
+ private loadFromUrl(url: string): void {
182
+ this.loading = true;
183
+ this.open();
184
+
185
+ this.apiService.url(url).get<IDialogResponse>()
186
+ .then((response) => {
187
+ this.applyResponse(response.body);
188
+ })
189
+ .catch((error) => {
190
+ this.logger.error('loadFromUrl failed', error);
191
+ this.close();
192
+ this.ea.publish(Channels.Toaster, <IToaster>{
193
+ action: ToasterAction.Add,
194
+ toast: { color: UiColor.Danger, content: 'Erreur de chargement' } as IToast
195
+ });
196
+ })
197
+ .finally(() => {
198
+ this.loading = false;
199
+ });
200
+ }
201
+
202
+ private applyResponse(response: IDialogResponse): void {
203
+ // Style
204
+ this.color = response.color ?? UiColor.Primary;
205
+ this.icon = response.icon ?? null;
206
+
207
+ // Content
208
+ if (response.header) this.headerView = response.header;
209
+ if (response.content) this.contentView = response.content;
210
+ if (response.footer) this.footerView = response.footer;
211
+
212
+ // Actions
213
+ this.executeActions(response);
214
+ }
215
+
216
+ private executeActions(response: IDialogResponse): void {
217
+ // 1. Primary action
218
+ if (response.action === DialogAction.Close) {
219
+ this.close();
220
+ }
221
+ // DialogAction.Keep → do nothing, dialog stays open
222
+
223
+ // 2. Secondary actions (combinable)
224
+ if (response.toast) {
225
+ this.ea.publish(Channels.Toaster, <IToaster>{
226
+ action: ToasterAction.Add,
227
+ toast: response.toast
228
+ });
229
+ }
230
+ if (response.ajaxify) {
231
+ this.ea.publish(Channels.Ajaxify, response.ajaxify);
232
+ } else if (response.redirect) {
233
+ this.platform.window.location.href = response.redirect;
234
+ } else if (response.refresh) {
235
+ this.platform.window.location.reload();
236
+ }
237
+ }
238
+
239
+ // Open / Close
240
+ private open(): void {
241
+ this.bindDialogEvents();
242
+ this.transitionService.run(this.dialogElement, (element) => {
243
+ this.ea.publish(Channels.Overlay, <IOverlay>{ action: OverlayAction.Open });
244
+ this.ea.publish(Channels.ModalStatus, <IModalStatus>{ status: ModalStatus.Opening, id: this.id });
245
+ (element as HTMLDialogElement).showModal();
246
+ this.platform.requestAnimationFrame(() => {
247
+ element.classList.add('opacity-100');
248
+ element.classList.remove('opacity-0');
249
+ });
250
+ }, () => {
251
+ this.ea.publish(Channels.ModalStatus, <IModalStatus>{ status: ModalStatus.Opened, id: this.id });
252
+ });
253
+ }
254
+
255
+ private close(): void {
256
+ this.transitionService.run(this.dialogElement, (element) => {
257
+ this.ea.publish(Channels.ModalStatus, <IModalStatus>{ status: ModalStatus.Closing, id: this.id });
258
+ this.ea.publish(Channels.Overlay, <IOverlay>{ action: OverlayAction.Close });
259
+ element.classList.add('opacity-0');
260
+ element.classList.remove('opacity-100');
261
+ }, (element) => {
262
+ (element as HTMLDialogElement).close();
263
+ this.unbindDialogEvents();
264
+ // Reset
265
+ this.headerView = null;
266
+ this.contentView = null;
267
+ this.footerView = null;
268
+ this.icon = null;
269
+ this.color = UiColor.Primary;
270
+ this.loading = false;
271
+ this.ea.publish(Channels.ModalStatus, <IModalStatus>{ status: ModalStatus.Closed, id: this.id });
272
+ });
273
+ }
274
+ }
@@ -0,0 +1,111 @@
1
+ import {customElement, IDisposable, IEventAggregator, ILogger, INode, IPlatform, resolve} from "aurelia";
2
+ import {Channels, OverlayAction, OverlayStatus} from '../enums/event-aggregator';
3
+ import {IOverlay, IOverlayStatus} from '../interfaces/event-aggregator';
4
+ import {ITransitionService} from '../services/transition-service';
5
+
6
+ @customElement({
7
+ name: 'bleet-overlay',
8
+ template: null,
9
+ })
10
+ export class BleetOverlay {
11
+
12
+ private disposable?: IDisposable;
13
+ public constructor(
14
+ private readonly logger: ILogger = resolve(ILogger).scopeTo('<bleet-overlay>'),
15
+ private readonly element: HTMLElement = resolve(INode) as HTMLElement,
16
+ private readonly ea: IEventAggregator = resolve(IEventAggregator),
17
+ private readonly platform: IPlatform = resolve(IPlatform),
18
+ private readonly transitionService: ITransitionService = resolve(ITransitionService),
19
+ ) {
20
+ this.logger.trace('constructor')
21
+ }
22
+
23
+ public attaching()
24
+ {
25
+ this.logger.trace('attaching');
26
+ }
27
+
28
+ public attached()
29
+ {
30
+ this.logger.trace('attached');
31
+ this.disposable = this.ea.subscribe(Channels.Overlay, this.onOverlayEvent);
32
+ this.element.addEventListener('click', this.onClickOverlay);
33
+ }
34
+
35
+ public detached()
36
+ {
37
+ this.logger.trace('detached');
38
+ this.element.removeEventListener('click', this.onClickOverlay);
39
+ this.disposable?.dispose();
40
+ }
41
+ public dispose()
42
+ {
43
+ this.logger.trace('dispose');
44
+ this.disposable?.dispose();
45
+ }
46
+
47
+ private toggle(fromOverlay: boolean = false) {
48
+ if (this.element.classList.contains('hidden')) {
49
+ this.open(fromOverlay);
50
+ } else {
51
+ this.close(fromOverlay);
52
+ }
53
+ }
54
+ private open(fromOverlay: boolean = false) {
55
+ if (this.element.classList.contains('hidden')) {
56
+ this.logger.trace('open');
57
+ this.transitionService.run(this.element, (element: HTMLElement) => {
58
+ if (fromOverlay) {
59
+ this.ea.publish(Channels.Overlay, <IOverlay>{action: OverlayAction.Open});
60
+ }
61
+ this.ea.publish(Channels.OverlayStatus, <IOverlayStatus>{status: OverlayStatus.Opening});
62
+ element.classList.remove('hidden');
63
+ this.platform.requestAnimationFrame(() => {
64
+ element.classList.remove('opacity-0');
65
+ element.classList.add('opacity-100');
66
+ });
67
+ this.logger.trace('open before()');
68
+ }, (element: HTMLElement) => {
69
+ this.ea.publish(Channels.OverlayStatus, <IOverlayStatus>{status: OverlayStatus.Opened});
70
+ this.logger.trace('open after()');
71
+ });
72
+ }
73
+ }
74
+ private close(fromOverlay: boolean = false) {
75
+ if (!this.element.classList.contains('hidden')) {
76
+ this.transitionService.run(this.element, (element: HTMLElement) => {
77
+ if (fromOverlay) {
78
+ this.ea.publish(Channels.Overlay, <IOverlay>{action: OverlayAction.Close});
79
+ }
80
+ this.ea.publish(Channels.OverlayStatus, <IOverlayStatus>{status: OverlayStatus.Closing});
81
+ element.classList.remove('opacity-100');
82
+ element.classList.add('opacity-0');
83
+ this.logger.trace('close before()');
84
+ }, (element: HTMLElement) => {
85
+ element.classList.add('hidden');
86
+ this.logger.trace('close after()');
87
+ this.ea.publish(Channels.OverlayStatus, <IOverlayStatus>{status: OverlayStatus.Closed});
88
+ });
89
+ }
90
+ }
91
+ private onOverlayEvent = (data: IOverlay) => {
92
+ if (data.action === OverlayAction.Open) {
93
+ this.logger.trace('onOverlayEvent', data);
94
+ this.open();
95
+ } else if (data.action === OverlayAction.Close) {
96
+ this.logger.trace('onOverlayEvent', data);
97
+ this.close();
98
+ } else if (data.action === OverlayAction.Toggle) {
99
+ this.logger.trace('onOverlayEvent', data);
100
+ this.toggle();
101
+ } else {
102
+ this.logger.trace('onOverlayEvent unhandled', data);
103
+ }
104
+ }
105
+ private onClickOverlay = (event: MouseEvent) => {
106
+ this.logger.trace('onClickOverlay', event);
107
+ event.preventDefault();
108
+ this.close(true);
109
+ }
110
+
111
+ }
@@ -0,0 +1,4 @@
1
+ export default `<template>
2
+ <input type="hidden" ref="hiddenField">
3
+ <div ref="editorElement"></div>
4
+ </template>`