1/** 2 * Main PDF Display Application 3 * 4 * Handles PDF display logic based on device detection. 5 */ 6 7(function(window) { 8 'use strict'; 9 10 /** 11 * Escape string for safe use in HTML content and attributes (prevents XSS). 12 * Uses the browser's own encoding so all special characters are handled. 13 * @param {string} str - Raw string 14 * @returns {string} HTML-escaped string 15 */ 16 function escapeHtml(str) { 17 if (str == null) return ''; 18 var div = document.createElement('div'); 19 div.textContent = str; 20 return div.innerHTML; 21 } 22 23 /** 24 * PDF Display Manager 25 */ 26 function PDFDisplayManager() { 27 this.initialized = false; 28 this.instances = {}; 29 } 30 31 /** 32 * Initialize PDF display for a container 33 * @param {Object} options - Configuration options 34 * @param {string} options.containerId - Container element ID 35 * @param {string} options.pdfUrl - URL of the PDF file 36 * @param {string} options.description - Description for accessibility 37 * @param {boolean} options.serverDetectedMobile - Server-side mobile detection 38 * @param {Object} options.analytics - Analytics configuration 39 */ 40 PDFDisplayManager.prototype.initialize = function(options) { 41 if (!options || !options.containerId) { 42 console.error('PDFDisplayManager: containerId is required'); 43 return; 44 } 45 46 var self = this; 47 var config = this._validateConfig(options); 48 49 // Store configuration 50 this.instances[config.containerId] = config; 51 52 // Wait for device detector to be ready 53 if (typeof getDeviceInfo === 'function') { 54 this._setupPDFDisplay(config); 55 } else { 56 window.addEventListener('deviceDetectorReady', function() { 57 self._setupPDFDisplay(config); 58 }); 59 } 60 }; 61 62 /** 63 * Validate and normalize configuration 64 * @param {Object} options - Raw options 65 * @returns {Object} Validated configuration 66 * @private 67 */ 68 PDFDisplayManager.prototype._validateConfig = function(options) { 69 return { 70 containerId: options.containerId, 71 pdfUrl: options.pdfUrl || '', 72 description: options.description || 'Document', 73 serverDetectedMobile: !!options.serverDetectedMobile, 74 analytics: options.analytics || {}, 75 onDisplayChange: options.onDisplayChange || null 76 }; 77 }; 78 79 /** 80 * Setup PDF display based on device detection 81 * @param {Object} config - Configuration object 82 * @private 83 */ 84 PDFDisplayManager.prototype._setupPDFDisplay = function(config) { 85 var deviceInfo = getDeviceInfo(); 86 var container = document.getElementById(config.containerId); 87 88 if (!container) { 89 console.error('PDFDisplayManager: Container not found:', config.containerId); 90 return; 91 } 92 93 // Determine display method 94 var shouldShowLink = deviceInfo.isMobile || deviceInfo.isTablet; 95 var displayMethod = shouldShowLink ? 'link' : 'embed'; 96 97 // Apply display method 98 this._applyDisplayMethod(container, config, displayMethod, deviceInfo); 99 100 // Track analytics 101 if (config.analytics.enabled) { 102 this._trackDisplayMethod(displayMethod, config, deviceInfo); 103 } 104 105 // Call change callback 106 if (typeof config.onDisplayChange === 'function') { 107 config.onDisplayChange(displayMethod, config.containerId, deviceInfo); 108 } 109 110 this.initialized = true; 111 }; 112 113 /** 114 * Apply the determined display method 115 * @param {HTMLElement} container - Container element 116 * @param {Object} config - Configuration object 117 * @param {string} method - Display method ('link' or 'embed') 118 * @param {Object} deviceInfo - Device information 119 * @private 120 */ 121 PDFDisplayManager.prototype._applyDisplayMethod = function(container, config, method, deviceInfo) { 122 // Clear existing content 123 container.innerHTML = ''; 124 125 if (method === 'link') { 126 this._createDownloadLink(container, config, deviceInfo); 127 } else { 128 this._createEmbeddedViewer(container, config, deviceInfo); 129 } 130 131 // Add CSS classes for styling 132 container.className = container.className.replace(/\b(pdf-display-link|pdf-display-embed)\b/g, ''); 133 container.classList.add('pdf-display-' + method); 134 }; 135 136 /** 137 * Create download link for mobile devices 138 * @param {HTMLElement} container - Container element 139 * @param {Object} config - Configuration object 140 * @param {Object} deviceInfo - Device information 141 * @private 142 */ 143 PDFDisplayManager.prototype._createDownloadLink = function(container, config, deviceInfo) { 144 var wrapper = document.createElement('div'); 145 wrapper.className = 'ss-pdf-fallback'; 146 147 var message = document.createElement('p'); 148 message.className = 'ss-no-preview'; 149 message.innerHTML = 'Preview unavailable. <br/>'; 150 151 var link = document.createElement('a'); 152 link.className = 'ss-pdf-fallback-link'; 153 link.href = config.pdfUrl; 154 link.textContent = 'Click here to view ' + config.description; 155 link.target = '_blank'; 156 link.rel = 'noopener noreferrer'; 157 158 // Add download attribute for better mobile experience 159 if (deviceInfo.isMobile) { 160 link.download = ''; 161 } 162 163 message.appendChild(link); 164 wrapper.appendChild(message); 165 container.appendChild(wrapper); 166 }; 167 168 /** 169 * Create embedded viewer for desktop devices 170 * @param {HTMLElement} container - Container element 171 * @param {Object} config - Configuration object 172 * @param {Object} deviceInfo - Device information 173 * @private 174 */ 175 PDFDisplayManager.prototype._createEmbeddedViewer = function(container, config, deviceInfo) { 176 var wrapper = document.createElement('div'); 177 wrapper.className = 'ss-pdf-embedded'; 178 179 var fileExtension = this._getFileExtension(config.pdfUrl); 180 181 if (fileExtension === 'pdf') { 182 this._createPDFEmbed(wrapper, config); 183 } else { 184 this._createDocumentEmbed(wrapper, config); 185 } 186 187 container.appendChild(wrapper); 188 189 if (typeof window.trackDocumentEmbedView === 'function') { 190 window.trackDocumentEmbedView(container); 191 } 192 }; 193 194 /** 195 * Create PDF embed using object tag 196 * @param {HTMLElement} wrapper - Wrapper element 197 * @param {Object} config - Configuration object 198 * @private 199 */ 200 PDFDisplayManager.prototype._createPDFEmbed = function(wrapper, config) { 201 var embedContainer = document.createElement('div'); 202 embedContainer.className = 'embed-responsive'; 203 embedContainer.style.cssText = 'height:700px; width:100%;'; 204 205 var object = document.createElement('object'); 206 object.data = config.pdfUrl; 207 object.type = 'application/pdf'; 208 object.setAttribute('title', escapeHtml(config.description)); 209 object.style.cssText = 'height:700px; width:100%;'; 210 211 var fallback = document.createElement('p'); 212 fallback.className = 'ss-no-preview'; 213 fallback.innerHTML = 'Preview unavailable. <br/><a href="' + config.pdfUrl + 214 '" target="_blank" rel="noopener noreferrer">Click here to view ' + 215 escapeHtml(config.description) + '</a>.'; 216 217 object.appendChild(fallback); 218 embedContainer.appendChild(object); 219 wrapper.appendChild(embedContainer); 220 }; 221 222 /** 223 * Create document embed using Google Docs viewer 224 * @param {HTMLElement} wrapper - Wrapper element 225 * @param {Object} config - Configuration object 226 * @private 227 */ 228 PDFDisplayManager.prototype._createDocumentEmbed = function(wrapper, config) { 229 var iframe = document.createElement('iframe'); 230 iframe.src = 'https://docs.google.com/gview?url=' + encodeURIComponent(config.pdfUrl) + '&embedded=true'; 231 iframe.style.cssText = 'width:100%; height:600px; padding-bottom:50px; border:0;'; 232 iframe.className = 'center-block'; 233 234 // Add loading and error handling 235 iframe.style.visibility = 'hidden'; 236 iframe.onload = function() { 237 iframe.style.visibility = 'visible'; 238 }; 239 240 wrapper.appendChild(iframe); 241 }; 242 243 /** 244 * Get file extension from URL 245 * @param {string} url - File URL 246 * @returns {string} File extension 247 * @private 248 */ 249 PDFDisplayManager.prototype._getFileExtension = function(url) { 250 if (!url) return ''; 251 var path = url.split('?')[0]; // Remove query parameters 252 var parts = path.split('.'); 253 return parts.length > 1 ? parts.pop().toLowerCase() : ''; 254 }; 255 256 /**
257 * Track display method analytics 258 * @param {string} method - Display method 259 * @param {Object} config - Configuration object 260 * @param {Object} deviceInfo - Device information 261 * @private 262 */ 263 PDFDisplayManager.prototype._trackDisplayMethod = function(method, config, deviceInfo) { 264 var analyticsData = { 265 method: method, 266 component_id: config.containerId, 267 device_type: deviceInfo.isTablet ? 'tablet' : (deviceInfo.isMobile ? 'mobile' : 'desktop'), 268 is_ipad: deviceInfo.isIPad, 269 browser: deviceInfo.browser, 270 os: deviceInfo.os 271 }; 272 273 // Google Analytics 4 274 if (typeof gtag !== 'undefined' && config.analytics.enableGoogleAnalytics !== false) { 275 gtag('event', config.analytics.eventName || 'pdf_display_method', analyticsData); 276 } 277 278 // Custom analytics callback 279 if (typeof config.analytics.customCallback === 'function') { 280 config.analytics.customCallback(analyticsData); 281 } 282 }; 283 284 /** 285 * Refresh display for a specific container 286 * @param {string} containerId - Container ID 287 */ 288 PDFDisplayManager.prototype.refresh = function(containerId) { 289 var config = this.instances[containerId]; 290 if (config) { 291 // Clear device detector cache 292 if (window.DeviceDetector && typeof window.DeviceDetector.clearCache === 'function') { 293 window.DeviceDetector.clearCache(); 294 } 295 this._setupPDFDisplay(config); 296 } 297 }; 298 299 /** 300 * Refresh all instances 301 */ 302 PDFDisplayManager.prototype.refreshAll = function() { 303 var self = this; 304 Object.keys(this.instances).forEach(function(containerId) { 305 self.refresh(containerId); 306 }); 307 }; 308 309 /** 310 * Get debug information 311 * @returns {Object} Debug information 312 */ 313 PDFDisplayManager.prototype.getDebugInfo = function() { 314 return { 315 initialized: this.initialized, 316 instanceCount: Object.keys(this.instances).length, 317 instances: Object.keys(this.instances), 318 deviceDetectorAvailable: typeof getDeviceInfo === 'function', 319 deviceInfo: typeof getDeviceInfo === 'function' ? getDeviceInfo() : null 320 }; 321 }; 322 323 // Create singleton instance 324 var pdfDisplayManager = new PDFDisplayManager(); 325 326 // Export main initialization function following the example pattern 327 window.initializePDFDisplay = function(options) { 328 pdfDisplayManager.initialize(options); 329 }; 330 331 // Auto-initialize on DOM ready 332 document.addEventListener('DOMContentLoaded', function() {
333 // Look for elements with data-pdf-config attribute 334 var elements = document.querySelectorAll('[data-pdf-config]'); 335 336 elements.forEach(function(element) { 337 try { 338 var config = JSON.parse(element.getAttribute('data-pdf-config')); 339 config.containerId = element.id || 'pdf-container-' + Math.random().toString(36).substr(2, 9); 340 element.id = config.containerId; 341 342 pdfDisplayManager.initialize(config); 343 } catch (e) { 344 console.error('PDFDisplayManager: Failed to parse data-pdf-config', e); 345 } 346 }); 347 }); 348 349 // Handle orientation change 350 window.addEventListener('orientationchange', function() { 351 setTimeout(function() { 352 pdfDisplayManager.refreshAll(); 353 }, 100); 354 }); 355 356})(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.