KULMS Popup Draggable

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

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği yüklemek için Tampermonkey gibi bir uzantı yüklemeniz gerekir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

Bu stili yüklemek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için Stylus gibi bir uzantı kurmanız gerekir.

Bu stili yükleyebilmek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı kurmanız gerekir.

Bu stili yükleyebilmek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

(Zateb bir user-style yöneticim var, yükleyeyim!)

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