|
@@ -1,4 +1,497 @@
|
|
|
$(document).ready(function () {
|
|
$(document).ready(function () {
|
|
|
|
|
+ function initQueuedUploads() {
|
|
|
|
|
+ const uploadForms = new WeakSet();
|
|
|
|
|
+ const state = {
|
|
|
|
|
+ modal: null,
|
|
|
|
|
+ modalElement: null,
|
|
|
|
|
+ queue: [],
|
|
|
|
|
+ running: new Set(),
|
|
|
|
|
+ completed: 0,
|
|
|
|
|
+ failed: 0,
|
|
|
|
|
+ totalBytes: 0,
|
|
|
|
|
+ lastRedirectUrl: '',
|
|
|
|
|
+ isCancelled: false,
|
|
|
|
|
+ activeForm: null,
|
|
|
|
|
+ };
|
|
|
|
|
+ const maxParallelUploads = 2;
|
|
|
|
|
+
|
|
|
|
|
+ function formatBytes(bytes) {
|
|
|
|
|
+ if (!bytes) {
|
|
|
|
|
+ return '0 МБ';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const units = ['Б', 'КБ', 'МБ', 'ГБ'];
|
|
|
|
|
+ let value = bytes;
|
|
|
|
|
+ let unitIndex = 0;
|
|
|
|
|
+
|
|
|
|
|
+ while (value >= 1024 && unitIndex < units.length - 1) {
|
|
|
|
|
+ value /= 1024;
|
|
|
|
|
+ unitIndex += 1;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return value.toFixed(unitIndex === 0 ? 0 : 1) + ' ' + units[unitIndex];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function ensureModal() {
|
|
|
|
|
+ if (state.modalElement) {
|
|
|
|
|
+ return state.modalElement;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const wrapper = document.createElement('div');
|
|
|
|
|
+ wrapper.innerHTML = '' +
|
|
|
|
|
+ '<div class="modal fade upload-queue-modal" id="uploadQueueModal" tabindex="-1" aria-labelledby="uploadQueueModalLabel" data-bs-backdrop="static" data-bs-keyboard="false" aria-hidden="true">' +
|
|
|
|
|
+ '<div class="modal-dialog modal-dialog-scrollable modal-lg">' +
|
|
|
|
|
+ '<div class="modal-content">' +
|
|
|
|
|
+ '<div class="modal-header">' +
|
|
|
|
|
+ '<h1 class="modal-title fs-5" id="uploadQueueModalLabel">Загрузка файлов</h1>' +
|
|
|
|
|
+ '<button type="button" class="btn-close d-none" data-upload-close aria-label="Закрыть"></button>' +
|
|
|
|
|
+ '</div>' +
|
|
|
|
|
+ '<div class="modal-body">' +
|
|
|
|
|
+ '<div class="upload-queue-summary">' +
|
|
|
|
|
+ '<div class="d-flex justify-content-between align-items-center gap-3 mb-2">' +
|
|
|
|
|
+ '<div class="fw-semibold" data-upload-status>Подготовка загрузки</div>' +
|
|
|
|
|
+ '<div class="text-muted small text-nowrap" data-upload-bytes>0 МБ / 0 МБ</div>' +
|
|
|
|
|
+ '</div>' +
|
|
|
|
|
+ '<div class="progress upload-queue-progress" role="progressbar" aria-valuemin="0" aria-valuemax="100">' +
|
|
|
|
|
+ '<div class="progress-bar" data-upload-total-progress style="width: 0%">0%</div>' +
|
|
|
|
|
+ '</div>' +
|
|
|
|
|
+ '<div class="small text-muted mt-2" data-upload-counts>0 из 0 файлов</div>' +
|
|
|
|
|
+ '</div>' +
|
|
|
|
|
+ '<div class="upload-queue-list list-group mt-3" data-upload-list></div>' +
|
|
|
|
|
+ '</div>' +
|
|
|
|
|
+ '<div class="modal-footer">' +
|
|
|
|
|
+ '<button type="button" class="btn btn-outline-secondary" data-upload-cancel>Отмена</button>' +
|
|
|
|
|
+ '<button type="button" class="btn btn-outline-primary d-none" data-upload-retry>Повторить ошибки</button>' +
|
|
|
|
|
+ '<button type="button" class="btn btn-primary d-none" data-upload-refresh>Обновить страницу</button>' +
|
|
|
|
|
+ '</div>' +
|
|
|
|
|
+ '</div>' +
|
|
|
|
|
+ '</div>' +
|
|
|
|
|
+ '</div>';
|
|
|
|
|
+
|
|
|
|
|
+ state.modalElement = wrapper.firstElementChild;
|
|
|
|
|
+ document.body.appendChild(state.modalElement);
|
|
|
|
|
+ state.modal = bootstrap.Modal.getOrCreateInstance(state.modalElement);
|
|
|
|
|
+
|
|
|
|
|
+ state.modalElement.querySelector('[data-upload-cancel]').addEventListener('click', cancelUploads);
|
|
|
|
|
+ state.modalElement.querySelector('[data-upload-retry]').addEventListener('click', retryFailedUploads);
|
|
|
|
|
+ state.modalElement.querySelector('[data-upload-refresh]').addEventListener('click', refreshAfterUpload);
|
|
|
|
|
+ state.modalElement.querySelector('[data-upload-close]').addEventListener('click', function () {
|
|
|
|
|
+ state.modal.hide();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return state.modalElement;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function getElements() {
|
|
|
|
|
+ const modal = ensureModal();
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ status: modal.querySelector('[data-upload-status]'),
|
|
|
|
|
+ bytes: modal.querySelector('[data-upload-bytes]'),
|
|
|
|
|
+ progress: modal.querySelector('[data-upload-total-progress]'),
|
|
|
|
|
+ counts: modal.querySelector('[data-upload-counts]'),
|
|
|
|
|
+ list: modal.querySelector('[data-upload-list]'),
|
|
|
|
|
+ cancel: modal.querySelector('[data-upload-cancel]'),
|
|
|
|
|
+ retry: modal.querySelector('[data-upload-retry]'),
|
|
|
|
|
+ refresh: modal.querySelector('[data-upload-refresh]'),
|
|
|
|
|
+ close: modal.querySelector('[data-upload-close]'),
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function getSelectedFiles(form) {
|
|
|
|
|
+ return Array.from(form.querySelectorAll('input[type="file"]'))
|
|
|
|
|
+ .flatMap(function (input) {
|
|
|
|
|
+ return Array.from(input.files || []).map(function (file) {
|
|
|
|
|
+ return { input: input, file: file };
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function buildQueue(form) {
|
|
|
|
|
+ const selectedFiles = getSelectedFiles(form);
|
|
|
|
|
+
|
|
|
|
|
+ return selectedFiles.map(function (entry, index) {
|
|
|
|
|
+ const row = document.createElement('div');
|
|
|
|
|
+ row.className = 'list-group-item upload-queue-item';
|
|
|
|
|
+ row.innerHTML = '' +
|
|
|
|
|
+ '<div class="d-flex justify-content-between gap-3">' +
|
|
|
|
|
+ '<div class="upload-queue-name text-truncate"></div>' +
|
|
|
|
|
+ '<div class="upload-queue-size text-muted small text-nowrap"></div>' +
|
|
|
|
|
+ '</div>' +
|
|
|
|
|
+ '<div class="d-flex justify-content-between gap-3 mt-1">' +
|
|
|
|
|
+ '<div class="small" data-upload-item-status>Ожидает</div>' +
|
|
|
|
|
+ '<div class="small text-muted" data-upload-item-percent>0%</div>' +
|
|
|
|
|
+ '</div>' +
|
|
|
|
|
+ '<div class="progress upload-queue-item-progress mt-2" role="progressbar" aria-valuemin="0" aria-valuemax="100">' +
|
|
|
|
|
+ '<div class="progress-bar" data-upload-item-progress style="width: 0%"></div>' +
|
|
|
|
|
+ '</div>';
|
|
|
|
|
+
|
|
|
|
|
+ row.querySelector('.upload-queue-name').textContent = entry.file.name;
|
|
|
|
|
+ row.querySelector('.upload-queue-size').textContent = formatBytes(entry.file.size);
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ id: index,
|
|
|
|
|
+ form: form,
|
|
|
|
|
+ input: entry.input,
|
|
|
|
|
+ file: entry.file,
|
|
|
|
|
+ row: row,
|
|
|
|
|
+ loaded: 0,
|
|
|
|
|
+ status: 'pending',
|
|
|
|
|
+ xhr: null,
|
|
|
|
|
+ error: '',
|
|
|
|
|
+ };
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function updateItem(item, status, loaded, error) {
|
|
|
|
|
+ item.status = status;
|
|
|
|
|
+ item.loaded = typeof loaded === 'number' ? loaded : item.loaded;
|
|
|
|
|
+ item.error = error || '';
|
|
|
|
|
+
|
|
|
|
|
+ const percent = item.file.size > 0 ? Math.round((item.loaded / item.file.size) * 100) : 0;
|
|
|
|
|
+ const statusElement = item.row.querySelector('[data-upload-item-status]');
|
|
|
|
|
+ const percentElement = item.row.querySelector('[data-upload-item-percent]');
|
|
|
|
|
+ const progressElement = item.row.querySelector('[data-upload-item-progress]');
|
|
|
|
|
+
|
|
|
|
|
+ item.row.classList.toggle('upload-queue-item-failed', status === 'failed');
|
|
|
|
|
+ item.row.classList.toggle('upload-queue-item-success', status === 'done');
|
|
|
|
|
+ progressElement.classList.toggle('bg-danger', status === 'failed');
|
|
|
|
|
+ progressElement.classList.toggle('bg-success', status === 'done');
|
|
|
|
|
+
|
|
|
|
|
+ const statusLabels = {
|
|
|
|
|
+ pending: 'Ожидает',
|
|
|
|
|
+ uploading: 'Загружается',
|
|
|
|
|
+ done: 'Загружено',
|
|
|
|
|
+ failed: item.error || 'Ошибка загрузки',
|
|
|
|
|
+ cancelled: 'Отменено',
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ statusElement.textContent = statusLabels[status] || status;
|
|
|
|
|
+ percentElement.textContent = Math.min(percent, 100) + '%';
|
|
|
|
|
+ progressElement.style.width = Math.min(percent, 100) + '%';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function updateSummary() {
|
|
|
|
|
+ const elements = getElements();
|
|
|
|
|
+ const uploadedBytes = state.queue.reduce(function (sum, item) {
|
|
|
|
|
+ if (item.status === 'done') {
|
|
|
|
|
+ return sum + item.file.size;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (item.status === 'uploading') {
|
|
|
|
|
+ return sum + item.loaded;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return sum;
|
|
|
|
|
+ }, 0);
|
|
|
|
|
+ const doneCount = state.queue.filter(function (item) { return item.status === 'done'; }).length;
|
|
|
|
|
+ const failedCount = state.queue.filter(function (item) { return item.status === 'failed'; }).length;
|
|
|
|
|
+ const pendingCount = state.queue.filter(function (item) { return item.status === 'pending'; }).length;
|
|
|
|
|
+ const percent = state.totalBytes > 0 ? Math.round((uploadedBytes / state.totalBytes) * 100) : 0;
|
|
|
|
|
+
|
|
|
|
|
+ elements.progress.style.width = Math.min(percent, 100) + '%';
|
|
|
|
|
+ elements.progress.textContent = Math.min(percent, 100) + '%';
|
|
|
|
|
+ elements.bytes.textContent = formatBytes(uploadedBytes) + ' / ' + formatBytes(state.totalBytes);
|
|
|
|
|
+ elements.counts.textContent = doneCount + ' из ' + state.queue.length + ' файлов';
|
|
|
|
|
+
|
|
|
|
|
+ if (state.isCancelled) {
|
|
|
|
|
+ elements.status.textContent = 'Загрузка отменена';
|
|
|
|
|
+ } else if (failedCount > 0 && state.running.size === 0 && pendingCount === 0) {
|
|
|
|
|
+ elements.status.textContent = 'Загружено с ошибками: ' + doneCount + ' из ' + state.queue.length;
|
|
|
|
|
+ } else if (doneCount === state.queue.length && state.queue.length > 0) {
|
|
|
|
|
+ elements.status.textContent = 'Все файлы загружены';
|
|
|
|
|
+ } else {
|
|
|
|
|
+ elements.status.textContent = 'Загрузка: ' + doneCount + ' из ' + state.queue.length;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ elements.cancel.classList.toggle('d-none', state.running.size === 0 && pendingCount === 0);
|
|
|
|
|
+ elements.retry.classList.toggle('d-none', failedCount === 0 || state.running.size > 0 || pendingCount > 0);
|
|
|
|
|
+ elements.refresh.classList.toggle('d-none', doneCount === 0 || state.running.size > 0 || pendingCount > 0);
|
|
|
|
|
+ elements.close.classList.toggle('d-none', state.running.size > 0 || pendingCount > 0);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function appendFormFields(formData, form, activeInput) {
|
|
|
|
|
+ Array.from(form.elements).forEach(function (element) {
|
|
|
|
|
+ if (!element.name || element.disabled || element === activeInput) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (element.type === 'file') {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if ((element.type === 'checkbox' || element.type === 'radio') && !element.checked) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ formData.append(element.name, element.value);
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function extractError(xhr) {
|
|
|
|
|
+ if (xhr.responseText) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const data = JSON.parse(xhr.responseText);
|
|
|
|
|
+
|
|
|
|
|
+ if (data.message) {
|
|
|
|
|
+ return data.message;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (data.errors) {
|
|
|
|
|
+ return Object.values(data.errors).flat().join(' ');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ // Response is not JSON. Fall through to generic status handling.
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const htmlError = extractHtmlError(xhr.responseText);
|
|
|
|
|
+
|
|
|
|
|
+ if (htmlError) {
|
|
|
|
|
+ return htmlError;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (xhr.status === 413) {
|
|
|
|
|
+ return 'Файл слишком большой';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (xhr.status === 419) {
|
|
|
|
|
+ return 'Сессия истекла, обновите страницу';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (xhr.status >= 500) {
|
|
|
|
|
+ return 'Ошибка сервера';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return xhr.status ? 'Ошибка HTTP ' + xhr.status : 'Ошибка сети';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function extractHtmlError(responseText) {
|
|
|
|
|
+ if (!responseText || !responseText.includes('alert-danger')) {
|
|
|
|
|
+ return '';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const documentFragment = new DOMParser().parseFromString(responseText, 'text/html');
|
|
|
|
|
+ const alert = documentFragment.querySelector('.main-alert.alert-danger, .alert-danger[role="alert"]');
|
|
|
|
|
+
|
|
|
|
|
+ return alert ? alert.textContent.trim().replace(/\s+/g, ' ') : '';
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ return '';
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function hasHtmlUploadError(xhr) {
|
|
|
|
|
+ const contentType = xhr.getResponseHeader('Content-Type') || '';
|
|
|
|
|
+
|
|
|
|
|
+ return contentType.includes('text/html') && Boolean(extractHtmlError(xhr.responseText));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function uploadItem(item) {
|
|
|
|
|
+ const formData = new FormData();
|
|
|
|
|
+ const xhr = new XMLHttpRequest();
|
|
|
|
|
+ const method = (item.form.getAttribute('method') || 'POST').toUpperCase();
|
|
|
|
|
+ const action = item.form.getAttribute('action') || window.location.href;
|
|
|
|
|
+ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
|
|
|
|
|
+
|
|
|
|
|
+ appendFormFields(formData, item.form, item.input);
|
|
|
|
|
+ formData.append(item.input.name, item.file, item.file.name);
|
|
|
|
|
+
|
|
|
|
|
+ item.xhr = xhr;
|
|
|
|
|
+ state.running.add(item);
|
|
|
|
|
+ updateItem(item, 'uploading', 0);
|
|
|
|
|
+ updateSummary();
|
|
|
|
|
+
|
|
|
|
|
+ xhr.open(method, action, true);
|
|
|
|
|
+ xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
|
|
|
|
|
+ xhr.setRequestHeader('Accept', 'application/json, text/html;q=0.9, */*;q=0.8');
|
|
|
|
|
+
|
|
|
|
|
+ if (csrfToken) {
|
|
|
|
|
+ xhr.setRequestHeader('X-CSRF-TOKEN', csrfToken);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ xhr.upload.onprogress = function (event) {
|
|
|
|
|
+ if (!event.lengthComputable) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ updateItem(item, 'uploading', event.loaded);
|
|
|
|
|
+ updateSummary();
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ xhr.onload = function () {
|
|
|
|
|
+ state.running.delete(item);
|
|
|
|
|
+ item.xhr = null;
|
|
|
|
|
+
|
|
|
|
|
+ if (xhr.status >= 200 && xhr.status < 300 && !hasHtmlUploadError(xhr)) {
|
|
|
|
|
+ state.lastRedirectUrl = xhr.responseURL || state.lastRedirectUrl;
|
|
|
|
|
+ state.completed += 1;
|
|
|
|
|
+ updateItem(item, 'done', item.file.size);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ state.failed += 1;
|
|
|
|
|
+ updateItem(item, 'failed', item.loaded, extractError(xhr));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ updateSummary();
|
|
|
|
|
+ runNext();
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ xhr.onerror = function () {
|
|
|
|
|
+ state.running.delete(item);
|
|
|
|
|
+ item.xhr = null;
|
|
|
|
|
+ state.failed += 1;
|
|
|
|
|
+ updateItem(item, 'failed', item.loaded, 'Ошибка сети');
|
|
|
|
|
+ updateSummary();
|
|
|
|
|
+ runNext();
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ xhr.onabort = function () {
|
|
|
|
|
+ state.running.delete(item);
|
|
|
|
|
+ item.xhr = null;
|
|
|
|
|
+ updateItem(item, 'cancelled', item.loaded);
|
|
|
|
|
+ updateSummary();
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ xhr.send(formData);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function runNext() {
|
|
|
|
|
+ if (state.isCancelled) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ while (state.running.size < maxParallelUploads) {
|
|
|
|
|
+ const nextItem = state.queue.find(function (item) {
|
|
|
|
|
+ return item.status === 'pending';
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (!nextItem) {
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ uploadItem(nextItem);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const hasPending = state.queue.some(function (item) { return item.status === 'pending'; });
|
|
|
|
|
+
|
|
|
|
|
+ if (!hasPending && state.running.size === 0 && state.failed === 0 && state.completed > 0) {
|
|
|
|
|
+ setTimeout(refreshAfterUpload, 700);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function startUpload(form) {
|
|
|
|
|
+ const hasActiveUpload = state.running.size > 0 || state.queue.some(function (item) {
|
|
|
|
|
+ return item.status === 'pending' || item.status === 'uploading';
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (hasActiveUpload) {
|
|
|
|
|
+ customAlert('Дождитесь завершения текущей загрузки файлов.');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const elements = getElements();
|
|
|
|
|
+ state.queue = buildQueue(form);
|
|
|
|
|
+ state.running = new Set();
|
|
|
|
|
+ state.completed = 0;
|
|
|
|
|
+ state.failed = 0;
|
|
|
|
|
+ state.totalBytes = state.queue.reduce(function (sum, item) {
|
|
|
|
|
+ return sum + item.file.size;
|
|
|
|
|
+ }, 0);
|
|
|
|
|
+ state.lastRedirectUrl = '';
|
|
|
|
|
+ state.isCancelled = false;
|
|
|
|
|
+ state.activeForm = form;
|
|
|
|
|
+
|
|
|
|
|
+ elements.list.innerHTML = '';
|
|
|
|
|
+ state.queue.forEach(function (item) {
|
|
|
|
|
+ elements.list.appendChild(item.row);
|
|
|
|
|
+ updateItem(item, 'pending', 0);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ state.modal.show();
|
|
|
|
|
+ updateSummary();
|
|
|
|
|
+ runNext();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function cancelUploads() {
|
|
|
|
|
+ state.isCancelled = true;
|
|
|
|
|
+ state.queue.forEach(function (item) {
|
|
|
|
|
+ if (item.status === 'pending') {
|
|
|
|
|
+ updateItem(item, 'cancelled', 0);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ state.running.forEach(function (item) {
|
|
|
|
|
+ if (item.xhr) {
|
|
|
|
|
+ item.xhr.abort();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ updateSummary();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function retryFailedUploads() {
|
|
|
|
|
+ state.queue.forEach(function (item) {
|
|
|
|
|
+ if (item.status === 'failed') {
|
|
|
|
|
+ updateItem(item, 'pending', 0);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ state.failed = 0;
|
|
|
|
|
+ state.isCancelled = false;
|
|
|
|
|
+ updateSummary();
|
|
|
|
|
+ runNext();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function refreshAfterUpload() {
|
|
|
|
|
+ const redirectUrl = state.lastRedirectUrl;
|
|
|
|
|
+
|
|
|
|
|
+ if (redirectUrl && redirectUrl.startsWith(window.location.origin)) {
|
|
|
|
|
+ window.location.href = redirectUrl;
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ window.location.reload();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ document.addEventListener('submit', function (event) {
|
|
|
|
|
+ const form = event.target;
|
|
|
|
|
+
|
|
|
|
|
+ if (!(form instanceof HTMLFormElement)
|
|
|
|
|
+ || uploadForms.has(form)
|
|
|
|
|
+ || form.matches('[data-chat-form]')
|
|
|
|
|
+ || form.dataset.uploadNative === '1'
|
|
|
|
|
+ || !form.matches('form[enctype="multipart/form-data"]')
|
|
|
|
|
+ || getSelectedFiles(form).length === 0) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ event.preventDefault();
|
|
|
|
|
+ event.stopPropagation();
|
|
|
|
|
+ uploadForms.add(form);
|
|
|
|
|
+ startUpload(form);
|
|
|
|
|
+ setTimeout(function () {
|
|
|
|
|
+ uploadForms.delete(form);
|
|
|
|
|
+ }, 0);
|
|
|
|
|
+ }, true);
|
|
|
|
|
+
|
|
|
|
|
+ document.addEventListener('change', function (event) {
|
|
|
|
|
+ const input = event.target;
|
|
|
|
|
+
|
|
|
|
|
+ if (!(input instanceof HTMLInputElement)
|
|
|
|
|
+ || input.type !== 'file'
|
|
|
|
|
+ || !input.getAttribute('onchange')?.includes('submit')
|
|
|
|
|
+ || !input.form
|
|
|
|
|
+ || input.form.matches('[data-chat-form]')
|
|
|
|
|
+ || input.form.dataset.uploadNative === '1'
|
|
|
|
|
+ || !input.form.matches('form[enctype="multipart/form-data"]')
|
|
|
|
|
+ || getSelectedFiles(input.form).length === 0) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ event.preventDefault();
|
|
|
|
|
+ event.stopImmediatePropagation();
|
|
|
|
|
+ startUpload(input.form);
|
|
|
|
|
+ }, true);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
function initMobileDatePicker() {
|
|
function initMobileDatePicker() {
|
|
|
const mobileQuery = window.matchMedia('(max-width: 767.98px)');
|
|
const mobileQuery = window.matchMedia('(max-width: 767.98px)');
|
|
|
const coarsePointer = window.matchMedia('(pointer: coarse)');
|
|
const coarsePointer = window.matchMedia('(pointer: coarse)');
|
|
@@ -366,6 +859,7 @@ $(document).ready(function () {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
cleanupStaleModalState();
|
|
cleanupStaleModalState();
|
|
|
|
|
+ initQueuedUploads();
|
|
|
initMobileDatePicker();
|
|
initMobileDatePicker();
|
|
|
window.addEventListener('pageshow', cleanupStaleModalState);
|
|
window.addEventListener('pageshow', cleanupStaleModalState);
|
|
|
|
|
|