Mobile Element Selector

모바일 요소 선택기

Ekde 2025/04/16. Vidu La ĝisdata versio.

// ==UserScript== 
// @name         Mobile Element Selector 
// @author       ZNJXL 
// @version      1.1.5
// @namespace    http://tampermonkey.net/ 
// @description  모바일 요소 선택기 
// @match        :///* /
// @license      MIT 
// @grant        GM_setClipboard // ==/UserScript==

(function() { 'use strict';

let selecting = false;
let selectedEl = null;
let initialTouchedElement = null;
let includeSiteName = true;
let touchStartX = 0, touchStartY = 0;
let touchMoved = false;
const moveThreshold = 10;

const style = document.createElement('style');
style.textContent = `
    .mobile-block-ui {
        z-index: 9999 !important;
        touch-action: manipulation !important;
        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        box-sizing: border-box;
        position: fixed;
    }
    #blocker-slider {
        width: 100%; margin: 10px 0; -webkit-appearance: none; appearance: none;
        background: #555; height: 8px; border-radius: 5px; outline: none;
    }
    #blocker-slider::-webkit-slider-thumb {
        -webkit-appearance: none; appearance: none; width: 20px; height: 20px;
        background: #4CAF50; border-radius: 50%; cursor: pointer;
        border: 2px solid #fff; box-shadow: 0 2px 5px rgba(0,0,0,0.3);
    }
    #blocker-slider::-moz-range-thumb {
        width: 20px; height: 20px; background: #4CAF50; border-radius: 50%;
        cursor: pointer; border: 2px solid #fff; box-shadow: 0 2px 5px rgba(0,0,0,0.3);
    }
    .selected-element {
        background-color: rgba(255, 0, 0, 0.25) !important;
        z-index: 9998 !important;
    }
    #mobile-block-panel {
        top: calc(100vh - 150px); left: 15px;
        width: 300px;
        background: rgba(40, 40, 40, 0.95); color: #eee; padding: 15px;
        border-radius: 12px; box-shadow: 0 5px 15px rgba(0,0,0,0.6);
        display: none; z-index: 10001; border-top: 1px solid rgba(255, 255, 255, 0.1);
    }
    #mobile-block-toggleBtn {
        top: 15px; left: 15px; z-index: 10002;
        background: linear-gradient(145deg, #f44336, #d32f2f); color: #fff;
        padding: 10px 18px; border-radius: 25px; font-size: 15px; font-weight: 500;
        border: none; cursor: pointer; box-shadow: 0 4px 10px rgba(0,0,0,0.3);
        transition: background 0.3s ease, transform 0.2s ease, box-shadow 0.3s ease;
        min-width: 100px;
    }
    #mobile-block-toggleBtn:hover {
        transform: translateY(-2px); box-shadow: 0 6px 15px rgba(0,0,0,0.4);
    }
    #mobile-block-toggleBtn.selecting { background: linear-gradient(145deg, #4CAF50, #388E3C); }
    .mb-btn {
        padding: 10px; border: none; border-radius: 8px; color: #fff;
        font-size: 14px; cursor: pointer;
        transition: background 0.2s ease, transform 0.1s ease, box-shadow 0.2s ease;
        background-color: #555; text-align: center; box-shadow: 0 2px 4px rgba(0,0,0,0.2);
        min-width: 80px;
    }
    .mb-btn:active { transform: scale(0.97); box-shadow: inset 0 2px 4px rgba(0,0,0,0.3); }
    #blocker-copy { background: linear-gradient(145deg, #2196F3, #1976D2); }
    #blocker-toggle-site { background: linear-gradient(145deg, #9C27B0, #7B1FA2); color: #fff; }
    #blocker-block { background: linear-gradient(145deg, #f44336, #c62828); }
    #blocker-cancel { background: linear-gradient(145deg, #607D8B, #455A64); }
    .button-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(80px, 1fr)); gap: 8px; margin-top: 15px; }
    #blocker-info-wrapper { position: relative; margin-bottom: 10px; }
    #blocker-info {
        display: block; color: #90ee90; font-size: 13px; line-height: 1.4;
        background-color: rgba(0,0,0,0.3); padding: 5px 8px; border-radius: 4px;
        word-break: break-all;
    }
`;
document.head.appendChild(style);

const panel = document.createElement('div');
panel.id = 'mobile-block-panel';
panel.classList.add('mobile-block-ui', 'ui-ignore');
panel.innerHTML = `
    <div id="blocker-info-wrapper">
        <span style="font-size: 12px; color: #ccc;">선택된 요소:</span>
        <span id="blocker-info">없음</span>
    </div>
    <input type="range" id="blocker-slider" min="0" max="10" value="0" class="ui-ignore">
    <div class="button-grid">
        <button id="blocker-copy" class="mb-btn ui-ignore">복사</button>
        <button id="blocker-toggle-site" class="mb-btn ui-ignore">${includeSiteName ? "사이트명: ON" : "사이트명: OFF"}</button>
        <button id="blocker-block" class="mb-btn ui-ignore">미리보기</button>
        <button id="blocker-cancel" class="mb-btn ui-ignore">취소</button>
    </div>
`;
document.body.appendChild(panel);

const toggleBtn = document.createElement('button');
toggleBtn.id = 'mobile-block-toggleBtn';
toggleBtn.classList.add('mobile-block-ui', 'ui-ignore');
toggleBtn.textContent = '차단 모드';
document.body.appendChild(toggleBtn);

function setBlockMode(enabled) {
    selecting = enabled;
    toggleBtn.textContent = enabled ? '선택 중...' : '차단 모드';
    toggleBtn.classList.toggle('selecting', enabled);
    panel.style.display = enabled ? 'block' : 'none';
    if (!enabled && selectedEl) {
        selectedEl.classList.remove('selected-element');
        selectedEl = null;
        initialTouchedElement = null;
    }
    panel.querySelector('#blocker-slider').value = 0;
    updateInfo();
}

function updateInfo() {
    const infoSpan = panel.querySelector('#blocker-info');
    infoSpan.textContent = selectedEl ? generateSelector(selectedEl) : '없음';
}

function generateSelector(el) {
    if (!el || el.nodeType !== 1) return '';
    const parts = [];
    let current = el;
    const maxDepth = 5;
    let depth = 0;
    while (current && current.tagName && current.tagName.toLowerCase() !== 'body' && current.tagName.toLowerCase() !== 'html' && depth < maxDepth) {
        const parent = current.parentElement;
        const tagName = current.tagName.toLowerCase();
        let selectorPart = tagName;
        if (current.id) {
            selectorPart = `#${current.id}`;
            parts.unshift(selectorPart);
            depth++;
            break;
        } else {
            const classes = Array.from(current.classList).filter(c => !['selected-element', 'mobile-block-ui', 'ui-ignore'].includes(c));
            if (classes.length > 0) {
                selectorPart = '.' + classes.join('.');
            } else if (parent) {
                const siblings = Array.from(parent.children);
                let sameTagIndex = 0;
                let found = false;
                for (let i = 0; i < siblings.length; i++) {
                    if (siblings[i].tagName === current.tagName) {
                        sameTagIndex++;
                        if (siblings[i] === current) { found = true; break; }
                    }
                }
                if (found && sameTagIndex > 0) {
                    selectorPart = `${tagName}:nth-of-type(${sameTagIndex})`;
                }
            }
            parts.unshift(selectorPart);
            depth++;
        }
        if (!parent || parent.tagName.toLowerCase() === 'body' || parent.tagName.toLowerCase() === 'html') break;
        current = parent;
    }
    return parts.join(' > ');
}

const uiExcludeClass = '.ui-ignore';
document.addEventListener('touchstart', e => {
    if (!selecting || e.target.closest(uiExcludeClass)) return;
    const touch = e.touches[0];
    touchStartX = touch.clientX; touchStartY = touch.clientY; touchMoved = false;
}, { passive: true });
document.addEventListener('touchmove', e => {
    if (!selecting || e.target.closest(uiExcludeClass) || !e.touches[0]) return;
    if (!touchMoved) {
        const touch = e.touches[0];
        const dx = touch.clientX - touchStartX, dy = touch.clientY - touchStartY;
        if (Math.sqrt(dx * dx + dy * dy) > moveThreshold) touchMoved = true;
    }
}, { passive: true });
document.addEventListener('touchend', e => {
    if (!selecting || e.target.closest(uiExcludeClass)) return;
    if (touchMoved) { touchMoved = false; return; }
    e.preventDefault(); e.stopImmediatePropagation();
    const touch = e.changedTouches[0];
    const targetEl = document.elementFromPoint(touch.clientX, touch.clientY);
    if (!targetEl || targetEl.closest(uiExcludeClass)) return;
    if (selectedEl) selectedEl.classList.remove('selected-element');
    selectedEl = targetEl;
    initialTouchedElement = targetEl;
    selectedEl.classList.add('selected-element');
    panel.querySelector('#blocker-slider').value = 0;
    updateInfo();
}, { capture: true, passive: false });

const slider = panel.querySelector('#blocker-slider');
slider.addEventListener('input', handleSlider);
function handleSlider(e) {
    if (!initialTouchedElement) return;
    const level = parseInt(e.target.value, 10);
    let current = initialTouchedElement;
    for (let i = 0; i < level && current.parentElement; i++) {
        if (current.parentElement.tagName.toLowerCase() === 'body' || current.parentElement.tagName.toLowerCase() === 'html') break;
        current = current.parentElement;
    }
    if (selectedEl) selectedEl.classList.remove('selected-element');
    selectedEl = current;
    selectedEl.classList.add('selected-element');
    updateInfo();
}

panel.querySelector('#blocker-copy').addEventListener('click', () => {
    if (selectedEl) {
        const fullSelector = generateSelector(selectedEl);
        let finalSelector = "##" + fullSelector;
        if (includeSiteName) finalSelector = location.hostname + finalSelector;
        try {
            GM_setClipboard(finalSelector);
            alert('✅ 선택자가 복사되었습니다!\n' + finalSelector);
        } catch (err) {
            console.error("클립보드 복사 실패:", err);
            alert("❌ 클립보드 복사에 실패했습니다.");
            prompt("선택자를 직접 복사하세요:", finalSelector);
        }
    } else { alert('선택된 요소가 없습니다.'); }
});

panel.querySelector('#blocker-toggle-site').addEventListener('click', () => {
    includeSiteName = !includeSiteName;
    panel.querySelector('#blocker-toggle-site').textContent = includeSiteName ? "사이트명: ON" : "사이트명: OFF";
});

const blockBtn = panel.querySelector('#blocker-block');
let isHidden = false;

blockBtn.textContent = '미리보기';
blockBtn.addEventListener('click', () => {
    if (!selectedEl) {
        alert('선택된 요소가 없습니다.');
        return;
    }
    if (!isHidden) {
        selectedEl.dataset._original_display = selectedEl.style.display || '';
        selectedEl.style.display = 'none';
        blockBtn.textContent = '되돌리기';
        isHidden = true;
    } else {
        selectedEl.style.display = selectedEl.dataset._original_display || '';
        blockBtn.textContent = '미리보기';
        isHidden = false;
    }
});

panel.querySelector('#blocker-cancel').addEventListener('click', () => setBlockMode(false));
toggleBtn.addEventListener('click', () => setBlockMode(!selecting));

function makeDraggable(el) {
    let startX, startY, origX, origY;
    let dragging = false, moved = false;
    el.addEventListener('touchstart', function(e) {
        if (e.target.tagName.toLowerCase() === 'input') return;
        startX = e.touches[0].clientX;
        startY = e.touches[0].clientY;
        const rect = el.getBoundingClientRect();
        origX = rect.left;
        origY = rect.top;
        dragging = true;
        moved = false;
    }, {passive: true});
    el.addEventListener('touchmove', function(e) {
        if (!dragging) return;
        const dx = e.touches[0].clientX - startX;
        const dy = e.touches[0].clientY - startY;
        if (Math.sqrt(dx * dx + dy * dy) > moveThreshold) {
            moved = true;
            el.style.left = (origX + dx) + 'px';
            el.style.top = (origY + dy) + 'px';
            el.style.right = 'auto';
            el.style.bottom = 'auto';
            e.preventDefault();
        }
    }, {passive: false});
    el.addEventListener('touchend', function(e) {
        dragging = false;
        if(moved) {
            e.preventDefault();
            e.stopPropagation();
        }
    }, {passive: false});
}

makeDraggable(panel);
makeDraggable(toggleBtn);

})();