Skip to content

Instantly share code, notes, and snippets.

@leastbad
Created July 17, 2021 20:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save leastbad/2c00555d7fa70c4e409ca92de66e08ed to your computer and use it in GitHub Desktop.
Save leastbad/2c00555d7fa70c4e409ca92de66e08ed to your computer and use it in GitHub Desktop.
timer_controller.js WIP
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