Greasy Fork 还支持 简体中文。

Mobile Element Selector

모바일 요소 선택기

目前為 2025-04-16 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name Mobile Element Selector
  3. // @author ZNJXL
  4. // @version 1.2.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 = true;
  19. let touchStartX = 0, touchStartY = 0;
  20. let touchMoved = false;
  21. const moveThreshold = 10;
  22.  
  23. const style = document.createElement('style');
  24. style.textContent = `
  25. .mobile-block-ui {
  26. z-index: 9999 !important;
  27. touch-action: manipulation !important;
  28. font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  29. box-sizing: border-box;
  30. position: fixed;
  31. }
  32. #blocker-slider {
  33. width: 100%; margin: 10px 0; -webkit-appearance: none; appearance: none;
  34. background: #555; height: 8px; border-radius: 5px; outline: none;
  35. }
  36. #blocker-slider::-webkit-slider-thumb {
  37. -webkit-appearance: none; appearance: none; width: 20px; height: 20px;
  38. background: #4CAF50; border-radius: 50%; cursor: pointer;
  39. border: 2px solid #fff; box-shadow: 0 2px 5px rgba(0,0,0,0.3);
  40. }
  41. #blocker-slider::-moz-range-thumb {
  42. width: 20px; height: 20px; background: #4CAF50; border-radius: 50%;
  43. cursor: pointer; border: 2px solid #fff; box-shadow: 0 2px 5px rgba(0,0,0,0.3);
  44. }
  45. .selected-element {
  46. background-color: rgba(255, 0, 0, 0.25) !important;
  47. z-index: 9998 !important;
  48. }
  49. #mobile-block-panel {
  50. top: calc(100vh - 150px); left: 15px;
  51. width: 300px;
  52. background: rgba(40, 40, 40, 0.95); color: #eee; padding: 15px;
  53. border-radius: 12px; box-shadow: 0 5px 15px rgba(0,0,0,0.6);
  54. display: none; z-index: 10001; border-top: 1px solid rgba(255, 255, 255, 0.1);
  55. }
  56. /* 차단모드 버튼 스타일 */
  57. #mobile-block-toggleBtn {
  58. top: 15px; left: 15px; z-index: 10002;
  59. background: rgba(0,0,0,0.1);
  60. width: 40px; height: 40px;
  61. border-radius: 50%;
  62. border: none;
  63. cursor: pointer;
  64. /* 기본 텍스트 제거 */
  65. font-size: 0;
  66. box-shadow: none;
  67. transition: background 0.3s ease;
  68. display: flex;
  69. align-items: center;
  70. justify-content: center;
  71. }
  72. #mobile-block-toggleBtn:hover {
  73. background: rgba(0,0,0,0.2);
  74. }
  75. #mobile-block-toggleBtn.selecting {
  76. background: rgba(0,0,0,0.15);
  77. }
  78. /* 중앙 + 아이콘 스타일 */
  79. #mobile-block-toggleBtn .button-plus {
  80. font-size: 24px;
  81. color: #fff;
  82. line-height: 40px;
  83. }
  84. .mb-btn {
  85. padding: 10px; border: none; border-radius: 8px; color: #fff;
  86. font-size: 14px; cursor: pointer;
  87. transition: background 0.2s ease, transform 0.1s ease, box-shadow 0.2s ease;
  88. background-color: #555; text-align: center; box-shadow: 0 2px 4px rgba(0,0,0,0.2);
  89. min-width: 80px;
  90. }
  91. .mb-btn:active { transform: scale(0.97); box-shadow: inset 0 2px 4px rgba(0,0,0,0.3); }
  92. #blocker-copy { background: linear-gradient(145deg, #2196F3, #1976D2); }
  93. #blocker-toggle-site { background: linear-gradient(145deg, #9C27B0, #7B1FA2); color: #fff; }
  94. #blocker-block { background: linear-gradient(145deg, #f44336, #c62828); }
  95. #blocker-cancel { background: linear-gradient(145deg, #607D8B, #455A64); }
  96. .button-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(80px, 1fr)); gap: 8px; margin-top: 15px; }
  97. #blocker-info-wrapper { position: relative; margin-bottom: 10px; }
  98. #blocker-info {
  99. display: block; color: #90ee90; font-size: 13px; line-height: 1.4;
  100. background-color: rgba(0,0,0,0.3); padding: 5px 8px; border-radius: 4px;
  101. word-break: break-all;
  102. }
  103. `;
  104. document.head.appendChild(style);
  105.  
  106. const panel = document.createElement('div');
  107. panel.id = 'mobile-block-panel';
  108. panel.classList.add('mobile-block-ui', 'ui-ignore');
  109. panel.innerHTML = `
  110. <div id="blocker-info-wrapper">
  111. <span style="font-size: 12px; color: #ccc;">선택된 요소:</span>
  112. <span id="blocker-info">없음</span>
  113. </div>
  114. <input type="range" id="blocker-slider" min="0" max="10" value="0" class="ui-ignore">
  115. <div class="button-grid">
  116. <button id="blocker-copy" class="mb-btn ui-ignore">복사</button>
  117. <button id="blocker-toggle-site" class="mb-btn ui-ignore">${includeSiteName ? "사이트명: ON" : "사이트명: OFF"}</button>
  118. <button id="blocker-block" class="mb-btn ui-ignore">미리보기</button>
  119. <button id="blocker-cancel" class="mb-btn ui-ignore">취소</button>
  120. </div>
  121. `;
  122. document.body.appendChild(panel);
  123.  
  124. // 토글 버튼에 흰색 '+' 아이콘 추가
  125. const toggleBtn = document.createElement('button');
  126. toggleBtn.id = 'mobile-block-toggleBtn';
  127. toggleBtn.classList.add('mobile-block-ui', 'ui-ignore');
  128. toggleBtn.innerHTML = '<span class="button-plus">+</span>';
  129. document.body.appendChild(toggleBtn);
  130.  
  131. function setBlockMode(enabled) {
  132. selecting = enabled;
  133. toggleBtn.classList.toggle('selecting', enabled);
  134. panel.style.display = enabled ? 'block' : 'none';
  135. if (!enabled && selectedEl) {
  136. selectedEl.classList.remove('selected-element');
  137. selectedEl = null;
  138. initialTouchedElement = null;
  139. }
  140. panel.querySelector('#blocker-slider').value = 0;
  141. updateInfo();
  142. }
  143.  
  144. function updateInfo() {
  145. const infoSpan = panel.querySelector('#blocker-info');
  146. infoSpan.textContent = selectedEl ? generateSelector(selectedEl) : '없음';
  147. }
  148.  
  149. function generateSelector(el) {
  150. if (!el || el.nodeType !== 1) return '';
  151. const parts = [];
  152. let current = el;
  153. const maxDepth = 5;
  154. let depth = 0;
  155. while (current && current.tagName && current.tagName.toLowerCase() !== 'body' && current.tagName.toLowerCase() !== 'html' && depth < maxDepth) {
  156. const parent = current.parentElement;
  157. const tagName = current.tagName.toLowerCase();
  158. let selectorPart = tagName;
  159. if (current.id) {
  160. selectorPart = `#${current.id}`;
  161. parts.unshift(selectorPart);
  162. depth++;
  163. break;
  164. } else {
  165. const classes = Array.from(current.classList).filter(c => !['selected-element', 'mobile-block-ui', 'ui-ignore'].includes(c));
  166. if (classes.length > 0) {
  167. selectorPart = '.' + classes.join('.');
  168. } else if (parent) {
  169. const siblings = Array.from(parent.children);
  170. let sameTagIndex = 0;
  171. let found = false;
  172. for (let i = 0; i < siblings.length; i++) {
  173. if (siblings[i].tagName === current.tagName) {
  174. sameTagIndex++;
  175. if (siblings[i] === current) { found = true; break; }
  176. }
  177. }
  178. if (found && sameTagIndex > 0) {
  179. selectorPart = `${tagName}:nth-of-type(${sameTagIndex})`;
  180. }
  181. }
  182. parts.unshift(selectorPart);
  183. depth++;
  184. }
  185. if (!parent || parent.tagName.toLowerCase() === 'body' || parent.tagName.toLowerCase() === 'html') break;
  186. current = parent;
  187. }
  188. return parts.join(' > ');
  189. }
  190.  
  191. const uiExcludeClass = '.ui-ignore';
  192. document.addEventListener('touchstart', e => {
  193. if (!selecting || e.target.closest(uiExcludeClass)) return;
  194. const touch = e.touches[0];
  195. touchStartX = touch.clientX; touchStartY = touch.clientY; touchMoved = false;
  196. }, { passive: true });
  197. document.addEventListener('touchmove', e => {
  198. if (!selecting || e.target.closest(uiExcludeClass) || !e.touches[0]) return;
  199. if (!touchMoved) {
  200. const touch = e.touches[0];
  201. const dx = touch.clientX - touchStartX, dy = touch.clientY - touchStartY;
  202. if (Math.sqrt(dx * dx + dy * dy) > moveThreshold) touchMoved = true;
  203. }
  204. }, { passive: true });
  205. document.addEventListener('touchend', e => {
  206. if (!selecting || e.target.closest(uiExcludeClass)) return;
  207. if (touchMoved) { touchMoved = false; return; }
  208. e.preventDefault(); e.stopImmediatePropagation();
  209. const touch = e.changedTouches[0];
  210. const targetEl = document.elementFromPoint(touch.clientX, touch.clientY);
  211. if (!targetEl || targetEl.closest(uiExcludeClass)) return;
  212. if (selectedEl) selectedEl.classList.remove('selected-element');
  213. selectedEl = targetEl;
  214. initialTouchedElement = targetEl;
  215. selectedEl.classList.add('selected-element');
  216. panel.querySelector('#blocker-slider').value = 0;
  217. updateInfo();
  218. }, { capture: true, passive: false });
  219.  
  220. const slider = panel.querySelector('#blocker-slider');
  221. slider.addEventListener('input', handleSlider);
  222. function handleSlider(e) {
  223. if (!initialTouchedElement) return;
  224. const level = parseInt(e.target.value, 10);
  225. let current = initialTouchedElement;
  226. for (let i = 0; i < level && current.parentElement; i++) {
  227. if (current.parentElement.tagName.toLowerCase() === 'body' || current.parentElement.tagName.toLowerCase() === 'html') break;
  228. current = current.parentElement;
  229. }
  230. if (selectedEl) selectedEl.classList.remove('selected-element');
  231. selectedEl = current;
  232. selectedEl.classList.add('selected-element');
  233. updateInfo();
  234. }
  235.  
  236. panel.querySelector('#blocker-copy').addEventListener('click', () => {
  237. if (selectedEl) {
  238. const fullSelector = generateSelector(selectedEl);
  239. let finalSelector = "##" + fullSelector;
  240. if (includeSiteName) finalSelector = location.hostname + finalSelector;
  241. try {
  242. GM_setClipboard(finalSelector);
  243. alert('✅ 선택자가 복사되었습니다!\n' + finalSelector);
  244. } catch (err) {
  245. console.error("클립보드 복사 실패:", err);
  246. alert("❌ 클립보드 복사에 실패했습니다.");
  247. prompt("선택자를 직접 복사하세요:", finalSelector);
  248. }
  249. } else { alert('선택된 요소가 없습니다.'); }
  250. });
  251.  
  252. panel.querySelector('#blocker-toggle-site').addEventListener('click', () => {
  253. includeSiteName = !includeSiteName;
  254. panel.querySelector('#blocker-toggle-site').textContent = includeSiteName ? "사이트명: ON" : "사이트명: OFF";
  255. });
  256.  
  257. const blockBtn = panel.querySelector('#blocker-block');
  258. let isHidden = false;
  259.  
  260. blockBtn.textContent = '미리보기';
  261. blockBtn.addEventListener('click', () => {
  262. if (!selectedEl) {
  263. alert('선택된 요소가 없습니다.');
  264. return;
  265. }
  266. if (!isHidden) {
  267. selectedEl.dataset._original_display = selectedEl.style.display || '';
  268. selectedEl.style.display = 'none';
  269. blockBtn.textContent = '되돌리기';
  270. isHidden = true;
  271. } else {
  272. selectedEl.style.display = selectedEl.dataset._original_display || '';
  273. blockBtn.textContent = '미리보기';
  274. isHidden = false;
  275. }
  276. });
  277.  
  278. panel.querySelector('#blocker-cancel').addEventListener('click', () => setBlockMode(false));
  279. toggleBtn.addEventListener('click', () => setBlockMode(!selecting));
  280.  
  281. function makeDraggable(el) {
  282. let startX, startY, origX, origY;
  283. let dragging = false, moved = false;
  284. el.addEventListener('touchstart', function(e) {
  285. if (e.target.tagName.toLowerCase() === 'input') return;
  286. startX = e.touches[0].clientX;
  287. startY = e.touches[0].clientY;
  288. const rect = el.getBoundingClientRect();
  289. origX = rect.left;
  290. origY = rect.top;
  291. dragging = true;
  292. moved = false;
  293. }, {passive: true});
  294. el.addEventListener('touchmove', function(e) {
  295. if (!dragging) return;
  296. const dx = e.touches[0].clientX - startX;
  297. const dy = e.touches[0].clientY - startY;
  298. if (Math.sqrt(dx * dx + dy * dy) > moveThreshold) {
  299. moved = true;
  300. el.style.left = (origX + dx) + 'px';
  301. el.style.top = (origY + dy) + 'px';
  302. el.style.right = 'auto';
  303. el.style.bottom = 'auto';
  304. e.preventDefault();
  305. }
  306. }, {passive: false});
  307. el.addEventListener('touchend', function(e) {
  308. dragging = false;
  309. if(moved) {
  310. e.preventDefault();
  311. e.stopPropagation();
  312. }
  313. }, {passive: false});
  314. }
  315.  
  316. makeDraggable(panel);
  317. makeDraggable(toggleBtn);
  318.  
  319. })();