Created
February 24, 2022 05:17
-
-
Save Mitscherlich/ab41b4915514005ba2d69354cdb1ab9d to your computer and use it in GitHub Desktop.
Simple Countdown Component (Vue 2)
This file contains 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 isNumber from 'lodash/isNumber'; | |
import PropTypes from 'vue-types'; | |
const MILLISECONDS_SECOND = 1000; | |
const MILLISECONDS_MINUTE = 60 * MILLISECONDS_SECOND; | |
const MILLISECONDS_HOUR = 60 * MILLISECONDS_MINUTE; | |
const MILLISECONDS_DAY = 24 * MILLISECONDS_HOUR; | |
const EVENT_VISIBILITY_CHANGE = 'visibilitychange'; | |
const PropTypeNatural = PropTypes.custom(val => isNumber(val) && val >= 0); | |
export const CountdownProps = { | |
autoStart: PropTypes.bool.def(true), | |
emitEvents: PropTypes.bool.def(true), | |
interval: PropTypeNatural, | |
now: PropTypes.func.def(() => Date.now()), | |
tag: PropTypes.string.def('span'), | |
time: PropTypeNatural, | |
transform: PropTypes.func.def(props => props), | |
}; | |
const Countdown = { | |
name: 'Countdown', | |
props: CountdownProps, | |
data() { | |
return { | |
counting: false, | |
endTime: 0, | |
totalMilliseconds: 0, | |
}; | |
}, | |
computed: { | |
days() { | |
return Math.floor(this.totalMilliseconds / MILLISECONDS_DAY); | |
}, | |
hours() { | |
return Math.floor((this.totalMilliseconds % MILLISECONDS_DAY) / MILLISECONDS_HOUR); | |
}, | |
minutes() { | |
return Math.floor((this.totalMilliseconds % MILLISECONDS_HOUR) / MILLISECONDS_MINUTE); | |
}, | |
seconds() { | |
return Math.floor((this.totalMilliseconds % MILLISECONDS_MINUTE) / MILLISECONDS_SECOND); | |
}, | |
milliseconds() { | |
return Math.floor(this.totalMilliseconds % MILLISECONDS_SECOND); | |
}, | |
totalDays() { | |
return this.days; | |
}, | |
totalHours() { | |
return Math.floor(this.totalMilliseconds / MILLISECONDS_HOUR); | |
}, | |
totalMinutes() { | |
return Math.floor(this.totalMilliseconds / MILLISECONDS_MINUTE); | |
}, | |
totalSeconds() { | |
return Math.floor(this.totalMilliseconds / MILLISECONDS_SECOND); | |
}, | |
}, | |
watch: { | |
$props: { | |
deep: true, | |
immediate: true, | |
handler() { | |
this.totalMilliseconds = this.time; | |
this.endTime = this.now() + this.time; | |
if (this.autoStart) { | |
this.start(); | |
} | |
}, | |
}, | |
}, | |
mounted() { | |
document.addEventListener(EVENT_VISIBILITY_CHANGE, this.handleVisibilityChange); | |
}, | |
beforeDestroy() { | |
document.removeEventListener(EVENT_VISIBILITY_CHANGE, this.handleVisibilityChange); | |
this.pause(); | |
}, | |
methods: { | |
start() { | |
if (this.counting) { | |
return; | |
} | |
this.counting = true; | |
if (this.emitEvents) { | |
this.$emit('start'); | |
} | |
if (document.visibilityState === 'visible') { | |
this.continue(); | |
} | |
}, | |
continue() { | |
if (!this.counting) { | |
return; | |
} | |
const delay = Math.min(this.totalMilliseconds, this.interval); | |
if (delay > 0) { | |
if (window.requestAnimationFrame) { | |
let start; | |
const step = timestamp => { | |
if (!start) { | |
start = timestamp; | |
} | |
if (timestamp - start < delay) { | |
this.requestId = requestAnimationFrame(step); | |
} else { | |
this.progress(); | |
} | |
}; | |
this.requestId = requestAnimationFrame(step); | |
} else { | |
this.timeoutId = setTimeout(() => { | |
this.progress(); | |
}, delay); | |
} | |
} else { | |
this.end(); | |
} | |
}, | |
pause() { | |
if (window.requestAnimationFrame) { | |
cancelAnimationFrame(this.requestId); | |
} else { | |
clearTimeout(this.timeoutId); | |
} | |
}, | |
progress() { | |
if (!this.counting) { | |
return; | |
} | |
this.totalMilliseconds -= this.interval; | |
if (this.emitEvents && this.totalMilliseconds > 0) { | |
this.$emit('progress', { | |
days: this.days, | |
hours: this.hours, | |
minutes: this.minutes, | |
seconds: this.seconds, | |
milliseconds: this.milliseconds, | |
totalDays: this.totalDays, | |
totalHours: this.totalHours, | |
totalMinutes: this.totalMinutes, | |
totalSeconds: this.totalSeconds, | |
totalMilliseconds: this.totalMilliseconds, | |
}); | |
} | |
this.continue(); | |
}, | |
abort() { | |
if (!this.counting) { | |
return; | |
} | |
this.pause(); | |
this.counting = false; | |
if (this.emitEvents) { | |
this.$emit('abort'); | |
} | |
}, | |
end() { | |
if (!this.counting) { | |
return; | |
} | |
this.pause(); | |
this.totalMilliseconds = 0; | |
this.counting = false; | |
if (this.emitEvents) { | |
this.$emit('end'); | |
} | |
}, | |
update() { | |
if (this.counting) { | |
this.totalMilliseconds = Math.max(0, this.endTime - this.now()); | |
} | |
}, | |
handleVisibilityChange() { | |
switch (document.visibilityState) { | |
case 'visible': | |
this.update(); | |
this.continue(); | |
break; | |
case 'hidden': | |
this.pause(); | |
break; | |
default: | |
} | |
}, | |
}, | |
render(h) { | |
return h( | |
this.tag, | |
this.$scopedSlots.default | |
? [ | |
this.$scopedSlots.default( | |
this.transform({ | |
days: this.days, | |
hours: this.hours, | |
minutes: this.minutes, | |
seconds: this.seconds, | |
milliseconds: this.milliseconds, | |
totalDays: this.totalDays, | |
totalHours: this.totalHours, | |
totalMinutes: this.totalMinutes, | |
totalSeconds: this.totalSeconds, | |
totalMilliseconds: this.totalMilliseconds, | |
}) | |
), | |
] | |
: this.$slots.default | |
); | |
}, | |
}; | |
export default Countdown; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment