KULMS Popup Draggable

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

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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);
})();