Mobile Element Selector

모바일 요소 선택기

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

  1. // ==UserScript==
  2. // @name Mobile Element Selector
  3. // @author ZNJXL
  4. // @version 1.1.3
  5. // @namespace http://tampermonkey.net/
  6. // @description 모바일 요소 선택기
  7. // @description:en Mobile Element Selector (especially for cromium browsers)
  8. // @match *://*/*
  9. // @license MIT
  10. // @grant GM_setClipboard
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. let selecting = false;
  17. let selectedEl = null;
  18. let initialTouchedElement = null;
  19. let includeSiteName = true;
  20. let touchStartX = 0, touchStartY = 0;
  21. let touchMoved = false;
  22. const moveThreshold = 10;
  23.  
  24. // 다국어 텍스트 정의
  25. const i18n = {
  26. ko: {
  27. blockerInfo: "선택된 요소:",
  28. blockerCopy: "복사",
  29. blockerToggleSite: (siteNameOn) => `사이트명: ${siteNameOn ? "ON" : "OFF"}`,
  30. blockerBlock: "미리보기",
  31. blockerCancel: "취소",
  32. blockerMode: "차단 모드",
  33. blockerSelecting: "선택 중...",
  34. blockerPreview: "미리보기",
  35. blockerRevert: "되돌리기",
  36. alertNoElement: "선택된 요소가 없습니다.",
  37. alertCopySuccess: "✅ 선택자가 복사되었습니다!",
  38. alertCopyFail: "❌ 클립보드 복사에 실패했습니다.",
  39. alertDirectCopy: "선택자를 직접 복사하세요:",
  40. },
  41. en: {
  42. blockerInfo: "Selected Element:",
  43. blockerCopy: "Copy",
  44. blockerToggleSite: (siteNameOn) => `Site Name: ${siteNameOn ? "ON" : "OFF"}`,
  45. blockerBlock: "Preview",
  46. blockerCancel: "Cancel",
  47. blockerMode: "Block Mode",
  48. blockerSelecting: "Selecting...",
  49. blockerPreview: "Preview",
  50. blockerRevert: "Revert",
  51. alertNoElement: "No element selected.",
  52. alertCopySuccess: "✅ Selector copied!",
  53. alertCopyFail: "❌ Failed to copy to clipboard.",
  54. alertDirectCopy: "Copy the selector manually:"
  55. }
  56. };
  57.  
  58. // 언어 감지 및 선택 (기본값은 한국어)
  59. const lang = navigator.language.startsWith('ko') ? 'ko' : 'en';
  60. const t = i18n[lang];
  61.  
  62. const style = document.createElement('style');
  63. style.textContent = `
  64. .mobile-block-ui { /* 스타일 내용은 동일 */ }
  65. #blocker-slider { /* 스타일 내용은 동일 */ }
  66. #blocker-slider::-webkit-slider-thumb { /* 스타일 내용은 동일 */ }
  67. #blocker-slider::-moz-range-thumb { /* 스타일 내용은 동일 */ }
  68. .selected-element { /* 스타일 내용은 동일 */ }
  69. #mobile-block-panel { /* 스타일 내용은 동일 */ }
  70. #mobile-block-toggleBtn { /* 스타일 내용은 동일 */ }
  71. .mb-btn { /* 스타일 내용은 동일 */ }
  72. #blocker-info-wrapper { /* 스타일 내용은 동일 */ }
  73. #blocker-info { /* 스타일 내용은 동일 */ }
  74. `;
  75. document.head.appendChild(style);
  76.  
  77. const panel = document.createElement('div');
  78. panel.id = 'mobile-block-panel';
  79. panel.classList.add('mobile-block-ui', 'ui-ignore');
  80. panel.innerHTML = `
  81. <div id="blocker-info-wrapper">
  82. <span style="font-size: 12px; color: #ccc;">${t.blockerInfo}</span>
  83. <span id="blocker-info">없음</span>
  84. </div>
  85. <input type="range" id="blocker-slider" min="0" max="10" value="0" class="ui-ignore">
  86. <div class="button-grid">
  87. <button id="blocker-copy" class="mb-btn ui-ignore">${t.blockerCopy}</button>
  88. <button id="blocker-toggle-site" class="mb-btn ui-ignore">${t.blockerToggleSite(includeSiteName)}</button>
  89. <button id="blocker-block" class="mb-btn ui-ignore">${t.blockerBlock}</button>
  90. <button id="blocker-cancel" class="mb-btn ui-ignore">${t.blockerCancel}</button>
  91. </div>
  92. `;
  93. document.body.appendChild(panel);
  94.  
  95. const toggleBtn = document.createElement('button');
  96. toggleBtn.id = 'mobile-block-toggleBtn';
  97. toggleBtn.classList.add('mobile-block-ui', 'ui-ignore');
  98. toggleBtn.textContent = t.blockerMode;
  99. document.body.appendChild(toggleBtn);
  100.  
  101. function setBlockMode(enabled) {
  102. selecting = enabled;
  103. toggleBtn.textContent = enabled ? t.blockerSelecting : t.blockerMode;
  104. toggleBtn.classList.toggle('selecting', enabled);
  105. panel.style.display = enabled ? 'block' : 'none';
  106. if (!enabled && selectedEl) {
  107. selectedEl.classList.remove('selected-element');
  108. selectedEl = null;
  109. initialTouchedElement = null;
  110. }
  111. panel.querySelector('#blocker-slider').value = 0;
  112. updateInfo();
  113. }
  114.  
  115. function updateInfo() {
  116. const infoSpan = panel.querySelector('#blocker-info');
  117. infoSpan.textContent = selectedEl ? generateSelector(selectedEl) : '없음';
  118. }
  119.  
  120. function generateSelector(el) { /* generateSelector 함수는 그대로 유지 */ }
  121.  
  122. const uiExcludeClass = '.ui-ignore';
  123. document.addEventListener('touchstart', e => {
  124. if (!selecting || e.target.closest(uiExcludeClass)) return;
  125. const touch = e.touches[0];
  126. touchStartX = touch.clientX; touchStartY = touch.clientY; touchMoved = false;
  127. }, { passive: true });
  128.  
  129. document.addEventListener('touchmove', e => {
  130. if (!selecting || e.target.closest(uiExcludeClass) || !e.touches[0]) return;
  131. if (!touchMoved) {
  132. const touch = e.touches[0];
  133. const dx = touch.clientX - touchStartX, dy = touch.clientY - touchStartY;
  134. if (Math.sqrt(dx * dx + dy * dy) > moveThreshold) touchMoved = true;
  135. }
  136. }, { passive: true });
  137.  
  138. document.addEventListener('touchend', e => {
  139. if (!selecting || e.target.closest(uiExcludeClass)) return;
  140. if (touchMoved) { touchMoved = false; return; }
  141. e.preventDefault(); e.stopImmediatePropagation();
  142. const touch = e.changedTouches[0];
  143. const targetEl = document.elementFromPoint(touch.clientX, touch.clientY);
  144. if (!targetEl || targetEl.closest(uiExcludeClass)) return;
  145. if (selectedEl) selectedEl.classList.remove('selected-element');
  146. selectedEl = targetEl;
  147. initialTouchedElement = targetEl;
  148. selectedEl.classList.add('selected-element');
  149. panel.querySelector('#blocker-slider').value = 0;
  150. updateInfo();
  151. }, { capture: true, passive: false });
  152.  
  153. const slider = panel.querySelector('#blocker-slider');
  154. slider.addEventListener('input', handleSlider);
  155. function handleSlider(e) { /* handleSlider 함수는 그대로 유지 */ }
  156.  
  157. panel.querySelector('#blocker-copy').addEventListener('click', () => {
  158. if (selectedEl) {
  159. const fullSelector = generateSelector(selectedEl);
  160. let finalSelector = "##" + fullSelector;
  161. if (includeSiteName) finalSelector = location.hostname + finalSelector;
  162. try {
  163. GM_setClipboard(finalSelector);
  164. alert(t.alertCopySuccess + '\n' + finalSelector);
  165. } catch (err) {
  166. console.error("클립보드 복사 실패:", err);
  167. alert(t.alertCopyFail);
  168. prompt(t.alertDirectCopy, finalSelector);
  169. }
  170. } else { alert(t.alertNoElement); }
  171. });
  172.  
  173. panel.querySelector('#blocker-toggle-site').addEventListener('click', () => {
  174. includeSiteName = !includeSiteName;
  175. panel.querySelector('#blocker-toggle-site').textContent = t.blockerToggleSite(includeSiteName);
  176. });
  177.  
  178. const blockBtn = panel.querySelector('#blocker-block');
  179. let isHidden = false;
  180.  
  181. blockBtn.textContent = t.blockerPreview;
  182. blockBtn.addEventListener('click', () => {
  183. if (!selectedEl) {
  184. alert(t.alertNoElement);
  185. return;
  186. }
  187. if (!isHidden) {
  188. selectedEl.dataset._original_display = selectedEl.style.display || '';
  189. selectedEl.style.display = 'none';
  190. blockBtn.textContent = t.blockerRevert;
  191. isHidden = true;
  192. } else {
  193. selectedEl.style.display = selectedEl.dataset._original_display || '';
  194. blockBtn.textContent = t.blockerPreview;
  195. isHidden = false;
  196. }
  197. });
  198.  
  199. panel.querySelector('#blocker-cancel').addEventListener('click', () => setBlockMode(false));
  200. toggleBtn.addEventListener('click', () => setBlockMode(!selecting));
  201.  
  202. function makeDraggable(el) { /* makeDraggable 함수는 그대로 유지 */ }
  203.  
  204. makeDraggable(panel);
  205. makeDraggable(toggleBtn);
  206.  
  207. })();