145 lines
8.1 KiB
PHP
145 lines
8.1 KiB
PHP
<?php /* Лёгкая inline-копия API browser-upload: единый контракт, без внешнего запроса. */ ?>
|
|
<script>
|
|
(() => {
|
|
if (window.ZoomzillaUpload) return;
|
|
const android = /Android/i.test(navigator.userAgent);
|
|
const mobile = android || /iPhone|iPad|iPod/i.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
|
|
document.documentElement.dataset.uploadDevice = android ? 'android' : (mobile ? 'mobile' : 'desktop');
|
|
let uploadQueue = Promise.resolve();
|
|
window.ZoomzillaUpload = {
|
|
// Резерв из пяти точек задаётся CSS: слово и соседняя иконка не двигаются.
|
|
renderLoadingLabel(label, dots, text) {
|
|
if (!label) return;
|
|
let output = label.querySelector('[data-upload-dots]');
|
|
if (!output) {
|
|
label.textContent = text + ' ';
|
|
const slot = document.createElement('span');
|
|
slot.className = 'upload-loading-dots';
|
|
slot.setAttribute('aria-hidden', 'true');
|
|
output = document.createElement('span');
|
|
output.dataset.uploadDots = '';
|
|
slot.append(output);
|
|
label.append(slot);
|
|
}
|
|
output.textContent = '.'.repeat(dots);
|
|
},
|
|
setPending(form, pending) {
|
|
if (pending) form.dataset.uploadPending = 'true';
|
|
else delete form.dataset.uploadPending;
|
|
const link = form.querySelector('[data-files-picker]');
|
|
if (!link) return;
|
|
if (pending) {
|
|
link.setAttribute('aria-disabled', 'true');
|
|
link.setAttribute('tabindex', '-1');
|
|
} else {
|
|
link.removeAttribute('aria-disabled');
|
|
link.removeAttribute('tabindex');
|
|
}
|
|
},
|
|
bindPicker(form, mainInput, fullAccept, onFiles) {
|
|
if (mobile) form.dataset.mobileUpload = '1';
|
|
const sync = () => {
|
|
const input = typeof mainInput === 'function' ? mainInput() : mainInput;
|
|
if (input) input.accept = android ? 'image/*' : (typeof fullAccept === 'function' ? fullAccept() : fullAccept);
|
|
};
|
|
sync();
|
|
// Dropzone пересоздаёт input после каждого выбора: обновляем до native click.
|
|
form.addEventListener('click', sync, true);
|
|
// svh не меняется при движении панелей браузера. На старых движках
|
|
// сохраняем минимальную высоту до смены ширины/ориентации.
|
|
const index = form.closest('.index-mobile-fit');
|
|
if (index) {
|
|
const probe = document.createElement('span');
|
|
probe.style.cssText = 'position:absolute;visibility:hidden;pointer-events:none;height:100vh;height:100svh;width:0';
|
|
index.append(probe);
|
|
let lastWidth = 0, fallbackHeight = Infinity;
|
|
const fit = () => {
|
|
if (innerWidth !== lastWidth) fallbackHeight = Infinity;
|
|
lastWidth = innerWidth;
|
|
const height = probe.getBoundingClientRect().height;
|
|
fallbackHeight = Math.min(fallbackHeight, height);
|
|
const budget = window.CSS?.supports('height', '100svh') ? height : fallbackHeight;
|
|
index.style.setProperty('--index-small-height', budget + 'px');
|
|
index.dataset.compactUpload = budget < 650 ? 'true' : 'false';
|
|
const hero = index.querySelector('#image-compare');
|
|
if (hero) hero.style.setProperty('--compare-width', hero.getBoundingClientRect().width + 'px');
|
|
};
|
|
fit();
|
|
window.addEventListener('resize', fit);
|
|
if (window.ResizeObserver) new ResizeObserver(fit).observe(index);
|
|
}
|
|
if (mobile && form.dataset.photoLabel && !form.querySelector('[data-other-files][multiple]')) {
|
|
const label = form.querySelector('[data-upload-label]');
|
|
if (label) {
|
|
label.textContent = form.dataset.photoLabel;
|
|
label.dataset.pickerLabel = form.dataset.photoLabel;
|
|
}
|
|
}
|
|
if (!android) return;
|
|
const link = form.querySelector('[data-files-picker]');
|
|
const other = form.querySelector('[data-other-files]');
|
|
if (!link || !other) return;
|
|
form.querySelector('[data-picker-or]').hidden = false;
|
|
link.hidden = false;
|
|
const label = form.querySelector('[data-upload-label]');
|
|
if (label) {
|
|
label.textContent = form.dataset.photoLabel;
|
|
label.dataset.pickerLabel = form.dataset.photoLabel;
|
|
}
|
|
other.addEventListener('click', event => event.stopPropagation());
|
|
link.addEventListener('click', event => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
if (form.querySelector('.dropzone-form-button')?.disabled) return;
|
|
if(event.isTrusted&&window.ZoomzillaAnalyticsConfig?.guestUpload!==false)window.ZoomzillaAnalytics?.emit('upload_click');
|
|
other.value = '';
|
|
other.click();
|
|
});
|
|
other.addEventListener('change', event => {
|
|
event.stopPropagation();
|
|
const files = Array.from(other.files || []);
|
|
other.value = '';
|
|
if (files.length) onFiles(files);
|
|
});
|
|
},
|
|
run(file, maxBytes, send) {
|
|
const upload = () => this.prepare(file, maxBytes).then(send);
|
|
if (!android) return upload();
|
|
// В кабинете до10 файлов: удерживаем в памяти только одну копию.
|
|
const pending = uploadQueue.then(upload);
|
|
uploadQueue = pending.catch(() => {});
|
|
return pending;
|
|
},
|
|
prepare(file, maxBytes) {
|
|
// FileProvider может отдавать несовпадающие lastModified для сетевого
|
|
// процесса Chromium. Blob из прочитанных байтов от него не зависит.
|
|
if (!android || !file.size || file.size > maxBytes) return Promise.resolve(file);
|
|
return new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
const timer = window.setTimeout(() => reader.abort(), 30000);
|
|
const failed = error => {
|
|
if(window.ZoomzillaAnalyticsConfig?.guestUpload!==false)window.ZoomzillaAnalytics?.emit('photo_error',{stage:'read',reason_code:'file_read_failed'});
|
|
window.clearTimeout(timer);
|
|
window.UploadDiagnostics?.record('buffer_error', { error_name: error.name, message: error.message });
|
|
reject(error);
|
|
};
|
|
window.UploadDiagnostics?.record('buffer_start', { size: file.size });
|
|
reader.onload = () => {
|
|
window.clearTimeout(timer);
|
|
if (reader.result.byteLength !== file.size) {
|
|
failed(new Error('Размер файла изменился при чтении.'));
|
|
return;
|
|
}
|
|
const blob = new Blob([reader.result], { type: file.type });
|
|
window.UploadDiagnostics?.record('buffer_ok', { size: blob.size, mime: blob.type });
|
|
resolve(blob);
|
|
};
|
|
reader.onerror = () => failed(reader.error || new Error('Не удалось прочитать файл.'));
|
|
reader.onabort = () => failed(new Error('Чтение файла прервано.'));
|
|
try { reader.readAsArrayBuffer(file.slice(0, file.size, file.type)); } catch (error) { failed(error); }
|
|
});
|
|
}
|
|
};
|
|
})();
|
|
</script>
|