Created
July 17, 2021 20:13
-
-
Save leastbad/2c00555d7fa70c4e409ca92de66e08ed to your computer and use it in GitHub Desktop.
Revisions
-
leastbad created this gist
Jul 17, 2021 .There are no files selected for viewing
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 charactersOriginal 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() } }