feat: Оптимизация навигации AdminJS в логические группы
- Объединены ресурсы в 5 логических групп: Контент сайта, Бронирования, Отзывы и рейтинги, Персонал и гиды, Администрирование - Удалены дублирующие настройки navigation для чистой группировки - Добавлены CSS стили для визуального отображения иерархии с отступами - Добавлены эмодзи-иконки для каждого типа ресурсов через CSS - Улучшена навигация с правильной вложенностью элементов
This commit is contained in:
477
public/js/universal-media-manager-integration.js
Normal file
477
public/js/universal-media-manager-integration.js
Normal file
@@ -0,0 +1,477 @@
|
||||
/**
|
||||
* Универсальная интеграция медиа-менеджера в AdminJS
|
||||
* Заменяет все стандартные диалоги выбора файлов на медиа-менеджер
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
console.log('🚀 Загружается универсальный медиа-менеджер для AdminJS...');
|
||||
|
||||
let mediaManagerModal = null;
|
||||
let currentCallback = null;
|
||||
|
||||
// Создание модального окна медиа-менеджера
|
||||
function createMediaManagerModal() {
|
||||
if (mediaManagerModal) return mediaManagerModal;
|
||||
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'universal-media-modal';
|
||||
modal.innerHTML = `
|
||||
<div class="universal-media-overlay"></div>
|
||||
<div class="universal-media-content">
|
||||
<div class="universal-media-header">
|
||||
<h3>📁 Выбор изображения</h3>
|
||||
<button class="universal-media-close">×</button>
|
||||
</div>
|
||||
<iframe class="universal-media-frame" src="/universal-media-manager.html"></iframe>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// CSS стили
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.universal-media-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 10000;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.universal-media-modal.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.universal-media-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.universal-media-content {
|
||||
position: relative;
|
||||
width: 90vw;
|
||||
height: 90vh;
|
||||
max-width: 1200px;
|
||||
max-height: 800px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.universal-media-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 15px 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.universal-media-header h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.universal-media-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.universal-media-close:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.universal-media-frame {
|
||||
width: 100%;
|
||||
height: calc(100% - 60px);
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Стили для кнопок медиа-менеджера */
|
||||
.media-manager-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.media-manager-btn:hover {
|
||||
background: #0056b3;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.media-manager-btn.small {
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Скрываем стандартные input[type="file"] */
|
||||
.media-replaced input[type="file"] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Стили для preview изображений */
|
||||
.media-preview {
|
||||
max-width: 200px;
|
||||
max-height: 150px;
|
||||
border-radius: 6px;
|
||||
margin: 10px 0;
|
||||
border: 2px solid #e9ecef;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.media-preview.selected {
|
||||
border-color: #28a745;
|
||||
}
|
||||
`;
|
||||
|
||||
if (!document.querySelector('#universal-media-styles')) {
|
||||
style.id = 'universal-media-styles';
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
// События
|
||||
const closeBtn = modal.querySelector('.universal-media-close');
|
||||
const overlay = modal.querySelector('.universal-media-overlay');
|
||||
|
||||
closeBtn.addEventListener('click', closeMediaManager);
|
||||
overlay.addEventListener('click', closeMediaManager);
|
||||
|
||||
document.body.appendChild(modal);
|
||||
mediaManagerModal = modal;
|
||||
|
||||
return modal;
|
||||
}
|
||||
|
||||
// Открытие медиа-менеджера
|
||||
function openMediaManager(callback, options = {}) {
|
||||
const modal = createMediaManagerModal();
|
||||
currentCallback = callback;
|
||||
|
||||
// Обновляем заголовок если нужно
|
||||
const header = modal.querySelector('.universal-media-header h3');
|
||||
header.textContent = options.title || '📁 Выбор изображения';
|
||||
|
||||
modal.classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
// Закрытие медиа-менеджера
|
||||
function closeMediaManager() {
|
||||
if (mediaManagerModal) {
|
||||
mediaManagerModal.classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
currentCallback = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Обработка сообщений от медиа-менеджера
|
||||
window.addEventListener('message', function(event) {
|
||||
if (event.data.type === 'media-manager-selection' && currentCallback) {
|
||||
const files = event.data.files;
|
||||
if (files && files.length > 0) {
|
||||
currentCallback(files);
|
||||
closeMediaManager();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Замена стандартных input[type="file"] на медиа-менеджер
|
||||
function replaceFileInputs() {
|
||||
const fileInputs = document.querySelectorAll('input[type="file"]:not(.media-replaced)');
|
||||
|
||||
fileInputs.forEach(input => {
|
||||
if (input.accept && !input.accept.includes('image')) {
|
||||
return; // Пропускаем не-изображения
|
||||
}
|
||||
|
||||
input.classList.add('media-replaced');
|
||||
|
||||
// Создаем кнопку медиа-менеджера
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'media-manager-btn';
|
||||
button.innerHTML = '📷 Выбрать изображение';
|
||||
|
||||
// Добавляем preview
|
||||
const preview = document.createElement('img');
|
||||
preview.className = 'media-preview';
|
||||
preview.style.display = 'none';
|
||||
|
||||
// Добавляем скрытый input для хранения пути
|
||||
const hiddenInput = document.createElement('input');
|
||||
hiddenInput.type = 'hidden';
|
||||
hiddenInput.name = input.name;
|
||||
hiddenInput.value = input.value || '';
|
||||
|
||||
// Событие клика
|
||||
button.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
openMediaManager((files) => {
|
||||
const file = files[0];
|
||||
|
||||
// Обновляем значения
|
||||
hiddenInput.value = file.url;
|
||||
input.value = file.url;
|
||||
|
||||
// Показываем preview
|
||||
preview.src = file.url;
|
||||
preview.style.display = 'block';
|
||||
preview.alt = file.name;
|
||||
|
||||
// Обновляем кнопку
|
||||
button.innerHTML = '✏️ Заменить изображение';
|
||||
|
||||
// Добавляем кнопку удаления
|
||||
if (!button.nextElementSibling?.classList.contains('media-remove-btn')) {
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.type = 'button';
|
||||
removeBtn.className = 'media-manager-btn small';
|
||||
removeBtn.style.background = '#dc3545';
|
||||
removeBtn.innerHTML = '🗑️ Удалить';
|
||||
|
||||
removeBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Очищаем значения
|
||||
hiddenInput.value = '';
|
||||
input.value = '';
|
||||
|
||||
// Скрываем preview
|
||||
preview.style.display = 'none';
|
||||
|
||||
// Восстанавливаем кнопку
|
||||
button.innerHTML = '📷 Выбрать изображение';
|
||||
removeBtn.remove();
|
||||
});
|
||||
|
||||
button.parentElement.insertBefore(removeBtn, button.nextSibling);
|
||||
}
|
||||
|
||||
// Вызываем событие change для совместимости
|
||||
const changeEvent = new Event('change', { bubbles: true });
|
||||
input.dispatchEvent(changeEvent);
|
||||
}, {
|
||||
title: input.dataset.title || 'Выбор изображения'
|
||||
});
|
||||
});
|
||||
|
||||
// Вставляем элементы
|
||||
input.parentElement.insertBefore(button, input.nextSibling);
|
||||
input.parentElement.insertBefore(preview, button.nextSibling);
|
||||
input.parentElement.insertBefore(hiddenInput, preview.nextSibling);
|
||||
|
||||
// Если есть начальное значение, показываем preview
|
||||
if (input.value) {
|
||||
preview.src = input.value;
|
||||
preview.style.display = 'block';
|
||||
button.innerHTML = '✏️ Заменить изображение';
|
||||
hiddenInput.value = input.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Замена кнопок "Browse" в формах AdminJS
|
||||
function replaceAdminJSBrowseButtons() {
|
||||
// Ищем кнопки загрузки файлов AdminJS
|
||||
const browseButtons = document.querySelectorAll('button[type="button"]:not(.media-replaced)');
|
||||
|
||||
browseButtons.forEach(button => {
|
||||
const buttonText = button.textContent.toLowerCase();
|
||||
|
||||
if (buttonText.includes('browse') ||
|
||||
buttonText.includes('выбрать') ||
|
||||
buttonText.includes('загрузить') ||
|
||||
buttonText.includes('upload')) {
|
||||
|
||||
button.classList.add('media-replaced');
|
||||
|
||||
// Заменяем обработчик клика
|
||||
const originalHandler = button.onclick;
|
||||
button.onclick = null;
|
||||
|
||||
button.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
openMediaManager((files) => {
|
||||
const file = files[0];
|
||||
|
||||
// Ищем соответствующий input
|
||||
const container = button.closest('.form-group, .field, .input-group');
|
||||
const input = container?.querySelector('input[type="text"], input[type="url"], input[type="hidden"]');
|
||||
|
||||
if (input) {
|
||||
input.value = file.url;
|
||||
|
||||
// Вызываем событие change
|
||||
const changeEvent = new Event('change', { bubbles: true });
|
||||
input.dispatchEvent(changeEvent);
|
||||
|
||||
// Обновляем preview если есть
|
||||
const preview = container.querySelector('img');
|
||||
if (preview) {
|
||||
preview.src = file.url;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Обновляем текст кнопки
|
||||
button.innerHTML = '📷 Медиа-менеджер';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Интеграция с полями изображений AdminJS
|
||||
function integrateWithAdminJSImageFields() {
|
||||
// Ищем поля с атрибутом accept="image/*"
|
||||
const imageFields = document.querySelectorAll('input[accept*="image"]:not(.media-replaced)');
|
||||
|
||||
imageFields.forEach(field => {
|
||||
field.classList.add('media-replaced');
|
||||
|
||||
const container = field.closest('.form-group, .field');
|
||||
if (!container) return;
|
||||
|
||||
// Создаем кнопку медиа-менеджера
|
||||
const mediaBtn = document.createElement('button');
|
||||
mediaBtn.type = 'button';
|
||||
mediaBtn.className = 'media-manager-btn';
|
||||
mediaBtn.innerHTML = '📷 Открыть медиа-менеджер';
|
||||
|
||||
mediaBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
openMediaManager((files) => {
|
||||
const file = files[0];
|
||||
|
||||
// Обновляем поле
|
||||
field.value = file.url;
|
||||
|
||||
// Создаем событие change
|
||||
const event = new Event('change', { bubbles: true });
|
||||
field.dispatchEvent(event);
|
||||
|
||||
// Если есть label, обновляем его
|
||||
const label = container.querySelector('label');
|
||||
if (label && !label.querySelector('.selected-file')) {
|
||||
const selectedSpan = document.createElement('span');
|
||||
selectedSpan.className = 'selected-file';
|
||||
selectedSpan.style.cssText = 'color: #28a745; font-weight: 500; margin-left: 10px;';
|
||||
selectedSpan.textContent = `✓ ${file.name}`;
|
||||
label.appendChild(selectedSpan);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Вставляем кнопку после поля
|
||||
field.parentElement.insertBefore(mediaBtn, field.nextSibling);
|
||||
});
|
||||
}
|
||||
|
||||
// Основная функция инициализации
|
||||
function initMediaManager() {
|
||||
console.log('🔧 Инициализация медиа-менеджера...');
|
||||
|
||||
// Замена различных типов полей
|
||||
replaceFileInputs();
|
||||
replaceAdminJSBrowseButtons();
|
||||
integrateWithAdminJSImageFields();
|
||||
|
||||
console.log('✅ Медиа-менеджер инициализирован');
|
||||
}
|
||||
|
||||
// Наблюдатель за изменениями DOM
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
let shouldReinit = false;
|
||||
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === 'childList') {
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (node.nodeType === 1) { // Element node
|
||||
if (node.querySelector && (
|
||||
node.querySelector('input[type="file"]') ||
|
||||
node.querySelector('input[accept*="image"]') ||
|
||||
node.querySelector('button[type="button"]')
|
||||
)) {
|
||||
shouldReinit = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (shouldReinit) {
|
||||
setTimeout(initMediaManager, 100);
|
||||
}
|
||||
});
|
||||
|
||||
// Запуск
|
||||
function start() {
|
||||
// Ждем загрузки DOM
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initMediaManager);
|
||||
} else {
|
||||
initMediaManager();
|
||||
}
|
||||
|
||||
// Запуск наблюдателя
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
|
||||
// Переинициализация при изменениях в SPA
|
||||
let lastUrl = location.href;
|
||||
setInterval(() => {
|
||||
if (location.href !== lastUrl) {
|
||||
lastUrl = location.href;
|
||||
setTimeout(initMediaManager, 500);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Глобальная функция для ручного открытия медиа-менеджера
|
||||
window.openUniversalMediaManager = function(callback, options) {
|
||||
openMediaManager(callback, options);
|
||||
};
|
||||
|
||||
// Запуск
|
||||
start();
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user