/** * Единый медиа-пикер /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 }; 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 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 || []); grid.innerHTML = ''; folders.forEach(function (folder) { 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 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; render(data); }) .catch(function () { 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 (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); }); document.addEventListener('keydown', onKey); load(state.dir, 1); search.focus(); } }; })(window, document);