KULMS Popup Draggable

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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