1document.addEventListener('alpine:init', () => { 2 3 Alpine.magic('clipboard', () => subject => { 4 const text = String(subject) 5 6 // Secure contexts (HTTPS / localhost) expose the async Clipboard API. 7 if (navigator.clipboard && window.isSecureContext) { 8 navigator.clipboard.writeText(text).catch(() => fallbackClipboardCopy(text)) 9 return 10 } 11 12 // Insecure contexts (plain HTTP) â navigator.clipboard is undefined, so 13 // fall back to the legacy textarea + execCommand('copy') approach. This 14 // keeps copy working on HTTP admin/dev origins, not just HTTPS. 15 fallbackClipboardCopy(text) 16 }) 17 18 function fallbackClipboardCopy(text) { 19 const ta = document.createElement('textarea') 20 ta.value = text 21 ta.setAttribute('readonly', '') 22 ta.style.position = 'fixed' 23 ta.style.top = '-9999px' 24 document.body.appendChild(ta) 25 ta.select() 26 try { document.execCommand('copy') } catch (e) { /* no-op */ } 27 document.body.removeChild(ta) 28 } 29 30 Alpine.directive('script', (el, { expression }, { evaluateLater }) => { 31 let [url, handler, method] = expression.split(",").map(item => item.trim()); 32 33 let scripts = Array.from(document.head.getElementsByTagName('script')); 34 let scriptTagExists = scripts.some(script => script.dataset.name === handler); 35 let callback = el.getAttribute('x-script-callback'); 36 37 const scriptReady = () => window[handler] && (! method || typeof window[handler][method] === 'function'); 38 39 if (! scriptTagExists && ! scriptReady()) { 40 let script = document.createElement('script'); 41 script.dataset.name = handler; 42 script.src = url; 43 document.head.appendChild(script); 44 45 script.onload = () => { 46 let checkExist = setInterval(function() { 47 if (scriptReady()) { 48 clearInterval(checkExist); 49 if(callback) { 50 evaluateLater(callback)(); 51 } 52 } 53 }, 50); 54 }; 55 } else if (scriptReady()) { 56 evaluateLater(callback)(); 57 } 58 }); 59 60 Alpine.directive('slug', (el, { modifiers, expression }, { evaluateLater, effect }) => { 61 const setValue = evaluateLater(expression) 62 63 const isInput = el.tagName === 'INPUT' 64 65 const spaceless = modifiers.includes('spaceless'); 66 67 let replacementChar = modifiers.includes('replacement') 68 ? modifiers[modifiers.indexOf('replacement') + 1] 69 : '-' 70 71 replacementChar = spaceless ? '' : replacementChar; 72 73 function slugify(string) { 74 return string 75 .normalize('NFKD') // split accented characters into their base characters and diacritical marks 76 .replace(/[\u0300-\u036f]/g, '') // remove all the accents, which happen to be all in the \u03xx UNICODE block. 77 .trim() // trim leading or trailing whitespace 78 .toLowerCase() // convert to lowercase 79 .replace(/æ/gi, 'ae') 80 .replace(/Ã/gi, 'ss') 81 .replace(/Å/gi, 'oe') 82 .replace(/[^a-z0-9 _-]/g, '') // remove non-alphanumeric characters 83 .replace(/\s+/g, replacementChar) // replace spaces 84 .replace(/_+/g, replacementChar) // replace underscores 85 .replace(/-+/g, replacementChar); // remove consecutive 86 } 87 88 function unicodeSlugify(string) { 89 return string 90 .trim() // trim leading or trailing whitespace 91 .toLowerCase() // convert to lowercase 92 .replace(/\s+/g, replacementChar) // replace spaces 93 .replace(/_+/g, replacementChar) // replace underscores 94 .replace(/-+/g, replacementChar); // remove consecutive 95 } 96 97 effect(() => { 98 setValue(passedString => { 99 if (! passedString) { 100 return; 101 } 102 103 const stringSlug = modifiers.includes('unicode') ? unicodeSlugify(passedString) : slugify(passedString); 104 105 isInput ? (el.value = stringSlug) : (el.innerText = stringSlug); 106 }) 107 }) 108 }) 109 110 Alpine.directive('htmx-preserve-scroll', (el, { expression, modifiers }, { cleanup }) => { 111 let selector = expression ? expression.replace(/['"]/g, '')
111: null; 112 let elementId = selector ? null : el.id; 113 let observer; 114 let savedScroll = null; 115 let isRestoring = false; 116 let isRelevantRequest = false; 117 118 function getTargetEl() { 119 if (modifiers.includes('window')) { 120 return window; 121 } 122 if (selector) { 123 return document.querySelector(selector); 124 } 125 return document.getElementById(elementId); 126 } 127 128 function saveScrollPosition(element) { 129 if (!element) return null; 130 const pos = element === window ? { 131 top: window.scrollY, 132 left: window.scrollX 133 } : { 134 top: element.scrollTop, 135 left: element.scrollLeft 136 }; 137 // console.log('Saving scroll position:', pos); 138 return pos; 139 } 140 141 function restoreScrollPosition(element, position) { 142 if (!element || !position) return; 143 // console.log('Restoring scroll position:', position); 144 isRestoring = true; 145 146 if (element === window) { 147 window.scrollTo({ 148 left: position.left, 149 top: position.top, 150 behavior: 'instant' 151 }); 152 } else { 153 element.scrollTo({ 154 left: position.left, 155 top: position.top, 156 behavior: 'instant' 157 }); 158 } 159 160 setTimeout(() => { 161 isRestoring = false; 162 }, 350); 163 } 164 165 function isEventRelevant(event, targetEl) { 166 if (targetEl === window) { 167 const triggerElement = event.detail.elt; 168 if (!triggerElement) return false; 169 return el === triggerElement || el.contains(triggerElement); 170 } 171 // For non-window elements, keep using the existing ID check 172 return event.detail.target.id === elementId; 173 } 174 175 // HTMX event handlers 176 function handleBeforeRequest(event) { 177 const targetEl = getTargetEl(); 178 if (!targetEl) return; 179 180 // Store the relevancy check result 181 isRelevantRequest = isEventRelevant(event, targetEl); 182 if (!isRelevantRequest) return; 183 184 savedScroll = saveScrollPosition(targetEl); 185 } 186 187 function handleBeforeSwap(event) { 188 const targetEl = getTargetEl(); 189 if (!targetEl || !isRelevantRequest) return; 190 191 if (!savedScroll) { 192 savedScroll = saveScrollPosition(targetEl); 193 } 194 } 195 196 function handleAfterSwap(event) { 197 const targetEl = getTargetEl(); 198 if (!targetEl || !isRelevantRequest) return; 199 200 if (targetEl !== window) { 201 targetEl.classList.add('fwd-opacity-0'); 202 } 203 204 requestAnimationFrame(() => { 205 restoreScrollPosition(targetEl, savedScroll); 206 if (targetEl !== window) { 207 targetEl.classList.remove('fwd-opacity-0'); 208 } 209 // Reset the flag after restore is complete 210 isRelevantRequest = false; 211 }); 212 } 213 214 // Set up mutation observer for non-HTMX updates 215 function setupScrollLock() { 216 const targetEl = getTargetEl(); 217 if (!targetEl || targetEl === window) return; // Skip MutationObserver for window 218 219 if (observer) { 220 observer.disconnect(); 221 } 222 223 let lastScrollPos = saveScrollPosition(targetEl); 224 225 observer = new MutationObserver((mutations) => { 226 if (document.documentElement.hasAttribute('data-htmx-swapping') || isRestoring) { 227 return; 228 } 229 230 const affectsScroll = mutations.some(mutation => { 231 return mutation.target === targetEl || targetEl.contains(mutation.target); 232 }); 233 234 if (!affectsScroll) return; 235 236 const currentPos = saveScrollPosition(targetEl); 237 // console.log('Mutation detected - current:', currentPos, 'last:', lastScrollPos); 238 239 if (lastScrollPos && (currentPos.top !== lastScrollPos.top || currentPos.left !== lastScrollPos.left)) { 240 requestAnimationFrame(() => { 241 restoreScrollPosition(targetEl, lastScrollPos); 242 }); 243 } 244 }); 245 246 observer.observe(targetEl, { 247 childList: true, 248 subtree: true, 249 characterData: true, 250 attributes: true, 251 attributeFilter: ['class', 'style', 'data-*'] 252 }); 253 254 // Update last scroll position when user scrolls 255 const scrollTarget = targetEl === window ? document : targetEl; 256 scrollTarget.addEventListener('scroll', () => { 257 if (!isRestoring) { 258 lastScrollPos = saveScrollPosition(targetEl); 259 // console.log('User scrolled - updating last position:', lastScrollPos); 260 } 261 }); 262 } 263 264 // Set up all event listeners 265 window.addEventListener('htmx:beforeRequest', handleBeforeRequest); 266 window.addEventListener('htmx:beforeSwap', handleBeforeSwap); 267 window.addEventListener('htmx:afterSwap', handleAfterSwap); 268 setupScrollLock(); 269 270 // Handle cleanup 271 cleanup(() => { 272 window.removeEventListener('htmx:beforeRequest', handleBeforeRequest); 273 window.removeEventListener('htmx:beforeSwap', handleBeforeSwap); 274 window.removeEventListener('htmx:afterSwap', handleAfterSwap); 275 if (observer) { 276 observer.disconnect(); 277 } 278 }); 279 }); 280});
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.