object.js

var inherit = require('./inherit');
var timestamp = require('./timestamp');

var lastInternalId = 0;

/**
 * Create a new instance of class object.
 *
 * @class
 * @alias geo.object
 * @extends geo.timestamp
 * @returns {geo.object}
 */
var object = function () {
  'use strict';
  if (!(this instanceof object)) {
    return new object();
  }

  var util = require('./util');

  var m_this = this,
      m_internalId = ++lastInternalId,
      m_eventHandlers = {},
      m_idleHandlers = [],
      m_promiseCount = 0;

  /**
   * Bind a handler that will be called one time when all internal promises are
   * resolved.  If there are no outstanding promises, this is invoked
   * synchronously.
   *
   * @param {function} handler A function taking no arguments.
   * @returns {this}
   */
  this.onIdle = function (handler) {
    if (m_promiseCount) {
      m_idleHandlers.push(handler);
    } else {
      handler();
    }
    return m_this;
  };

  /**
   * Getter for the idle state.  Read only.
   *
   * @property {boolean} idle `true` if the object is idle (`onIdle` would call
   *    a handler immediately).
   * @name geo.object#idle
   */
  Object.defineProperty(this, 'idle', {
    get: function () {
      return !m_promiseCount;
    },
    configurable: true
  });

  /**
   * Private getter for the number of outstanding promises.
   *
   * @property {number} _promises The number of outstanding promises.  If this
   *    is zero, the object is idle.
   * @name geo.object#_promises
   */
  Object.defineProperty(this, '_promises', {
    get: function () {
      return m_promiseCount;
    },
    configurable: true
  });

  /**
   * Add a new promise object preventing idle event handlers from being called
   * until it is resolved.
   *
   * @param {Promise} promise A promise object.
   * @returns {this}
   */
  this.addPromise = function (promise) {
    // called on any resolution of the promise
    function onDone() {
      if (promise._geojsTracked && promise._geojsTracked[m_internalId]) {
        m_promiseCount -= 1;
        delete promise._geojsTracked[m_internalId];
      }
      if (!m_promiseCount) {
        m_idleHandlers.splice(0, m_idleHandlers.length)
          .forEach(function (handler) {
            handler();
          });
      }
    }
    if (!promise._geojsTracked) {
      promise._geojsTracked = {};
    }
    if (!promise._geojsTracked[m_internalId]) {
      promise._geojsTracked[m_internalId] = true;
      m_promiseCount += 1;
    }
    if (promise.always) {
      promise.always(onDone);
    } else {
      promise.then(onDone, onDone);
    }
    return m_this;
  };

  /**
   * Mark a promise as no longer required to resolve before the idle state is
   * reached.
   *
   * @param {Promise} promise A promise object.
   * @returns {this}
   */
  this.removePromise = function (promise) {
    if (promise._geojsTracked && promise._geojsTracked[m_internalId]) {
      m_promiseCount -= 1;
      delete promise._geojsTracked[m_internalId];
      if (!m_promiseCount) {
        m_idleHandlers.splice(0, m_idleHandlers.length)
          .forEach(function (handler) {
            handler();
          });
      }
    }
    return m_this;
  };

  /**
   * Bind an event handler to this object.
   *
   * @param {string} event An event from {@link geo.event} or a user-defined
   *   value.
   * @param {function} handler A function that is called when `event` is
   *   triggered.  The function is passed a {@link geo.event} object.
   * @returns {this}
   */
  this.geoOn = function (event, handler) {
    if (Array.isArray(event)) {
      event.forEach(function (e) {
        m_this.geoOn(e, handler);
      });
      return m_this;
    }
    if (!util.isFunction(handler)) {
      console.warn('Handler for ' + event + ' is not a function', handler, m_this);
      return m_this;
    }
    if (!m_eventHandlers.hasOwnProperty(event)) {
      m_eventHandlers[event] = [];
    }
    m_eventHandlers[event].push(handler);
    return m_this;
  };

  /**
   * Bind an event handler to this object that will fire once and then
   * deregister itself.
   *
   * @param {string} event An event from {@link geo.event} or a user-defined
   *   value.
   * @param {function} handler A function that is called when `event` is
   *   triggered.  The function is passed a {@link geo.event} object.
   * @returns {function} The actual bound handler.  This is a wrapper around
   *   the handler that was passed to the function.
   */
  this.geoOnce = function (event, handler) {
    const wrapper = function (args) {
      m_this.geoOff(event, wrapper);
      handler.call(m_this, args);
    };
    m_this.geoOn(event, wrapper);
    return wrapper;
  };

  /**
   * Report if an event handler is bound to this object.
   *
   * @param {string|string[]} event An event or list of events to check.
   * @param {function} [handler] A function that might be bound.  If
   *   `undefined`, this will report `true` if there is any handler for the
   *   specified event.
   * @returns {boolean} true if any of the specified events are bound to the
   *   specified handler.
   */
  this.geoIsOn = function (event, handler) {
    if (Array.isArray(event)) {
      return event.some(function (e) {
        return m_this.geoIsOn(e, handler);
      });
    }
    return (m_eventHandlers[event] || []).some(function (h) {
      return h === handler || handler === undefined;
    });
  };

  /**
   * Trigger an event (or events) on this object and call all handlers.
   *
   * @param {string|string[]} event An event or list of events from
   *   {@link geo.event} or defined by the user.
   * @param {object} [args] Additional information to add to the
   *   {@link geo.event} object passed to the handlers.
   * @returns {this}
   */
  this.geoTrigger = function (event, args) {

    // if we have an array of events, recall with single events
    if (Array.isArray(event)) {
      event.forEach(function (e) {
        m_this.geoTrigger(e, args);
      });
      return m_this;
    }

    // append the event type to the argument object
    args = args || {};
    args.event = event;

    if (m_eventHandlers.hasOwnProperty(event)) {
      m_eventHandlers[event].forEach(function (handler) {
        try {
          handler.call(m_this, args);
        } catch (err) {
          console.warn('Event handler for ' + event + ' threw an error', err);
        }
      });
    }

    return m_this;
  };

  /**
   * Remove handlers from one event or an array of events.  If no event is
   * provided all handlers will be removed.
   *
   * @param {string|string[]} [event] An event or a list of events from
   *   {@link geo.event} or defined by the user, or `undefined` to remove all
   *   events (in which case `arg` is ignored).
   * @param {(function|function[])?} [arg] A function or array of functions to
   *   remove from the events or a falsy value to remove all handlers from the
   *   events.
   * @returns {this}
   */
  this.geoOff = function (event, arg) {
    if (event === undefined) {
      m_eventHandlers = {};
      m_idleHandlers = [];
      m_promiseCount = 0;
      // assign a new id so that adding and removing promises behave properly
      m_internalId = ++lastInternalId;
    }
    if (Array.isArray(event)) {
      event.forEach(function (e) {
        m_this.geoOff(e, arg);
      });
      return m_this;
    }
    if (!arg) {
      m_eventHandlers[event] = [];
    } else if (Array.isArray(arg)) {
      arg.forEach(function (handler) {
        m_this.geoOff(event, handler);
      });
      return m_this;
    }
    if (m_eventHandlers.hasOwnProperty(event)) {
      m_eventHandlers[event] = m_eventHandlers[event].filter(function (f) {
        return f !== arg;
      });
    }
    return m_this;
  };

  /**
   * Report the current event handlers.
   *
   * @returns {object} An object with all of the event handlers.
   */
  this._eventHandlers = function () {
    return m_eventHandlers;
  };

  /**
   * Free all resources and destroy the object.
   */
  this._exit = function () {
    m_this.geoOff();
  };

  timestamp.call(this);
  this.modified();

  return this;
};

inherit(object, timestamp);
module.exports = object;