PageSourceSearch

https://freelance.ru/assets/db7ac538/attachment.js?v=1790077317

js freelance.ru collected 2026-09-24 08:27:02 UTC 9,545 bytes, 235 lines download raw bytes

1
2// Заполняется при загрузке страницы
3const taskFormConfig = {
4    formId: '',
5    sFileRemoveUrl: '',
6    sFileUploadUrl: '',
7    aImagesIds: [],
8    iImagesUploadLimit: 0,
9    allowedExtensions: [],
10};
11
12function esc(s) {
13    return String(s).replace(/[&<>"]/g, c => ({'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;'}[c]));
14}
15
16const oFiles = {
17    aFilesIds: [],
18    count() { return this.aFilesIds.length; },
19    uploadCount() { return Math.max(0, taskFormConfig.iImagesUploadLimit - this.count()); },
20    add(iFileId) {
21        if (!this.uploadCount()) return;
22        iFileId = iFileId * 1;
23        for (let i = 0; i < this.aFilesIds.length; i++) {
24            if (this.aFilesIds[i] === iFileId) return;
25        }
26        this.aFilesIds.push(iFileId);
27    },
28    del(iFileId) {
29        if (!this.count()) return;
30        iFileId = iFileId * 1;
31        this.aFilesIds = this.aFilesIds.filter(id => id !== iFileId);
32    },
33    toString() { return JSON.stringify(this.aFilesIds); }
34};
35
36document.addEventListener('DOMContentLoaded', function () {
37    oFiles.aFilesIds = taskFormConfig.aImagesIds.slice();
38
39    const imagesSection = document.getElementById('imagesSection');
40    const fileInput = document.getElementById('taskFiles');
41    const previewContainer = document.getElementById('taskFilePreview');
42    const dropzone = document.getElementById('taskFileDropzone');
43
44    const allowedExtensions = taskFormConfig.allowedExtensions;
45    let pendingFiles = [];
46
47    // Прячем зону загрузки, когда достигнут лимит файлов.
48    function updateDropzone() {
49        if (!dropzone) return;
50        const total = oFiles.count() + pendingFiles.length;
51        dropzone.style.display = total >= taskFormConfig.iImagesUploadLimit ? 'none' : '';
52    }
53
54    // Удаление существующих файлов
55    if (imagesSection) {
56        imagesSection.addEventListener('click', function (e) {
57            const btn = e.target.closest('.remove-file');
58            if (!btn) return;
59            e.preventDefault();
60
61            const fileId = btn.getAttribute('data-fileid');
62
63            swal({
64                title: 'Удалить файл?',
65                icon: 'warning',
66                buttons: ['Отмена', 'Удалить'],
67                dangerMode: true,
68            }).then(function (confirmed) {
69                if (!confirmed) return;
70
71                btn.disabled = true;
72
73                const fd = new FormData();
74                fd.append(yii.getCsrfParam(), yii.getCsrfToken());
75
76                fetch(taskFormConfig.sFileRemoveUrl + '?' + new URLSearchParams({iFileId: fileId}), {
77                    method: 'POST',
78                    body: fd,
79                    headers: {'X-Requested-With': 'XMLHttpRequest'}
80                })
81                    .then(r => r.json())
82                    .then(response => {
83                        if (!response.success) {
84                            alert(response.message || 'Ошибка удаления');
85                            btn.disabled = false;
86                            return;
87                        }
88
89                        const tile = document.getElementById('fileId-' + fileId);
90                        if (!tile) return;
91                        tile.style.transition = 'opacity .3s';
92                        tile.style.opacity = '0';
93                        setTimeout(() => tile.remove(), 300);
94
95                        oFiles.del(parseInt(fileId, 10));
96                        updateDropzone();
97                    });
98            });
99        });
100    }
101
102    function processFiles(files) {
103        const available = oFiles.uploadCount() - pendingFiles.length;
104        if (available <= 0 || files.length > available) {
105            alert('Можно прикрепить ещё не более ' + available + ' файл(ов). Лимит: ' + taskFormConfig.iImagesUploadLimit + ', уже загружено: ' + oFiles.count() + ', выбрано: ' + pendingFiles.length + '.');
106            return;
107        }
108
109        for (let i = 0; i < files.length; i++) {
110            const ext = files[i].name.split('.').pop().toLowerCase();
111            if (!allowedExtensions.includes(ext)) {
112                alert('Файл ' + files[i].name + ' имеет недопустимый формат!');
113                return;
114            }
115        }
116
117        pendingFiles = pendingFiles.concat(Array.from(files));
118
119        files.forEach(function (file) {
120            const wrapper = document.createElement('div');
121            wrapper.className = 'tfile tfile--pending';
122            wrapper._pendingFile = file;
123            const ext = esc((file.name.split('.').pop() || 'file').toUpperCase());
124            const name = esc(file.name);
125
126            function bindRemove() {
127                wrapper.querySelector('.tfile__remove').addEventListener('click', function () {
128                    pendingFiles = pendingFiles.filter(f => f !== file);
129                    wrapper.remove();
130                    updateDropzone();
131                });
132            }
133
134            if (file.type.startsWith('image/')) {
135                const reader = new FileReader();
136                reader.onload = function (e) {
137                    wrapper.innerHTML = `
138                        <div class="tfile__card">
139                            <div class="tfile__open"><img src="${e.target.result}" class="tfile__img" alt=""></div>
140                            <button type="button" class="tfile__remove" title="Удалить"><i class="fa fa-times" aria-hidden="true"></i></button>
141                        </div>
142                        <div class="tfile__name" title="${name}">${name}</div>`;
143                    bindRemove();
144                };
145                reader.readAsDataURL(file);
146            } else {
147                wrapper.innerHTML = `
148                    <div class="tfile__card">
149                        <div class="tfile__open"><span class="tfile__doc"><i class="fa fa-file-o" aria-hidden="true"></i><span class="tfile__ext">${ext}</span></span></div>
150                        <button type="button" class="tfile__remove" title="Удалить"><i class="fa fa-times" aria-hidden="true"></i></button>
151                    </div>
152                    <div class="tfile__name" title="${name}">${name}</div>`;
153                bindRemove();
154            }
155
156            previewContainer.appendChild(wrapper);
157        });
158
159        updateDropzone();
160    }
161
162    // Drag-over эффект на dropzone
163    if (dropzone && fileInput) {
164        let dragCounter = 0;
165
166        dropzone.addEventListener('dragenter', e => { e.preventDefault(); if (++dragCounter === 1) dropzone.classList.add('is-dragover'); });
167        dropzone.addEventListener('dragleave', e => { e.preventDefault(); if (--dragCounter === 0) dropzone.classList.remove('is-dragover'); });
168        dropzone.addEventListener('dragover',  e => e.preventDefault());
169        dropzone.addEventListener('drop', function (e) {
170            e.preventDefault();
171            dragCounter = 0;
172            dropzone.classList.remove('is-dragover');
173            const files = Array.from(e.dataTransfer.files);
174            if (files.length) processFiles(files);
175        });
176    }
177
178    // Выбор файлов через input → превью
179    if (fileInput) {
180        fileInput.addEventListener('change', function () {
181            processFiles(Array.from(this.files));
182        });
183    }
184
185    updateDropzone();
186
187    // Отправка формы: сначала загружаем файлы, потом сабмитим
188    const createTaskBtn = document.getElementById('createTaskBtn');
189    if (createTaskBtn) {
190        createTaskBtn.addEventListener('click', function (e) {
191            e.preventDefault();
192
193            if (pendingFiles.length === 0) {
194                submitForm();
195                return;
196            }
197
198            createTaskBtn.disabled = true;
199
200            const uploadPromises = pendingFiles.map(function (file) {
201                const fd = new FormData();
202                fd.append('FileUploadForm[file]', file);
203                fd.append(yii.getCsrfParam(), yii.getCsrfToken());
204                return fetch(taskFormConfig.sFileUploadUrl, {
205                    method: 'POST',
206                    body: fd,
207                    headers: {'X-Requested-With': 'XMLHttpRequest'}
208                }).then(r => r.json());
209            });
210
211            Promise.all(uploadPromises)
212                .then(function (responses) {
213                    responses.forEach(function (response) {
214                        if (response.success) {
215                            oFiles.add(response.data.file.id);
216                        }
217                    });
218                    submitForm();
219                })
220                .catch(function () {
221                    alert('Ошибка при загрузке файлов');
222                    createTaskBtn.disabled = false;
223                });
224        });
225    }
226
227    function submitForm() {
228        document.getElementById('imagesSort').value = JSON.stringify(oFiles.aFilesIds);
229        var $form = $('#' + taskFormConfig.formId);
230        // 'form-ready-for-submit' даёт другим виджетам (напр. select.php — навыки)
231        // синхронно дополнить форму перед отправкой; сам сабмит — всегда наша забота.
232        $form.trigger('form-ready-for-submit');
233        $form.get(0).submit();
234    }
235});

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.