KULMS Popup Draggable

팝업 드래그 이동 및 클릭 시 최상단 배치 기능

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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);
})();