@windwalker-io/unicorn-next 0.1.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 (175) hide show
  1. package/.editorconfig +18 -0
  2. package/.gulp.json +7 -0
  3. package/bin/release.mjs +47 -0
  4. package/dist/chunks/_arrayPush.js +168 -0
  5. package/dist/chunks/_arrayPush.js.map +1 -0
  6. package/dist/chunks/_baseRest.js +73 -0
  7. package/dist/chunks/_baseRest.js.map +1 -0
  8. package/dist/chunks/_commonjsHelpers.js +7 -0
  9. package/dist/chunks/_commonjsHelpers.js.map +1 -0
  10. package/dist/chunks/_getPrototype.js +130 -0
  11. package/dist/chunks/_getPrototype.js.map +1 -0
  12. package/dist/chunks/button-radio.js +147 -0
  13. package/dist/chunks/button-radio.js.map +1 -0
  14. package/dist/chunks/checkboxes-multi-select.js +44 -0
  15. package/dist/chunks/checkboxes-multi-select.js.map +1 -0
  16. package/dist/chunks/cloneDeep.js +287 -0
  17. package/dist/chunks/cloneDeep.js.map +1 -0
  18. package/dist/chunks/cropper.min.js +5 -0
  19. package/dist/chunks/cropper.min.js.map +1 -0
  20. package/dist/chunks/field-cascade-select.js +256 -0
  21. package/dist/chunks/field-cascade-select.js.map +1 -0
  22. package/dist/chunks/field-file-drag.js +218 -0
  23. package/dist/chunks/field-file-drag.js.map +1 -0
  24. package/dist/chunks/field-flatpickr.js +893 -0
  25. package/dist/chunks/field-flatpickr.js.map +1 -0
  26. package/dist/chunks/field-modal-select.js +403 -0
  27. package/dist/chunks/field-modal-select.js.map +1 -0
  28. package/dist/chunks/field-modal-tree.js +790 -0
  29. package/dist/chunks/field-modal-tree.js.map +1 -0
  30. package/dist/chunks/field-multi-uploader.js +256 -0
  31. package/dist/chunks/field-multi-uploader.js.map +1 -0
  32. package/dist/chunks/field-repeatable.js +132 -0
  33. package/dist/chunks/field-repeatable.js.map +1 -0
  34. package/dist/chunks/field-single-image-drag.js +338 -0
  35. package/dist/chunks/field-single-image-drag.js.map +1 -0
  36. package/dist/chunks/form.js +154 -0
  37. package/dist/chunks/form.js.map +1 -0
  38. package/dist/chunks/grid.js +345 -0
  39. package/dist/chunks/grid.js.map +1 -0
  40. package/dist/chunks/http-client.js +229 -0
  41. package/dist/chunks/http-client.js.map +1 -0
  42. package/dist/chunks/iframe-modal.js +124 -0
  43. package/dist/chunks/iframe-modal.js.map +1 -0
  44. package/dist/chunks/index.js +309 -0
  45. package/dist/chunks/index.js.map +1 -0
  46. package/dist/chunks/isArguments.js +146 -0
  47. package/dist/chunks/isArguments.js.map +1 -0
  48. package/dist/chunks/keep-tab.js +101 -0
  49. package/dist/chunks/keep-tab.js.map +1 -0
  50. package/dist/chunks/legacy.js +210 -0
  51. package/dist/chunks/legacy.js.map +1 -0
  52. package/dist/chunks/list-dependent.js +231 -0
  53. package/dist/chunks/list-dependent.js.map +1 -0
  54. package/dist/chunks/s3-multipart-uploader.js +172 -0
  55. package/dist/chunks/s3-multipart-uploader.js.map +1 -0
  56. package/dist/chunks/s3-uploader.js +136 -0
  57. package/dist/chunks/s3-uploader.js.map +1 -0
  58. package/dist/chunks/show-on.js +237 -0
  59. package/dist/chunks/show-on.js.map +1 -0
  60. package/dist/chunks/tinymce.js +196 -0
  61. package/dist/chunks/tinymce.js.map +1 -0
  62. package/dist/chunks/ui-bootstrap5.js +71 -0
  63. package/dist/chunks/ui-bootstrap5.js.map +1 -0
  64. package/dist/chunks/unicorn.js +2202 -0
  65. package/dist/chunks/unicorn.js.map +1 -0
  66. package/dist/chunks/validation.js +854 -0
  67. package/dist/chunks/validation.js.map +1 -0
  68. package/dist/editor.css +1 -0
  69. package/dist/index.d.ts +1427 -0
  70. package/dist/multi-level-menu.css +1 -0
  71. package/dist/switcher.css +1 -0
  72. package/dist/unicorn-next.css +12 -0
  73. package/dist/unicorn.js +125 -0
  74. package/dist/unicorn.js.map +1 -0
  75. package/fusionfile.mjs +155 -0
  76. package/images/ajax-loader.gif +0 -0
  77. package/images/placeholder/avatar.png +0 -0
  78. package/images/placeholder/image-16x10.png +0 -0
  79. package/images/placeholder/image-16x9.png +0 -0
  80. package/images/placeholder/image-1x1.png +0 -0
  81. package/images/placeholder/image-4x3.png +0 -0
  82. package/package.json +102 -0
  83. package/scss/bootstrap/multi-level-menu.scss +121 -0
  84. package/scss/editor.scss +116 -0
  85. package/scss/field/file-drag.scss +102 -0
  86. package/scss/field/single-image-drag.scss +88 -0
  87. package/scss/field/vue-drag-uploader.scss +160 -0
  88. package/scss/switcher.scss +156 -0
  89. package/src/app.ts +128 -0
  90. package/src/bootstrap/button-radio.ts +208 -0
  91. package/src/bootstrap/keep-tab.ts +155 -0
  92. package/src/composable/index.ts +21 -0
  93. package/src/composable/useCheckboxesMultiSelect.ts +22 -0
  94. package/src/composable/useFieldCascadeSelect.ts +9 -0
  95. package/src/composable/useFieldFileDrag.ts +9 -0
  96. package/src/composable/useFieldFlatpickr.ts +3 -0
  97. package/src/composable/useFieldModalSelect.ts +6 -0
  98. package/src/composable/useFieldModalTree.ts +3 -0
  99. package/src/composable/useFieldMultiUploader.ts +3 -0
  100. package/src/composable/useFieldRepeatable.ts +9 -0
  101. package/src/composable/useFieldSingleImageDrag.ts +5 -0
  102. package/src/composable/useForm.ts +43 -0
  103. package/src/composable/useGrid.ts +57 -0
  104. package/src/composable/useHttp.ts +8 -0
  105. package/src/composable/useIframeModal.ts +9 -0
  106. package/src/composable/useListDependent.ts +26 -0
  107. package/src/composable/useQueue.ts +13 -0
  108. package/src/composable/useS3Uploader.ts +32 -0
  109. package/src/composable/useShowOn.ts +9 -0
  110. package/src/composable/useStack.ts +13 -0
  111. package/src/composable/useTinymce.ts +29 -0
  112. package/src/composable/useTomSelect.ts +72 -0
  113. package/src/composable/useUIBootstrap5.ts +48 -0
  114. package/src/composable/useUniDirective.ts +32 -0
  115. package/src/composable/useValidation.ts +39 -0
  116. package/src/data.ts +36 -0
  117. package/src/events.ts +73 -0
  118. package/src/legacy/legacy.ts +186 -0
  119. package/src/legacy/loader.ts +125 -0
  120. package/src/module/checkboxes-multi-select.ts +54 -0
  121. package/src/module/field-cascade-select.ts +292 -0
  122. package/src/module/field-file-drag.ts +292 -0
  123. package/src/module/field-flatpickr.ts +127 -0
  124. package/src/module/field-modal-select.ts +174 -0
  125. package/src/module/field-modal-tree.ts +27 -0
  126. package/src/module/field-multi-uploader.ts +361 -0
  127. package/src/module/field-repeatable.ts +202 -0
  128. package/src/module/field-single-image-drag.ts +468 -0
  129. package/src/module/form.ts +223 -0
  130. package/src/module/grid.ts +465 -0
  131. package/src/module/http-client.ts +243 -0
  132. package/src/module/iframe-modal.ts +167 -0
  133. package/src/module/list-dependent.ts +321 -0
  134. package/src/module/s3-multipart-uploader.ts +300 -0
  135. package/src/module/s3-uploader.ts +234 -0
  136. package/src/module/show-on.ts +173 -0
  137. package/src/module/tinymce.ts +263 -0
  138. package/src/module/ui-bootstrap5.ts +107 -0
  139. package/src/module/validation.ts +1019 -0
  140. package/src/plugin/index.ts +1 -0
  141. package/src/plugin/php-adapter.ts +65 -0
  142. package/src/polyfill/form-request-submit.ts +31 -0
  143. package/src/polyfill/index.ts +9 -0
  144. package/src/service/animate.ts +58 -0
  145. package/src/service/crypto.ts +27 -0
  146. package/src/service/dom-watcher.ts +62 -0
  147. package/src/service/dom.ts +265 -0
  148. package/src/service/helper.ts +48 -0
  149. package/src/service/index.ts +10 -0
  150. package/src/service/lang.ts +122 -0
  151. package/src/service/loader.ts +152 -0
  152. package/src/service/router.ts +118 -0
  153. package/src/service/ui.ts +497 -0
  154. package/src/service/uri.ts +106 -0
  155. package/src/types/base.ts +9 -0
  156. package/src/types/index.ts +4 -0
  157. package/src/types/modal-tree.ts +12 -0
  158. package/src/types/plugin.ts +6 -0
  159. package/src/types/shims.d.ts +18 -0
  160. package/src/types/ui.ts +6 -0
  161. package/src/unicorn.ts +63 -0
  162. package/src/utilities/arr.ts +25 -0
  163. package/src/utilities/base.ts +9 -0
  164. package/src/utilities/data.ts +48 -0
  165. package/src/utilities/index.ts +5 -0
  166. package/src/utilities/tree.ts +20 -0
  167. package/src/vue/components/ModalTree/ModalTreeApp.vue +175 -0
  168. package/src/vue/components/ModalTree/TreeItem.vue +262 -0
  169. package/src/vue/components/ModalTree/TreeModal.vue +225 -0
  170. package/tests/test.js +4 -0
  171. package/tsconfig.js.json +25 -0
  172. package/tsconfig.json +17 -0
  173. package/vite.assets.config.ts +61 -0
  174. package/vite.config.test.ts +36 -0
  175. package/vite.config.ts +112 -0
@@ -0,0 +1,54 @@
1
+ import { selectAll, selectOne } from '../service';
2
+
3
+ export class CheckboxesMultiSelect {
4
+ defaultOptions = {
5
+ duration: 100,
6
+ inputSelector: 'input[type=checkbox][data-role=grid-checkbox]'
7
+ }
8
+
9
+ $element: HTMLElement;
10
+ options: any;
11
+ boxes: HTMLInputElement[];
12
+ last: HTMLInputElement | false = false;
13
+
14
+ static handle(selector: any, options: any = {}) {
15
+ return selectAll(selector, (ele: any) => new this(ele, options));
16
+ }
17
+
18
+ constructor(selector: any, options = {}) {
19
+ this.$element = selectOne<HTMLElement>(selector)!;
20
+ this.options = Object.assign({}, this.defaultOptions, options);
21
+ this.boxes = Array.from(this.$element.querySelectorAll(this.options.inputSelector));
22
+ this.last = false;
23
+
24
+ selectAll(this.boxes, (box: HTMLInputElement) => {
25
+ box.addEventListener('click', (e: MouseEvent) => {
26
+ this.select(box, e);
27
+ });
28
+ });
29
+ }
30
+
31
+ select(box: HTMLInputElement, event: MouseEvent) {
32
+ if (!this.last) {
33
+ this.last = box;
34
+
35
+ return;
36
+ }
37
+
38
+ if (event.shiftKey) {
39
+ // @ts-ignore
40
+ const start = [].indexOf.call(this.boxes, box);
41
+
42
+ // @ts-ignore
43
+ const end = [].indexOf.call(this.boxes, this.last);
44
+
45
+ const chs = [].slice.call(this.boxes, Math.min(start, end), Math.max(start, end) + 1);
46
+
47
+ [].forEach.call(chs, (ele: HTMLInputElement, i) => {
48
+ ele.checked = (this.last as HTMLInputElement).checked;
49
+ });
50
+ }
51
+
52
+ this.last = box;
53
+ }
54
+ }
@@ -0,0 +1,292 @@
1
+ import AlpineComponent from '@rubenbimmel/alpine-class-component/dist/alpineComponent';
2
+ import Component from '@rubenbimmel/alpine-class-component/dist/decorators/component';
3
+ import { useHttpClient } from '../composable';
4
+ import { initAlpineComponent, module, prepareAlpineDefer, uid } from '../service';
5
+ import { mergeDeep } from '../utilities';
6
+
7
+ interface CascadeSelectOptions {
8
+ id: string;
9
+ selected: string;
10
+ path: string[];
11
+ ignoreSelf: boolean | null;
12
+ placeholder: string;
13
+ placeholders: string[];
14
+ ajaxUrl: string;
15
+ ajaxValueField: string;
16
+ source: string[];
17
+ labels: string[];
18
+ labelWidth: string;
19
+ fieldWidth: string;
20
+ readonly: boolean;
21
+ disabled: boolean;
22
+ valueField: string;
23
+ textField: string;
24
+ horizontal: boolean | null;
25
+ horizontalColWidth: string | null;
26
+ defaultValue: string;
27
+ onSelectInit: Function,
28
+ onChange: Function,
29
+ onValueInit: Function,
30
+ }
31
+
32
+ const defaultOptions = {
33
+ id: '',
34
+ selected: '',
35
+ path: [],
36
+ ignoreSelf: null,
37
+ placeholder: '- Select -',
38
+ placeholders: [],
39
+ ajaxUrl: '',
40
+ ajaxValueField: 'value',
41
+ source: [],
42
+ labels: [],
43
+ labelWidth: 'col-md-3',
44
+ fieldWidth: 'col',
45
+ readonly: false,
46
+ disabled: false,
47
+ valueField: 'id',
48
+ textField: 'title',
49
+ horizontal: null,
50
+ horizontalColWidth: null,
51
+ defaultValue: '',
52
+ onSelectInit: () => {
53
+ },
54
+ onChange: () => {
55
+ },
56
+ onValueInit: () => {
57
+ },
58
+ };
59
+
60
+ @Component
61
+ class FieldCascadeSelect extends AlpineComponent {
62
+ options: CascadeSelectOptions;
63
+ el?: HTMLElement;
64
+ canModify: boolean = false;
65
+ lists: any[] = [];
66
+ ajaxUrl: string = '';
67
+ values: Array<string | null> = [];
68
+
69
+ constructor(options: Partial<CascadeSelectOptions> = {}) {
70
+ super();
71
+
72
+ this.options = mergeDeep({}, defaultOptions, options);
73
+
74
+ this.options.id = this.options.id || 'cascade-select-' + uid();
75
+ }
76
+
77
+ init() {
78
+ this.canModify = !this.options.readonly && !this.options.disabled;
79
+ this.ajaxUrl = this.options.ajaxUrl;
80
+ this.values = this.options.path.slice().map(String);
81
+
82
+ let values: Array<string | null> = this.options.path.slice();
83
+
84
+ if (values.length === 0) {
85
+ values = [null];
86
+ } else {
87
+ values.unshift(null);
88
+ }
89
+
90
+ let promise = Promise.resolve();
91
+ let lastValue: string | null = null;
92
+
93
+ values.forEach((v, i) => {
94
+ promise = promise.then(() => {
95
+ return this.loadItems(v, i).then((list) => {
96
+ if (list.length > 0) {
97
+ this.lists.push(list);
98
+ }
99
+ });
100
+ });
101
+
102
+ lastValue = v;
103
+ });
104
+
105
+ this.el = this.$el;
106
+
107
+ module(this.$el, 'cascade.select', () => this);
108
+
109
+ this.valueInit(this.$el, lastValue, values);
110
+ }
111
+
112
+ getLabel(i: number) {
113
+ return this.options.labels[i] || `Level ${i + 1}`;
114
+ }
115
+
116
+ getId(i: number) {
117
+ return `${this.options.id}__level-${i}`;
118
+ }
119
+
120
+ getListValue(i: number) {
121
+ return this.values[i] || '';
122
+ }
123
+
124
+ isSelected(i: number, item: any) {
125
+ return String(this.getListValue(i)) === String(item[this.options.valueField]);
126
+ }
127
+
128
+ getFinalValue() {
129
+ const values = this.values.slice();
130
+
131
+ if (values.length === 0) {
132
+ return this.options.defaultValue;
133
+ }
134
+
135
+ const v = values
136
+ .filter(v => v != null)
137
+ .filter(v => v !== '')
138
+ .pop();
139
+
140
+ if (v == undefined) {
141
+ return this.options.defaultValue;
142
+ }
143
+
144
+ return v;
145
+ }
146
+
147
+ getLevel() {
148
+ return this.values.length;
149
+ }
150
+
151
+ async onChange(i: number, event: Event) {
152
+ const el = event.target as HTMLSelectElement;
153
+
154
+ this.values[i] = el.value;
155
+
156
+ this.options.onChange(event);
157
+
158
+ event.stopPropagation();
159
+
160
+ const changeEvent = new CustomEvent('change', {
161
+ detail: {
162
+ el,
163
+ component: this,
164
+ value: el.value,
165
+ path: this.values
166
+ }
167
+ });
168
+
169
+ this.el?.dispatchEvent(changeEvent);
170
+
171
+ if (el.value === '') {
172
+ // Clear child
173
+ this.lists.splice(i + 1);
174
+ this.values.splice(i + 1);
175
+ return;
176
+ }
177
+
178
+ // Get child list
179
+ let list = await this.loadItems(el.value, i);
180
+ this.lists.splice(i + 1);
181
+ this.values.splice(i + 1);
182
+ if (list.length > 0) {
183
+ this.lists.push(list);
184
+ }
185
+ }
186
+
187
+ async loadItems(parentId: string | null, i: number) {
188
+ // Ajax
189
+ if (this.ajaxUrl) {
190
+ const http = await useHttpClient();
191
+
192
+ let res = await http.get(
193
+ this.ajaxUrl,
194
+ {
195
+ params: {
196
+ [this.options.ajaxValueField]: parentId,
197
+ self: this.options.ignoreSelf || null
198
+ }
199
+ }
200
+ );
201
+ return await res.data.data;
202
+ }
203
+
204
+ // Source
205
+ if (parentId) {
206
+ return Promise.resolve(
207
+ this.handleSourceItems(
208
+ this.findFromList(this.lists[i - 1] || [], parentId)?.children || []
209
+ )
210
+ );
211
+ }
212
+
213
+ return Promise.resolve(this.handleSourceItems(this.options.source));
214
+ }
215
+
216
+ valueInit($select: HTMLElement, value: string | null, path: Array<string | null>) {
217
+ const event = new CustomEvent('value.init', {
218
+ detail: {
219
+ el: $select,
220
+ component: this,
221
+ value,
222
+ path
223
+ }
224
+ });
225
+
226
+ this.options.onSelectInit(event);
227
+
228
+ this.$el.dispatchEvent(event);
229
+ }
230
+
231
+ selectInit($select: HTMLElement) {
232
+ const event = new CustomEvent('select.init', {
233
+ detail: {
234
+ el: $select,
235
+ component: this,
236
+ }
237
+ });
238
+
239
+ this.options.onSelectInit(event);
240
+
241
+ this.$el.dispatchEvent(event);
242
+ }
243
+
244
+ handleSourceItems(items: any[]) {
245
+ return items.map(item => {
246
+ return {
247
+ [this.options.valueField]: item.value[this.options.valueField],
248
+ [this.options.textField]: item.value[this.options.textField],
249
+ children: item.children
250
+ };
251
+ })
252
+ .filter(item => {
253
+ if (this.options.ignoreSelf) {
254
+ return item[this.options.valueField] != this.options.ignoreSelf;
255
+ }
256
+
257
+ return item;
258
+ });
259
+ }
260
+
261
+ findFromList(items: any[], value: string) {
262
+ const found = items.filter(item => item[this.options.valueField] == value);
263
+
264
+ return found.shift();
265
+ }
266
+
267
+ getPlaceholder(i: number) {
268
+ if (this.options.placeholders[i]) {
269
+ return this.options.placeholders[i];
270
+ }
271
+
272
+ return this.options.placeholder;
273
+ }
274
+ }
275
+
276
+ declare global {
277
+ var S: any;
278
+ }
279
+
280
+ async function init() {
281
+ await prepareAlpineDefer(() => {
282
+ Alpine.data('CascadeSelect', (options: CascadeSelectOptions) => new FieldCascadeSelect(options));
283
+ });
284
+
285
+ await initAlpineComponent('data-cascade-select');
286
+ }
287
+
288
+ export const ready = init();
289
+
290
+ export interface CascadeSelectModule {
291
+ ready: typeof ready;
292
+ }
@@ -0,0 +1,292 @@
1
+ import css from '../../scss/field/file-drag.scss?inline';
2
+ import { useUniDirective } from '../composable';
3
+ import { __, html, injectCssToDocument, simpleAlert, uid, watchAttributes } from '../service';
4
+ import { mergeDeep } from '../utilities';
5
+
6
+ injectCssToDocument(document, css);
7
+
8
+ export interface FileDragOptions {
9
+ maxFiles: number | undefined;
10
+ maxSize: number | undefined;
11
+ placeholder: string;
12
+ height: number;
13
+ }
14
+
15
+ const defaultOptions: FileDragOptions = {
16
+ maxFiles: undefined,
17
+ maxSize: undefined,
18
+ placeholder: '',
19
+ height: 125,
20
+ }
21
+
22
+ export class FileDragElement extends HTMLElement {
23
+ static is = 'uni-file-drag';
24
+
25
+ element!: HTMLInputElement;
26
+ overlayLabel!: HTMLLabelElement;
27
+ button!: HTMLButtonElement;
28
+ options!: FileDragOptions;
29
+
30
+ get inputSelector() {
31
+ return this.getAttribute('selector') || 'input[type=file]';
32
+ }
33
+
34
+ get multiple() {
35
+ return this.element.multiple;
36
+ }
37
+
38
+ connectedCallback(): void {
39
+ this.element = this.querySelector(this.inputSelector)!;
40
+
41
+ this.prepareElements();
42
+
43
+ const options = JSON.parse(this.getAttribute('options') || '{}') || {};
44
+
45
+ const observer = watchAttributes(this.element);
46
+ observer.watch('readonly', (el) => {
47
+ el.disabled = el.readOnly;
48
+ });
49
+
50
+ if (this.element.readOnly) {
51
+ this.element.disabled = true;
52
+ }
53
+
54
+ this.options = mergeDeep({}, defaultOptions, options);
55
+
56
+ this.bindEvent();
57
+
58
+ this.style.visibility = '';
59
+
60
+ this.style.height = (this.options.height || 100) + 'px';
61
+ }
62
+
63
+ bindEvent() {
64
+ this.element.addEventListener('dragover', () => {
65
+ this.element.classList.add('hover');
66
+ });
67
+
68
+ this.element.addEventListener('dragleave', () => {
69
+ this.element.classList.remove('hover');
70
+ });
71
+
72
+ this.element.addEventListener('drop', () => {
73
+ this.element.classList.remove('hover');
74
+ });
75
+
76
+ this.onChange();
77
+
78
+ this.element.addEventListener('change', (e) => {
79
+ this.onChange(e);
80
+ });
81
+ this.element.addEventListener('input', (e) => {
82
+ this.onChange(e);
83
+ });
84
+ }
85
+
86
+ prepareElements() {
87
+ if (this.children.length === 0) {
88
+ this.createElementsLayout();
89
+ }
90
+
91
+ this.overlayLabel = this.querySelector<HTMLLabelElement>('[data-overlay-label]')!;
92
+
93
+ let button = this.overlayLabel.querySelector('button');
94
+
95
+ // B/C for new file drag style
96
+ if (!button) {
97
+ button = document.createElement('button');
98
+ button.type = 'button';
99
+ button.setAttribute('class', 'c-file-drag-input__button btn btn-success btn-sm px-2 py-1');
100
+ button.innerText = __('unicorn.field.file.drag.button.text');
101
+ this.overlayLabel.appendChild(button);
102
+ }
103
+
104
+ this.button = button;
105
+ }
106
+
107
+ createElementsLayout() {
108
+ this.id ||= 'c-file-drag-' + uid();
109
+ const name = this.getAttribute('name') || 'file';
110
+ const inputId = this.id + '__input';
111
+ const btnText = __('unicorn.field.file.drag.button.text');
112
+
113
+ const input = html(`<input id="${inputId}" type="file" name="${name}" />`);
114
+ const label = html(`<label class="px-3 c-file-drag-input__label"
115
+ data-overlay-label
116
+ for="${inputId}">
117
+ <span class="label-text d-block">
118
+ <span class="fa fa-upload"></span>
119
+ </span>
120
+ <button type="button" class="c-file-drag-input__button btn btn-success btn-sm px-2 py-1">
121
+ ${btnText}
122
+ </button>
123
+ </label>`);
124
+
125
+ this.element = input as HTMLInputElement;
126
+ this.overlayLabel = label as HTMLLabelElement;
127
+
128
+ this.appendChild(input);
129
+ this.appendChild(label);
130
+ }
131
+
132
+ onChange(evt?: Event) {
133
+ const files = this.element.files || [];
134
+ const limit = this.options.maxFiles;
135
+ const maxSize = this.options.maxSize;
136
+ let placeholder = this.options.placeholder;
137
+
138
+ const accepted = (this.element.getAttribute('accept') || this.element.getAttribute('data-accepted') || '')
139
+ .split(',')
140
+ .map(v => v.trim())
141
+ .filter(v => v.length > 0)
142
+ .map(v => {
143
+ if (v.indexOf('/') === -1 && v[0] === '.') {
144
+ return v.substring(1);
145
+ }
146
+
147
+ return v;
148
+ });
149
+
150
+ let text: string;
151
+
152
+ if (!placeholder) {
153
+ if (this.multiple) {
154
+ placeholder = __('unicorn.field.file.drag.placeholder.multiple');
155
+ } else {
156
+ placeholder = __('unicorn.field.file.drag.placeholder.single');
157
+ }
158
+ }
159
+
160
+ // Files limit
161
+ if (limit && files.length > limit) {
162
+ this.alert(__('unicorn.field.file.drag.message.max.files', limit), '', 'warning');
163
+ evt?.preventDefault();
164
+ return;
165
+ }
166
+
167
+ // Files size
168
+ let fileSize = 0;
169
+ Array.prototype.forEach.call(files, file => {
170
+ this.checkFileType(accepted, file);
171
+
172
+ fileSize += file.size;
173
+ });
174
+
175
+ if (maxSize && (fileSize / 1024 / 1024) > maxSize) {
176
+ this.alert(
177
+ __(
178
+ 'unicorn.field.file.drag.message.max.size',
179
+ maxSize < 1 ? (maxSize * 1024) + 'KB' : maxSize + 'MB'
180
+ ),
181
+ '',
182
+ 'warning'
183
+ );
184
+ evt?.preventDefault();
185
+ return;
186
+ }
187
+
188
+ if (files.length > 1) {
189
+ text = `<span class="fa fa-files fa-copy"></span> ${__('unicorn.field.file.drag.selected', files.length)}`;
190
+ } else if (files.length === 1) {
191
+ text = `<span class="fa fa-file"></span> ${files[0].name}`;
192
+ } else {
193
+ text = `<span class="fa fa-upload"></span> ${placeholder}`;
194
+ }
195
+
196
+ //replace the "Choose a file" label
197
+ this.overlayLabel.querySelector<HTMLSpanElement>('span')!.innerHTML = text;
198
+ }
199
+
200
+ checkFileType(accepted: string[], file: File) {
201
+ const fileExt = file.name.split('.').pop() || '';
202
+
203
+ if (accepted.length) {
204
+ let allow = false;
205
+
206
+ accepted.forEach((type) => {
207
+ if (allow) {
208
+ return;
209
+ }
210
+
211
+ if (type.indexOf('/') !== -1) {
212
+ if (this.compareMimeType(type, file.type)) {
213
+ allow = true;
214
+ }
215
+ } else {
216
+ if (type.toLowerCase() === fileExt.toLowerCase()) {
217
+ allow = true;
218
+ }
219
+ }
220
+ });
221
+
222
+ if (!allow) {
223
+ this.alert(
224
+ __('unicorn.field.file.drag.message.unaccepted.files'),
225
+ __('unicorn.field.file.drag.message.unaccepted.files.desc', accepted.join(', ')),
226
+ 'warning'
227
+ );
228
+ throw new Error('Not accepted file ext');
229
+ }
230
+ }
231
+ }
232
+
233
+ compareMimeType(accepted: string, mime: string) {
234
+ const accepted2 = accepted.split('/');
235
+ const mime2 = mime.split('/');
236
+
237
+ if (accepted2[1] === '*') {
238
+ return accepted2[0] === mime2[0];
239
+ }
240
+
241
+ return accepted === mime;
242
+ }
243
+
244
+ async alert(title: string, text: string = '', type: string = 'info') {
245
+ await simpleAlert(title, text, type);
246
+ }
247
+ }
248
+
249
+ customElements.define(FileDragElement.is, FileDragElement);
250
+
251
+ export const ready = useUniDirective('file-drag-field', {
252
+ mounted(el) {
253
+ const input = el.querySelector<HTMLInputElement>('input[type=file]')!;
254
+ const placeholderInput = el.querySelector<HTMLInputElement>('[data-role=placeholder]')!;
255
+
256
+ const preview = el.querySelector('.c-file-drag-preview');
257
+
258
+ if (preview) {
259
+ const previewLink = preview.querySelector<HTMLAnchorElement>('.c-file-drag-preview__link')!;
260
+ const delButton = preview.querySelector<HTMLAnchorElement>('.c-file-drag-preview__delete')!;
261
+ // let linkTitle = previewLink.textContent;
262
+ let inputValue = placeholderInput.value;
263
+ let required = input.required;
264
+
265
+ if (placeholderInput.value) {
266
+ input.required = false;
267
+ }
268
+
269
+ delButton.addEventListener('click', () => {
270
+ if (delButton.classList.contains('active')) {
271
+ // Restore
272
+ previewLink.style.textDecoration = '';
273
+ previewLink.style.setProperty('color', '');
274
+ placeholderInput.value = inputValue;
275
+ delButton.classList.remove('active');
276
+ input.required = false;
277
+ } else {
278
+ // Delete
279
+ previewLink.style.textDecoration = 'line-through';
280
+ previewLink.style.color = 'var(--fd-delete-color, var(--bs-danger))';
281
+ placeholderInput.value = '';
282
+ delButton.classList.add('active');
283
+ input.required = required;
284
+ }
285
+ });
286
+ }
287
+ }
288
+ });
289
+
290
+ export interface FileDragModule {
291
+ FileDragElement: typeof FileDragElement;
292
+ }