Mobile Element Selector

모바일 요소 선택기

当前为 2025-04-15 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Mobile Element Selector
  3. // @author ZNJXL
  4. // @version 1
  5. // @namespace http://tampermonkey.net/
  6. // @description 모바일 요소 선택기
  7. // @match *://*/*
  8. // @license MIT
  9. // @grant GM_setClipboard
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. 'use strict';
  14.  
  15. let selecting = false;
  16. let selectedEl = null;
  17. let initialTouchedElement = null;
  18. let includeSiteName = false; // 사이트명 포함 여부
  19.  
  20. // 터치 탭/스크롤 구분용 변수
  21. let touchStartX = 0, touchStartY = 0;
  22. let touchMoved = false;
  23. const moveThreshold = 10; // 픽셀 이동 임계값
  24.  
  25. // UI 관련 CSS
  26. const style = document.createElement('style');
  27. style.textContent = `
  28. /* 기본 폰트 및 UI */
  29. .mobile-block-ui {
  30. z-index: 9999 !important;
  31. touch-action: manipulation !important;
  32. font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  33. box-sizing: border-box;
  34. }
  35. /* 슬라이더 스타일 */
  36. #blocker-slider {
  37. width: 100%; margin: 10px 0; -webkit-appearance: none; appearance: none;
  38. background: #555; height: 8px; border-radius: 5px; outline: none;
  39. }
  40. #blocker-slider::-webkit-slider-thumb {
  41. -webkit-appearance: none; appearance: none; width: 20px; height: 20px;
  42. background: #4CAF50; border-radius: 50%; cursor: pointer;
  43. border: 2px solid #fff; box-shadow: 0 2px 5px rgba(0,0,0,0.3);
  44. }
  45. #blocker-slider::-moz-range-thumb {
  46. width: 20px; height: 20px; background: #4CAF50; border-radius: 50%;
  47. cursor: pointer; border: 2px solid #fff; box-shadow: 0 2px 5px rgba(0,0,0,0.3);
  48. }
  49.  
  50. /* --- 선택된 요소 하이라이트 스타일 변경 --- */
  51. .selected-element {
  52. /* 기존 outline, box-shadow 제거 */
  53. background-color: rgba(255, 0, 0, 0.25) !important; /* 반투명 붉은 배경 */
  54. /* outline: none !important; */ /* 필요 시 outline 명시적 제거 */
  55. /* box-shadow: none !important; */ /* 필요 시 box-shadow 명시적 제거 */
  56. z-index: 9998 !important; /* 다른 요소 위에 보이도록 */
  57. }
  58. /* --- 하이라이트 스타일 변경 끝 --- */
  59.  
  60. /* 패널 스타일 */
  61. #mobile-block-panel {
  62. position: fixed; bottom: 15px; left: 15px; right: 15px;
  63. background: rgba(40, 40, 40, 0.95); color: #eee; padding: 15px;
  64. border-radius: 12px; box-shadow: 0 5px 15px rgba(0,0,0,0.6);
  65. display: none; z-index: 10001; border-top: 1px solid rgba(255, 255, 255, 0.1);
  66. }
  67. /* 토글 버튼 스타일 */
  68. #mobile-block-toggleBtn {
  69. position: fixed; top: 15px; right: 15px; z-index: 10002;
  70. background: linear-gradient(145deg, #f44336, #d32f2f); color: #fff;
  71. padding: 10px 18px; border-radius: 25px; font-size: 15px; font-weight: 500;
  72. border: none; cursor: pointer; box-shadow: 0 4px 10px rgba(0,0,0,0.3);
  73. transition: background 0.3s ease, transform 0.2s ease, box-shadow 0.3s ease;
  74. }
  75. #mobile-block-toggleBtn:hover {
  76. transform: translateY(-2px); box-shadow: 0 6px 15px rgba(0,0,0,0.4);
  77. }
  78. #mobile-block-toggleBtn.selecting { background: linear-gradient(145deg, #4CAF50, #388E3C); }
  79. /* 버튼 스타일 */
  80. .mb-btn {
  81. padding: 10px; border: none; border-radius: 8px; color: #fff;
  82. font-size: 14px; cursor: pointer;
  83. transition: background 0.2s ease, transform 0.1s ease, box-shadow 0.2s ease;
  84. background-color: #555; text-align: center; box-shadow: 0 2px 4px rgba(0,0,0,0.2);
  85. }
  86. .mb-btn:active { transform: scale(0.97); box-shadow: inset 0 2px 4px rgba(0,0,0,0.3); }
  87. #blocker-copy { background: linear-gradient(145deg, #2196F3, #1976D2); }
  88. #blocker-toggle-site { background: linear-gradient(145deg, #9C27B0, #7B1FA2); }
  89. #blocker-block { background: linear-gradient(145deg, #f44336, #c62828); }
  90. #blocker-cancel { background: linear-gradient(145deg, #607D8B, #455A64); }
  91. /* 정보 텍스트 및 버튼 그리드 */
  92. #blocker-info-wrapper { position: relative; margin-bottom: 10px; }
  93. #blocker-info {
  94. display: block; color: #90ee90; font-size: 13px; line-height: 1.4;
  95. background-color: rgba(0,0,0,0.3); padding: 5px 8px; border-radius: 4px;
  96. word-break: break-all;
  97. }
  98. .button-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(80px, 1fr)); gap: 8px; margin-top: 15px; }
  99. `;
  100. document.head.appendChild(style);
  101.  
  102. // UI 패널 생성
  103. const panel = document.createElement('div');
  104. panel.id = 'mobile-block-panel';
  105. panel.classList.add('mobile-block-ui', 'ui-ignore');
  106. panel.innerHTML = `
  107. <div id="blocker-info-wrapper">
  108. <span style="font-size: 12px; color: #ccc;">선택된 요소:</span>
  109. <span id="blocker-info">없음</span>
  110. </div>
  111. <input type="range" id="blocker-slider" min="0" max="10" value="0" class="ui-ignore">
  112. <div class="button-grid">
  113. <button id="blocker-copy" class="mb-btn ui-ignore">복사</button>
  114. <button id="blocker-toggle-site" class="mb-btn ui-ignore">사이트명: OFF</button>
  115. <button id="blocker-block" class="mb-btn ui-ignore">차단</button>
  116. <button id="blocker-cancel" class="mb-btn ui-ignore">취소</button>
  117. </div>
  118. `;
  119. document.body.appendChild(panel);
  120.  
  121. // 토글 버튼 생성
  122. const toggleBtn = document.createElement('button');
  123. toggleBtn.id = 'mobile-block-toggleBtn';
  124. toggleBtn.classList.add('mobile-block-ui', 'ui-ignore');
  125. toggleBtn.textContent = '차단 모드';
  126. document.body.appendChild(toggleBtn);
  127.  
  128. // 모드 토글 함수
  129. function setBlockMode(enabled) {
  130. selecting = enabled;
  131. toggleBtn.textContent = enabled ? '선택 중...' : '차단 모드';
  132. toggleBtn.classList.toggle('selecting', enabled);
  133. panel.style.display = enabled ? 'block' : 'none';
  134. if (!enabled && selectedEl) {
  135. selectedEl.classList.remove('selected-element'); // 하이라이트 제거
  136. selectedEl = null;
  137. initialTouchedElement = null;
  138. }
  139. // 모드 진입/종료 시 슬라이더와 정보 초기화
  140. panel.querySelector('#blocker-slider').value = 0;
  141. updateInfo(); // selectedEl이 null이면 '없음'으로 표시됨
  142. }
  143.  
  144. // 선택된 요소 정보 업데이트
  145. function updateInfo() {
  146. const infoSpan = panel.querySelector('#blocker-info');
  147. infoSpan.textContent = selectedEl ? generateSelector(selectedEl) : '없음';
  148. }
  149.  
  150. // *** 상세 CSS 선택자 생성 (간결화: 최대 깊이 제한 추가) ***
  151. function generateSelector(el) {
  152. if (!el || el.nodeType !== 1) return '';
  153. const parts = [];
  154. let current = el;
  155. const maxDepth = 5; // *** 선택자 최대 깊이 제한 (예: 5단계) ***
  156. let depth = 0;
  157.  
  158. // 최대 깊이 조건 추가: && depth < maxDepth
  159. while (current && current.tagName && current.tagName.toLowerCase() !== 'body' && current.tagName.toLowerCase() !== 'html' && depth < maxDepth) {
  160. const parent = current.parentElement;
  161. const tagName = current.tagName.toLowerCase();
  162. let selectorPart = tagName;
  163.  
  164. if (current.id) {
  165. selectorPart = `#${current.id}`;
  166. parts.unshift(selectorPart);
  167. depth++; // ID도 깊이에 포함
  168. break; // ID 발견 시 종료
  169. } else {
  170. const classes = Array.from(current.classList)
  171. .filter(c => !['selected-element', 'mobile-block-ui', 'ui-ignore'].includes(c));
  172. if (classes.length > 0) {
  173. selectorPart = '.' + classes.join('.'); // 클래스 사용 (태그 생략)
  174. } else if (parent) {
  175. const siblings = Array.from(parent.children);
  176. let sameTagIndex = 0;
  177. let found = false;
  178. for (let i = 0; i < siblings.length; i++) {
  179. if (siblings[i].tagName === current.tagName) {
  180. sameTagIndex++;
  181. if (siblings[i] === current) {
  182. found = true; break;
  183. }
  184. }
  185. }
  186. if (found && sameTagIndex > 0) {
  187. selectorPart = `${tagName}:nth-of-type(${sameTagIndex})`; // nth-of-type 사용
  188. }
  189. // else: Fallback to just tagName (already default)
  190. }
  191. parts.unshift(selectorPart);
  192. depth++; // 깊이 증가
  193. }
  194.  
  195. if (!parent || parent.tagName.toLowerCase() === 'body' || parent.tagName.toLowerCase() === 'html') {
  196. break; // body 또는 html 도달 시 종료
  197. }
  198. current = parent;
  199. }
  200. return parts.join(' > '); // 최종 선택자 반환
  201. }
  202.  
  203. // 터치 이벤트 관련 설정
  204. const uiExcludeClass = '.ui-ignore';
  205. document.addEventListener('touchstart', e => {
  206. if (!selecting || e.target.closest(uiExcludeClass)) return;
  207. const touch = e.touches[0];
  208. touchStartX = touch.clientX; touchStartY = touch.clientY; touchMoved = false;
  209. }, { passive: true });
  210.  
  211. document.addEventListener('touchmove', e => {
  212. if (!selecting || e.target.closest(uiExcludeClass) || !e.touches[0]) return;
  213. if (!touchMoved) {
  214. const touch = e.touches[0];
  215. const dx = touch.clientX - touchStartX; const dy = touch.clientY - touchStartY;
  216. if (Math.sqrt(dx * dx + dy * dy) > moveThreshold) touchMoved = true;
  217. }
  218. }, { passive: true });
  219.  
  220. document.addEventListener('touchend', e => {
  221. if (!selecting || e.target.closest(uiExcludeClass)) return;
  222. if (touchMoved) { touchMoved = false; return; } // 스크롤 시 무시
  223.  
  224. e.preventDefault(); e.stopImmediatePropagation();
  225. const touch = e.changedTouches[0];
  226. // 가장 안쪽 요소 찾기
  227. const targetEl = document.elementFromPoint(touch.clientX, touch.clientY);
  228.  
  229. if (!targetEl || targetEl.closest(uiExcludeClass)) return; // UI 요소면 무시
  230.  
  231. if (selectedEl) selectedEl.classList.remove('selected-element'); // 이전 하이라이트 제거
  232.  
  233. selectedEl = targetEl; // 새 요소 선택
  234. initialTouchedElement = targetEl; // 슬라이더 기준점 설정
  235. selectedEl.classList.add('selected-element'); // 하이라이트 적용
  236.  
  237. // !!! 중요: 요소 선택 후 즉시 슬라이더 리셋 및 정보 업데이트 !!!
  238. panel.querySelector('#blocker-slider').value = 0;
  239. updateInfo();
  240.  
  241. }, { capture: true, passive: false });
  242.  
  243. // 슬라이더 이벤트 처리
  244. const slider = panel.querySelector('#blocker-slider');
  245. slider.addEventListener('input', handleSlider);
  246. function handleSlider(e) {
  247. if (!initialTouchedElement) return;
  248. const level = parseInt(e.target.value, 10);
  249. let current = initialTouchedElement;
  250. for (let i = 0; i < level && current.parentElement; i++) {
  251. if (current.parentElement.tagName.toLowerCase() === 'body' || current.parentElement.tagName.toLowerCase() === 'html') break;
  252. current = current.parentElement;
  253. }
  254. if (selectedEl) selectedEl.classList.remove('selected-element');
  255. selectedEl = current;
  256. selectedEl.classList.add('selected-element');
  257. updateInfo(); // 슬라이더 조작 시 정보 업데이트
  258. }
  259.  
  260. // 복사 버튼 이벤트
  261. panel.querySelector('#blocker-copy').addEventListener('click', () => {
  262. if (selectedEl) {
  263. const fullSelector = generateSelector(selectedEl); // 간결화된 선택자 생성
  264. let finalSelector = "##" + fullSelector;
  265. if (includeSiteName) finalSelector = location.hostname + finalSelector;
  266. try {
  267. GM_setClipboard(finalSelector);
  268. alert('✅ 선택자가 복사되었습니다!\n' + finalSelector);
  269. } catch (err) {
  270. console.error("클립보드 복사 실패:", err);
  271. alert("❌ 클립보드 복사에 실패했습니다.");
  272. prompt("선택자를 직접 복사하세요:", finalSelector);
  273. }
  274. } else { alert('선택된 요소가 없습니다.'); }
  275. });
  276.  
  277. // 사이트명 토글 버튼
  278. const toggleSiteBtn = panel.querySelector('#blocker-toggle-site');
  279. toggleSiteBtn.addEventListener('click', () => {
  280. includeSiteName = !includeSiteName;
  281. toggleSiteBtn.textContent = includeSiteName ? "사이트명: ON" : "사이트명: OFF";
  282. toggleSiteBtn.style.background = includeSiteName ? 'linear-gradient(145deg, #8E24AA, #6A1B9A)' : 'linear-gradient(145deg, #9C27B0, #7B1FA2)';
  283. });
  284.  
  285. // 차단 버튼
  286. panel.querySelector('#blocker-block').addEventListener('click', () => {
  287. if (selectedEl) {
  288. console.log("차단 요청:", generateSelector(selectedEl));
  289. selectedEl.style.display = 'none';
  290. setBlockMode(false);
  291. } else { alert('차단할 요소가 선택되지 않았습니다.'); }
  292. });
  293.  
  294. // 취소 버튼
  295. panel.querySelector('#blocker-cancel').addEventListener('click', () => setBlockMode(false));
  296.  
  297. // 메인 토글 버튼
  298. toggleBtn.addEventListener('click', () => setBlockMode(!selecting));
  299.  
  300. })();