416 lines
13 KiB
JavaScript
Executable File
416 lines
13 KiB
JavaScript
Executable File
/*Copyright (c) 2019 Jason Zissman
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
this software and associated documentation files (the "Software"), to deal in
|
|
the Software without restriction, including without limitation the rights to
|
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in all
|
|
copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
*/
|
|
|
|
(function () {
|
|
(function (root, factory) {
|
|
if (typeof module !== 'undefined' && module.exports) {
|
|
// CommonJS
|
|
return module.exports = factory();
|
|
} else if (typeof define === 'function' && define.amd) {
|
|
// AMD
|
|
define([], function () {
|
|
return (root.TimeMe = factory());
|
|
});
|
|
} else {
|
|
// Global Variables
|
|
return root.TimeMe = factory();
|
|
}
|
|
})(this, function () {
|
|
|
|
var TimeMe = {
|
|
|
|
startStopTimes: {},
|
|
idleTimeoutMs: 30 * 1000,
|
|
currentIdleTimeMs: 0,
|
|
checkStateRateMs: 250,
|
|
active: false, // state if we are actively recording time
|
|
idle: false, // state if user on page but not interacting
|
|
currentPageName: "default-page-name",
|
|
timeElapsedCallbacks: [],
|
|
userLeftCallbacks: [],
|
|
userReturnCallbacks: [],
|
|
|
|
trackTimeOnElement: function (elementId) {
|
|
var element = document.getElementById(elementId);
|
|
if (element) {
|
|
element.addEventListener("mouseover", function () {
|
|
TimeMe.startTimer(elementId);
|
|
});
|
|
element.addEventListener("mousemove", function () {
|
|
TimeMe.startTimer(elementId);
|
|
});
|
|
element.addEventListener("mouseleave", function () {
|
|
TimeMe.stopTimer(elementId);
|
|
});
|
|
element.addEventListener("keypress", function () {
|
|
TimeMe.startTimer(elementId);
|
|
});
|
|
element.addEventListener("focus", function () {
|
|
TimeMe.startTimer(elementId);
|
|
});
|
|
}
|
|
},
|
|
|
|
getTimeOnElementInSeconds: function (elementId) {
|
|
var time = TimeMe.getTimeOnPageInSeconds(elementId);
|
|
if (time) {
|
|
return time;
|
|
} else {
|
|
return 0;
|
|
}
|
|
},
|
|
|
|
// startTime is optional. If provided, must be of type Date(). By providing
|
|
// startTime, you are overriding the internal timing mechanism and manually
|
|
// indicating the start time.
|
|
startTimer: function (pageName, startTime) {
|
|
if (!pageName) {
|
|
pageName = TimeMe.currentPageName;
|
|
}
|
|
|
|
if (TimeMe.startStopTimes[pageName] === undefined) {
|
|
TimeMe.startStopTimes[pageName] = [];
|
|
} else {
|
|
var arrayOfTimes = TimeMe.startStopTimes[pageName];
|
|
var latestStartStopEntry = arrayOfTimes[arrayOfTimes.length - 1];
|
|
if (latestStartStopEntry !== undefined && latestStartStopEntry.stopTime === undefined) {
|
|
// Can't start new timer until previous finishes.
|
|
return;
|
|
}
|
|
}
|
|
TimeMe.startStopTimes[pageName].push({
|
|
"startTime": startTime || new Date(),
|
|
"stopTime": undefined
|
|
});
|
|
TimeMe.active = true;
|
|
TimeMe.idle = false;
|
|
},
|
|
|
|
stopAllTimers: function () {
|
|
var pageNames = Object.keys(TimeMe.startStopTimes);
|
|
for (var i = 0; i < pageNames.length; i++) {
|
|
TimeMe.stopTimer(pageNames[i]);
|
|
}
|
|
},
|
|
|
|
// stopTime is optional. If provided, must be of type Date(). By providing
|
|
// stopTime, you are overriding the internal timing mechanism and manually
|
|
// indicating the stop time.
|
|
stopTimer: function (pageName, stopTime) {
|
|
if (!pageName) {
|
|
pageName = TimeMe.currentPageName;
|
|
}
|
|
var arrayOfTimes = TimeMe.startStopTimes[pageName];
|
|
if (arrayOfTimes === undefined || arrayOfTimes.length === 0) {
|
|
// Can't stop timer before you've started it.
|
|
return;
|
|
}
|
|
if (arrayOfTimes[arrayOfTimes.length - 1].stopTime === undefined) {
|
|
arrayOfTimes[arrayOfTimes.length - 1].stopTime = stopTime || new Date();
|
|
}
|
|
TimeMe.active = false;
|
|
},
|
|
|
|
getTimeOnCurrentPageInSeconds: function () {
|
|
return TimeMe.getTimeOnPageInSeconds(TimeMe.currentPageName);
|
|
},
|
|
|
|
getTimeOnPageInSeconds: function (pageName) {
|
|
var timeInMs = TimeMe.getTimeOnPageInMilliseconds(pageName);
|
|
if (timeInMs === undefined) {
|
|
return undefined;
|
|
} else {
|
|
return timeInMs / 1000;
|
|
}
|
|
},
|
|
|
|
getTimeOnCurrentPageInMilliseconds: function () {
|
|
return TimeMe.getTimeOnPageInMilliseconds(TimeMe.currentPageName);
|
|
},
|
|
|
|
getTimeOnPageInMilliseconds: function (pageName) {
|
|
|
|
var totalTimeOnPage = 0;
|
|
|
|
var arrayOfTimes = TimeMe.startStopTimes[pageName];
|
|
if (arrayOfTimes === undefined) {
|
|
// Can't get time on page before you've started the timer.
|
|
return;
|
|
}
|
|
|
|
var timeSpentOnPageInSeconds = 0;
|
|
for (var i = 0; i < arrayOfTimes.length; i++) {
|
|
var startTime = arrayOfTimes[i].startTime;
|
|
var stopTime = arrayOfTimes[i].stopTime;
|
|
if (stopTime === undefined) {
|
|
stopTime = new Date();
|
|
}
|
|
var difference = stopTime - startTime;
|
|
timeSpentOnPageInSeconds += (difference);
|
|
}
|
|
|
|
totalTimeOnPage = Number(timeSpentOnPageInSeconds);
|
|
return totalTimeOnPage;
|
|
},
|
|
|
|
getTimeOnAllPagesInSeconds: function () {
|
|
var allTimes = [];
|
|
var pageNames = Object.keys(TimeMe.startStopTimes);
|
|
for (var i = 0; i < pageNames.length; i++) {
|
|
var pageName = pageNames[i];
|
|
var timeOnPage = TimeMe.getTimeOnPageInSeconds(pageName);
|
|
allTimes.push({
|
|
"pageName": pageName,
|
|
"timeOnPage": timeOnPage
|
|
});
|
|
}
|
|
return allTimes;
|
|
},
|
|
|
|
setIdleDurationInSeconds: function (duration) {
|
|
var durationFloat = parseFloat(duration);
|
|
if (isNaN(durationFloat) === false) {
|
|
TimeMe.idleTimeoutMs = duration * 1000;
|
|
} else {
|
|
throw {
|
|
name: "InvalidDurationException",
|
|
message: "An invalid duration time (" + duration + ") was provided."
|
|
};
|
|
}
|
|
return this;
|
|
},
|
|
|
|
setCurrentPageName: function (pageName) {
|
|
TimeMe.currentPageName = pageName;
|
|
return this;
|
|
},
|
|
|
|
resetRecordedPageTime: function (pageName) {
|
|
delete TimeMe.startStopTimes[pageName];
|
|
},
|
|
|
|
resetAllRecordedPageTimes: function () {
|
|
var pageNames = Object.keys(TimeMe.startStopTimes);
|
|
for (var i = 0; i < pageNames.length; i++) {
|
|
TimeMe.resetRecordedPageTime(pageNames[i]);
|
|
}
|
|
},
|
|
|
|
resetIdleCountdown: function () {
|
|
if (TimeMe.idle) {
|
|
TimeMe.triggerUserHasReturned();
|
|
}
|
|
TimeMe.idle = false;
|
|
TimeMe.currentIdleTimeMs = 0;
|
|
},
|
|
|
|
callWhenUserLeaves: function (callback, numberOfTimesToInvoke) {
|
|
this.userLeftCallbacks.push({
|
|
callback: callback,
|
|
numberOfTimesToInvoke: numberOfTimesToInvoke
|
|
})
|
|
},
|
|
|
|
callWhenUserReturns: function (callback, numberOfTimesToInvoke) {
|
|
this.userReturnCallbacks.push({
|
|
callback: callback,
|
|
numberOfTimesToInvoke: numberOfTimesToInvoke
|
|
})
|
|
},
|
|
|
|
triggerUserHasReturned: function () {
|
|
if (!TimeMe.active) {
|
|
for (var i = 0; i < this.userReturnCallbacks.length; i++) {
|
|
var userReturnedCallback = this.userReturnCallbacks[i];
|
|
var numberTimes = userReturnedCallback.numberOfTimesToInvoke;
|
|
if (isNaN(numberTimes) || (numberTimes === undefined) || numberTimes > 0) {
|
|
userReturnedCallback.numberOfTimesToInvoke -= 1;
|
|
userReturnedCallback.callback();
|
|
}
|
|
}
|
|
}
|
|
TimeMe.startTimer();
|
|
},
|
|
|
|
triggerUserHasLeftPage: function () {
|
|
if (TimeMe.active) {
|
|
for (var i = 0; i < this.userLeftCallbacks.length; i++) {
|
|
var userHasLeftCallback = this.userLeftCallbacks[i];
|
|
var numberTimes = userHasLeftCallback.numberOfTimesToInvoke;
|
|
if (isNaN(numberTimes) || (numberTimes === undefined) || numberTimes > 0) {
|
|
userHasLeftCallback.numberOfTimesToInvoke -= 1;
|
|
userHasLeftCallback.callback();
|
|
}
|
|
}
|
|
}
|
|
TimeMe.stopAllTimers();
|
|
},
|
|
|
|
callAfterTimeElapsedInSeconds: function (timeInSeconds, callback) {
|
|
TimeMe.timeElapsedCallbacks.push({
|
|
timeInSeconds: timeInSeconds,
|
|
callback: callback,
|
|
pending: true
|
|
});
|
|
},
|
|
|
|
checkState: function () {
|
|
for (var i = 0; i < TimeMe.timeElapsedCallbacks.length; i++) {
|
|
if (TimeMe.timeElapsedCallbacks[i].pending && TimeMe.getTimeOnCurrentPageInSeconds() > TimeMe.timeElapsedCallbacks[i].timeInSeconds) {
|
|
TimeMe.timeElapsedCallbacks[i].callback();
|
|
TimeMe.timeElapsedCallbacks[i].pending = false;
|
|
}
|
|
}
|
|
if (TimeMe.idle === false && TimeMe.currentIdleTimeMs > TimeMe.idleTimeoutMs) {
|
|
TimeMe.idle = true;
|
|
TimeMe.triggerUserHasLeftPage();
|
|
} else {
|
|
TimeMe.currentIdleTimeMs += TimeMe.checkStateRateMs;
|
|
}
|
|
},
|
|
|
|
visibilityChangeEventName: undefined,
|
|
hiddenPropName: undefined,
|
|
|
|
listenForVisibilityEvents: function () {
|
|
|
|
if (typeof document.hidden !== "undefined") {
|
|
TimeMe.hiddenPropName = "hidden";
|
|
TimeMe.visibilityChangeEventName = "visibilitychange";
|
|
} else if (typeof document.mozHidden !== "undefined") {
|
|
TimeMe.hiddenPropName = "mozHidden";
|
|
TimeMe.visibilityChangeEventName = "mozvisibilitychange";
|
|
} else if (typeof document.msHidden !== "undefined") {
|
|
TimeMe.hiddenPropName = "msHidden";
|
|
TimeMe.visibilityChangeEventName = "msvisibilitychange";
|
|
} else if (typeof document.webkitHidden !== "undefined") {
|
|
TimeMe.hiddenPropName = "webkitHidden";
|
|
TimeMe.visibilityChangeEventName = "webkitvisibilitychange";
|
|
}
|
|
|
|
document.addEventListener(TimeMe.visibilityChangeEventName, function () {
|
|
if (document[TimeMe.hiddenPropName]) {
|
|
TimeMe.triggerUserHasLeftPage();
|
|
} else {
|
|
TimeMe.triggerUserHasReturned();
|
|
}
|
|
}, false);
|
|
|
|
window.addEventListener('blur', function () {
|
|
TimeMe.triggerUserHasLeftPage();
|
|
});
|
|
|
|
window.addEventListener('focus', function () {
|
|
TimeMe.triggerUserHasReturned();
|
|
});
|
|
|
|
document.addEventListener("mousemove", function () { TimeMe.resetIdleCountdown(); });
|
|
document.addEventListener("keyup", function () { TimeMe.resetIdleCountdown(); });
|
|
document.addEventListener("touchstart", function () { TimeMe.resetIdleCountdown(); });
|
|
window.addEventListener("scroll", function () { TimeMe.resetIdleCountdown(); });
|
|
|
|
setInterval(function () {
|
|
TimeMe.checkState();
|
|
}, TimeMe.checkStateRateMs);
|
|
},
|
|
|
|
websocket: undefined,
|
|
|
|
websocketHost: undefined,
|
|
|
|
setUpWebsocket: function (websocketOptions) {
|
|
if (window.WebSocket && websocketOptions) {
|
|
var websocketHost = websocketOptions.websocketHost; // "ws://hostname:port"
|
|
try {
|
|
TimeMe.websocket = new WebSocket(websocketHost);
|
|
window.onbeforeunload = function (event) {
|
|
TimeMe.sendCurrentTime(websocketOptions.appId);
|
|
};
|
|
TimeMe.websocket.onopen = function () {
|
|
TimeMe.sendInitWsRequest(websocketOptions.appId);
|
|
}
|
|
TimeMe.websocket.onerror = function (error) {
|
|
if (console) {
|
|
console.log("Error occurred in websocket connection: " + error);
|
|
}
|
|
}
|
|
TimeMe.websocket.onmessage = function (event) {
|
|
if (console) {
|
|
console.log(event.data);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
if (console) {
|
|
console.error("Failed to connect to websocket host. Error:" + error);
|
|
}
|
|
}
|
|
}
|
|
return this;
|
|
},
|
|
|
|
websocketSend: function (data) {
|
|
TimeMe.websocket.send(JSON.stringify(data));
|
|
},
|
|
|
|
sendCurrentTime: function (appId) {
|
|
var timeSpentOnPage = TimeMe.getTimeOnCurrentPageInMilliseconds();
|
|
var data = {
|
|
type: "INSERT_TIME",
|
|
appId: appId,
|
|
timeOnPageMs: timeSpentOnPage,
|
|
pageName: TimeMe.currentPageName
|
|
};
|
|
TimeMe.websocketSend(data);
|
|
},
|
|
sendInitWsRequest: function (appId) {
|
|
var data = {
|
|
type: "INIT",
|
|
appId: appId
|
|
};
|
|
TimeMe.websocketSend(data);
|
|
},
|
|
|
|
initialize: function (options) {
|
|
|
|
var idleTimeoutInSeconds = TimeMe.idleTimeoutMs || 30;
|
|
var currentPageName = TimeMe.currentPageName || "default-page-name";
|
|
var websocketOptions = undefined;
|
|
var initialStartTime = undefined;
|
|
|
|
if (options) {
|
|
idleTimeoutInSeconds = options.idleTimeoutInSeconds || idleTimeoutInSeconds;
|
|
currentPageName = options.currentPageName || currentPageName;
|
|
websocketOptions = options.websocketOptions;
|
|
initialStartTime = options.initialStartTime;
|
|
}
|
|
|
|
TimeMe.setIdleDurationInSeconds(idleTimeoutInSeconds)
|
|
.setCurrentPageName(currentPageName)
|
|
.setUpWebsocket(websocketOptions)
|
|
.listenForVisibilityEvents();
|
|
|
|
// TODO - only do this if page currently visible.
|
|
|
|
TimeMe.startTimer(undefined, initialStartTime);
|
|
}
|
|
};
|
|
return TimeMe;
|
|
});
|
|
}).call(this);
|