PageSourceSearch

https://gisd.org/js/components/documents/main.js?v=20260923224145

js gisd.org collected 2026-09-24 09:56:45 UTC 12,495 bytes, 356 lines download raw bytes

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.