Skip to content

Instantly share code, notes, and snippets.

@Mitscherlich
Created February 24, 2022 05:15
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 Mitscherlich/653f412edde0a1f000413455b55312b0 to your computer and use it in GitHub Desktop.
Save Mitscherlich/653f412edde0a1f000413455b55312b0 to your computer and use it in GitHub Desktop.
Simple Timer component (Vue 2)
import Vue, { VNode } from 'vue'
import isNumber from 'lodash/isNumber'
import PropTypes from 'vue-types'
const EVENT_VISIBILITY_CHANGE = 'visibilitychange'
const PropTypeNatural = PropTypes.custom((val: number) => isNumber(val) && val >= 0)
export const TimerProps = {
interval: PropTypeNatural.def(1000),
silent: PropTypes.bool.def(false),
autoStart: PropTypes.bool.def(true),
}
export const Timer = Vue.extend({
name: 'Timer',
props: TimerProps,
data() {
const now = new Date()
return {
hours: now.getHours(),
minutes: now.getMinutes(),
seconds: now.getSeconds(),
ticking: false,
timerId: null as unknown as number,
}
},
created() {
if (this.autoStart) {
this.start()
}
document.addEventListener(EVENT_VISIBILITY_CHANGE, this.handleVisibilityChange)
},
beforeDestroy() {
document.removeEventListener(EVENT_VISIBILITY_CHANGE, this.handleVisibilityChange)
this.pause()
},
methods: {
start() {
if (this.ticking) {
return
}
this.ticking = true
if (!this.silent) {
this.$emit('start')
}
if (document.visibilityState === 'visible') {
this.continue()
}
},
stop() {
this.pause()
this.ticking = false
this.$emit('stop')
},
pause() {
if ('requestAnimationFrame' in window) {
cancelAnimationFrame(this.timerId)
} else {
clearTimeout(this.timerId)
}
this.$emit('pause')
},
resume() {
if (this.ticking) {
return
}
this.continue()
this.$emit('resume')
},
continue() {
if (!this.ticking) {
return
}
const now = new Date()
const delay = Math.min(this.interval, this.interval - now.getMilliseconds())
if ('requestAnimationFrame' in window) {
let start: number
const step = (timestamp: number) => {
if (!start) {
start = timestamp
}
if (timestamp - start < delay) {
this.timerId = requestAnimationFrame(step)
} else {
this.progress()
}
}
this.timerId = requestAnimationFrame(step)
} else {
this.timerId = setTimeout(() => {
this.progress()
}, delay, [])
}
},
progress() {
if (!this.ticking) {
return
}
this.update()
if (!this.silent) {
this.$emit('tick', {
hours: this.hours,
minutes: this.minutes,
seconds: this.seconds,
})
}
this.continue()
},
update() {
const now = new Date()
this.hours = now.getHours()
this.minutes = now.getMinutes()
this.seconds = now.getSeconds()
},
handleVisibilityChange() {
switch (document.visibilityState) {
case 'visible':
this.update()
this.continue()
break
case 'hidden':
this.pause()
break
default:
break
}
},
},
render(): VNode {
return this.$scopedSlots.default?.({
hours: this.hours,
minutes: this.minutes,
seconds: this.seconds,
}) as unknown as VNode
},
})
export interface TimerOptions {
interval: number
silent: boolean
autoStart: boolean
}
export default class TimerFactory {
private options: TimerOptions
constructor(options: Partial<TimerOptions> = {}) {
const { interval = 1000, silent = false, autoStart = true } = options
this.options = {
interval,
silent,
autoStart,
}
Object.setPrototypeOf(this, this.create())
}
create(options = this.options) {
return new Timer({
propsData: options,
})
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment