1/** 2 * Cite Plugin - Frontend JavaScript 3 * Version: 2.8.0 4 * 5 * Handles citation display, copying, exporting, analytics tracking, 6 * and multiple display modes (dropdown, tabs, buttons). 7 */ 8(function() { 9 'use strict'; 10 11 if (typeof window.wpcpCitationInitialized !== 'undefined') { 12 return; 13 } 14 window.wpcpCitationInitialized = true; 15 16 function escapeHtml(text) { 17 var div = document.createElement('div'); 18 div.textContent = text; 19 return div.innerHTML; 20 } 21 22 function showNotification(notification) { 23 if (!notification) return; 24 notification.classList.add('show'); 25 setTimeout(function() { 26 notification.classList.remove('show'); 27 }, 2000); 28 } 29 30 function hasAnalyticsConsent() { 31 var cookies = document.cookie; 32 if (cookies.indexOf('cookieyes-consent') !== -1) { 33 return cookies.indexOf('analytics:yes') !== -1; 34 } 35 if (cookies.indexOf('cmplz_statistics=allow') !== -1) { 36 return true; 37 } 38 if (cookies.indexOf('CookieConsent') !== -1) { 39 return cookies.indexOf('statistics:true') !== -1; 40 } 41 if (cookies.indexOf('moove_gdpr_popup') !== -1) { 42 try { 43 var match = cookies.match(/moove_gdpr_popup=([^;]+)/); 44 if (match) { 45 var moove = JSON.parse(decodeURIComponent(match[1])); 46 return moove && moove.thirdparty === '1'; 47 } 48 } catch (e) { /* ignore */ } 49 } 50 // No recognised consent plugin detected â default to permitted. 51 // Only block when a known plugin is present and has explicitly denied analytics. 52 return true; 53 } 54 55 function trackAction(postId, action, style, ajaxUrl, enableAnalytics, requireConsent) { 56 if (!enableAnalytics) return; 57 if (postId <= 0) return; 58 if (typeof jQuery === 'undefined') return; 59 60 // Consent is evaluated server-side. The JS check was unreliable because 61 // it only recognised a subset of cookie formats and blocked valid requests. 62 // We pass the JS-detected hint as a best-effort signal; PHP makes the 63 // final decision via citepress_detect_consent_from_cookies() and the 64 // citepress_analytics_consent_granted filter. 65 var consentHint = !requireConsent || hasAnalyticsConsent() ? '1' : '0'; 66 67 // FIX #3: nonce removed â nonces break under full-page caching. 68 // This endpoint writes only non-sensitive aggregated counters; no CSRF risk. 69 jQuery.post(ajaxUrl, { 70 action: 'citepress_track_analytics', 71 post_id: postId, 72 citation_style: style, 73 action_type: action, 74 consent_granted: consentHint 75 }).fail(function(xhr, status, error) { 76 console.error('Analytics tracking failed:', error); 77 }); 78 } 79 80 /** 81 * Get the currently selected style from any display mode. 82 */ 83 function getSelectedStyle(config) { 84 var mode = config.displayMode || 'dropdown'; 85 var container = config.container; 86 87 if (mode === 'tabs') { 88 var activeTab = container.querySelector('.citepress-style-tab--active'); 89 return activeTab ? activeTab.getAttribute('data-style') : config.defaultStyle; 90 } else if (mode === 'buttons') { 91 var activeBtn = container.querySelector('.citepress-style-btn--active'); 92 return activeBtn ? activeBtn.getAttribute('data-style') : config.defaultStyle; 93 } else { 94 return config.dropdown ? config.dropdown.value : config.defaultStyle; 95 } 96 } 97 98 function updateCitation(config) { 99 var citationBox = config.citationBox; 100 var styles = config.styles; 101 if (!citationBox) return; 102 103 var selectedStyle = getSelectedStyle(config); 104 var template = styles[selectedStyle]; 105 if (!template) return; 106 107 var citation = template 108 .replace('{author}', escapeHtml(config.author)) 109 .replace('{sitename}', escapeHtml(config.siteName)) 110 .replace('{title}', escapeHtml(config.title)) 111 .replace('{date}', escapeHtml(config.dateAccessed)) 112 .replace('{date_dmy}', escapeHtml(config.dateAccessedDMY || config.dateAccessed)) 113 .replace('{publication_date}', escapeHtml(config.publicationDate)) 114 .replace('{publication_date_dmy}', escapeHtml(config.publicationDateDMY || config.publicationDate)) 115 .replace('{publication_year}', escapeHtml(config.publicationYear)) 116 .replace('{permalink_url}', escapeHtml(config.permalink)) 117 .replace('{permalink}', config.permalinkHTML) 118 .replace('{language}', escapeHtml(config.language || 'en')); 119 120 if (selectedStyle === 'wikipedia') {
121 // Wikipedia output is plain markup â use textContent to avoid HTML interpretation 122 citationBox.textContent = citation; 123 citationBox.classList.add('wikipedia-format'); 124 } else { 125 citation = citation.replace(/\s\./g, '.').replace(/\s\,/g, ',').replace(/\s+/g, ' '); 126 citationBox.innerHTML = citation; 127 citationBox.classList.remove('wikipedia-format'); 128 } 129 130 trackAction(config.postId, 'view', selectedStyle, config.ajaxUrl, config.enableAnalytics, config.requireConsent); 131 } 132 133 function copyToClipboard(config) { 134 var citationBox = config.citationBox; 135 var notification = config.notification; 136 if (!citationBox) return; 137 138 var tempDiv = document.createElement('div'); 139 tempDiv.innerHTML = citationBox.innerHTML; 140 var plainText = tempDiv.textContent || tempDiv.innerText || ''; 141 var selectedStyle = getSelectedStyle(config); 142 143 if (navigator.clipboard && navigator.clipboard.writeText) { 144 navigator.clipboard.writeText(plainText).then(function() { 145 showNotification(notification); 146 trackAction(config.postId, 'copy', selectedStyle, config.ajaxUrl, config.enableAnalytics, config.requireConsent); 147 }).catch(function() { 148 fallbackCopy(plainText, config); 149 }); 150 } else { 151 fallbackCopy(plainText, config); 152 } 153 } 154 155 function fallbackCopy(text, config) { 156 var textarea = document.createElement('textarea'); 157 textarea.value = text; 158 textarea.style.position = 'fixed'; 159 textarea.style.opacity = '0'; 160 textarea.setAttribute('readonly', ''); 161 document.body.appendChild(textarea); 162 textarea.select(); 163 try { 164 document.execCommand('copy'); 165 showNotification(config.notification); 166 var selectedStyle = getSelectedStyle(config); 167 trackAction(config.postId, 'copy', selectedStyle, config.ajaxUrl, config.enableAnalytics, config.requireConsent); 168 } catch (err) { 169 console.error('Failed to copy citation:', err); 170 } 171 document.body.removeChild(textarea); 172 } 173 174 function exportCitation(format, config) { 175 trackAction(config.postId, 'export', format, config.ajaxUrl, config.enableAnalytics, config.requireConsent); 176 177 var content = ''; 178 var mimeType = 'text/plain;charset=utf-8'; 179 var cleanTitle = config.title.replace(/"/g, '\\"').replace(/</g, '<').replace(/>/g, '>'); 180 var cleanAuthor = config.author.replace(/"/g, '\\"').replace(/</g, '<').replace(/>/g, '>'); 181 var cleanSiteName = config.siteName.replace(/"/g, '\\"').replace(/</g, '<').replace(/>/g, '>'); 182 var citeKey = config.siteName.replace(/\s/g, '').replace(/[^a-zA-Z0-9]/g, '') + config.publicationYear; 183 184 if (format === 'bibtex') { 185 content = '@article{' + citeKey + ',\n title={' + cleanTitle + '},\n author={' + cleanAuthor + '},\n journal={' + cleanSiteName + '},\n year={' + config.publicationYear + '},\n url={' + config.permalink + '}\n}'; 186 } else if (format === 'ris') { 187 content = 'TY - JOUR\nTI - ' + cleanTitle + '\nAU - ' + cleanAuthor + '\nJO - ' + cleanSiteName + '\nPY - ' + config.publicationYear + '\nUR - ' + config.permalink + '\nER -'; 188 } else if (format === 'endnote') { 189 content = '%0 Journal Article\n%T ' + cleanTitle + '\n%A ' + cleanAuthor + '\n%J ' + cleanSiteName + '\n%D ' + config.publicationYear + '\n%U ' + config.permalink; 190 } else if (format === 'csl-json') { 191 var cslObj = { 192 'type': 'webpage', 193 'id': citeKey, 194 'title': config.title, 195 'author': [{ 'literal': config.author }], 196 'container-title': config.siteName, 197 'URL': config.permalink, 198 'issued': { 'date-parts': [[parseInt(config.publicationYear, 10)]] }, 199 'accessed': { 'date-parts': [[new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()]] }, 200 'language': config.language || 'en' 201 }; 202 content = JSON.stringify([cslObj], null, 2); 203 mimeType = 'application/json;charset=utf-8'; 204 } else if (format === 'cff') { 205 var dateISO = config.publicationDateISO || config.publicationYear + '-01-01'; 206 content = 'cff-version: 1.2.0\n'; 207 content += 'message: "If you use this work, please cite it as below."\n'; 208 content += 'title: "' + config.title.replace(/"/g, '\\"') + '"\n'; 209 content += 'authors:\n'; 210 content += ' - name: "' + config.author.replace(/"/g, '\\"') + '"\n'; 211 content += 'date-released: ' + dateISO + '\n'; 212 content += 'url: "' + config.permalink + '"\n'; 213 content += 'type: article\n'; 214 content += 'journal: "' + config.siteName.replace(/"/g, '\\"') + '"\n'; 215 } else if (format === 'dublin-core') { 216 var dateISO = config.publicationDateISO || config.publicationYear + '-01-01'; 217 content = '<?xml version="1.0" encoding="UTF-8"?>\n'; 218 content += '<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">\n'; 219 content += ' <dc:title>' + config.title.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') + '</dc:title>\n'; 220 content += ' <dc:creator>' + config.author.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') + '</dc:creator>\n'; 221 content += ' <dc:publisher>' + config.siteName.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') + '</dc:publisher>\n'; 222 content += ' <dc:date>' + dateISO + '</dc:date>\n'; 223 content += ' <dc:identifier>' + config.permalink.replace(/&/g, '&') + '</dc:identifier>\n'; 224 content += ' <dc:type>Text</dc:type>\n'; 225 content += ' <dc:format>text/html</dc:format>\n'; 226 content += ' <dc:language>' + (config.language || 'en') + '</dc:language>\n'; 227 content += '</metadata>\n'; 228 mimeType = 'application/xml;charset=utf-8'; 229 } 230 231 var fileExtensions = { 232 'bibtex': 'bib', 233 'ris': 'ris', 234 'endnote': 'enw', 235 'csl-json': 'json', 236 'cff': 'cff', 237 'dublin-core': 'xml' 238 }; 239 240 var blob = new Blob([content], { type: mimeType }); 241 var url = window.URL.createObjectURL(blob); 242 var a = document.createElement('a'); 243 a.href = url; 244 a.download = 'citation.' + (fileExtensions[format] || 'txt'); 245 document.body.appendChild(a); 246 a.click(); 247 document.body.removeChild(a); 248 window.URL.revokeObjectURL(url); 249 } 250 251 /** 252 * Set up tabs display mode event handlers. 253 */ 254 function initTabsMode(config) { 255 var container = config.container; 256 var tabs = container.querySelectorAll('.citepress-style-tab'); 257 if (!tabs.length) return; 258
259 tabs.forEach(function(tab) { 260 tab.addEventListener('click', function(e) { 261 e.preventDefault(); 262 tabs.forEach(function(t) { 263 t.classList.remove('citepress-style-tab--active'); 264 t.setAttribute('aria-selected', 'false'); 265 t.setAttribute('tabindex', '-1'); 266 }); 267 this.classList.add('citepress-style-tab--active'); 268 this.setAttribute('aria-selected', 'true'); 269 this.setAttribute('tabindex', '0'); 270 updateCitation(config); 271 }); 272 273 tab.addEventListener('keydown', function(e) { 274 var tabsArray = Array.prototype.slice.call(tabs); 275 var index = tabsArray.indexOf(this); 276 var newIndex = -1; 277 278 if (e.key === 'ArrowRight' || e.key === 'ArrowDown') { 279 e.preventDefault(); 280 newIndex = (index + 1) % tabsArray.length; 281 } else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { 282 e.preventDefault(); 283 newIndex = (index - 1 + tabsArray.length) % tabsArray.length; 284 } else if (e.key === 'Home') { 285 e.preventDefault(); 286 newIndex = 0; 287 } else if (e.key === 'End') { 288 e.preventDefault(); 289 newIndex = tabsArray.length - 1; 290 } 291 292 if (newIndex >= 0) { 293 tabsArray[newIndex].click(); 294 tabsArray[newIndex].focus(); 295 } 296 }); 297 }); 298 } 299 300 /** 301 * Set up buttons (pill) display mode event handlers. 302 */ 303 function initButtonsMode(config) { 304 var container = config.container; 305 var buttons = container.querySelectorAll('.citepress-style-btn'); 306 if (!buttons.length) return; 307 308 buttons.forEach(function(btn) { 309 btn.addEventListener('click', function(e) { 310 e.preventDefault(); 311 buttons.forEach(function(b) { 312 b.classList.remove('citepress-style-btn--active'); 313 b.setAttribute('aria-checked', 'false'); 314 }); 315 this.classList.add('citepress-style-btn--active'); 316 this.setAttribute('aria-checked', 'true'); 317 updateCitation(config); 318 }); 319 320 btn.addEventListener('keydown', function(e) { 321 if (e.key === 'Enter' || e.key === ' ') { 322 e.preventDefault(); 323 this.click(); 324 } 325 }); 326 }); 327 } 328 329 function initCitationBox(data) { 330 var postIdStr = data.postId.toString(); 331 var instanceStr = data.instance ? data.instance.toString() : '1'; 332 var containerId = 'citation-box-' + postIdStr + '-' + instanceStr; 333 var container = document.getElementById(containerId); 334 335 if (!container) return; 336 337 var citationBox = container.querySelector('.citation-output'); 338 var dropdown = container.querySelector('.citation-style-select'); 339 var copyButton = container.querySelector('.copy-button'); 340 var notification = container.querySelector('.copy-notification'); 341 var exportButton = container.querySelector('.export-button'); 342 var exportDropdown = exportButton ? exportButton.nextElementSibling : null; 343 var toggleButton = container.querySelector('.toggle-button'); 344 var citationContent = container.querySelector('.citation-content'); 345 346 var displayMode = data.displayMode || 'dropdown'; 347 var defaultStyle = (data.activeFormats && data.activeFormats.length > 0) ? data.activeFormats[0] : 'apa'; 348 349 // FIX #15: styles, ajaxUrl, enableAnalytics, requireConsent now live in 350 // citepressSharedData (output once per page) rather than duplicated per citation. 351 var shared = window.citepressSharedData || {}; 352 353 var config = { 354 postId: data.postId, 355 container: container, 356 styles: shared.styles || {}, 357 author: data.author, 358 authors: data.authors || [data.author], 359 siteName: data.siteName, 360 title: data.title, 361 dateAccessed: data.dateAccessed, 362 dateAccessedDMY: data.dateAccessedDMY || data.dateAccessed, 363 publicationDate: data.publicationDate, 364 publicationDateDMY: data.publicationDateDMY || data.publicationDate, 365 publicationYear: data.publicationYear, 366 publicationDateISO: data.publicationDateISO || data.publicationYear + '-01-01', 367 permalink: data.permalink, 368 permalinkHTML: '<a href="' + escapeHtml(data.permalinkUrl) + '" rel="bookmark">' + escapeHtml(data.permalink) + '</a>', 369 language: data.language || 'en', 370 ajaxUrl: shared.ajaxUrl || '', 371 enableAnalytics: shared.enableAnalytics || false, 372 requireConsent: shared.requireConsent || false, 373 dropdown: dropdown, 374 citationBox: citationBox, 375 notification: notification, 376 displayMode: displayMode, 377 defaultStyle: defaultStyle 378 }; 379 380 // Bind display mode handlers 381 if (displayMode === 'tabs') { 382 initTabsMode(config); 383 } else if (displayMode === 'buttons') { 384 initButtonsMode(config); 385 } else if (dropdown) { 386 dropdown.addEventListener('change', function() { 387 updateCitation(config); 388 }); 389 } 390 391 if (copyButton) { 392 copyButton.addEventListener('click', function(e) { 393 e.preventDefault(); 394 copyToClipboard(config); 395 }); 396 copyButton.addEventListener('keydown', function(e) { 397 if (e.key === 'Enter' || e.key === ' ') { 398 e.preventDefault(); 399 copyToClipboard(config); 400 } 401 }); 402 } 403 404 if (exportButton && exportDropdown) { 405 exportButton.addEventListener('click', function(e) { 406 e.preventDefault(); 407 e.stopPropagation(); 408 var isExpanded = exportDropdown.classList.toggle('show'); 409 exportButton.setAttribute('aria-expanded', isExpanded.toString()); 410 }); 411 412 var exportOptions = exportDropdown.querySelectorAll('.export-option'); 413 exportOptions.forEach(function(option) { 414 option.addEventListener('click', function(e) { 415 e.preventDefault(); 416 var format = this.getAttribute('data-format'); 417 if (format) { 418 exportCitation(format, config); 419 exportDropdown.classList.remove('show'); 420 exportButton.setAttribute('aria-expanded', 'false'); 421 } 422 }); 423 option.addEventListener('keydown', function(e) { 424 if (e.key === 'Enter' || e.key === ' ') { 425 e.preventDefault(); 426 var format = this.getAttribute('data-format'); 427 if (format) { 428 exportCitation(format, config); 429 exportDropdown.classList.remove('show'); 430 exportButton.setAttribute('aria-expanded', 'false'); 431 } 432 } 433 }); 434 }); 435 436 document.addEventListener('click', function(e) { 437 if (!exportButton.contains(e.target) && !exportDropdown.contains(e.target)) { 438 exportDropdown.classList.remove('show'); 439 exportButton.setAttribute('aria-expanded', 'false'); 440 } 441 }); 442 } 443 444 if (toggleButton && citationContent) { 445 toggleButton.addEventListener('click', function(e) { 446 e.preventDefault(); 447 var isExpanded = !citationContent.classList.toggle('collapsed'); 448 toggleButton.setAttribute('aria-expanded', isExpanded.toString()); 449 toggleButton.classList.toggle('collapsed'); 450 }); 451 } 452 453 updateCitation(config); 454 } 455
456 function init() { 457 if (typeof window.citepressCitationData === 'undefined' || !Array.isArray(window.citepressCitationData)) { 458 return; 459 } 460 window.citepressCitationData.forEach(function(data) { 461 initCitationBox(data); 462 }); 463 } 464 465 if (document.readyState === 'loading') { 466 document.addEventListener('DOMContentLoaded', init); 467 } else { 468 init(); 469 } 470})();
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.