|
1 | 1 | import { Controller } from '@hotwired/stimulus';
|
2 | 2 |
|
3 | 3 | class default_1 extends Controller {
|
| 4 | + constructor() { |
| 5 | + super(...arguments); |
| 6 | + this.files = new Map(); |
| 7 | + } |
4 | 8 | initialize() {
|
5 | 9 | this.clear = this.clear.bind(this);
|
6 | 10 | this.onInputChange = this.onInputChange.bind(this);
|
7 |
| - this.onDragEnter = this.onDragEnter.bind(this); |
8 | 11 | this.onDragLeave = this.onDragLeave.bind(this);
|
| 12 | + this.onDragOver = this.onDragOver.bind(this); |
| 13 | + this.onDrop = this.onDrop.bind(this); |
| 14 | + this.onPreviewButtonClick = this.onPreviewButtonClick.bind(this); |
| 15 | + this.onPreviewContainerClick = this.onPreviewContainerClick.bind(this); |
9 | 16 | }
|
10 | 17 | connect() {
|
11 | 18 | this.clear();
|
12 |
| - this.previewClearButtonTarget.addEventListener('click', this.clear); |
13 | 19 | this.inputTarget.addEventListener('change', this.onInputChange);
|
14 |
| - this.element.addEventListener('dragenter', this.onDragEnter); |
15 | 20 | this.element.addEventListener('dragleave', this.onDragLeave);
|
| 21 | + this.element.addEventListener('dragover', this.onDragOver); |
| 22 | + this.element.addEventListener('drop', this.onDrop); |
| 23 | + if (!this.isLegacy && this.optionsValue.preview.can_open_file_picker) { |
| 24 | + this.previewContainerTarget.addEventListener('click', this.onPreviewContainerClick); |
| 25 | + } |
16 | 26 | this.dispatchEvent('connect');
|
17 | 27 | }
|
18 | 28 | disconnect() {
|
19 |
| - this.previewClearButtonTarget.removeEventListener('click', this.clear); |
| 29 | + this.clear(); |
20 | 30 | this.inputTarget.removeEventListener('change', this.onInputChange);
|
21 |
| - this.element.removeEventListener('dragenter', this.onDragEnter); |
22 | 31 | this.element.removeEventListener('dragleave', this.onDragLeave);
|
| 32 | + this.element.removeEventListener('dragover', this.onDragOver); |
| 33 | + this.element.removeEventListener('drop', this.onDrop); |
| 34 | + if (!this.isLegacy && this.optionsValue.preview.can_open_file_picker) { |
| 35 | + this.previewContainerTarget.removeEventListener('click', this.onPreviewContainerClick); |
| 36 | + } |
23 | 37 | }
|
24 | 38 | clear() {
|
25 |
| - this.inputTarget.value = ''; |
26 |
| - this.inputTarget.style.display = 'block'; |
27 |
| - this.placeholderTarget.style.display = 'block'; |
28 |
| - this.previewTarget.style.display = 'none'; |
29 |
| - this.previewImageTarget.style.display = 'none'; |
30 |
| - this.previewImageTarget.style.backgroundImage = 'none'; |
31 |
| - this.previewFilenameTarget.textContent = ''; |
| 39 | + this.files.clear(); |
| 40 | + this.updateFileInput(); |
| 41 | + this.refreshPreview(); |
| 42 | + this.element.classList.remove('dropzone-active'); |
| 43 | + if (this.isLegacy) { |
| 44 | + this.showLegacyFileInput(); |
| 45 | + } |
32 | 46 | this.dispatchEvent('clear');
|
33 | 47 | }
|
34 | 48 | onInputChange(event) {
|
35 |
| - const file = event.target.files[0]; |
36 |
| - if (typeof file === 'undefined') { |
| 49 | + const files = Array.from(event.target.files).filter((file) => typeof file !== 'undefined'); |
| 50 | + if (files.length === 0) { |
37 | 51 | return;
|
38 | 52 | }
|
39 |
| - this.inputTarget.style.display = 'none'; |
40 |
| - this.placeholderTarget.style.display = 'none'; |
41 |
| - this.previewFilenameTarget.textContent = file.name; |
42 |
| - this.previewTarget.style.display = 'flex'; |
43 |
| - this.previewImageTarget.style.display = 'none'; |
44 |
| - if (file.type && file.type.indexOf('image') !== -1) { |
45 |
| - this._populateImagePreview(file); |
| 53 | + this.files.clear(); |
| 54 | + this.addFiles(files); |
| 55 | + this.refreshPreview(); |
| 56 | + this.dispatchEvent('change', this.isLegacy ? this.firstFile : Array.from(this.files.values())); |
| 57 | + } |
| 58 | + onDragLeave(event) { |
| 59 | + event.preventDefault(); |
| 60 | + if (!this.element.contains(event.relatedTarget)) { |
| 61 | + this.element.classList.remove('dropzone-active'); |
| 62 | + if (this.isLegacy) { |
| 63 | + this.hideLegacyFileInput(); |
| 64 | + this.showLegacyPreview(); |
| 65 | + } |
| 66 | + } |
| 67 | + } |
| 68 | + onDragOver(event) { |
| 69 | + event.preventDefault(); |
| 70 | + this.element.classList.add('dropzone-active'); |
| 71 | + if (this.isLegacy) { |
| 72 | + this.hideLegacyPreview(); |
| 73 | + this.showLegacyFileInput(); |
46 | 74 | }
|
47 |
| - this.dispatchEvent('change', file); |
48 | 75 | }
|
49 |
| - _populateImagePreview(file) { |
50 |
| - if (typeof FileReader === 'undefined') { |
| 76 | + onDrop(event) { |
| 77 | + event.preventDefault(); |
| 78 | + const files = Array.from(event.dataTransfer.files).filter((file) => typeof file !== 'undefined'); |
| 79 | + if (files.length === 0) { |
51 | 80 | return;
|
52 | 81 | }
|
53 |
| - const reader = new FileReader(); |
54 |
| - reader.addEventListener('load', (event) => { |
55 |
| - this.previewImageTarget.style.display = 'block'; |
56 |
| - this.previewImageTarget.style.backgroundImage = `url("${event.target.result}")`; |
57 |
| - }); |
58 |
| - reader.readAsDataURL(file); |
| 82 | + if (!this.isMultiple) { |
| 83 | + this.files.clear(); |
| 84 | + } |
| 85 | + this.addFiles(files); |
| 86 | + this.updateFileInput(); |
| 87 | + this.refreshPreview(); |
| 88 | + this.element.classList.remove('dropzone-active'); |
| 89 | + this.dispatchEvent('change', Array.from(this.files.values())); |
59 | 90 | }
|
60 |
| - onDragEnter() { |
61 |
| - this.inputTarget.style.display = 'block'; |
62 |
| - this.placeholderTarget.style.display = 'block'; |
63 |
| - this.previewTarget.style.display = 'none'; |
| 91 | + onPreviewContainerClick(event) { |
| 92 | + event.stopPropagation(); |
| 93 | + this.inputTarget.click(); |
64 | 94 | }
|
65 |
| - onDragLeave(event) { |
66 |
| - event.preventDefault(); |
67 |
| - if (!this.element.contains(event.relatedTarget)) { |
68 |
| - this.inputTarget.style.display = 'none'; |
69 |
| - this.placeholderTarget.style.display = 'none'; |
70 |
| - this.previewTarget.style.display = 'block'; |
| 95 | + onPreviewButtonClick(event) { |
| 96 | + event.stopPropagation(); |
| 97 | + if (this.isLegacy) { |
| 98 | + return this.clear(); |
71 | 99 | }
|
| 100 | + const button = event.currentTarget; |
| 101 | + button.removeEventListener('click', this.onPreviewButtonClick); |
| 102 | + const preview = button.closest('.dropzone-preview'); |
| 103 | + preview.remove(); |
| 104 | + if (!button.dataset.filename) { |
| 105 | + return; |
| 106 | + } |
| 107 | + this.files.delete(button.dataset.filename); |
| 108 | + this.updateFileInput(); |
| 109 | + this.refreshPreview(); |
72 | 110 | }
|
73 | 111 | dispatchEvent(name, payload = {}) {
|
74 | 112 | this.dispatch(name, { detail: payload, prefix: 'dropzone' });
|
75 | 113 | }
|
| 114 | + addFiles(files) { |
| 115 | + for (const file of files) { |
| 116 | + this.files.set(file.name, file); |
| 117 | + } |
| 118 | + } |
| 119 | + buildPreview(file, el) { |
| 120 | + if (!el) { |
| 121 | + el = this.previewTemplateTarget.content.firstElementChild?.cloneNode(true); |
| 122 | + } |
| 123 | + const button = el.querySelector('.dropzone-preview-button'); |
| 124 | + if (button) { |
| 125 | + button.dataset.filename = file.name; |
| 126 | + button.addEventListener('click', this.onPreviewButtonClick); |
| 127 | + } |
| 128 | + const filename = el.querySelector('.dropzone-preview-filename'); |
| 129 | + if (filename) { |
| 130 | + filename.textContent = file.name; |
| 131 | + } |
| 132 | + const size = el.querySelector('.dropzone-preview-file-size'); |
| 133 | + if (size) { |
| 134 | + size.textContent = this.formatBytes(file.size); |
| 135 | + } |
| 136 | + const image = el.querySelector('.dropzone-preview-image'); |
| 137 | + if (image && this.isImage(file) && typeof FileReader !== 'undefined') { |
| 138 | + const reader = new FileReader(); |
| 139 | + image.classList.add('dropzone-preview-image-hidden'); |
| 140 | + reader.addEventListener('load', (event) => { |
| 141 | + image.querySelector('.dropzone-preview-image-placeholder')?.remove(); |
| 142 | + image.style.backgroundImage = `url('${event.target.result}')`; |
| 143 | + image.classList.remove('dropzone-preview-image-hidden'); |
| 144 | + }); |
| 145 | + reader.readAsDataURL(file); |
| 146 | + } |
| 147 | + return el; |
| 148 | + } |
| 149 | + refreshPreview() { |
| 150 | + if (this.isLegacy) { |
| 151 | + return this.refreshLegacyPreview(); |
| 152 | + } |
| 153 | + this.element.classList.add('dropzone-preview-container-hidden'); |
| 154 | + for (const preview of this.previewTargets) { |
| 155 | + preview |
| 156 | + .querySelector('.dropzone-preview-button') |
| 157 | + ?.removeEventListener('click', this.onPreviewButtonClick); |
| 158 | + preview.remove(); |
| 159 | + } |
| 160 | + for (const file of this.files.values()) { |
| 161 | + const preview = this.buildPreview(file); |
| 162 | + this.previewContainerTarget.appendChild(preview); |
| 163 | + } |
| 164 | + if (this.previewTargets.length > 0) { |
| 165 | + this.element.classList.remove('dropzone-preview-container-hidden'); |
| 166 | + } |
| 167 | + const canToggle = this.optionsValue.preview.can_toggle_placeholder; |
| 168 | + if (canToggle) { |
| 169 | + const hide = this.previewTargets.length > 0 && |
| 170 | + (canToggle === true || |
| 171 | + (canToggle === 'auto' && this.previewTargets.length < 2)); |
| 172 | + this.element.classList.toggle('dropzone-placeholder-hidden', hide); |
| 173 | + } |
| 174 | + } |
| 175 | + isImage(file) { |
| 176 | + return (typeof file.type !== 'undefined' && file.type.indexOf('image') !== -1); |
| 177 | + } |
| 178 | + get isMultiple() { |
| 179 | + return this.inputTarget.multiple; |
| 180 | + } |
| 181 | + updateFileInput() { |
| 182 | + const dataTransfer = new DataTransfer(); |
| 183 | + for (const file of this.files.values()) { |
| 184 | + dataTransfer.items.add(file); |
| 185 | + } |
| 186 | + this.inputTarget.files = dataTransfer.files; |
| 187 | + } |
| 188 | + formatBytes(bytes, decimals = 2) { |
| 189 | + if (bytes === 0) |
| 190 | + return '0 Bytes'; |
| 191 | + const k = 1024; |
| 192 | + const dm = decimals || 2; |
| 193 | + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; |
| 194 | + const i = Math.floor(Math.log(bytes) / Math.log(k)); |
| 195 | + return `${Number.parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`; |
| 196 | + } |
| 197 | + get firstFile() { |
| 198 | + return this.files.values().next().value; |
| 199 | + } |
| 200 | + get isLegacy() { |
| 201 | + return this.optionsValue.preview.style === 'legacy'; |
| 202 | + } |
| 203 | + refreshLegacyPreview() { |
| 204 | + const preview = this.previewTargets[0]; |
| 205 | + const image = preview.querySelector('.dropzone-preview-image'); |
| 206 | + const filename = preview.querySelector('.dropzone-preview-filename'); |
| 207 | + const file = this.firstFile; |
| 208 | + if (!file) { |
| 209 | + this.hideLegacyPreview(); |
| 210 | + if (filename) { |
| 211 | + filename.textContent = ''; |
| 212 | + } |
| 213 | + if (image) { |
| 214 | + image.style.display = 'none'; |
| 215 | + image.style.backgroundImage = 'none'; |
| 216 | + } |
| 217 | + return; |
| 218 | + } |
| 219 | + this.buildPreview(file, preview); |
| 220 | + const fileCount = this.files.size; |
| 221 | + if (filename && fileCount > 1) { |
| 222 | + filename.textContent += ` +${fileCount - 1}`; |
| 223 | + filename.title = Array.from(this.files.values()) |
| 224 | + .map((file) => file.name) |
| 225 | + .join('\n'); |
| 226 | + } |
| 227 | + if (image) { |
| 228 | + if (this.isImage(file)) { |
| 229 | + image.style.display = 'block'; |
| 230 | + } |
| 231 | + else { |
| 232 | + image.style.display = 'none'; |
| 233 | + image.style.backgroundImage = 'none'; |
| 234 | + } |
| 235 | + } |
| 236 | + this.showLegacyPreview(); |
| 237 | + this.hideLegacyFileInput(); |
| 238 | + } |
| 239 | + showLegacyPreview() { |
| 240 | + this.previewTargets[0].style.display = 'flex'; |
| 241 | + } |
| 242 | + hideLegacyPreview() { |
| 243 | + this.previewTargets[0].style.display = 'none'; |
| 244 | + } |
| 245 | + showLegacyFileInput() { |
| 246 | + this.inputTarget.style.display = 'block'; |
| 247 | + this.placeholderTarget.style.display = 'block'; |
| 248 | + } |
| 249 | + hideLegacyFileInput() { |
| 250 | + this.inputTarget.style.display = 'none'; |
| 251 | + this.placeholderTarget.style.display = 'none'; |
| 252 | + } |
76 | 253 | }
|
77 |
| -default_1.targets = ['input', 'placeholder', 'preview', 'previewClearButton', 'previewFilename', 'previewImage']; |
| 254 | +default_1.values = { |
| 255 | + options: { |
| 256 | + type: Object, |
| 257 | + default: { |
| 258 | + preview: { |
| 259 | + style: 'legacy', |
| 260 | + can_open_file_picker: true, |
| 261 | + can_toggle_placeholder: true, |
| 262 | + }, |
| 263 | + }, |
| 264 | + }, |
| 265 | +}; |
| 266 | +default_1.targets = [ |
| 267 | + 'input', |
| 268 | + 'placeholder', |
| 269 | + 'preview', |
| 270 | + 'previewContainer', |
| 271 | + 'previewTemplate', |
| 272 | +]; |
78 | 273 |
|
79 | 274 | export { default_1 as default };
|
0 commit comments