feat: улучшен drag & drop для multiview панелей
- Реализована перестановка панелей только при отпускании кнопки мыши - Исправлен алгоритм swapPanels для корректной работы с любыми панелями - Устранена ошибка TypeError при обращении к classList - Добавлено подробное логирование для отладки - Улучшена визуальная обратная связь с анимацией перестановки - Панели теперь меняются местами без сдвига остальных элементов Автор: Сергей Антропов Сайт: https://devops.org.ru
This commit is contained in:
parent
d697797577
commit
4077142f79
@ -610,6 +610,86 @@ a{color:var(--link)}
|
||||
overflow: hidden;
|
||||
padding: 2px;
|
||||
height: 100%;
|
||||
/* Drag & Drop стили */
|
||||
cursor: move;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Стили для перетаскиваемого элемента */
|
||||
.multi-view-panel.dragging {
|
||||
opacity: 0.7;
|
||||
transform: scale(1.02);
|
||||
z-index: 1000;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
/* Стили для области drop - готовность к перестановке */
|
||||
.multi-view-panel.drop-target {
|
||||
border-color: var(--ok);
|
||||
background: var(--tab-active);
|
||||
box-shadow: 0 0 0 2px var(--ok), 0 0 15px rgba(158, 206, 106, 0.2);
|
||||
transform: scale(1.01);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
/* Стили для области drop - перестановка в процессе */
|
||||
.multi-view-panel.drop-target.swapping {
|
||||
box-shadow: 0 0 0 3px var(--ok), 0 0 25px rgba(158, 206, 106, 0.4);
|
||||
transform: scale(1.02);
|
||||
animation: dropTargetSwap 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Анимация перестановки */
|
||||
@keyframes dropTargetSwap {
|
||||
0% {
|
||||
box-shadow: 0 0 0 3px var(--ok), 0 0 25px rgba(158, 206, 106, 0.4);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 0 4px var(--ok), 0 0 35px rgba(158, 206, 106, 0.6);
|
||||
transform: scale(1.03);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 3px var(--ok), 0 0 25px rgba(158, 206, 106, 0.4);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
}
|
||||
|
||||
/* Стили для заголовка панели - область для захвата */
|
||||
.multi-view-header {
|
||||
cursor: grab;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.multi-view-header:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* Индикатор возможности перетаскивания */
|
||||
.multi-view-header::before {
|
||||
content: "⋮⋮";
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.multi-view-panel:hover .multi-view-header::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Скрываем индикатор при перетаскивании */
|
||||
.multi-view-panel.dragging .multi-view-header::before {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.multi-view-content {
|
||||
@ -2003,6 +2083,8 @@ a{color:var(--link)}
|
||||
/* Равная высота строк для нескольких рядов (3+ окон) */
|
||||
grid-auto-rows: 1fr;
|
||||
align-items: stretch;
|
||||
/* Поддержка drag & drop */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.multi-view-panel {
|
||||
@ -2016,7 +2098,7 @@ a{color:var(--link)}
|
||||
}
|
||||
|
||||
.multi-view-header {
|
||||
padding: 5px 16px;
|
||||
padding: 5px 16px 5px 24px; /* Увеличиваем левый отступ для индикатора перетаскивания */
|
||||
background: var(--chip);
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
|
@ -1867,6 +1867,14 @@ async function setupMultiView() {
|
||||
|
||||
console.log(`setupMultiView: Multi-view setup completed for ${state.selectedContainers.length} containers`);
|
||||
|
||||
// Применяем сохраненный порядок панелей
|
||||
setTimeout(() => {
|
||||
// Сначала очищаем дубликаты, если они есть
|
||||
cleanupDuplicatePanels();
|
||||
// Затем применяем порядок
|
||||
applyPanelOrder();
|
||||
}, 100); // Небольшая задержка для завершения создания панелей
|
||||
|
||||
// Обновляем счетчики для multi view
|
||||
setTimeout(() => {
|
||||
recalculateMultiViewCounters();
|
||||
@ -1937,6 +1945,9 @@ function createMultiViewPanel(service) {
|
||||
console.error(`Failed to create multi-view log element for ${service.name}`);
|
||||
}
|
||||
|
||||
// Добавляем drag & drop функциональность
|
||||
setupDragAndDrop(panel);
|
||||
|
||||
// Инициализируем состояние кнопок уровней логирования для этого контейнера
|
||||
setTimeout(() => {
|
||||
const levelBtns = panel.querySelectorAll('.level-btn');
|
||||
@ -5850,4 +5861,405 @@ function reinitializeElements() {
|
||||
return result;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// DRAG & DROP ФУНКЦИОНАЛЬНОСТЬ ДЛЯ MULTI-VIEW ПАНЕЛЕЙ
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Находит целевую панель для drop с расширенной зоной поиска
|
||||
* @param {number} x - X координата курсора
|
||||
* @param {number} y - Y координата курсора
|
||||
* @param {HTMLElement} draggedElement - Перетаскиваемый элемент
|
||||
* @returns {HTMLElement|null} Найденная панель или null
|
||||
*/
|
||||
function findTargetPanel(x, y, draggedElement) {
|
||||
// Сначала пробуем найти панель точно под курсором
|
||||
let elementBelow = document.elementFromPoint(x, y);
|
||||
let targetPanel = elementBelow?.closest('.multi-view-panel');
|
||||
|
||||
if (targetPanel && targetPanel !== draggedElement) {
|
||||
const targetId = targetPanel.getAttribute('data-container-id');
|
||||
console.log(`Found target panel: ${targetId} at (${x}, ${y})`);
|
||||
return targetPanel;
|
||||
}
|
||||
|
||||
// Если не нашли, расширяем зону поиска в радиусе 50px
|
||||
const searchRadius = 50;
|
||||
const searchPoints = [
|
||||
{ x: x - searchRadius, y: y },
|
||||
{ x: x + searchRadius, y: y },
|
||||
{ x: x, y: y - searchRadius },
|
||||
{ x: x, y: y + searchRadius },
|
||||
{ x: x - searchRadius/2, y: y - searchRadius/2 },
|
||||
{ x: x + searchRadius/2, y: y - searchRadius/2 },
|
||||
{ x: x - searchRadius/2, y: y + searchRadius/2 },
|
||||
{ x: x + searchRadius/2, y: y + searchRadius/2 }
|
||||
];
|
||||
|
||||
for (const point of searchPoints) {
|
||||
elementBelow = document.elementFromPoint(point.x, point.y);
|
||||
targetPanel = elementBelow?.closest('.multi-view-panel');
|
||||
|
||||
if (targetPanel && targetPanel !== draggedElement) {
|
||||
return targetPanel;
|
||||
}
|
||||
}
|
||||
|
||||
// Если все еще не нашли, проверяем все панели на пересечение с курсором
|
||||
const allPanels = document.querySelectorAll('.multi-view-panel');
|
||||
for (const panel of allPanels) {
|
||||
if (panel === draggedElement) continue;
|
||||
|
||||
const rect = panel.getBoundingClientRect();
|
||||
if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {
|
||||
return panel;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Настраивает drag & drop функциональность для панели multi-view
|
||||
* @param {HTMLElement} panel - Панель для настройки drag & drop
|
||||
*/
|
||||
function setupDragAndDrop(panel) {
|
||||
const header = panel.querySelector('.multi-view-header');
|
||||
if (!header) return;
|
||||
|
||||
let isDragging = false;
|
||||
let dragStartX = 0;
|
||||
let dragStartY = 0;
|
||||
let draggedElement = null;
|
||||
let draggedIndex = -1;
|
||||
let dropTarget = null;
|
||||
|
||||
// Обработчик начала перетаскивания
|
||||
header.addEventListener('mousedown', (e) => {
|
||||
// Проверяем, что клик по заголовку, а не по кнопкам уровней
|
||||
if (e.target.closest('.level-btn')) return;
|
||||
|
||||
isDragging = true;
|
||||
draggedElement = panel;
|
||||
draggedIndex = Array.from(panel.parentNode.children).indexOf(panel);
|
||||
|
||||
dragStartX = e.clientX;
|
||||
dragStartY = e.clientY;
|
||||
|
||||
// Добавляем класс для визуального эффекта
|
||||
panel.classList.add('dragging');
|
||||
|
||||
// Предотвращаем выделение текста
|
||||
e.preventDefault();
|
||||
|
||||
console.log(`Drag started for panel: ${panel.getAttribute('data-container-id')}`);
|
||||
});
|
||||
|
||||
// Обработчик движения мыши
|
||||
document.addEventListener('mousemove', (e) => {
|
||||
if (!isDragging || !draggedElement) return;
|
||||
|
||||
const deltaX = e.clientX - dragStartX;
|
||||
const deltaY = e.clientY - dragStartY;
|
||||
|
||||
// Минимальное расстояние для начала перетаскивания
|
||||
if (Math.abs(deltaX) < 5 && Math.abs(deltaY) < 5) return;
|
||||
|
||||
// Обновляем позицию элемента
|
||||
draggedElement.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
|
||||
|
||||
// Находим элемент под курсором с расширенной зоной поиска
|
||||
const targetPanel = findTargetPanel(e.clientX, e.clientY, draggedElement);
|
||||
|
||||
// Убираем подсветку с предыдущей цели
|
||||
if (dropTarget && dropTarget !== targetPanel) {
|
||||
dropTarget.classList.remove('drop-target');
|
||||
}
|
||||
|
||||
// Подсвечиваем новую цель (без перестановки во время перетаскивания)
|
||||
if (targetPanel && targetPanel !== draggedElement) {
|
||||
// Убираем подсветку с предыдущей цели
|
||||
if (dropTarget && dropTarget !== targetPanel) {
|
||||
dropTarget.classList.remove('drop-target');
|
||||
}
|
||||
|
||||
dropTarget = targetPanel;
|
||||
targetPanel.classList.add('drop-target');
|
||||
} else {
|
||||
if (dropTarget) {
|
||||
dropTarget.classList.remove('drop-target');
|
||||
}
|
||||
dropTarget = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Обработчик окончания перетаскивания
|
||||
document.addEventListener('mouseup', (e) => {
|
||||
if (!isDragging || !draggedElement) return;
|
||||
|
||||
isDragging = false;
|
||||
|
||||
// Убираем визуальные эффекты
|
||||
draggedElement.classList.remove('dragging');
|
||||
draggedElement.style.transform = '';
|
||||
|
||||
// Если есть целевая панель, выполняем перестановку
|
||||
if (dropTarget) {
|
||||
const containerId = draggedElement.getAttribute('data-container-id');
|
||||
const targetContainerId = dropTarget.getAttribute('data-container-id');
|
||||
|
||||
console.log(`Dropping panel ${containerId} at position of ${targetContainerId}`);
|
||||
|
||||
// Сохраняем ссылку на элемент для setTimeout
|
||||
const targetElement = dropTarget;
|
||||
|
||||
// Добавляем класс для анимации перестановки
|
||||
targetElement.classList.add('swapping');
|
||||
|
||||
// Выполняем перестановку
|
||||
swapPanels(draggedElement, targetElement);
|
||||
|
||||
// Убираем класс анимации через короткое время
|
||||
setTimeout(() => {
|
||||
if (targetElement && targetElement.classList) {
|
||||
targetElement.classList.remove('swapping');
|
||||
}
|
||||
}, 300);
|
||||
|
||||
// Убираем подсветку
|
||||
targetElement.classList.remove('drop-target');
|
||||
|
||||
// Сохраняем новый порядок в localStorage
|
||||
savePanelOrder();
|
||||
}
|
||||
|
||||
// Сбрасываем переменные
|
||||
draggedElement = null;
|
||||
draggedIndex = -1;
|
||||
dropTarget = null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Мгновенно меняет местами две панели в DOM и обновляет массив selectedContainers
|
||||
* @param {HTMLElement} panel1 - Первая панель
|
||||
* @param {HTMLElement} panel2 - Вторая панель
|
||||
*/
|
||||
function swapPanels(panel1, panel2) {
|
||||
if (!panel1 || !panel2 || panel1 === panel2) return;
|
||||
|
||||
const containerId1 = panel1.getAttribute('data-container-id');
|
||||
const containerId2 = panel2.getAttribute('data-container-id');
|
||||
|
||||
if (!containerId1 || !containerId2) {
|
||||
console.error('Missing container IDs:', { containerId1, containerId2 });
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Before swap - Panel1: ${containerId1}, Panel2: ${containerId2}`);
|
||||
console.log('Before swap - selectedContainers:', [...state.selectedContainers]);
|
||||
|
||||
// Меняем местами панели в DOM
|
||||
const parent = panel1.parentNode;
|
||||
|
||||
// Получаем позиции панелей в DOM
|
||||
const panel1NextSibling = panel1.nextSibling;
|
||||
const panel2NextSibling = panel2.nextSibling;
|
||||
|
||||
// Вставляем panel1 на место panel2
|
||||
parent.insertBefore(panel1, panel2NextSibling);
|
||||
|
||||
// Вставляем panel2 на место panel1
|
||||
parent.insertBefore(panel2, panel1NextSibling);
|
||||
|
||||
// Обновляем массив selectedContainers
|
||||
const index1 = state.selectedContainers.indexOf(containerId1);
|
||||
const index2 = state.selectedContainers.indexOf(containerId2);
|
||||
|
||||
if (index1 !== -1 && index2 !== -1) {
|
||||
// Меняем местами элементы в массиве
|
||||
state.selectedContainers[index1] = containerId2;
|
||||
state.selectedContainers[index2] = containerId1;
|
||||
|
||||
console.log(`After swap - Panel1: ${containerId1}, Panel2: ${containerId2}`);
|
||||
console.log('After swap - selectedContainers:', [...state.selectedContainers]);
|
||||
|
||||
// Проверяем, что атрибуты data-container-id остались правильными
|
||||
const newContainerId1 = panel1.getAttribute('data-container-id');
|
||||
const newContainerId2 = panel2.getAttribute('data-container-id');
|
||||
console.log(`After swap - Panel1 data-container-id: ${newContainerId1}, Panel2 data-container-id: ${newContainerId2}`);
|
||||
} else {
|
||||
console.error('Could not find container IDs in selectedContainers:', { index1, index2, containerId1, containerId2 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Переставляет контейнеры в массиве selectedContainers
|
||||
* @param {string} draggedId - ID перетаскиваемого контейнера
|
||||
* @param {string} targetId - ID целевого контейнера
|
||||
*/
|
||||
function reorderContainers(draggedId, targetId) {
|
||||
const draggedIndex = state.selectedContainers.indexOf(draggedId);
|
||||
const targetIndex = state.selectedContainers.indexOf(targetId);
|
||||
|
||||
if (draggedIndex === -1 || targetIndex === -1) return;
|
||||
|
||||
// Удаляем перетаскиваемый элемент
|
||||
state.selectedContainers.splice(draggedIndex, 1);
|
||||
|
||||
// Вставляем его в новую позицию
|
||||
const newTargetIndex = draggedIndex < targetIndex ? targetIndex - 1 : targetIndex;
|
||||
state.selectedContainers.splice(newTargetIndex, 0, draggedId);
|
||||
|
||||
console.log('Reordered containers:', state.selectedContainers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохраняет порядок панелей в localStorage
|
||||
*/
|
||||
function savePanelOrder() {
|
||||
const panelOrder = Array.from(document.querySelectorAll('.multi-view-panel'))
|
||||
.map(panel => panel.getAttribute('data-container-id'))
|
||||
.filter(id => id);
|
||||
|
||||
// Удаляем дубликаты, сохраняя порядок первого вхождения
|
||||
const uniquePanelOrder = [...new Set(panelOrder)];
|
||||
|
||||
localStorage.setItem('lb_panel_order', JSON.stringify(uniquePanelOrder));
|
||||
console.log('Panel order saved:', uniquePanelOrder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Загружает порядок панелей из localStorage
|
||||
* @returns {Array} Массив ID контейнеров в сохраненном порядке
|
||||
*/
|
||||
function loadPanelOrder() {
|
||||
try {
|
||||
const savedOrder = localStorage.getItem('lb_panel_order');
|
||||
if (savedOrder) {
|
||||
const order = JSON.parse(savedOrder);
|
||||
// Удаляем дубликаты из загруженного порядка
|
||||
const uniqueOrder = [...new Set(order)];
|
||||
console.log('Panel order loaded:', uniqueOrder);
|
||||
return uniqueOrder;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading panel order:', error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Применяет сохраненный порядок панелей
|
||||
*/
|
||||
function applyPanelOrder() {
|
||||
if (!state.multiViewMode) return;
|
||||
|
||||
const savedOrder = loadPanelOrder();
|
||||
if (!savedOrder || savedOrder.length === 0) return;
|
||||
|
||||
const grid = document.getElementById('multiViewGrid');
|
||||
if (!grid) return;
|
||||
|
||||
// Создаем карту панелей по ID контейнера
|
||||
const panels = Array.from(grid.children);
|
||||
const panelMap = {};
|
||||
panels.forEach(panel => {
|
||||
const containerId = panel.getAttribute('data-container-id');
|
||||
if (containerId) {
|
||||
panelMap[containerId] = panel;
|
||||
}
|
||||
});
|
||||
|
||||
// Переставляем панели согласно сохраненному порядку
|
||||
savedOrder.forEach(containerId => {
|
||||
const panel = panelMap[containerId];
|
||||
if (panel && panel.parentNode === grid) {
|
||||
grid.appendChild(panel);
|
||||
}
|
||||
});
|
||||
|
||||
// Добавляем панели для новых контейнеров, которых нет в сохраненном порядке
|
||||
const currentContainers = [...state.selectedContainers];
|
||||
const newContainers = currentContainers.filter(id => !savedOrder.includes(id));
|
||||
|
||||
newContainers.forEach(containerId => {
|
||||
// Проверяем, что панель для этого контейнера еще не существует
|
||||
const existingPanel = grid.querySelector(`[data-container-id="${containerId}"]`);
|
||||
if (!existingPanel) {
|
||||
const service = state.services.find(s => s.id === containerId);
|
||||
if (service) {
|
||||
console.log(`Creating new panel for container: ${service.name} (${containerId})`);
|
||||
const panel = createMultiViewPanel(service);
|
||||
grid.appendChild(panel);
|
||||
}
|
||||
} else {
|
||||
console.log(`Panel for container ${containerId} already exists, skipping creation`);
|
||||
}
|
||||
});
|
||||
|
||||
// Обновляем массив selectedContainers, сохраняя все текущие контейнеры
|
||||
// но применяя сохраненный порядок для тех, которые есть в сохраненном порядке
|
||||
const orderedContainers = savedOrder.filter(id => currentContainers.includes(id));
|
||||
state.selectedContainers = [...orderedContainers, ...newContainers];
|
||||
|
||||
// Обновляем grid template columns для нового количества панелей
|
||||
const totalPanels = state.selectedContainers.length;
|
||||
let columns = 1;
|
||||
if (totalPanels === 1) columns = 1;
|
||||
else if (totalPanels === 2) columns = 2;
|
||||
else if (totalPanels <= 4) columns = 2;
|
||||
else if (totalPanels <= 6) columns = 3;
|
||||
else columns = 4;
|
||||
|
||||
grid.style.gridTemplateColumns = `repeat(${columns}, 1fr)`;
|
||||
console.log(`Updated grid template columns to: repeat(${columns}, 1fr) for ${totalPanels} panels`);
|
||||
|
||||
console.log('Applied panel order:', state.selectedContainers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Очищает дубликаты из localStorage и обновляет порядок панелей
|
||||
*/
|
||||
function cleanupDuplicatePanels() {
|
||||
try {
|
||||
const savedOrder = localStorage.getItem('lb_panel_order');
|
||||
if (savedOrder) {
|
||||
const order = JSON.parse(savedOrder);
|
||||
const uniqueOrder = [...new Set(order)];
|
||||
|
||||
// Если были найдены дубликаты, обновляем localStorage
|
||||
if (order.length !== uniqueOrder.length) {
|
||||
console.log('Found duplicates in panel order, cleaning up:', order, '->', uniqueOrder);
|
||||
localStorage.setItem('lb_panel_order', JSON.stringify(uniqueOrder));
|
||||
|
||||
// Если мы в multiview режиме, пересоздаем панели
|
||||
if (state.multiViewMode) {
|
||||
console.log('Recreating panels to remove duplicates');
|
||||
// Очищаем текущий grid
|
||||
const grid = document.getElementById('multiViewGrid');
|
||||
if (grid) {
|
||||
grid.innerHTML = '';
|
||||
}
|
||||
|
||||
// Пересоздаем панели в правильном порядке
|
||||
setTimeout(() => {
|
||||
applyPanelOrder();
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error cleaning up duplicate panels:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Экспортируем функции для использования в других частях кода
|
||||
window.setupDragAndDrop = setupDragAndDrop;
|
||||
window.savePanelOrder = savePanelOrder;
|
||||
window.loadPanelOrder = loadPanelOrder;
|
||||
window.applyPanelOrder = applyPanelOrder;
|
||||
window.cleanupDuplicatePanels = cleanupDuplicatePanels;
|
||||
window.swapPanels = swapPanels;
|
||||
|
||||
})();
|
||||
|
Loading…
x
Reference in New Issue
Block a user