xowf::test_item::Answer_manager method countdown_timer (public)
<instance of xowf::test_item::Answer_manager> countdown_timer \ -target_time target_time -id id [ -audio_alarm on|off ] \ [ -audio_alarm_cookie audio_alarm_cookie ] \ [ -audio_alarm_times audio_alarm_times ]
Defined in packages/xowf/tcl/test-item-procs.tcl
Accepted formats for target_time, determined by JavaScript ISO 8601, e.g. YYYY-MM-DDTHH:mm:ss.sss" Set current time based on host time instead of new Date().getTime() to avoid surprises, in cases, the time at the client browser is set incorrectly.
- Switches:
- -target_time (required)
- -id (required)
- -audio_alarm (optional, boolean, defaults to
"true"
)- -audio_alarm_cookie (optional, defaults to
"incass_exam_audio_alarm"
)- -audio_alarm_times (optional, defaults to
"60,30,20,10,5,2"
)- Testcases:
- No testcase defined.
Source code: set nowMs [clock milliseconds] set nowIsoTime [clock format [expr {$nowMs/1000}] -format "%Y-%m-%dT%H:%M:%S"].[format %.3d [expr {$nowMs % 1000}]] template::add_body_script -script [subst { var countdown_target_date = new Date('$target_time').getTime(); var countdown_days, countdown_hours, countdown_minutes, countdown_seconds; var countdown = document.getElementById('$id'); // adjust target time by the difference between the host and client time countdown_target_date = countdown_target_date - (new Date('$nowIsoTime').getTime() - new Date().getTime()); setInterval(function () { var current_date = new Date().getTime(); var absolute_seconds_left = (countdown_target_date - current_date) / 1000; var seconds_left = absolute_seconds_left var HTML = ''; countdown_days = parseInt(seconds_left / 86400); seconds_left = seconds_left % 86400; countdown_hours = parseInt(seconds_left / 3600); seconds_left = seconds_left % 3600; countdown_minutes = parseInt(seconds_left / 60); countdown_seconds = parseInt(seconds_left % 60); var alarmseconds = countdown.parentNode.dataset.alarmseconds; if (typeof alarmseconds !== 'undefined') { var full_seconds = Math.trunc(absolute_seconds_left); // for testing purposes, use: (full_seconds % 5 == 0) if (alarmseconds.includes(full_seconds)) { beep(200); } } if (seconds_left < -60) { countdown.innerHTML = "<span style='color:red;'> [_ xowf.Countdown_timer_expired]</span>" return } if (countdown_days != 0) { HTML += '<span class="days">' + countdown_days + ' <b> ' + (countdown_days != 1 ? '[_ xowf.Days]' : '[_ xowf.Day]') + '</b></span> '; } if (countdown_hours != 0 || countdown_days != 0) { HTML += '<span class="hours">' + countdown_hours + ' <b> ' + (countdown_hours != 1 ? '[_ xowf.Hours]' : '[_ xowf.Hour]') + '</b></span> '; } HTML += '<span class="minutes">' + countdown_minutes + ' <b> ' + (countdown_minutes != 1 ? '[_ xowf.Minutes]' : '[_ xowf.Minute]') + '</b></span> ' + '<span class="seconds">' + countdown_seconds + ' <b> ' + (countdown_seconds != 1 ? '[_ xowf.Seconds]' : '[_ xowf.Second]') + '</b></span> [_ xowf.remaining]' ; countdown.innerHTML = HTML; }, 1000); var beep = (function () { return function (duration, finishedCallback) { var container = document.getElementById('$id').parentNode; //console.log("beep attempt " + duration + ' ' + audioContext + ' ' + container.dataset.alarm); if (typeof audioContext !== 'undefined' && (container.dataset.alarm == 'active')) { //console.log("true beep duration " + duration + ' ' + audioContext + ' ' + audioContext.state); var osc = audioContext.createOscillator(); osc.type = "sine"; osc.connect(audioContext.destination); if (osc.noteOn) osc.noteOn(0); // old browsers if (osc.start) osc.start(); // new browsers setTimeout(function () { if (osc.noteOff) osc.noteOff(0); // old browsers if (osc.stop) osc.stop(); // new browsers }, duration); } }; })(); }] if {$audio_alarm} { # # Audio alarm handling is more tricky than expected, since # modern browsers do not allow one to create an active sound # context without a "user gesture" (requires e.g. a click to # start). # # The code tries to remember the audio state between different # pages, such when e.g. being in an exam, the user has to # activate/deactivate the audio not on every page. However, # when the user does a full reload, then the user has to # activate the audio alarm again. # # The state is symbolized using bootstrap 3 glyphicons or # bootstrap icons. The code is tested primarily with chrome. # template::add_body_script -script [subst { var audioContext = new AudioContext(); var audioContext_setSate = (function (targetState) { var container = document.getElementById('$id').parentNode; //console.log('--- state = ' + audioContext.state + ' want ' + targetState); if (targetState == 'active') { var elements = container.querySelector('i'); var prefix = 'bi'; if (!elements) { elements = container.querySelector('span'); prefix = 'glyphicon'; } elements.classList.remove(prefix + '-volume-off'); elements.classList.add(prefix + '-volume-up'); container.dataset.alarm = 'active'; document.cookie = '$audio_alarm_cookie=active; sameSite=strict'; audioContext.resume().then(() => {console.log('Playback resumed successfully ' + targetState);}); } else { var elements = container.querySelector('i'); var prefix = 'bi'; if (!elements) { elements = container.querySelector('span'); prefix = 'glyphicon'; } elements.classList.remove(prefix + '-volume-up'); elements.classList.add(prefix + '-volume-off'); container.dataset.alarm = 'inactive'; document.cookie = '$audio_alarm_cookie=inactive; sameSite=strict'; audioContext.suspend().then(() => {console.log('Playback suspended successfully ' + targetState);}); } //console.log('setSate ' + audioContext.state + ' alarm ' + container.dataset.alarm); }); var audioContext_toggle = (function (event) { var container = document.getElementById('$id').parentNode; //console.log('audioContext_toggle ' + audioContext.state); if (container.dataset.alarm != 'active') { audioContext_setSate('active'); beep(200); } else { audioContext_setSate('inactive'); } }); var audioContext_onload = (function (event) { var m = document.cookie.match('(^|;)\\s*$audio_alarm_cookie\\s*=\\s*(\[^;\]+)'); var cookieValue = (m ? m.pop() : 'inactive'); console.log('audioContext_onload ' + audioContext.state + ' cookie ' + cookieValue); // // When the current state is 'running' the behavior seems // cross browser uniform, we can set it to the state we got // from the cookie. // if (audioContext.state == 'running') { audioContext_setSate(cookieValue); } else { // // FireFox can switch to "active" after reload, while // this does not work on Chrome and friends. // if (navigator.userAgent.toLowerCase().indexOf('firefox') > -1) { audioContext_setSate(cookieValue); } else { audioContext_setSate('inactive'); } } }); console.log('onload'); console.log(document.getElementById('$id')); console.log('register audiocontext_toggle'); document.getElementById('$id').parentNode.addEventListener('click', audioContext_toggle); window.addEventListener('load', audioContext_onload); }] if {[ns_conn isconnected]} { # # The icon names "volume-off" and "volume-up" exist in the # glyph icons and for the bootstrap icons (Bootstrap 5) # set alarmState [ns_getcookie $audio_alarm_cookie "inactive"] set icon [expr {$alarmState eq "inactive" ? "volume-off":"volume-up"}] } else { set alarmState "inactive" set icon "volume-off" } #ns_log notice "C=$alarmState" return [subst { <div data-alarm='$alarmState' data-alarmseconds='\[$audio_alarm_times\]'> <adp:icon name='$icon'> <div style='display: inline-block;' id='$id'></div> </div> }] } else { return [subst { <div style='display: inline-block;' id='$id'></div> }] }XQL Not present: Generic, PostgreSQL, Oracle