stopwatch-emitter
Version:
Just a stopwatch-like timer class
476 lines (409 loc) • 15.8 kB
JavaScript
/**
* EventEmitter v4.0.5 - git.io/ee
* Oliver Caldwell
* MIT license
* @preserve
*/
;(function(exports) {
// JSHint config - http://www.jshint.com/
/*jshint laxcomma:true*/
/*global define:true*/
// Place the script in strict mode
'use strict';
/**
* Class for managing events.
* Can be extended to provide event functionality in other classes.
*
* @class Manages event registering and emitting.
*/
function EventEmitter(){}
// Shortcuts to improve speed and size
// Easy access to the prototype
var proto = EventEmitter.prototype
// Existence of a native indexOf
, nativeIndexOf = Array.prototype.indexOf ? true : false;
/**
* Finds the index of the listener for the event in it's storage array
*
* @param {Function} listener Method to look for.
* @param {Function[]} listeners Array of listeners to search through.
* @return {Number} Index of the specified listener, -1 if not found
*/
function indexOfListener(listener, listeners) {
// Return the index via the native method if possible
if(nativeIndexOf) {
return listeners.indexOf(listener);
}
// There is no native method
// Use a manual loop to find the index
var i = listeners.length;
while(i--) {
// If the listener matches, return it's index
if(listeners[i] === listener) {
return i;
}
}
// Default to returning -1
return -1;
}
/**
* Fetches the events object and creates one if required.
*
* @return {Object} The events storage object.
*/
proto._getEvents = function() {
return this._events || (this._events = {});
};
/**
* Returns the listener array for the specified event.
* Will initialise the event object and listener arrays if required.
*
* @param {String} evt Name of the event to return the listeners from.
* @return {Function[]} All listener functions for the event.
* @doc
*/
proto.getListeners = function(evt) {
// Create a shortcut to the storage object
// Initialise it if it does not exists yet
var events = this._getEvents();
// Return the listener array
// Initialise it if it does not exist
return events[evt] || (events[evt] = []);
};
/**
* Adds a listener function to the specified event.
* The listener will not be added if it is a duplicate.
* If the listener returns true then it will be removed after it is called.
*
* @param {String} evt Name of the event to attach the listener to.
* @param {Function} listener Method to be called when the event is emitted. If the function returns true then it will be removed after calling.
* @return {Object} Current instance of EventEmitter for chaining.
* @doc
*/
proto.addListener = function(evt, listener) {
// Fetch the listeners
var listeners = this.getListeners(evt);
// Push the listener into the array if it is not already there
if(indexOfListener(listener, listeners) === -1) {
listeners.push(listener);
}
// Return the instance of EventEmitter to allow chaining
return this;
};
/**
* Alias of addListener
* @doc
*/
proto.on = proto.addListener;
/**
* Removes a listener function from the specified event.
*
* @param {String} evt Name of the event to remove the listener from.
* @param {Function} listener Method to remove from the event.
* @return {Object} Current instance of EventEmitter for chaining.
* @doc
*/
proto.removeListener = function(evt, listener) {
// Fetch the listeners
// And get the index of the listener in the array
var listeners = this.getListeners(evt)
, index = indexOfListener(listener, listeners);
// If the listener was found then remove it
if(index !== -1) {
listeners.splice(index, 1);
// If there are no more listeners in this array then remove it
if(listeners.length === 0) {
this.removeEvent(evt);
}
}
// Return the instance of EventEmitter to allow chaining
return this;
};
/**
* Alias of removeListener
* @doc
*/
proto.off = proto.removeListener;
/**
* Adds listeners in bulk using the manipulateListeners method.
* If you pass an object as the second argument you can add to multiple events at once. The object should contain key value pairs of events and listeners or listener arrays.
* You can also pass it an event name and an array of listeners to be added.
*
* @param {String|Object} evt An event name if you will pass an array of listeners next. An object if you wish to add to multiple events at once.
* @param {Function[]} [listeners] An optional array of listener functions to add.
* @return {Object} Current instance of EventEmitter for chaining.
* @doc
*/
proto.addListeners = function(evt, listeners) {
// Pass through to manipulateListeners
return this.manipulateListeners(false, evt, listeners);
};
/**
* Removes listeners in bulk using the manipulateListeners method.
* If you pass an object as the second argument you can remove from multiple events at once. The object should contain key value pairs of events and listeners or listener arrays.
* You can also pass it an event name and an array of listeners to be removed.
*
* @param {String|Object} evt An event name if you will pass an array of listeners next. An object if you wish to remove from multiple events at once.
* @param {Function[]} [listeners] An optional array of listener functions to remove.
* @return {Object} Current instance of EventEmitter for chaining.
* @doc
*/
proto.removeListeners = function(evt, listeners) {
// Pass through to manipulateListeners
return this.manipulateListeners(true, evt, listeners);
};
/**
* Edits listeners in bulk. The addListeners and removeListeners methods both use this to do their job. You should really use those instead, this is a little lower level.
* The first argument will determine if the listeners are removed (true) or added (false).
* If you pass an object as the second argument you can add/remove from multiple events at once. The object should contain key value pairs of events and listeners or listener arrays.
* You can also pass it an event name and an array of listeners to be added/removed.
*
* @param {Boolean} remove True if you want to remove listeners, false if you want to add.
* @param {String|Object} evt An event name if you will pass an array of listeners next. An object if you wish to add/remove from multiple events at once.
* @param {Function[]} [listeners] An optional array of listener functions to add/remove.
* @return {Object} Current instance of EventEmitter for chaining.
* @doc
*/
proto.manipulateListeners = function(remove, evt, listeners) {
// Initialise any required variables
var i
, value
, single = remove ? this.removeListener : this.addListener
, multiple = remove ? this.removeListeners : this.addListeners;
// If evt is an object then pass each of it's properties to this method
if(typeof evt === 'object') {
for(i in evt) {
if(evt.hasOwnProperty(i) && (value = evt[i])) {
// Pass the single listener straight through to the singular method
if(typeof value === 'function') {
single.call(this, i, value);
}
else {
// Otherwise pass back to the multiple function
multiple.call(this, i, value);
}
}
}
}
else {
// So evt must be a string
// And listeners must be an array of listeners
// Loop over it and pass each one to the multiple method
i = listeners.length;
while(i--) {
single.call(this, evt, listeners[i]);
}
}
// Return the instance of EventEmitter to allow chaining
return this;
};
/**
* Removes all listeners from a specified event.
* If you do not specify an event then all listeners will be removed.
* That means every event will be emptied.
*
* @param {String} [evt] Optional name of the event to remove all listeners for. Will remove from every event if not passed.
* @return {Object} Current instance of EventEmitter for chaining.
* @doc
*/
proto.removeEvent = function(evt) {
// Remove different things depending on the state of evt
if(evt) {
// Remove all listeners for the specified event
delete this._getEvents()[evt];
}
else {
// Remove all listeners in all events
delete this._events;
}
// Return the instance of EventEmitter to allow chaining
return this;
};
/**
* Emits an event of your choice.
* When emitted, every listener attached to that event will be executed.
* If you pass the optional argument array then those arguments will be passed to every listener upon execution.
* Because it uses `apply`, your array of arguments will be passed as if you wrote them out separately.
* So they will not arrive within the array on the other side, they will be separate.
*
* @param {String} evt Name of the event to emit and execute listeners for.
* @param {Array} [args] Optional array of arguments to be passed to each listener.
* @return {Object} Current instance of EventEmitter for chaining.
* @doc
*/
proto.emitEvent = function(evt, args) {
// Get the listeners for the event
// Also initialise any other required variables
var listeners = this.getListeners(evt)
, i = listeners.length
, response;
// Loop over all listeners assigned to the event
// Apply the arguments array to each listener function
while(i--) {
// If the listener returns true then it shall be removed from the event
// The function is executed either with a basic call or an apply if there is an args array
response = args ? listeners[i].apply(null, args) : listeners[i]();
if(response === true) {
this.removeListener(evt, listeners[i]);
}
}
// Return the instance of EventEmitter to allow chaining
return this;
};
/**
* Alias of emitEvent
* @doc
*/
proto.trigger = proto.emitEvent;
/**
* Subtly different from emitEvent in that it will pass its arguments on to the listeners, as
* opposed to taking a single array of arguments to pass on.
*
* @param {String} evt Name of the event to emit and execute listeners for.
* @param {...*} Optional additional arguments to be passed to each listener.
* @return {Object} Current instance of EventEmitter for chaining.
* @doc
*/
proto.emit = function(evt) {
var args = Array.prototype.slice.call(arguments, 1);
return this.emitEvent(evt, args);
};
// Expose the class either via AMD or the global object
if(typeof define === 'function' && define.amd) {
define(function() {
return EventEmitter;
});
}
else {
exports.EventEmitter = EventEmitter;
}
}(this));
/* global EventEmitter, Timer */
(function(exports){
var hasProp = {}.hasOwnProperty
, _extends = function(child, parent) {
for (var key in parent) {
if (hasProp.call(parent, key)) child[key] = parent[key];
}
function ctor() {
this.constructor = child;
}
ctor.prototype = parent.prototype;
child.prototype = new ctor();
child.__super__ = parent.prototype;
return child;
};
var Stopwatch = (function(_super){
_extends(Stopwatch, _super);
function Stopwatch(maxTime) {
// Set defaults
if (maxTime == null) maxTime = '5m';
this.currentTime = 0;
this._stopEmitted = false;
this._running = false;
this.toQ = [];
this.parseTime(maxTime);
this._setupEvents();
}
var proto = Stopwatch.prototype;
// Translate strings into seconds and set maxTime
proto.parseTime = function(time) {
// Create an override for numbers. We'll expect seconds.
if (!isNaN(time)) return this.maxTime = time;
time = time.toString().match(/([\d\.]+)(\w{1})/);
var timeValue = parseFloat(time[1], 10)
, timeInterval = time[2];
switch (timeInterval) {
case 's':
timeValue *= 1;
break;
case 'm':
timeValue *= 60;
break;
case 'h':
timeValue *= 60 * 60;
}
this.maxTime = Math.round(timeValue);
};
// Setup some instance events
proto._setupEvents = function() {
var self = this;
// Stop emitted tells us we specifically called stop so the we don't
// emit twice.
self.on('stop', function(){ self._stopEmitted = true; self.clear(); });
// Make sure to toggle stopEmitted we any other event fires
// Also, clear any timeouts if we are already running.
var stopCb = function() {
self._stopEmitted = false;
if (self._running) self.clear();
};
self.on('start', stopCb);
self.on('pause', stopCb);
self.on('restart', stopCb);
};
proto.tick = function() {
var self = this;
if (self.currentTime >= self.maxTime) {
if (!self._stopEmitted) self.emit('stop'); // If we reach the end
self._running = false; // Make sure we're not running and return
return;
}
// Make sure nothing naughty gets through
if (!self._running) return;
// Emit the tick
self.emit('tick');
// Store the timeout so we can clear it later if necessary
var tO = setTimeout(function(){
self.currentTime++;
self.tick();
}, 1000);
// Push to timeout queue
self.toQ.push(tO);
};
// Clear the timeout queue
proto.clear = function() {
var q = this.toQ;
while(q.length) {
clearTimeout(q[q.length - 1]);
q.pop();
}
};
proto.pause = function() {
if (!this._running) return;
this._running = false;
this.emit('pause');
};
proto.stop = function() {
if (!this._running) return;
this._running = false;
this.currentTime = 0;
this.emit('stop');
};
proto.restart = function() {
this.currentTime = 0;
this.emit('restart');
this.start(false);
};
proto.start = function(emit) {
if (emit == null) emit = true;
if ((this._running && emit) || this.currentTime >= this.maxTime) return;
if (emit) this.emit('start');
this._running = true;
this.tick();
};
proto.getCurrentTime = function() {
return this.currentTime;
};
proto.getRemainingTime = function() {
return this.maxTime - this.currentTime;
};
proto.getMaxTime = function() {
return this.maxTime;
};
proto.isRunning = function() {
return !!this._running;
};
return Stopwatch;
}(EventEmitter));
exports.Stopwatch = Stopwatch;
}(this));