text: // Use 'yourlabs' as namespace.
  if (window.yourlabs == undefined) window.yourlabs = {};
  
  // Session security constructor. These are the required options:
  //
  // - pingUrl: url to ping with last activity in this tab to get global last
  //   activity time,
  // - warnAfter: number of seconds of inactivity before warning,
  // - expireAfter: number of seconds of inactivity before expiring the session.
  //
  // Optional options:
  //
  // - confirmFormDiscard: message that will be shown when the user tries to
  //   leave a page with unsaved form data. Setting this will enable an
  //   onbeforeunload handler that doesn't block expire().
  // - events: a list of event types to watch for activity updates.
  // - returnToUrl: a url to redirect users to expired sessions to. If this is not defined we just reload the page
  yourlabs.SessionSecurity = function(options) {
      // **HTML element** that should show to warn the user that his session will
      // expire.
      this.$warning = $('#session_security_warning');
  
      // Last recorded activity datetime.
      this.lastActivity = new Date();
  
      // Events that would trigger an activity
      this.events = ['mousemove', 'scroll', 'keyup', 'click', 'touchstart', 'touchend', 'touchmove'];
  
      // Merge the options dict here.
      $.extend(this, options);
  
      // Bind activity events to update this.lastActivity.
      var $document = $(document);
      for(var i=0; i<this.events.length; i++) {
          if ($document[this.events[i]]) {
              $document[this.events[i]]($.proxy(this.activity, this));
          }
      }
  
      // Initialize timers.
      this.apply()
  
      if (this.confirmFormDiscard) {
          window.onbeforeunload = $.proxy(this.onbeforeunload, this);
          $document.on('change', ':input', $.proxy(this.formChange, this));
          $document.on('submit', 'form', $.proxy(this.formClean, this));
          $document.on('reset', 'form', $.proxy(this.formClean, this));
      }
  }
  
  yourlabs.SessionSecurity.prototype = {
      // Called when there has been no activity for more than expireAfter
      // seconds.
      expire: function() {
          this.expired = true;
          if (this.returnToUrl !== undefined) {
              window.location.href = this.returnToUrl;
          }
          else {
              window.location.reload();
          }
      },
  
      // Called when there has been no activity for more than warnAfter
      // seconds.
      showWarning: function() {
          this.$warning.fadeIn('slow');
          this.$warning.attr('aria-hidden', 'false');
          $('.session_security_modal').focus();
      },
  
      // Called to hide the warning, for example if there has been activity on
      // the server side - in another browser tab.
      hideWarning: function() {
          this.$warning.hide();
          this.$warning.attr('aria-hidden', 'true');
      },
  
      // Called by click, scroll, mousemove, keyup, touchstart, touchend, touchmove
      activity: function() {
          var now = new Date();
          if (now - this.lastActivity < 1000)
              // Throttle these checks to once per second
              return;
  
          this.lastActivity = now;
  
          if (this.$warning.is(':visible')) {
              // Inform the server that the user came back manually, this should
              // block other browser tabs from expiring.
              this.ping();
          }
  
          this.hideWarning();
      },
  
      // Hit the PingView with the number of seconds since last activity.
      ping: function() {
          var idleFor = Math.floor((new Date() - this.lastActivity) / 1000);
  
          $.ajax(this.pingUrl, {
              data: {idleFor: idleFor},
              cache: false,
              success: $.proxy(this.pong, this),
              // In case of network error, we still want to hide potentially
              // confidential data !!
              error: $.proxy(this.apply, this),
              dataType: 'json',
              type: 'get'
          });
      },
  
      // Callback to process PingView response.
      pong: function(data) {
          if (data == 'logout') return this.expire();
  
          this.lastActivity = new Date();
          this.lastActivity.setSeconds(this.lastActivity.getSeconds() - data);
          this.apply();
      },
  
      // Apply warning or expiry, setup next ping
      apply: function() {
          // Cancel timeout if any, since we're going to make our own
          clearTimeout(this.timeout);
  
          var idleFor = Math.floor((new Date() - this.lastActivity) / 1000);
  
          if (idleFor >= this.expireAfter) {
              return this.expire();
          } else if (idleFor >= this.warnAfter) {
              this.showWarning();
              nextPing = this.expireAfter - idleFor;
          } else {
              this.hideWarning();
              nextPing = this.warnAfter - idleFor;
          }
  
          this.timeout = setTimeout($.proxy(this.ping, this), nextPing * 1000);
      },
  
      // onbeforeunload handler.
      onbeforeunload: function(e) {
          if ($('form[data-dirty]').length && !this.expired) {
              return this.confirmFormDiscard;
          }
      },
  
      // When an input change, set data-dirty attribute on its form.
      formChange: function(e) {
          $(e.target).closest('form').attr('data-dirty', true);
      },
  
      // When a form is submitted or resetted, unset data-dirty attribute.
      formClean: function(e) {
          $(e.target).removeAttr('data-dirty');
      }
  }