/** * Единый медиа-пикер /adminx. Один компонент для всех мест, где выбирают файл * из медиатеки (поля документов, значения по умолчанию рубрик, навигация, * визуальный редактор). Дергает общий эндпоинт GET /media/picker. * * Использование: * Adminx.MediaPicker.open({ * type: 'image' | 'file' | 'all', // что показывать (по умолчанию image) * dir: '/uploads', // стартовая папка * title, description, // тексты шапки (опц.) * onPick: function (file) { ... }, // выбран файл из медиатеки * onFolder: function (dir) { ... }, // выбрана открытая папка (опц.) * onUrl: function (url) { ... } // опц.: показать «вставить по ссылке» * }); */ (function (window, document) { 'use strict'; var Adminx = window.Adminx || (window.Adminx = {}); function esc(value) { return String(value == null ? '' : value).replace(/[&<>"]/g, function (c) { return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); } Adminx.MediaPicker = { open: function (options) { options = options || {}; var type = (options.type === 'file' || options.type === 'all') ? options.type : 'image'; var onPick = typeof options.onPick === 'function' ? options.onPick : function () {}; var onFolder = typeof options.onFolder === 'function' ? options.onFolder : null; var onUrl = typeof options.onUrl === 'function' ? options.onUrl : null; var base = (Adminx.base && Adminx.base()) || ''; var title = options.title || 'Выбрать файл'; var description = options.description || (type === 'image' ? 'Выберите изображение из медиатеки.' : 'Выберите файл из медиатеки.'); var placeholder = type === 'image' ? 'Поиск изображений' : 'Поиск файлов'; var overlay = document.createElement('div'); overlay.className = 'overlay media-picker-overlay'; overlay.innerHTML = ''; document.body.appendChild(overlay); window.requestAnimationFrame(function () { overlay.classList.add('show'); }); var state = { dir: options.dir || '/uploads', parent: '', page: 1, pages: 1, q: '', type: type, canManage: false, busy: false }; var grid = overlay.querySelector('[data-mp-grid]'); var crumbs = overlay.querySelector('[data-mp-crumbs]'); var status = overlay.querySelector('[data-mp-status]'); var count = overlay.querySelector('[data-mp-count]'); var search = overlay.querySelector('[data-mp-search]'); var manage = overlay.querySelector('[data-mp-manage]'); var uploadInput = overlay.querySelector('[data-mp-upload-input]'); var folderForm = overlay.querySelector('[data-mp-folder-form]'); var folderName = overlay.querySelector('[data-mp-folder-name]'); var timer = null; function close() { overlay.classList.remove('show'); window.setTimeout(function () { overlay.remove(); document.removeEventListener('keydown', onKey); }, 160); } function onKey(e) { if (e.key === 'Escape') { close(); } } function renderCrumbs(items) { crumbs.innerHTML = ''; (items || []).forEach(function (item, index) { if (index > 0) { crumbs.insertAdjacentHTML('beforeend', '/'); } crumbs.insertAdjacentHTML('beforeend', ''); }); } function render(data) { var folders = data.folders || []; var files = data.files || []; renderCrumbs(data.breadcrumbs || []); state.canManage = !!data.can_manage; manage.hidden = !state.canManage; grid.innerHTML = ''; folders.forEach(function (folder) { var folderMeta = state.q ? '' + esc(String(folder.path || '').replace(/^\/uploads\/?/, '') || 'uploads') : '' + esc(folder.count || 0) + ' объектов'; grid.insertAdjacentHTML('beforeend', ''); }); files.forEach(function (file) { var thumb = file.thumb_url || file.preview_url || file.url || ''; var isImage = /\.(jpe?g|png|gif|webp|bmp|svg)(\?.*)?$/i.test(thumb); var preview = isImage ? '' : ''; var meta = (file.width && file.height ? file.width + 'x' + file.height : '') + (file.size_label ? ' · ' + file.size_label : ''); grid.insertAdjacentHTML('beforeend', ''); }); status.hidden = folders.length + files.length > 0; status.textContent = state.q ? 'Ничего не найдено' : (state.type === 'image' ? 'В этой папке нет изображений' : 'В этой папке нет файлов'); count.textContent = data.total ? 'Файлов: ' + data.total + ', страница ' + state.page + ' из ' + state.pages : ''; overlay.querySelector('[data-mp-prev]').disabled = state.page <= 1; overlay.querySelector('[data-mp-next]').disabled = state.page >= state.pages; overlay.querySelector('[data-mp-up]').disabled = !state.parent; } function responseData(payload) { var response = payload && payload.data ? payload.data : {}; if (!payload || !payload.ok || response.success === false) { throw new Error(response.message || (response.error && response.error.message) || 'Не удалось выполнить действие'); } return response; } function setBusy(value, message) { state.busy = !!value; manage.querySelectorAll('button').forEach(function (button) { button.disabled = state.busy; }); status.hidden = !message; if (message) { status.textContent = message; } } function clearSearch() { state.q = ''; search.value = ''; } function createFolder() { var name = folderName.value.trim(); if (!name || state.busy) { folderName.focus(); return; } var data = new FormData(); data.set('_csrf', Adminx.csrf()); data.set('dir', state.dir); data.set('name', name); setBusy(true, 'Создаём папку…'); Adminx.Ajax.post(base + '/media/folders', data).then(responseData).then(function (response) { var result = response.data || {}; folderForm.hidden = true; folderName.value = ''; clearSearch(); Adminx.Toast.show(response.message || 'Папка создана', 'success'); load(result.path || state.dir, 1); }).catch(function (error) { setBusy(false, ''); Adminx.Toast.show(error.message || 'Не удалось создать папку', 'error'); folderName.focus(); }); } function upload(files) { if (!files || !files.length || state.busy) { return; } setBusy(true, 'Подготовка к загрузке: ' + files.length + '…'); Adminx.Upload.files(base + '/media/upload', files, function (chunk) { var data = new FormData(); data.set('_csrf', Adminx.csrf()); data.set('dir', state.dir); chunk.forEach(function (file) { data.append('files[]', file); }); return data; }, function (processed, total) { status.textContent = 'Загружено: ' + processed + ' из ' + total + '…'; }).then(function (result) { uploadInput.value = ''; clearSearch(); Adminx.Toast.show('Загружено файлов: ' + result.files.length, 'success'); load(state.dir, 1); }).catch(function (error) { uploadInput.value = ''; setBusy(false, ''); var uploaded = error && error.uploaded ? error.uploaded.length : 0; var message = error.message || 'Не удалось загрузить файлы'; if (uploaded) { message += '. До ошибки загружено: ' + uploaded; } Adminx.Toast.show(message, 'error'); }); } function load(dir, page) { state.dir = dir || state.dir; state.page = page || 1; status.textContent = 'Загрузка…'; status.hidden = false; grid.innerHTML = ''; var params = new URLSearchParams(); params.set('dir', state.dir); params.set('page', state.page); params.set('per_page', 36); params.set('type', state.type); if (state.q) { params.set('q', state.q); } fetch(base + '/media/picker?' + params.toString(), { headers: { 'Accept': 'application/json' }, credentials: 'same-origin' }) .then(function (res) { return res.json(); }) .then(function (payload) { var data = (payload && payload.data) || {}; state.dir = data.dir || state.dir; state.parent = data.parent_dir || ''; state.page = data.page || 1; state.pages = data.pages || 1; setBusy(false, ''); render(data); }) .catch(function () { setBusy(false, ''); status.textContent = 'Не удалось загрузить медиа'; status.hidden = false; }); } overlay.addEventListener('click', function (e) { if (e.target === overlay || e.target.closest('[data-mp-close]')) { close(); return; } var dir = e.target.closest('[data-mp-dir]'); if (dir) { load(dir.getAttribute('data-mp-dir') || '/uploads', 1); return; } var file = e.target.closest('[data-mp-file]'); if (file && !onFolder) { onPick(JSON.parse(file.getAttribute('data-mp-file') || '{}')); close(); return; } if (e.target.closest('[data-mp-folder-apply]') && onFolder) { onFolder(state.dir); close(); return; } if (e.target.closest('[data-mp-prev]') && state.page > 1) { load(state.dir, state.page - 1); } if (e.target.closest('[data-mp-next]') && state.page < state.pages) { load(state.dir, state.page + 1); } if (e.target.closest('[data-mp-up]') && state.parent) { load(state.parent, 1); } if (e.target.closest('[data-mp-folder-toggle]')) { folderForm.hidden = !folderForm.hidden; if (!folderForm.hidden) { folderName.focus(); } return; } if (e.target.closest('[data-mp-folder-cancel]')) { folderForm.hidden = true; folderName.value = ''; return; } if (e.target.closest('[data-mp-folder-create]')) { createFolder(); return; } if (e.target.closest('[data-mp-upload]')) { uploadInput.click(); return; } if (onUrl) { if (e.target.closest('[data-mp-url-toggle]')) { var box = overlay.querySelector('[data-mp-url]'); box.hidden = !box.hidden; if (!box.hidden) { box.querySelector('[data-mp-url-input]').focus(); } } if (e.target.closest('[data-mp-url-apply]')) { var value = overlay.querySelector('[data-mp-url-input]').value.trim(); if (value) { onUrl(value); close(); } } } }); search.addEventListener('input', function () { window.clearTimeout(timer); timer = window.setTimeout(function () { state.q = search.value.trim(); load(state.dir, 1); }, 220); }); folderName.addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); createFolder(); } }); uploadInput.addEventListener('change', function () { upload(uploadInput.files); }); document.addEventListener('keydown', onKey); load(state.dir, 1); search.focus(); } }; })(window, document);