https://kb-allgaeu.de/wp-content/plugins/quiz-master-next/renderer/assets/js/qsm-timer.js?ver=11.2.4
1/** 2 * QSM Timer - Enhanced with LocalStorage, Resume Functionality, and Conditional Popups 3 * @package QSM 4 */ 5var qsmTimerInterval = []; 6(function($) { 7 if (typeof qsm_timer_consumed_obj === 'undefined') { 8 window.qsm_timer_consumed_obj = { 9 qmn_count_upward_status : false 10 }; 11 } 12 13 QSMPagination.Timer = { 14 quizObjects: {}, 15 intervals: {}, 16 originalTitle: document.title, 17 warningThresholds: { 18 ninety: 90, // 90% time consumed 19 ten: 10 // 10% time remaining 20 }, 21 22 init: function() { 23 const self = this; 24 // Look for both new and legacy form selectors 25 $('.qsm-quiz-container').each(function() { 26 const $container = $(this); 27 const $form = $container.find('.qsm-quiz-form'); 28 const quizId = $container.data('quiz-id'); 29 if (quizId && !self.quizObjects[quizId]) { 30 self.initTimer(quizId, $form); 31 } 32 }); 33 this.bindEvents(); 34 }, 35 36 initTimer: function(quizId, $form) { 37 const data = this.getQuizData(quizId); 38 39 if (!data.timer_limit || data.timer_limit <= 0) { 40 return; 41 } 42 43 const $timer = $form.find('.mlw_qmn_timer'); 44 const totalTime = data.timer_limit * 60; 45 const consumedTime = Number.parseInt(localStorage.getItem('mlw_time_consumed_quiz' + quizId), 10) || 1; 46 const remainingTime = this.calculateInitialTime(quizId, totalTime, consumedTime); 47 48 this.quizObjects[quizId] = { 49 form: $form, 50 timer: $timer.length ? $timer : null, 51 totalTime: totalTime, 52 remainingTime: remainingTime, 53 consumedTime: consumedTime, 54 isActive: false, 55 data: data, 56 warnings: { 57 ninety: false, 58 ten: false, 59 expired: false 60 }, 61 timerStatus: false, 62 timerInterval: null 63 }; 64 65 // Update qmn_quiz_data for legacy compatibility 66 this.updateLegacyQuizData(quizId); 67 $form.find(".hiddentimer").html(remainingTime); 68 69 if ($timer && $timer.length) { 70 $timer.show(); 71 } 72 this.updateDisplay(quizId); 73 74 // Auto-start conditions matching legacy behavior 75 if (this.shouldAutoStart(quizId, data)) { 76 this.start(quizId); 77 } 78 }, 79 80 bindEvents: function() { 81 const self = this; 82 83 // QSM-11 events 84 $(document).on('qsm_quiz_initialized', function(e, quizId, instance) { 85 if (!self.quizObjects[quizId]) { 86 self.initTimer(quizId, instance.form); 87 } 88 }); 89 90 $(document).on('qsm_after_page_change', function(e, quizId, pageNumber) { 91 if (pageNumber > 1 && self.quizObjects[quizId] && !self.quizObjects[quizId].isActive) { 92 self.start(quizId); 93 } 94 }); 95 96 $(document).on('qsm_quiz_started', function(e, quizId) { 97 if (self.quizObjects[quizId] && !self.quizObjects[quizId].isActive) { 98 self.start(quizId); 99 } 100 }); 101 102 $(document).on('qsm_form_submit', function(e, quizId) { 103 self.endTimer(quizId); 104 }); 105 106 // Legacy events compatibility 107 $(document).on('qsm_activate_time_before qsm_activate_time_after', function(e, quizId, qmn_quiz_data) { 108 if (e.type === 'qsm_activate_time_before') { 109 self.start(quizId); 110 } 111 }); 112 }, 113 114 start: function(quizId) { 115 const currentQuiz = this.quizObjects[quizId]; 116 if (!currentQuiz || currentQuiz.isActive) return; 117 118 currentQuiz.isActive = true; 119 currentQuiz.timerStatus = true; 120 121 // Update legacy data 122 this.updateLegacyQuizData(quizId); 123 124 // Mark quiz as started in localStorage (legacy keys) 125 localStorage.setItem('mlw_started_quiz' + quizId, 'yes'); 126 127 // Set initial time in localStorage if not set 128 if (!localStorage.getItem('mlw_quiz_start_date' + quizId)) { 129 localStorage.setItem('mlw_quiz_start_date' + quizId, new Date().getTime()); 130 } 131 132 // Start the interval 133 currentQuiz.timerInterval = setInterval(function() { 134 this.tick(quizId); 135 }.bind(this), 1000); 136 137 this.intervals[quizId] = currentQuiz.timerInterval; 138 139 // Show stop timer element if available 140 $('.qsm-quiz-container-' + quizId).find('.stoptimer-p').show(); 141 142 // Trigger legacy events 143 $(document).trigger('qsm_activate_time_after', [quizId, qmn_quiz_data]); 144 }, 145 146 stop: function(quizId) { 147 const currentQuiz = this.quizObjects[quizId]; 148 if (!currentQuiz) return; 149 150 if (this.intervals[quizId]) { 151 clearInterval(this.intervals[quizId]); 152 delete this.intervals[quizId]; 153 } 154 155 if (currentQuiz.timerInterval) { 156 clearInterval(currentQuiz.timerInterval); 157 currentQuiz.timerInterval = null; 158 } 159 160 currentQuiz.isActive = false;
161 currentQuiz.timerStatus = false; 162 163 // Update legacy data 164 this.updateLegacyQuizData(quizId); 165 }, 166 167 expire: function(quizId) { 168 const currentQuiz = this.quizObjects[quizId]; 169 if (!currentQuiz || currentQuiz.warnings.expired) return; 170 171 currentQuiz.warnings.expired = true; 172 this.stop(quizId); 173 currentQuiz.remainingTime = 0; 174 this.updateDisplay(quizId); 175 176 // Add visual indicators 177 currentQuiz.timer?.addClass('qsm-timer-expired qsm-timer--danger'); 178 179 // Disable form inputs (matching legacy behavior) 180 const $quizForm = currentQuiz.form; 181 $quizForm.find('input[type="radio"]').attr('disabled', true); 182 $quizForm.find('input[type="checkbox"]').attr('disabled', true); 183 $quizForm.find('select').attr('disabled', true); 184 $quizForm.find('.mlw_qmn_question_comment').attr('disabled', true); 185 $quizForm.find('.mlw_answer_open_text').attr('disabled', true); 186 $quizForm.find('.mlw_answer_number').attr('readonly', true); 187 188 // Add timer ended class and error message 189 const $container = $quizForm.closest('.qmn_quiz_container, .qsm-quiz-container'); 190 $container.addClass('qsm_timer_ended'); 191 192 // Add error message if not already present 193 if (!$container.find('.qmn_error_message').length) { 194 const errorMsg = currentQuiz.data.quiz_time_over || 'Time is up!'; 195 $container.prepend('<p class="qmn_error_message" style="color: red;">' + errorMsg + '</p>'); 196 } 197 198 // Handle auto-submit or show modal 199 if (Number(currentQuiz.data.enable_result_after_timer_end) === 1) { 200 // Auto-submit the form 201 setTimeout(function() { 202 $container.find('.qsm-submit-btn, .qsm_submit_btn').trigger('click'); 203 }, 1000); 204 } else { 205 // Show timer expired modal 206 $('.qsm-quiz-container-' + quizId).find('.stoptimer-p').hide(); 207 MicroModal.show('modal-3'); 208 } 209 210 // Trigger events 211 $(document).trigger('qsm_timer_expired', [quizId, currentQuiz]); 212 $(document).trigger('qsm_timer_ended', [quizId, qmn_quiz_data, { 213 qmn_count_upward_status: false 214 }]); 215 }, 216 217 tick: function(quizId) { 218 const currentQuiz = this.quizObjects[quizId]; 219 if (!currentQuiz || currentQuiz.warnings.expired) return; 220 221 // Check for scheduled time expiration 222 if (this.checkScheduledTimeExpiration(quizId)) { 223 return; // Exit if quiz expired due to scheduled time 224 } 225 226 currentQuiz.remainingTime--; 227 currentQuiz.consumedTime++; 228 229 if (currentQuiz.remainingTime < 0) { 230 currentQuiz.remainingTime = 0; 231 } 232 233 // Update legacy data 234 this.updateLegacyQuizData(quizId); 235 236 // Save current state to localStorage (both new and legacy keys) 237 localStorage.setItem('mlw_time_consumed_quiz' + quizId, currentQuiz.consumedTime); 238 localStorage.setItem('mlw_time_quiz' + quizId, currentQuiz.remainingTime); 239 240 this.updateDisplay(quizId); 241 this.checkWarnings(quizId); 242 243 // Trigger events 244 $(document).trigger('qsm_timer_tick', [quizId, currentQuiz.remainingTime, currentQuiz.consumedTime]); 245 $(document).trigger('qmn_timer_consumed_seconds', [quizId, qmn_quiz_data, qsm_timer_consumed_obj]); 246 $(document).trigger('load_timer_faces', [quizId, currentQuiz.remainingTime, currentQuiz.totalTime, this.secondsToTimer(currentQuiz.remainingTime)]); 247 248 if (currentQuiz.remainingTime <= 0) { 249 this.expire(quizId); 250 } 251 }, 252 253 updateDisplay: function(quizId) { 254 const currentQuiz = this.quizObjects[quizId]; 255 if (!currentQuiz || currentQuiz.warnings.expired) return; 256 257 const display = this.secondsToTimer(currentQuiz.remainingTime); 258 259 if (currentQuiz.timer) { 260 currentQuiz.timer.text(display); 261 } 262 263 // Update browser tab title 264 if (currentQuiz.remainingTime > 0) { 265 document.title = display + ' - ' + this.originalTitle; 266 } 267 }, 268 269 calculateInitialTime: function(quizId, totalTime, consumedTime) { 270 const timerStarted = localStorage.getItem('mlw_started_quiz' + quizId) || localStorage.getItem('qsm_started_quiz_' + quizId); 271 const storedConsumed = Number.parseInt(localStorage.getItem('mlw_time_consumed_quiz' + quizId), 10) || consumedTime || 1; 272 let remainingTime = totalTime; 273 274 if (timerStarted === 'yes' && storedConsumed > 1) { 275 remainingTime = totalTime - storedConsumed + 1; 276 if (remainingTime < 0) remainingTime = 0; 277 } 278 279 return remainingTime; 280 }, 281 282 checkWarnings: function(quizId) { 283 const currentQuiz = this.quizObjects[quizId]; 284 if (!currentQuiz) return; 285 286 const percentConsumed = ((currentQuiz.totalTime - currentQuiz.remainingTime) / currentQuiz.totalTime) * 100; 287 const percentRemaining = (currentQuiz.remainingTime / currentQuiz.totalTime) * 100; 288 289 // Check for 90% time consumed warning (matching legacy behavior) 290 const ninetyPercent = currentQuiz.totalTime - (currentQuiz.totalTime * 90 / 100); 291 if (currentQuiz.remainingTime <= ninetyPercent && !currentQuiz.warnings.ninety) { 292 currentQuiz.warnings.ninety = true; 293 this.showNinetyPercentWarning(quizId); 294 } 295 296 // Check for 10% remaining warning 297 if (percentRemaining <= 10 && percentRemaining > 0 && !currentQuiz.warnings.ten) { 298 currentQuiz.warnings.ten = true; 299 currentQuiz.timer?.addClass('qsm-timer--warning'); 300 301 // Trigger warning event 302 $(document).trigger('qsm_timer_warning', [quizId, currentQuiz.remainingTime, percentRemaining]); 303 } 304 }, 305 306 secondsToTimer: function(seconds) { 307 seconds = Number.parseInt(seconds, 10); 308 if (seconds < 0) seconds = 0; 309 310 const safeSeconds = seconds; 311 312 const hours = Math.floor(safeSeconds / 3600); 313 const minutes = Math.floor((safeSeconds % 3600) / 60); 314 const secs = safeSeconds % 60; 315 316 let formattedTime = ''; 317 318 // Hours 319 if (hours === 0) { 320 formattedTime = '00:'; 321 } else if (hours < 10) { 322 formattedTime = '0' + hours + ':'; 323 } else { 324 formattedTime = hours + ':'; 325 } 326 327 // Minutes 328 if (minutes === 0) { 329 formattedTime += '00:'; 330 } else if (minutes < 10) { 331 formattedTime += '0' + minutes + ':'; 332 } else { 333 formattedTime += minutes + ':'; 334 } 335 336 // Seconds 337 if (secs === 0) { 338 formattedTime += '00'; 339 } else if (secs < 10) { 340 formattedTime += '0' + secs; 341 } else { 342 formattedTime += secs; 343 } 344 345 return formattedTime; 346 }, 347 348 endTimer: function(quizId) { 349 this.stop(quizId); 350
351 // Clean up localStorage (both new and legacy keys) 352 localStorage.setItem('mlw_time_quiz' + quizId, 'completed'); 353 localStorage.setItem('mlw_started_quiz' + quizId, 'no'); 354 localStorage.removeItem('mlw_time_consumed_quiz' + quizId); 355 356 // Restore original title 357 document.title = this.originalTitle; 358 359 // Trigger events 360 $(document).trigger('qsm_end_timer', [quizId, qmn_quiz_data]); 361 }, 362 363 // Public API methods for external access 364 getTimerInfo: function(quizId) { 365 const currentQuiz = this.quizObjects[quizId]; 366 if (!currentQuiz || currentQuiz.warnings.expired) return null; 367 368 return { 369 quizId: quizId, 370 totalTime: currentQuiz.totalTime, 371 remainingTime: currentQuiz.remainingTime, 372 consumedTime: currentQuiz.consumedTime, 373 isActive: currentQuiz.isActive, 374 formattedTime: this.secondsToTimer(currentQuiz.remainingTime), 375 percentRemaining: (currentQuiz.remainingTime / currentQuiz.totalTime) * 100 376 }; 377 }, 378 379 getAllTimers: function() { 380 const timers = {}; 381 const self = this; 382 Object.keys(this.quizObjects).forEach(function(quizId) { 383 timers[quizId] = self.getTimerInfo(quizId); 384 }); 385 return timers; 386 }, 387 388 // New methods for enhanced functionality 389 getQuizData: function(quizId) { 390 // Try multiple data sources for compatibility 391 let data = {}; 392 393 if (qmn_quiz_data && qmn_quiz_data[quizId]) { 394 data = qmn_quiz_data[quizId]; 395 } 396 397 return data; 398 }, 399 400 updateLegacyQuizData: function(quizId) { 401 const currentQuiz = this.quizObjects[quizId]; 402 if (!currentQuiz || currentQuiz.warnings.expired) return; 403 404 // Ensure qmn_quiz_data exists and update it 405 if (typeof qmn_quiz_data === 'undefined') { 406 qmn_quiz_data = {}; 407 } 408 409 if (!qmn_quiz_data[quizId]) { 410 qmn_quiz_data[quizId] = currentQuiz.data; 411 } 412 413 // Update timer-specific properties 414 qmn_quiz_data[quizId].timerStatus = currentQuiz.timerStatus; 415 qmn_quiz_data[quizId].timerRemaning = currentQuiz.remainingTime; 416 qmn_quiz_data[quizId].timerConsumed = currentQuiz.consumedTime; 417 qmn_quiz_data[quizId].timerInterval = currentQuiz.timerInterval; 418 }, 419 420 shouldAutoStart: function(quizId, data) { 421 // Check if timer should auto-start based on various conditions 422 if (data.timer_auto_start) return true; 423 if (!data.first_page && !data.disable_first_page) return true; 424 if (Number(data.disable_first_page) === 1) return true; 425 426 return false; 427 }, 428 429 checkScheduledTimeExpiration: function(quizId) { 430 const currentQuiz = this.quizObjects[quizId]; 431 if (!currentQuiz || currentQuiz.warnings.expired) return false; 432 433 const data = currentQuiz.data; 434 if (data.not_allow_after_expired_time === '1' && data.scheduled_time_end) { 435 const systemTime = Math.round(new Date().getTime() / 1000); 436 if (systemTime > data.scheduled_time_end) { 437 MicroModal.show('modal-4'); 438 return true; 439 } 440 } 441 442 return false; 443 }, 444 445 showNinetyPercentWarning: function(quizId) { 446 const currentQuiz = this.quizObjects[quizId]; 447 if (!currentQuiz || currentQuiz.warnings.expired) return; 448 449 const $container = currentQuiz.form.closest('.qmn_quiz_container, .qsm-quiz-container'); 450 $container.find('.qsm_ninety_warning').fadeIn(); 451 }, 452 }; 453 454 // Initialize timer on document ready 455 $(document).ready(function() { 456 QSMPagination.Timer.init(); 457 }); 458 459})(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.