Skip to content

Instantly share code, notes, and snippets.

@leastbad
Created July 17, 2021 20:13
Show Gist options
  • Select an option

  • Save leastbad/2c00555d7fa70c4e409ca92de66e08ed to your computer and use it in GitHub Desktop.

Select an option

Save leastbad/2c00555d7fa70c4e409ca92de66e08ed to your computer and use it in GitHub Desktop.

Revisions

  1. leastbad created this gist Jul 17, 2021.
    295 changes: 295 additions & 0 deletions timer_controller.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,295 @@
    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()
    }
    }