factories/idle-timer-factory.js

/* eslint-disable no-underscore-dangle */
/* eslint-disable no-unused-expressions */
/* eslint-disable no-param-reassign */
/* eslint-disable no-func-assign */
/* eslint-disable no-use-before-define */
/**
 * Copy of Angular Activity Monitor.
 * Wrapped in an AngularJS Factory to allow multiple instances.
 *
 * @class IdleTimer
 * @see https://github.com/sean3z/angular-activity-monitor
 * @see https://docs.angularjs.org/api/ng/type/angular.Module#factory
 *
 */
angular.module('tgaApp')
  .factory('IdleTimer', ['$document', '$interval',
    function IdleTimerFactory($document, $interval) {
      /**
       * @constructs
       * @lends IdleTimer
       *
       * @borrows IdleTimer~activity as IdleTimer#activity
       * @borrows IdleTimer~subscribe as IdleTimer#on
       * @borrows IdleTimer~subscribe as IdleTimer#bind
       * @borrows IdleTimer~unsubscribe as IdleTimer#off
       * @borrows IdleTimer~unsubscribe as IdleTimer#unbind
       */
      const instance = function IdleTimer() {
        const MILLISECOND = 1000;
        const EVENT_KEEPALIVE = 'keepAlive';
        const EVENT_INACTIVE = 'inactive';
        const EVENT_WARNING = 'warning';
        const EVENT_ACTIVITY = 'activity';
        /** @alias IdleTimer# */
        const service = this;

        /**
         * Configuration object.
         *
         * @property {boolean} enabled - Is the ActivityMonitor enabled?
         * @property {number} keepAlive - KeepAlive ping interval (seconds).
         * @property {number} inactive - How long until user is considered inactive (seconds)?
         * @property {number} warning - When to warn user nearing inactive state (at `inactive - warning` seconds).
         * @property {number} monitor - How frequently to check if the user is inactive (seconds).
         * @property {boolean} disableOnInactive - Whether to detach all listeners when user goes inactive.
         * @property {string[]} DOMevents - List of DOM events to determine user's activity.
         * @default
         */
        service.options = {
          enabled: false,
          keepAlive: 800,
          inactive: 900,
          warning: 60,
          monitor: 3,
          disableOnInactive: true,
          DOMevents: [
            'mousemove',
            'mousedown',
            'mouseup',
            'keypress',
            'wheel',
            'touchstart',
            'scroll',
          ],
        };

        /**
         * User activity.
         *
         * @property {number} action - Timestamp of the users' last action.
         * @property {boolean} active - Is the user considered active?
         * @property {boolean} warning - Is the user in warning state?
         * @default
         */
        service.user = {
          action: Date.now(),
          active: true,
          warning: false,
        };

        service.activity = activity; /* method consumers can use to supply activity */
        service.on = service.bind = subscribe; /* expose method to subscribe to events */
        service.off = service.unbind = unsubscribe; /* expose method to unsubscribe from events */

        const events = {};
        events[EVENT_KEEPALIVE] = {}; /* functions to invoke along with ping (options.frequency) */
        events[EVENT_INACTIVE] = {}; /* functions to invoke when user goes inactive (options.threshold) */
        events[EVENT_WARNING] = {}; /* functions to invoke when warning user about inactivity (options.warning) */
        events[EVENT_ACTIVITY] = {}; /* functions to invoke any time a user makes a move */

        const timer = {
          inactivity: null, /* setInterval handle to determine whether the user is inactive */
          keepAlive: null, /* setInterval handle for ping handler (options.frequency) */
        };

        enable.timer = timer;
        service.enable = enable;
        service.disable = disable;

        /// ////////////

        function disable() {
          service.options.enabled = false;

          disableIntervals();

          $document.off(service.options.DOMevents.join(' '), activity);
        }

        function disableIntervals() {
          if (timer.inactivity) {
            $interval.cancel(timer.inactivity);
            delete timer.inactivity;
          }

          if (timer.keepAlive) {
            $interval.cancel(timer.keepAlive);
            delete timer.keepAlive;
          }
        }

        function enable() {
          $document.on(service.options.DOMevents.join(' '), activity);
          service.options.enabled = true;
          service.user.warning = false;

          enableIntervals();
        }

        function enableIntervals() {
          timer.keepAlive = $interval(() => {
            publish(EVENT_KEEPALIVE);
          }, service.options.keepAlive * MILLISECOND);

          timer.inactivity = $interval(() => {
            const now = Date.now();
            const warning = now - (service.options.inactive - service.options.warning) * MILLISECOND;
            const inactive = now - service.options.inactive * MILLISECOND;

            /* should we display warning */
            if (!service.user.warning && service.user.action <= warning) {
              service.user.warning = true;
              publish(EVENT_WARNING);
            }

            /* should user be considered inactive? */
            if (service.user.active && service.user.action <= inactive) {
              service.user.active = false;
              publish(EVENT_INACTIVE);

              if (service.options.disableOnInactive) {
                disable();
              } else {
                disableIntervals();// user inactive is known, lets stop checking, for now
                dynamicActivity = reactivate;// hot swap method that handles document event watching
              }
            }
          }, service.options.monitor * MILLISECOND);
        }

        /* function that lives in memory with the intention of being swapped out */
        function dynamicActivity() {
          regularActivityMonitor();
        }

        /* after user inactive, this method is hot swapped as the
        dynamicActivity method in-which the next user activity reactivates monitors */
        function reactivate() {
          enableIntervals();
          dynamicActivity = regularActivityMonitor;
        }

        /* invoked on every user action */
        /**
         * Manually invoke user activity.
         */
        function activity() {
          dynamicActivity();
        }

        /* during a users active state the following method is called */
        function regularActivityMonitor() {
          service.user.active = true;
          service.user.action = Date.now();

          publish(EVENT_ACTIVITY);

          if (service.user.warning) {
            service.user.warning = false;
            publish(EVENT_KEEPALIVE);
          }
        }

        function publish(event) {
          if (!service.options.enabled) return;
          const spaces = Object.keys(events[event]);
          if (!event || !spaces.length) return;
          spaces.forEach((space) => {
            events[event][space] && events[event][space]();
          });
        }

        /**
         * Subscribe to a particular event.
         *
         * @param {string} event - Event to subscribe to (`keepAlive`, `inactive`, `warning`, `activity`).
         * @param {function} callback - Callback to run on event.
         */
        function subscribe(event, callback) {
          if (!event || !angular.isFunction(callback)) return;
          event = _namespace(event, callback);
          events[event.name][event.space] = callback;
          !service.options.enabled && enable();
        }

        /**
         * Unsubscribe to a particular event.
         *
         * @param {string} event - Event to unsubscribe from (`keepAlive`, `inactive`, `warning`, `activity`).
         * @param {function} [callback] - If no callback or namespace provided, all subscribers for the given event
         *   will be cleared.
         */
        function unsubscribe(event, callback) {
          event = _namespace(event, callback);

          if (!event.space) {
            events[event.name] = {};
            return;
          }

          events[event.name][event.space] = null;
        }

        /* method to return event namespace */
        function _namespace(event, callback) {
          event = event.split('.');

          if (!event[1] && angular.isFunction(callback)) {
            /* if no namespace, use callback and strip all linebreaks and spaces */
            event[1] = callback.toString().substr(0, 150).replace(/\r?\n|\r|\s+/gm, '');
          }

          return {
            name: event[0],
            space: event[1],
          };
        }

        return service;
      };

      return instance;
    },
  ]);