1/* DOKUWIKI:include_once lib/chart.js */ 2/* DOKUWIKI:include_once lib/chartjs-plugin-datalabels.js */ 3 4/* globals JSINFO, DOKU_BASE, DokuCookie */ 5 6/** 7 * Modern Statistics Plugin 8 */ 9class StatisticsPlugin { 10 constructor() { 11 this.data = {}; 12 this.sessionTimeout = 15 * 60 * 1000; // 15 minutes 13 this.uid = this.initializeUserTracking(); 14 this.sessionId = this.getSession(); 15 } 16 17 /** 18 * Initialize the statistics plugin 19 */ 20 async init() { 21 try { 22 this.buildTrackingData(); 23 await this.logPageView(); 24 this.attachEventListeners(); 25 } catch (error) { 26 console.error('Statistics plugin initialization failed:', error); 27 } 28 } 29 30 /** 31 * Initialize user tracking with visitor cookie 32 * @returns {string} User ID (UUID) 33 */ 34 initializeUserTracking() { 35 let uid = DokuCookie.getValue('plgstats'); 36 if (!uid) { 37 uid = this.generateUUID(); 38 DokuCookie.setValue('plgstats', uid); 39 } 40 return uid; 41 } 42 43 44 /** 45 * Build tracking data object 46 */ 47 buildTrackingData() { 48 const now = Date.now(); 49 this.data = { 50 uid: this.uid, 51 ses: this.sessionId, 52 p: JSINFO.id, 53 r: document.referrer, 54 sx: screen.width, 55 sy: screen.height, 56 vx: window.innerWidth, 57 vy: window.innerHeight, 58 js: 1, 59 rnd: now 60 }; 61 } 62 63 /** 64 * Log page view based on action 65 */ 66 async logPageView() { 67 const action = JSINFO.act === 'show' ? 'v' : 's'; 68 await this.logView(action); 69 } 70 71 /** 72 * Attach event listeners for tracking 73 */ 74 attachEventListeners() { 75 // Track external link clicks 76 document.querySelectorAll('a.urlextern').forEach(link => { 77 link.addEventListener('click', this.logExternal.bind(this)); 78 }); 79 80 // Track page unload 81 window.addEventListener('beforeunload', this.logExit.bind(this)); 82 } 83 84 /** 85 * Log a view or session 86 * @param {string} action 'v' = view, 's' = session 87 */ 88 async logView(action) { 89 const params = new URLSearchParams(this.data); 90 const url = `${DOKU_BASE}lib/plugins/statistics/log.php?do=${action}&${params}`; 91 92 try { 93 // Use fetch with keepalive for better reliability 94 await fetch(url, { 95 method: 'GET', 96 keepalive: true, 97 cache: 'no-cache' 98 }); 99 } catch (error) { 100 // Fallback to image beacon for older browsers 101 const img = new Image(); 102 img.src = url; 103 } 104 } 105 106 /** 107 * Log clicks to external URLs 108 * @param {Event} event Click event 109 */ 110 logExternal(event) { 111 const params = new URLSearchParams(this.data); 112 const url = `${DOKU_BASE}lib/plugins/statistics/log.php?do=o&ol=${encodeURIComponent(event.target.href)}&${params}`; 113 114 // Use sendBeacon for reliable tracking 115 if (navigator.sendBeacon) { 116 navigator.sendBeacon(url); 117 } else { 118 // Fallback for older browsers 119 const img = new Image(); 120 img.src = url; 121 } 122 123 return true; 124 } 125 126 /** 127 * Log page exit as session info 128 */ 129 logExit() { 130 const currentSession = this.getSession(); 131 if (currentSession !== this.sessionId) { 132 return; // Session expired, don't log 133 } 134 135 const params = new URLSearchParams(this.data); 136 const url = `${DOKU_BASE}lib/plugins/statistics/log.php?do=s&${params}`; 137 138 if (navigator.sendBeacon) { 139 navigator.sendBeacon(url); 140 } 141 } 142 143 /** 144 * Get current session identifier 145 * Auto clears expired sessions and creates new ones after 15 min idle time 146 * @returns {string} Session ID 147 */ 148 getSession() { 149 const now = Date.now(); 150 151 // Load session cookie 152 let sessionData = DokuCookie.getValue('plgstatsses'); 153 let sessionId = ''; 154 155 if (sessionData) { 156 const [timestamp, id] = sessionData.split('-', 2); 157 if (now - parseInt(timestamp, 10) <= this.sessionTimeout) { 158 sessionId = id; 159 } 160 } 161 162 // Generate new session if needed 163 if (!sessionId) { 164 sessionId = this.generateUUID(); 165 } 166 167 // Update session cookie 168 DokuCookie.setValue('plgstatsses', `${now}-${sessionId}`); 169 return sessionId; 170 } 171 172 /** 173 * Generate a UUID v4 174 * @returns {string} UUID 175 */ 176 generateUUID() { 177 function s4() { 178 return Math.floor((1 + Math.random()) * 0x10000) 179 .toString(16) 180 .substring(1); 181 } 182 183 return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); 184 } 185} 186 187// Initialize when DOM is ready 188if (document.readyState === 'loading') { 189 document.addEventListener('DOMContentLoaded', () => { 190 new StatisticsPlugin().init(); 191 }); 192} else { 193 // DOM already loaded 194 new StatisticsPlugin().init(); 195} 196 197class ChartComponent extends HTMLElement { 198 connectedCallback() { 199 this.renderChart(); 200 } 201 202 renderChart() { 203 const chartType = this.getAttribute('type'); 204 const data = JSON.parse(this.getAttribute('data')); 205 206 console.log('data', data); 207 208 const canvas = document.createElement("canvas"); 209 canvas.height = this.getAttribute('height') || 300; 210 canvas.width = this.getAttribute('width') || 300; 211 212 this.appendChild(canvas); 213 214 const ctx = canvas.getContext('2d'); 215 216 // basic config 217 const config = { 218 type: chartType, 219 data: data, 220 options: { 221 responsive: false, 222 }, 223 }; 224 225 // percentage labels and tooltips for pie charts 226 if (chartType === "pie") { 227 // chartjs-plugin-datalabels needs to be registered 228 Chart.register(ChartDataLabels); 229 230 config.options.plugins = { 231 datalabels: { 232 formatter: (value, context) => { 233 const total = context.chart.data.datasets[0].data.reduce((a, b) => a + b, 0); 234 return ((value / total) * 100).toFixed(2) + '%'; // percentage 235 }, 236 color: '#fff', 237 } 238 }; 239 } 240 241 new Chart(ctx, config); 242 } 243} 244 245customElements.define('chart-component', ChartComponent); 246