PageSourceSearch

https://funeraldirectors.co.nz/_resources/co-cmd--core/client/java…/forms/GeccoUploadField.js?m=1781732799

js funeraldirectors.co.nz collected 2026-10-06 13:41:41 UTC 9,392 bytes, 219 lines download raw bytes

1// TODO: may be able to remove the uid requirement on GeccoUploadField.php if
2// we return the silverstripe file ID after upload and track it in javascript
3
4class FileHandler {
5    constructor(container) {
6        this.container = container;
7        this.input = container.querySelector('input');
8        this.maxFiles = this.input.getAttribute('data-max-files');
9        this.files = [];
10        this.currentFilesContainer = container.querySelector('.GeccoUploadCurrent');
11        this.init();
12    }
13
14    init() {
15        this.updateUploadedFiles();
16
17        this.container.addEventListener('click', this.handleDelete.bind(this));
18        this.container.addEventListener('drop', this.handleDrop.bind(this));
19        this.container.addEventListener('dragenter', (e) => { this.container.classList.add('dragover'); });
20        this.container.addEventListener('dragover',  (e) => { e.preventDefault(); });
21        this.container.addEventListener('dragleave', (e) => {
22            // Only remove .dragover if the drag is moving to an element outside the container
23            if (!e.relatedTarget || !this.container.contains(e.relatedTarget)) {
24                this.container.classList.remove('dragover');
25            }
26        });
27        this.input.addEventListener('change', this.handleChange.bind(this));
28
29        const form = this.getForm();
30        if (form) form.addEventListener('submit', this.handleSubmit.bind(this));
31    }
32
33    getForm() {
34        let form = this.input.closest('form') || this.input.closest('.htmx_form_container')?.querySelector('form');
35        return form;
36    }
37
38    getInputURL(action) {
39        const formAction = this.getForm().getAttribute('action');
40        let fieldName = this.input.getAttribute('name').replace('[]', '');
41        const baseURL = new URL(formAction, window.location.href);
42        baseURL.pathname = `${baseURL.pathname.replace(/\/$/, '')}/field/${fieldName}/${action}`;
43        return baseURL.toString();
44    }
45
46    renderFile(file) {
47        let html = `<li class='d-flex justify-content-between my-1 border-bottom'>`;
48        if (file.status === 'failed') {
49            html += `
50                <div class='d-flex flex-grow-1 align-items-center text-break'>${file.name}</div>
51                <div class='d-flex flex-grow-1 align-items-center ml-2 text-danger'>Upload failed: ${file.errorMessage}</div>
52                <div class='text-danger btn d-flex align-items-center' data-delete-file-type='uploading' data-delete-file-name='${file.name}' data-toggle='tooltip' title='Remove file'>
53                    <i class='fas fa-times'></i>
54                </div>`;
55        } else if (file.status === 'uploading') {
56            html += `
57                <div class='d-flex flex-grow-1 align-items-center text-break'>${file.name}</div>
58                <div class='d-flex flex-grow-1 align-items-center ml-2 text-success'>Uploading: ${file.uploadPercent.toFixed(2)}%</div>`;
59        } else if (file.status === 'uploaded') {
60            html += `
61                <div class='gecco-upload-thumbnail-container'><img class='img-fluid gecco-upload-thumbnail' src='${file.thumbnailURL}' alt='${file.extension}'></div>
62                <div class='file-uploaded d-flex align-items-center flex-grow-1 pl-2 text-break'>
63                    <a href='${file.downloadLink}' target='_blank'>${file.name}</a>
64                    <small class='text-muted ml-2'>${file.sizeNice}</small>
65                </div>
66                <div class='text-danger btn d-flex align-items-center' data-delete-file-type='uploaded' data-delete-file-name='${file.name}' data-toggle='tooltip' title='Remove file'>
67                    <i class='fas fa-times'></i>
68                </div>`;
69        }
70        html += `</li>`;
71        return html;
72    }
73
74    updateDisplay() {
75        let fileListHTML = '';
76        if (this.files.length) {
77            this.files.forEach(f => {
78                fileListHTML += this.renderFile(f);
79            });
80        } else {
81            fileListHTML += 'Nothing uploaded.';
82        }
83
84        this.container.querySelector('ul.GeccoUploadList').innerHTML = fileListHTML;
85    }
86
87    async updateUploadedFiles() {
88        this.currentFilesContainer.classList.add('loading');
89        const formData = new FormData(this.getForm());
90        try {
91            const response = await fetch(this.getInputURL('currentFiles'), {
92                method: 'POST',
93                headers: { 'Gecco-Upload': 'Gecco-Upload' },
94                body: formData
95            });
96            if (response.ok) {
97                const data = await response.json();
98                this.files = this.files.filter(f => f.status !== 'uploaded');
99                data.forEach(fileData => {
100                    this.files.push({
101                        status: 'uploaded',
102                        id: fileData.fileID,
103                        name: fileData.name,
104                        sizeMB: fileData.sizeMB,
105                        thumbnailURL: fileData.thumbnail || '',
106                        extension: fileData.extension,
107                        sizeNice: fileData.sizeNice,
108                        downloadLink: fileData.downloadLink
109                    });
110                });
111                this.currentFilesContainer.classList.toggle('has-values', data.length > 0);
112            }
113        } catch (e) {
114            console.log('Error fetching uploaded files:', e);
115        }
116        this.currentFilesContainer.classList.remove('loading');
117        this.updateDisplay();
118    }
119
120    async uploadFiles(fileList) {
121        fileList.forEach(file => {
122            if (!this.files.some(f => f.name === file.name)) {
123                this.files.push({ status: 'pending', name: file.name, uploadPercent: 0, file });
124            }
125        });
126        this.currentFilesContainer.classList.add('loading', 'has-values');
127        for (const fileObj of this.files.filter(f => f.status === 'pending')) {
128            fileObj.status = 'uploading';
129            const formData = new FormData(this.getForm());
130            formData.append('AJAXFile', fileObj.file);
131            const xhr = new XMLHttpRequest();
132            xhr.upload.onprogress = (event) => {
133                if (event.lengthComputable) {
134                    fileObj.uploadPercent = (event.loaded / event.total) * 100;
135                    this.updateDisplay();
136                }
137            };
138            xhr.onloadend = () => {
139                if (xhr.status === 200) {
140                    fileObj.status = 'uploaded';
141                    this.updateUploadedFiles();
142                } else {
143                    fileObj.status = 'failed';
144                    fileObj.errorMessage = xhr.responseText;
145                    this.updateDisplay();
146                }
147            };
148            xhr.open('POST', this.getInputURL('upload'), true);
149            xhr.setRequestHeader('Gecco-Upload', 'Gecco-Upload');
150            xhr.send(formData);
151        }
152        this.currentFilesContainer.classList.remove('loading');
153    }
154
155    handleDrop(event) {
156        event.preventDefault();
157        this.container.classList.remove('dragover');
158        if (event.dataTransfer.files) {
159            this.uploadFiles([...event.dataTransfer.files]);
160        }
161    }
162
163    handleChange(event) {
164        if (this.input.files) {
165            this.uploadFiles([...this.input.files]);
166            this.input.value = null;
167        }
168    }
169
170    handleDelete(event) {
171        const button = event.target.closest('[data-delete-file-type]');
172        if (!button) return;
173        const type = button.getAttribute('data-delete-file-type');
174        const name = button.getAttribute('data-delete-file-name');
175        if (type === 'uploading') {
176            this.files = this.files.filter(f => f.name !== name);
177            this.updateDisplay();
178        } else if (type === 'uploaded') {
179            const index = this.files.findIndex(f => f.name === name);
180            if (index !== -1) {
181                const formData = new FormData(this.getForm());
182                formData.append('fileIndex', index);
183                fetch(this.getInputURL('remove'), {
184                    method: 'POST',
185                    headers: { 'Gecco-Upload': 'Gecco-Upload' },
186                    body: formData
187                }).then(() => this.updateUploadedFiles());
188            }
189        }
190    }
191
192    handleSubmit() {
193        const uploadedIDs = this.files.filter(f => f.status === 'uploaded').map(f => f.id);
194        const textInput = document.createElement('input');
195        textInput.type = 'text';
196        textInput.name = this.input.getAttribute('name');
197        textInput.className = 'text form-control';
198        textInput.value = JSON.stringify(uploadedIDs);
199        this.input.parentNode.replaceChild(textInput, this.input);
200    }
201}
202
203document.addEventListener('DOMContentLoaded', () => {
204    document.querySelectorAll('.GeccoUploadContainer').forEach(el => {
205        el.fileHandler = new FileHandler(el);
206    });
207});
208
209new MutationObserver(mutations => {
210    mutations.forEach(mutation => {
211        mutation.addedNodes.forEach(node => {
212            if (node.querySelectorAll) {
213                node.querySelectorAll(':scope .GeccoUploadContainer').forEach(el => {
214                    if (!el.fileHandler) el.fileHandler = new FileHandler(el);
215                });
216            }
217        });
218    });
219}).observe(document.body, { subtree: true, childList: true });

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.