1/* 2 * Delegated replacements for inline on* attributes. 3 * 4 * Two behaviours, both of which existed as inline handlers repeated across the themes: 5 * 6 * [data-rm-copy] click copies that value to the clipboard. 7 * [data-rm-fallback] an image that fails to load swaps to that URL, once. 8 * 9 * copyToClipboard() was defined ten times across six themes â byte-identical every time, an 10 * IE-era implementation using window.clipboardData with a document.execCommand fallback â and 11 * called from eight inline onclick attributes. One copy lives here now, and the call sites are 12 * data attributes, which is what guardrail 11 asks for: nothing executable in markup, so a 13 * Content-Security-Policy without 'unsafe-inline' becomes possible. 14 * 15 * The visible behaviour is deliberately unchanged: the same #url-copied element is faded in and 16 * out, through jQuery when it is present so the animation matches what the themes did, and with a 17 * plain show/hide when it is not. This file has no jQuery dependency of its own. 18 * 19 * Listeners are delegated from the document, so markup injected later â winner cards, search 20 * results, anything built in the browser â is covered without re-binding. 21 * 22 * Build: 2026.08.28.03 23 */ 24( function ( document, window ) { 25 'use strict'; 26 27 var FEEDBACK_ID = 'url-copied'; 28 var FEEDBACK_MS = 2500; 29 30 /** 31 * Show the "copied" confirmation, matching what the inline handlers did. 32 * 33 * @return {void} 34 */ 35 function confirmCopy( selector ) { 36 var node = selector 37 ? document.querySelector( selector ) 38 : document.getElementById( FEEDBACK_ID ); 39 40 if ( ! node ) { 41 return; 42 } 43 44 if ( window.jQuery ) { 45 window.jQuery( node ).stop( true, true ).fadeIn( 'slow' ).delay( FEEDBACK_MS ).fadeOut( 'slow' ); 46 47 return; 48 } 49 50 node.style.display = 'block'; 51 window.setTimeout( function () { 52 node.style.display = 'none'; 53 }, FEEDBACK_MS ); 54 } 55 56 /** 57 * Copy text, preferring the asynchronous clipboard API. 58 * 59 * execCommand('copy') is deprecated but still the only thing that works without a secure 60 * context or permission, so it stays as the fallback rather than being dropped. 61 * 62 * @param {string} text 63 * @return {void} 64 */ 65 function copy( text, feedback ) { 66 if ( navigator.clipboard && navigator.clipboard.writeText ) { 67 navigator.clipboard.writeText( text ).then( 68 function () { 69 confirmCopy( feedback ); 70 }, 71 function () { 72 legacyCopy( text, feedback ); 73 } 74 ); 75 76 return; 77 } 78 79 legacyCopy( text, feedback ); 80 } 81 82 /** 83 * @param {string} text 84 * @return {void} 85 */ 86 function legacyCopy( text, feedback ) { 87 var field = document.createElement( 'textarea' ); 88 89 field.value = text; 90 // Fixed positioning keeps the page from scrolling to the element before it is removed. 91 field.style.position = 'fixed'; 92 field.style.top = '0'; 93 field.style.left = '-9999px'; 94 field.setAttribute( 'readonly', 'readonly' ); 95 field.setAttribute( 'aria-hidden', 'true' ); 96 97 document.body.appendChild( field ); 98 field.select(); 99 100 try { 101 document.execCommand( 'copy' ); 102 confirmCopy( feedback ); 103 } catch ( e ) { 104 // Nothing useful to do: the browser refused, and there is no other path. The 105 // confirmation is deliberately not shown, so the visitor is not told it worked. 106 } 107 108 document.body.removeChild( field ); 109 } 110 111 document.addEventListener( 'click', function ( event ) { 112 var trigger = event.target.closest ? event.target.closest( '[data-rm-copy]' ) : null; 113 114 if ( ! trigger ) { 115 return; 116 } 117 118 event.preventDefault(); 119 copy( trigger.getAttribute( 'data-rm-copy' ), trigger.getAttribute( 'data-rm-copy-feedback' ) ); 120 } ); 121 122 /* 123 * Links that must not navigate. 124 * 125 * These were href="#" with onclick="return false;" â accordion openers and dropdown triggers 126 * that need to be focusable and styled as links but have nothing to go to. Telly already 127 * carried a js-telly-noop class doing this for its category openers; this generalises it so 128 * every theme uses one handler. 129 * 130 * A real <button> would be better markup, but swapping the element changes what the themes' 131 * stylesheets match, which is a visual change this pass is not making. 132 */ 133 document.addEventListener( 'click', function ( event ) { 134 var link = event.target.closest ? event.target.closest( '[data-rm-noop]' ) : null; 135 136 if ( link ) { 137 event.preventDefault(); 138 } 139 } ); 140 141 /* 142 * Share popups. The URL is opened in a sized window, which is what the inline handlers did; 143 * a browser that blocks the popup falls back to opening normally rather than doing nothing. 144 */ 145 document.addEventListener( 'click', function ( event ) { 146 var trigger = event.target.closest ? event.target.closest( '[data-rm-share]' ) : null; 147 148 if ( ! trigger ) { 149 return; 150 } 151 152 event.preventDefault(); 153 154 var url = trigger.getAttribute( 'data-rm-share' ); 155 var name = trigger.getAttribute( 'data-rm-share-name' ) || 'share'; 156 var features = trigger.getAttribute( 'data-rm-share-features' ) || 157 'height=320,width=640,toolbar=no,menubar=no,scrollbars=no,resizable=no,location=no,directories=no,status=no'; 158 159 var opened = window.open( url, name, features ); 160 161 if ( ! opened ) { 162 window.open( url, '_blank', 'noopener' ); 163 } 164 } ); 165 166 /* 167 * Image fallbacks. The error event does not bubble, so this listens in the capture phase â 168 * the same reason the inline onerror attributes existed. The attribute is removed after the 169 * swap so a fallback that is itself missing cannot loop. 170 */ 171 document.addEventListener( 172 'error', 173 function ( event ) { 174 var image = event.target; 175 176 if ( ! image || image.tagName !== 'IMG' ) { 177 return; 178 } 179 180 var fallback = image.getAttribute( 'data-rm-fallback' ); 181 182 if ( ! fallback || image.getAttribute( 'src' ) === fallback ) { 183 return; 184 } 185 186 image.removeAttribute( 'data-rm-fallback' ); 187 image.setAttribute( 'src', fallback ); 188 }, 189 true 190 ); 191}( document, window ) );
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.