xowf::test_item::Answer_manager method countdown_timer (public)

 <instance of xowf::test_item::Answer_manager[i]> 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;'>&nbsp;[_ 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
[ hide source ] | [ make this the default ]
Show another procedure: