1/** 2 * Codesh - Code Syntax Highlighter 3 * Copy to clipboard and code group tab functionality 4 */ 5(function() { 6 'use strict'; 7 8 const STORAGE_KEY = 'codesh-tabs'; 9 10 /** 11 * Get stored tab preferences 12 */ 13 function getStoredTabs() { 14 try { 15 return JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}'); 16 } catch (e) { 17 return {}; 18 } 19 } 20 21 /** 22 * Store tab preference 23 */ 24 function storeTab(syncKey, tabTitle) { 25 try { 26 const stored = getStoredTabs(); 27 stored[syncKey] = tabTitle; 28 localStorage.setItem(STORAGE_KEY, JSON.stringify(stored)); 29 } catch (e) { 30 // Ignore storage errors 31 } 32 } 33 34 /** 35 * Copy code to clipboard 36 */ 37 async function copyCode(button) { 38 // Check if we're in a code group or a regular block 39 const group = button.closest('.codesh-group'); 40 const block = button.closest('.codesh-block'); 41 42 let codeEl; 43 if (group) { 44 // For groups, get the active panel's code 45 const activePanel = group.querySelector('.codesh-group-panel.active'); 46 codeEl = activePanel ? activePanel.querySelector('code') : null; 47 } else if (block) { 48 codeEl = block.querySelector('.codesh-code code'); 49 } 50 51 if (!codeEl) return; 52 53 try { 54 // Clone and remove line numbers for clean copy 55 const clone = codeEl.cloneNode(true); 56 const lineNumbers = clone.querySelectorAll('.line-number'); 57 lineNumbers.forEach(el => el.remove()); 58 59 await navigator.clipboard.writeText(clone.textContent); 60 61 // Show success state 62 const textEl = button.querySelector('.codesh-copy-text'); 63 const iconEl = button.querySelector('.codesh-copy-icon'); 64 65 if (textEl) { 66 const originalText = textEl.textContent; 67 textEl.textContent = 'Copied!'; 68 button.classList.add('copied'); 69 70 // Swap icon to checkmark 71 if (iconEl) { 72 iconEl.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>'; 73 } 74 75 setTimeout(() => { 76 textEl.textContent = originalText; 77 button.classList.remove('copied'); 78 if (iconEl) { 79 iconEl.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>'; 80 } 81 }, 2000); 82 } 83 } catch (err) { 84 console.error('Codesh: Failed to copy code:', err); 85 } 86 } 87 88 /** 89 * Switch to a tab in a code group 90 */ 91 function switchTab(group, tabButton) { 92 const tabId = tabButton.dataset.tab; 93 const tabTitle = tabButton.textContent.trim(); 94 95 // Update tab active states 96 group.querySelectorAll('.codesh-group-tab').forEach(tab => { 97 tab.classList.toggle('active', tab.dataset.tab === tabId); 98 }); 99 100 // Update panel active states 101 group.querySelectorAll('.codesh-group-panel').forEach(panel => { 102 panel.classList.toggle('active', panel.dataset.panel === tabId); 103 }); 104 105 // Sync with other groups if sync key is present 106 const syncKey = group.dataset.sync; 107 if (syncKey) { 108 storeTab(syncKey, tabTitle); 109 syncTabs(syncKey, tabTitle, group); 110 } 111 } 112 113 /** 114 * Sync tabs across groups with same sync key 115 */ 116 function syncTabs(syncKey, tabTitle, excludeGroup) { 117 document.querySelectorAll(`.codesh-group[data-sync="${syncKey}"]`).forEach(group => { 118 if (group === excludeGroup) return; 119 120 // Find a tab with matching title 121 const matchingTab = Array.from(group.querySelectorAll('.codesh-group-tab')) 122 .find(tab => tab.textContent.trim() === tabTitle); 123 124 if (matchingTab) { 125 switchTab(group, matchingTab); 126 } 127 }); 128 } 129 130 /** 131 * Restore stored tab preferences 132 */ 133 function restoreStoredTabs() { 134 const stored = getStoredTabs(); 135
136 Object.entries(stored).forEach(([syncKey, tabTitle]) => { 137 document.querySelectorAll(`.codesh-group[data-sync="${syncKey}"]`).forEach(group => { 138 const matchingTab = Array.from(group.querySelectorAll('.codesh-group-tab')) 139 .find(tab => tab.textContent.trim() === tabTitle); 140 141 if (matchingTab && !matchingTab.classList.contains('active')) { 142 switchTab(group, matchingTab); 143 } 144 }); 145 }); 146 } 147 148 /** 149 * Initialize all functionality. Click handling is delegated from the 150 * document so code blocks injected after load â live updates, htmx 151 * swaps, infinite scroll â work without rebinding. 152 */ 153 function init() { 154 document.addEventListener('click', function(e) { 155 const copy = e.target.closest('.codesh-copy'); 156 if (copy) { 157 e.preventDefault(); 158 copyCode(copy); 159 return; 160 } 161 const tab = e.target.closest('.codesh-group-tab'); 162 if (tab) { 163 const group = tab.closest('.codesh-group'); 164 if (group) { 165 e.preventDefault(); 166 switchTab(group, tab); 167 } 168 } 169 }); 170 171 restoreStoredTabs(); 172 173 // Newly swapped-in groups should honor the stored tab preferences. 174 document.addEventListener('htmx:afterSettle', restoreStoredTabs); 175 } 176 177 // Initialize when DOM is ready 178 if (document.readyState === 'loading') { 179 document.addEventListener('DOMContentLoaded', init); 180 } else { 181 init(); 182 } 183})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.