팝업 드래그 이동 및 클릭 시 최상단 배치 기능
// ==UserScript==
// @name KULMS Popup Draggable
// @namespace https://explainpark101.github.io/kulms-popup-draggable
// @version 1.3
// @description 팝업 드래그 이동 및 클릭 시 최상단 배치 기능
// @author explainpark101
// @match https://lms.korea.ac.kr/
// @grant GM_addStyle
// @license MIT
// ==/UserScript==
(function() {
'use strict';
GM_addStyle(`
.xnp-item-close {
cursor: move !important;
user-select: none !important;
}
.xnp-item-close * {
cursor: pointer;
}
/* 드래그 중인 요소에 대한 시각적 피드백 (선택사항) */
.xnp-item-wrapper:active {
opacity: 0.9;
}
`);
const STORAGE_KEY = 'xnp_popup_positions';
let popupList = []; // 현재 화면에 있는 팝업 요소를 관리하는 배열
const getSavedPositions = () => JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}');
const savePosition = (id, top, left) => {
const positions = getSavedPositions();
positions[id] = { top, left };
localStorage.setItem(STORAGE_KEY, JSON.stringify(positions));
};
// 요소를 가장 앞으로 가져오는 함수 (DOM 순서 변경)
const bringToFront = (wrapper) => {
const parent = wrapper.parentNode;
if (parent && parent.lastChild !== wrapper) {
parent.appendChild(wrapper); // 부모 노드의 마지막 자식으로 옮겨 가장 위에 표시되게 함
// 배열 순서 업데이트
popupList = popupList.filter(item => item !== wrapper);
popupList.push(wrapper);
console.log(`[Draggable] Popup moved to front: ${wrapper.dataset.popupId}`);
}
};
const attachDragEvent = (wrapper) => {
if (wrapper.dataset.draggableInitialized === "true") return;
const popupId = wrapper.getAttribute('data-popup-id');
const handle = wrapper.querySelector('.xnp-item-close');
if (!handle || !popupId) return;
// 초기 로드 시 배열에 추가
popupList.push(wrapper);
// 위치 복원
const savedPositions = getSavedPositions();
if (savedPositions[popupId]) {
wrapper.style.setProperty('--popup-top', savedPositions[popupId].top);
wrapper.style.setProperty('--popup-left', savedPositions[popupId].left);
}
// 클릭(포커스) 시 최상단으로 이동
wrapper.addEventListener('mousedown', () => bringToFront(wrapper), true);
// 드래그 로직
handle.addEventListener('mousedown', (e) => {
if (e.target.closest('input') || e.target.closest('button')) return;
e.preventDefault();
const computed = getComputedStyle(wrapper);
let currentTop = parseInt(computed.getPropertyValue('--popup-top')) || 0;
let currentLeft = parseInt(computed.getPropertyValue('--popup-left')) || 0;
const startX = e.clientX;
const startY = e.clientY;
const onMouseMove = (moveEvent) => {
const deltaX = moveEvent.clientX - startX;
const deltaY = moveEvent.clientY - startY;
wrapper.style.setProperty('--popup-top', `${currentTop + deltaY}px`);
wrapper.style.setProperty('--popup-left', `${currentLeft + deltaX}px`);
};
const onMouseUp = () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
savePosition(popupId, wrapper.style.getPropertyValue('--popup-top'), wrapper.style.getPropertyValue('--popup-left'));
};
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
});
wrapper.dataset.draggableInitialized = "true";
console.log(`[Draggable] Registered & Added to Array: ${popupId}`, wrapper);
};
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType !== 1) return;
if (node.classList && node.classList.contains('xnp-item-wrapper')) {
attachDragEvent(node);
} else {
const wrappers = node.querySelectorAll('.xnp-item-wrapper');
wrappers.forEach(w => attachDragEvent(w));
}
});
// 제거된 노드 처리 (배열에서 제거)
mutation.removedNodes.forEach(node => {
if (node.nodeType !== 1) return;
popupList = popupList.filter(item => item !== node && !node.contains(item));
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
document.querySelectorAll('.xnp-item-wrapper').forEach(attachDragEvent);
})();