dev-mode.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. /* eslint-disable n/no-unsupported-features/node-builtins */
  2. /* global document, window */
  3. /**
  4. * PROTOTYPE DEV MODE
  5. *
  6. * Developer/feedback mode that allows users to easily copy Object IDs to clipboard
  7. * for providing precise feedback on prototype elements.
  8. *
  9. * Features:
  10. * - Toggle dev mode with button or Ctrl+E
  11. * - Prototype works NORMALLY when dev mode is on
  12. * - Hold Shift + Click any element to copy its Object ID
  13. * - Visual highlights show what will be copied (green when Shift is held)
  14. * - Tooltip shows Object ID on hover
  15. * - Success feedback when copied
  16. *
  17. * Usage:
  18. * 1. Include this script in your prototype HTML
  19. * 2. Add the HTML toggle button and tooltip (see HTML template)
  20. * 3. Add the CSS styles (see CSS template)
  21. * 4. Call initDevMode() on page load
  22. *
  23. * How it works:
  24. * - Activate dev mode (Ctrl+E or click button)
  25. * - Hover over elements to see their Object IDs (gray outline)
  26. * - Hold Shift key (outline turns green)
  27. * - Click while holding Shift to copy Object ID
  28. * - Prototype works normally without Shift held
  29. * - **Shift is disabled when typing in form fields** (input, textarea, etc.)
  30. */
  31. // ============================================================================
  32. // DEV MODE STATE
  33. // ============================================================================
  34. let devModeActive = false;
  35. let shiftKeyPressed = false;
  36. let currentHighlightedElement = null;
  37. // ============================================================================
  38. // INITIALIZATION
  39. // ============================================================================
  40. function initDevMode() {
  41. const toggleButton = document.querySelector('#dev-mode-toggle');
  42. const tooltip = document.querySelector('#dev-mode-tooltip');
  43. if (!toggleButton || !tooltip) {
  44. console.warn('⚠️ Dev Mode: Toggle button or tooltip not found');
  45. return;
  46. }
  47. // Check if user agent supports clipboard API
  48. if (typeof navigator !== 'undefined' && navigator.clipboard) {
  49. // Clipboard API available
  50. } else {
  51. console.warn('⚠️ Clipboard API not supported in this browser');
  52. return;
  53. }
  54. setupKeyboardShortcuts();
  55. setupToggleButton(toggleButton, tooltip);
  56. setupHoverHighlight(tooltip);
  57. setupClickCopy();
  58. console.log('%c💡 Dev Mode available: Press Ctrl+E or click the Dev Mode button', 'color: #0066CC; font-weight: bold;');
  59. }
  60. // ============================================================================
  61. // KEYBOARD SHORTCUTS
  62. // ============================================================================
  63. function setupKeyboardShortcuts() {
  64. // Track Shift key for container selection
  65. document.addEventListener('keydown', (e) => {
  66. if (e.key === 'Shift') {
  67. // Don't activate if user is typing in a form field
  68. if (isTypingInField()) {
  69. return;
  70. }
  71. shiftKeyPressed = true;
  72. document.body.classList.add('shift-held');
  73. if (devModeActive) {
  74. console.log('%c⬆️ Shift held: Click any element to copy its Object ID', 'color: #10B981; font-weight: bold;');
  75. }
  76. }
  77. // Ctrl+E toggle
  78. if (e.ctrlKey && e.key === 'e') {
  79. e.preventDefault();
  80. document.querySelector('#dev-mode-toggle')?.click();
  81. }
  82. });
  83. document.addEventListener('keyup', (e) => {
  84. if (e.key === 'Shift') {
  85. shiftKeyPressed = false;
  86. document.body.classList.remove('shift-held');
  87. if (devModeActive) {
  88. console.log('%c⬇️ Shift released: Prototype works normally (hold Shift to copy)', 'color: #6b7280;');
  89. }
  90. }
  91. });
  92. }
  93. // ============================================================================
  94. // TOGGLE BUTTON
  95. // ============================================================================
  96. function setupToggleButton(toggleButton, tooltip) {
  97. toggleButton.addEventListener('click', function (e) {
  98. e.stopPropagation();
  99. if (typeof globalThis !== 'undefined') {
  100. globalThis.devModeActive = true;
  101. } else if (globalThis.window !== undefined) {
  102. globalThis.devModeActive = true;
  103. }
  104. devModeActive = !devModeActive;
  105. // Update UI
  106. document.body.classList.toggle('dev-mode-active', devModeActive);
  107. toggleButton.classList.toggle('active', devModeActive);
  108. const statusText = toggleButton.querySelector('span');
  109. if (statusText) {
  110. statusText.textContent = devModeActive ? 'Dev Mode: ON' : 'Dev Mode: OFF';
  111. }
  112. // Log status
  113. console.log(`🔧 Dev Mode: ${devModeActive ? 'ACTIVATED' : 'DEACTIVATED'}`);
  114. if (devModeActive) {
  115. console.log('%c🔧 DEV MODE ACTIVE', 'color: #0066CC; font-size: 16px; font-weight: bold;');
  116. console.log('%c⚠️ Hold SHIFT + Click any element to copy its Object ID', 'color: #FFB800; font-size: 14px; font-weight: bold;');
  117. console.log('%cWithout Shift: Prototype works normally', 'color: #6b7280;');
  118. console.log('%cPress Ctrl+E to toggle Dev Mode', 'color: #6b7280;');
  119. } else {
  120. tooltip.style.display = 'none';
  121. if (currentHighlightedElement) {
  122. clearHighlight();
  123. }
  124. }
  125. });
  126. }
  127. // ============================================================================
  128. // HOVER HIGHLIGHT
  129. // ============================================================================
  130. function setupHoverHighlight(tooltip) {
  131. // Show tooltip and highlight on hover
  132. document.addEventListener('mouseover', function (e) {
  133. if (!devModeActive) return;
  134. // Don't highlight if user is typing in a field
  135. if (isTypingInField()) {
  136. tooltip.style.display = 'none';
  137. clearHighlight();
  138. return;
  139. }
  140. clearHighlight();
  141. let element = findElementWithId(e.target);
  142. if (!element || !element.id || isSystemElement(element.id)) {
  143. tooltip.style.display = 'none';
  144. return;
  145. }
  146. // Highlight element
  147. highlightElement(element, shiftKeyPressed);
  148. currentHighlightedElement = element;
  149. // Show tooltip
  150. const prefix = shiftKeyPressed ? '✓ Click to Copy: ' : '⬆️ Hold Shift + Click: ';
  151. tooltip.textContent = prefix + element.id;
  152. tooltip.style.display = 'block';
  153. tooltip.style.background = shiftKeyPressed ? '#10B981' : '#6b7280';
  154. tooltip.style.color = '#fff';
  155. updateTooltipPosition(e, tooltip);
  156. });
  157. // Update tooltip position on mouse move
  158. document.addEventListener('mousemove', function (e) {
  159. if (devModeActive && tooltip.style.display === 'block') {
  160. updateTooltipPosition(e, tooltip);
  161. }
  162. });
  163. // Clear highlight on mouse out
  164. document.addEventListener('mouseout', function (e) {
  165. if (!devModeActive) return;
  166. if (e.target.id) {
  167. tooltip.style.display = 'none';
  168. clearHighlight();
  169. }
  170. });
  171. }
  172. // ============================================================================
  173. // CLICK TO COPY
  174. // ============================================================================
  175. function setupClickCopy() {
  176. // Use capture phase to intercept clicks with Shift
  177. document.addEventListener(
  178. 'click',
  179. function (e) {
  180. if (!devModeActive) return;
  181. // Allow toggle button to work normally
  182. if (isToggleButton(e.target)) return;
  183. // ONLY copy if Shift is held
  184. if (!shiftKeyPressed) {
  185. // Let prototype work normally without Shift
  186. return;
  187. }
  188. // Don't intercept if user is clicking in/around a form field
  189. if (isTypingInField() || isFormElement(e.target)) {
  190. return;
  191. }
  192. // Shift is held and not in a form field - intercept and copy
  193. e.preventDefault();
  194. e.stopPropagation();
  195. e.stopImmediatePropagation();
  196. let element = findElementWithId(e.target);
  197. if (!element || !element.id || isSystemElement(element.id)) {
  198. console.log('❌ No Object ID found');
  199. return false;
  200. }
  201. // Copy to clipboard
  202. const objectId = element.id;
  203. copyToClipboard(objectId);
  204. // Show feedback
  205. showCopyFeedback(element, objectId);
  206. return false;
  207. },
  208. true,
  209. ); // Capture phase
  210. }
  211. // ============================================================================
  212. // HELPER FUNCTIONS
  213. // ============================================================================
  214. function findElementWithId(element) {
  215. let current = element;
  216. let attempts = 0;
  217. while (current && !current.id && attempts < 10) {
  218. current = current.parentElement;
  219. attempts++;
  220. }
  221. return current;
  222. }
  223. function isSystemElement(id) {
  224. const systemIds = ['app', 'dev-mode-toggle', 'dev-mode-tooltip'];
  225. return systemIds.includes(id);
  226. }
  227. function isToggleButton(element) {
  228. return element.id === 'dev-mode-toggle' || element.closest('#dev-mode-toggle') || element.classList.contains('dev-mode-toggle');
  229. }
  230. function isTypingInField() {
  231. const activeElement = document.activeElement;
  232. if (!activeElement) return false;
  233. const tagName = activeElement.tagName.toLowerCase();
  234. const isEditable = activeElement.isContentEditable;
  235. // Check if user is currently typing in a form field
  236. return tagName === 'input' || tagName === 'textarea' || tagName === 'select' || isEditable;
  237. }
  238. function isFormElement(element) {
  239. if (!element) return false;
  240. const tagName = element.tagName.toLowerCase();
  241. const isEditable = element.isContentEditable;
  242. // Check if the clicked element is a form element
  243. return tagName === 'input' || tagName === 'textarea' || tagName === 'select' || isEditable;
  244. }
  245. function highlightElement(element, isShiftHeld) {
  246. const color = isShiftHeld ? '#10B981' : '#6b7280';
  247. const width = isShiftHeld ? '3px' : '2px';
  248. const offset = isShiftHeld ? '3px' : '2px';
  249. const shadowSpread = isShiftHeld ? '5px' : '2px';
  250. const shadowOpacity = isShiftHeld ? '0.4' : '0.2';
  251. element.style.outline = `${width} solid ${color}`;
  252. element.style.outlineOffset = offset;
  253. element.style.boxShadow = `0 0 0 ${shadowSpread} rgba(${isShiftHeld ? '16, 185, 129' : '107, 114, 128'}, ${shadowOpacity})`;
  254. }
  255. function clearHighlight() {
  256. if (currentHighlightedElement) {
  257. currentHighlightedElement.style.outline = '';
  258. currentHighlightedElement.style.boxShadow = '';
  259. currentHighlightedElement = null;
  260. }
  261. }
  262. function updateTooltipPosition(e, tooltip) {
  263. const offset = 15;
  264. let x = e.clientX + offset;
  265. let y = e.clientY + offset;
  266. // Keep tooltip on screen
  267. const rect = tooltip.getBoundingClientRect();
  268. if (x + rect.width > window.innerWidth) {
  269. x = e.clientX - rect.width - offset;
  270. }
  271. if (y + rect.height > window.innerHeight) {
  272. y = e.clientY - rect.height - offset;
  273. }
  274. tooltip.style.left = x + 'px';
  275. tooltip.style.top = y + 'px';
  276. }
  277. function copyToClipboard(text) {
  278. if (typeof navigator !== 'undefined' && navigator.clipboard && navigator.clipboard.writeText) {
  279. navigator.clipboard
  280. .writeText(text)
  281. .then(() => {
  282. console.log(`📋 Copied to clipboard: ${text}`);
  283. })
  284. .catch((error) => {
  285. console.error('Dev Mode error:', error);
  286. fallbackCopy(text);
  287. });
  288. } else {
  289. fallbackCopy(text);
  290. }
  291. }
  292. function fallbackCopy(text) {
  293. const textarea = document.createElement('textarea');
  294. textarea.value = text;
  295. textarea.style.position = 'fixed';
  296. textarea.style.left = '-999999px';
  297. document.body.append(textarea);
  298. textarea.focus();
  299. textarea.select();
  300. try {
  301. document.execCommand('copy');
  302. console.log(`📋 Copied (fallback): ${text}`);
  303. } catch (error) {
  304. console.error('Dev Mode error:', error);
  305. }
  306. textarea.remove();
  307. }
  308. function showCopyFeedback(element, objectId) {
  309. // Create feedback overlay
  310. const feedback = document.createElement('div');
  311. feedback.textContent = '✓ Copied: ' + objectId;
  312. feedback.style.cssText = `
  313. position: fixed;
  314. top: 50%;
  315. left: 50%;
  316. transform: translate(-50%, -50%);
  317. background: #10B981;
  318. color: #fff;
  319. padding: 16px 32px;
  320. border-radius: 8px;
  321. font-size: 16px;
  322. font-weight: 600;
  323. z-index: 100000;
  324. box-shadow: 0 10px 25px rgba(0,0,0,0.3);
  325. animation: fadeInOut 1.5s ease-in-out;
  326. pointer-events: none;
  327. `;
  328. document.body.append(feedback);
  329. setTimeout(() => {
  330. feedback.remove();
  331. }, 1500);
  332. // Flash element
  333. const originalOutline = element.style.outline;
  334. element.style.outline = '3px solid #10B981';
  335. setTimeout(() => {
  336. element.style.outline = originalOutline;
  337. }, 300);
  338. }
  339. // Add CSS animation
  340. const style = document.createElement('style');
  341. style.textContent = `
  342. @keyframes fadeInOut {
  343. 0% { opacity: 0; transform: translate(-50%, -50%) scale(0.9); }
  344. 20% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
  345. 80% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
  346. 100% { opacity: 0; transform: translate(-50%, -50%) scale(0.9); }
  347. }
  348. `;
  349. document.head.append(style);
  350. // ============================================================================
  351. // EXPORT
  352. // ============================================================================
  353. // Make available globally
  354. globalThis.initDevMode = initDevMode;
  355. // Export for use in other scripts
  356. if (typeof globalThis !== 'undefined' && globalThis.exports) {
  357. globalThis.exports = { initDevMode };
  358. }