KULMS Popup Draggable

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

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

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