Created
July 17, 2021 20:13
-
-
Save leastbad/2c00555d7fa70c4e409ca92de66e08ed to your computer and use it in GitHub Desktop.
timer_controller.js WIP
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Controller } from 'stimulus' | |
export default class extends Controller { | |
static values = { | |
idleTimeoutMs: 30000, | |
currentIdleTimeMs: Number, | |
checkIdleStateRateMs: 250, | |
isUserCurrentlyOnPage: true, | |
isUserCurrentlyIdle: Boolean, | |
currentPageName: 'default', | |
trackWhenUserLeavesPage: true, | |
trackWhenUserGoesIdle: true | |
} | |
initialize () { | |
this.trackedElements = [] | |
this.timeElapsedCallbacks = [] | |
this.userLeftCallbacks = [] | |
this.userReturnCallbacks = [] | |
this.startStopTimes = {} | |
} | |
connect () { | |
if (this.preview) return | |
this.element.time = this | |
if (this.trackWhenUserLeavesPageValue) this.listenForUserLeavesOrReturns() | |
if (this.trackWhenUserGoesIdleValue) this.listenForIdle() | |
this.startTimer() | |
} | |
disconnect () { | |
if (this.preview) return | |
this.stopAllTimers() | |
this.trackedElements.forEach(element => { | |
element.removeEventListener('mouseover', this.startTimer) | |
element.removeEventListener('mousemove', this.startTimer) | |
element.removeEventListener('mouseleave', this.stopTimer) | |
element.removeEventListener('keypress', this.startTimer) | |
element.removeEventListener('focus', this.startTimer) | |
}) | |
this.trackedElements = [] | |
if (this.trackWhenUserLeavesPageValue) { | |
document.removeEventListener('visibilitychange', this.handleVisibility) | |
window.removeEventListener('blur', this.userLeftOrIdle) | |
window.removeEventListener('focus', this.userReturned) | |
} | |
if (this.trackWhenUserGoesIdleValue) { | |
document.removeEventListener('mousemove', this.userActivityDetected) | |
document.removeEventListener('keyup', this.userActivityDetected) | |
document.removeEventListener('touchstart', this.userActivityDetected) | |
window.removeEventListener('scroll', this.userActivityDetected) | |
clearInterval(this.idleInterval) | |
} | |
this.element.time = undefined | |
} | |
get preview () { | |
return ( | |
document.documentElement.hasAttribute('data-turbolinks-preview') || | |
document.documentElement.hasAttribute('data-turbo-preview') | |
) | |
} | |
trackTimeOnElement = elementId => { | |
const element = | |
elementId instanceof HTMLElement | |
? elementId | |
: document.getElementById(elementId) | |
if (element) { | |
this.trackedElements.push(element) | |
element.addEventListener('mouseover', this.startTimer) | |
element.addEventListener('mousemove', this.startTimer) | |
element.addEventListener('mouseleave', this.stopTimer) | |
element.addEventListener('keypress', this.startTimer) | |
element.addEventListener('focus', this.startTimer) | |
} | |
} | |
getTimeOnElementInSeconds = elementId => { | |
const time = this.getTimeOnPageInSeconds(elementId) | |
return time ? time : 0 | |
} | |
startTimer = (pageName, startTime) => { | |
if (pageName instanceof Event) pageName = pageName.target.id | |
if (!pageName) pageName = this.currentPageNameValue | |
if (this.startStopTimes[pageName] === undefined) { | |
this.startStopTimes[pageName] = [] | |
} else { | |
const arrayOfTimes = this.startStopTimes[pageName] | |
const latestStartStopEntry = arrayOfTimes[arrayOfTimes.length - 1] | |
if ( | |
latestStartStopEntry !== undefined && | |
latestStartStopEntry.stopTime === undefined | |
) | |
return | |
} | |
this.startStopTimes[pageName].push({ | |
startTime: startTime || new Date(), | |
stopTime: undefined | |
}) | |
} | |
stopAllTimers = () => { | |
const pageNames = Object.keys(this.startStopTimes) | |
for (let i = 0; i < pageNames.length; i++) this.stopTimer(pageNames[i]) | |
} | |
stopTimer = (pageName, stopTime) => { | |
if (pageName instanceof Event) pageName = pageName.target.id | |
if (!pageName) pageName = this.currentPageNameValue | |
const arrayOfTimes = this.startStopTimes[pageName] | |
if (arrayOfTimes === undefined || arrayOfTimes.length === 0) return | |
if (arrayOfTimes[arrayOfTimes.length - 1].stopTime === undefined) { | |
arrayOfTimes[arrayOfTimes.length - 1].stopTime = stopTime || new Date() | |
} | |
} | |
getTimeOnCurrentPageInSeconds = () => { | |
return this.getTimeOnPageInSeconds(this.currentPageNameValue) | |
} | |
getTimeOnPageInSeconds = pageName => { | |
const timeInMs = this.getTimeOnPageInMilliseconds(pageName) | |
return timeInMs === undefined ? undefined : timeInMs / 1000 | |
} | |
getTimeOnCurrentPageInMilliseconds = () => { | |
return this.getTimeOnPageInMilliseconds(this.currentPageNameValue) | |
} | |
getTimeOnPageInMilliseconds = pageName => { | |
let totalTimeOnPage = 0 | |
const arrayOfTimes = this.startStopTimes[pageName] | |
if (arrayOfTimes === undefined) return | |
let timeSpentOnPageInSeconds = 0 | |
for (let i = 0; i < arrayOfTimes.length; i++) { | |
const startTime = arrayOfTimes[i].startTime | |
let stopTime = arrayOfTimes[i].stopTime | |
if (stopTime === undefined) stopTime = new Date() | |
const difference = stopTime - startTime | |
timeSpentOnPageInSeconds += difference | |
} | |
totalTimeOnPage = Number(timeSpentOnPageInSeconds) | |
return totalTimeOnPage | |
} | |
getTimeOnAllPagesInSeconds = () => { | |
const allTimes = [] | |
let pageNames = Object.keys(this.startStopTimes) | |
for (let i = 0; i < pageNames.length; i++) { | |
const pageName = pageNames[i] | |
const timeOnPage = this.getTimeOnPageInSeconds(pageName) | |
allTimes.push({ pageName, timeOnPage }) | |
} | |
return allTimes | |
} | |
setIdleDurationInSeconds = duration => { | |
const durationFloat = parseFloat(duration) | |
if (isNaN(durationFloat) === false) { | |
this.idleTimeoutMsValue = duration * 1000 | |
} else { | |
throw { | |
name: 'InvalidDurationException', | |
message: 'An invalid duration time (' + duration + ') was provided.' | |
} | |
} | |
} | |
setCurrentPageName = pageName => { | |
this.currentPageNameValue = pageName | |
} | |
resetRecordedPageTime = pageName => { | |
delete this.startStopTimes[pageName] | |
} | |
resetAllRecordedPageTimes = () => { | |
const pageNames = Object.keys(this.startStopTimes) | |
for (let i = 0; i < pageNames.length; i++) { | |
this.resetRecordedPageTime(pageNames[i]) | |
} | |
} | |
userActivityDetected = () => { | |
if (this.isUserCurrentlyIdleValue) this.userReturned() | |
this.resetIdleCountdown() | |
} | |
resetIdleCountdown = () => { | |
this.isUserCurrentlyIdleValue = false | |
this.currentIdleTimeMsValue = 0 | |
} | |
callWhenUserLeaves = (callback, numberOfTimesToInvoke) => { | |
this.userLeftCallbacks.push({ | |
callback, | |
numberOfTimesToInvoke | |
}) | |
} | |
callWhenUserReturns = (callback, numberOfTimesToInvoke) => { | |
this.userReturnCallbacks.push({ | |
callback, | |
numberOfTimesToInvoke | |
}) | |
} | |
userReturned = () => { | |
if (!this.isUserCurrentlyOnPageValue) { | |
this.isUserCurrentlyOnPageValue = true | |
this.resetIdleCountdown() | |
for (let i = 0; i < this.userReturnCallbacks.length; i++) { | |
const userReturnedCallback = this.userReturnCallbacks[i] | |
const times = userReturnedCallback.numberOfTimesToInvoke | |
if (isNaN(times) || times === undefined || times > 0) { | |
userReturnedCallback.numberOfTimesToInvoke -= 1 | |
userReturnedCallback.callback() | |
} | |
} | |
} | |
this.startTimer() | |
} | |
userLeftOrIdle = () => { | |
if (this.isUserCurrentlyOnPageValue) { | |
this.isUserCurrentlyOnPageValue = false | |
for (let i = 0; i < this.userLeftCallbacks.length; i++) { | |
const userHasLeftCallback = this.userLeftCallbacks[i] | |
const times = userHasLeftCallback.numberOfTimesToInvoke | |
if (isNaN(times) || times === undefined || times > 0) { | |
userHasLeftCallback.numberOfTimesToInvoke -= 1 | |
userHasLeftCallback.callback() | |
} | |
} | |
} | |
this.stopAllTimers() | |
} | |
callAfterTimeElapsedInSeconds = (timeInSeconds, callback) => { | |
this.timeElapsedCallbacks.push({ | |
timeInSeconds, | |
callback, | |
pending: true | |
}) | |
} | |
checkIdleState = () => { | |
for (let i = 0; i < this.timeElapsedCallbacks.length; i++) { | |
if ( | |
this.timeElapsedCallbacks[i].pending && | |
this.getTimeOnCurrentPageInSeconds() > | |
this.timeElapsedCallbacks[i].timeInSeconds | |
) { | |
this.timeElapsedCallbacks[i].callback() | |
this.timeElapsedCallbacks[i].pending = false | |
} | |
} | |
if ( | |
this.isUserCurrentlyIdleValue === false && | |
this.currentIdleTimeMsValue > this.idleTimeoutMsValue | |
) { | |
this.isUserCurrentlyIdleValue = true | |
this.userLeftOrIdle() | |
} else { | |
this.currentIdleTimeMsValue += this.checkIdleStateRateMsValue | |
} | |
} | |
listenForUserLeavesOrReturns = () => { | |
document.addEventListener('visibilitychange', this.handleVisibility) | |
window.addEventListener('blur', this.userLeftOrIdle) | |
window.addEventListener('focus', this.userReturned) | |
} | |
listenForIdle = () => { | |
document.addEventListener('mousemove', this.userActivityDetected) | |
document.addEventListener('keyup', this.userActivityDetected) | |
document.addEventListener('touchstart', this.userActivityDetected) | |
window.addEventListener('scroll', this.userActivityDetected) | |
this.idleInterval = setInterval( | |
this.handleUserIdle, | |
this.checkIdleStateRateMsValue | |
) | |
} | |
handleVisibility = () => { | |
document.hidden ? this.userLeftOrIdle() : this.userReturned() | |
} | |
handleUserIdle = () => { | |
if (this.isUserCurrentlyIdleValue !== true) this.checkIdleState() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment