1(function ($) { 2 const CTCCore = { 3 4 /** 5 * Init 6 */ 7 init: function () { 8 this._bind(); 9 }, 10 11 /** 12 * Binds events 13 */ 14 _bind: function () { 15 $( document ).on( 'click', 'body:not(.block-editor-page) .ctc-block-copy', this.doCopy ); 16 }, 17 18 /** 19 * Copy text to clipboard using CTC CopyEngine. 20 * 21 * @param {string} text Text to copy. 22 * @return {Promise<boolean>} Success status. 23 */ 24 copyToClipboard: async function ( text ) { 25 // Method 1: Use CTC CopyEngine. 26 if ( window.CTC && window.CTC.CopyEngine ) { 27 try { 28 const copyEngine = new window.CTC.CopyEngine(); 29 const result = await copyEngine.execute( { value: text } ); 30 if ( result.success ) { 31 return true; 32 } 33 } catch ( err ) { 34 // Fall through to fallbacks. 35 } 36 } 37 38 // Method 2: Clipboard API. 39 if ( navigator.clipboard && navigator.clipboard.writeText ) { 40 try { 41 await navigator.clipboard.writeText( text ); 42 return true; 43 } catch ( err ) { 44 // Fall through. 45 } 46 } 47 48 // Method 3: execCommand fallback. 49 try { 50 const textArea = document.createElement( 'textarea' ); 51 textArea.value = text; 52 textArea.style.cssText = 'position:fixed;opacity:0;pointer-events:none;'; 53 document.body.appendChild( textArea ); 54 textArea.select(); 55 const success = document.execCommand( 'copy' ); 56 document.body.removeChild( textArea ); 57 return success; 58 } catch ( err ) { 59 return false; 60 } 61 }, 62 63 /** 64 * Copy selection from element. 65 * 66 * @param {jQuery} $source jQuery element to copy selection from. 67 * @return {Promise<boolean>} Success status. 68 */ 69 copySelection: async function ( $source ) { 70 if ( ! $source || ! $source.length ) { 71 return false; 72 } 73 74 const element = $source.get( 0 ); 75 76 // Get text content, preserving line breaks. 77 const clone = element.cloneNode( true ); 78 clone.querySelectorAll( 'br' ).forEach( ( br ) => br.replaceWith( '\n' ) ); 79 const text = clone.textContent || ''; 80 81 return this.copyToClipboard( text.trim() ); 82 }, 83 84 /** 85 * Detect coarse device type from user agent (for block analytics). 86 * 87 * @return {string} desktop|mobile|tablet 88 */ 89 _detectDevice: function () { 90 const ua = navigator.userAgent; 91 if ( /tablet|ipad|playbook|silk/i.test( ua ) ) { 92 return 'tablet'; 93 } 94 if ( /mobile|iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i.test( ua ) ) { 95 return 'mobile'; 96 } 97 return 'desktop'; 98 }, 99 100 /** 101 * Detect browser family (for block analytics). 102 * 103 * @return {string} Browser slug. 104 */ 105 _detectBrowser: function () { 106 const ua = navigator.userAgent; 107 if ( ua.indexOf( 'Chrome' ) > -1 ) { 108 return 'chrome'; 109 } 110 if ( ua.indexOf( 'Firefox' ) > -1 ) { 111 return 'firefox'; 112 } 113 if ( ua.indexOf( 'Safari' ) > -1 ) { 114 return 'safari'; 115 } 116 if ( ua.indexOf( 'Edge' ) > -1 ) { 117 return 'edge'; 118 } 119 return 'unknown'; 120 }, 121 122 /** 123 * Send analytics event for Gutenberg block copy (non-blocking). 124 * 125 * @param {jQuery} block Block wrapper (.ctc-block). 126 * @param {boolean} success Whether copy succeeded. 127 * @param {string|null} errorReason Optional failure reason. 128 */ 129 _sendBlockAnalytics: function ( block, success, errorReason ) { 130 if ( ! block || ! block.length ) { 131 return; 132 } 133 const analytics = block.attr( 'data-ctc-analytics' ); 134 const source = block.attr( 'data-ctc-source' ); 135 if ( ! analytics || source !== 'gutenberg-block' ) { 136 return; 137 } 138 if ( typeof window.ctcBlockAnalytics === 'undefined' || ! window.ctcBlockAnalytics.eventsUrl ) { 139 return; 140 } 141 const event = { 142 success: !! success, 143 error_reason: errorReason || null, 144 source: 'gutenberg-block', 145 device: this._detectDevice(), 146 browser: this._detectBrowser(), 147 metadata: { 148 source: 'gutenberg-block', 149 block_type: block.attr( 'data-ctc-block-type' ) || null, 150 post_id: window.ctcBlockAnalytics.postId || null, 151 post_type: window.ctcBlockAnalytics.postType || null, 152 page_url: window.location.href, 153 }, 154 }; 155 const send = () => { 156 fetch( window.ctcBlockAnalytics.eventsUrl, { 157 method: 'POST', 158 headers: { 'Content-Type': 'application/json' }, 159 body: JSON.stringify( event ), 160 } ).catch( function ( err ) { 161 if ( typeof console !== 'undefined' && console.debug ) { 162 console.debug( 'CTC: Block analytics event failed:', err ); 163 } 164 } ); 165 }; 166 if ( window.requestIdleCallback ) { 167 window.requestIdleCallback( send, { timeout: 500 } ); 168 } else { 169 setTimeout( send, 0 ); 170 } 171 }, 172 173 /** 174 * Do Copy to Clipboard 175 */ 176 doCopy: async function (event) { 177 event.preventDefault(); 178 179 let btn = $( this ), 180 btnText = btn.find( '.ctc-button-text' ), 181 oldText = btnText.text(), 182 copiedText = btn.attr( 'data-copied' ) || 'Copied', 183 copyAsRaw = btn.attr( 'copy-as-raw' ) || '', 184 block = btn.parents( '.ctc-block' ), 185 textarea = block.find( '.ctc-copy-content' ), 186 content = textarea.val(), 187 selectionTarget = textarea.attr( 'selection-target' ) || ''; 188 189 let copySuccess = false;
190 191 // Copy as selection. 192 if ( selectionTarget ) { 193 const source = $( selectionTarget ); 194 if ( ! source.length ) { 195 return; 196 } 197 copySuccess = await CTCCore.copySelection( source ); 198 } else { 199 if ( ! copyAsRaw ) { 200 // Convert the <br/> tags into new line. 201 content = content.replace( /<br\s*[\/]?>/gi, "\n" ); 202 203 // Convert the <div> tags into new line. 204 content = content.replace( /<div\s*[\/]?>/gi, "\n" ); 205 206 // Convert the <p> tags into new line. 207 content = content.replace( /<p\s*[\/]?>/gi, "\n\n" ); 208 209 // Convert the <li> tags into new line. 210 content = content.replace( /<li\s*[\/]?>/gi, "\n" ); 211 212 // Remove all tags. 213 content = content.replace( /(<([^>]+)>)/ig, '' ); 214 215 // Remove white spaces. 216 content = content.replace( new RegExp( "/^\s+$/" ), "" ); 217 } 218 219 // Remove first and last new line. 220 content = content.trim(); 221 222 // Copy using CTC CopyEngine. 223 copySuccess = await CTCCore.copyToClipboard( content ); 224 } 225 226 if ( btn.hasClass( 'ctc-block-copy-icon' ) ) { 227 // Copied! 228 btn.addClass( 'copied' ); 229 setTimeout( 230 function () { 231 btn.removeClass( 'copied' ); 232 }, 233 1000 234 ); 235 } else { 236 // Copied! 237 btnText.text( copiedText ); 238 block.addClass( 'copied' ); 239 setTimeout( 240 function () { 241 btnText.text( oldText ); 242 block.removeClass( 'copied' ); 243 }, 244 1000 245 ); 246 } 247 248 // Block analytics (non-blocking). 249 CTCCore._sendBlockAnalytics( block, copySuccess, copySuccess ? null : 'copy_failed' ); 250 } 251 }; 252 253 /** 254 * Initialization 255 */ 256 $( 257 function () { 258 CTCCore.init(); 259 } 260 ); 261 262})( jQuery );
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.