Skip to content

Instantly share code, notes, and snippets.

@Mitscherlich
Created February 24, 2022 05:17
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/ab41b4915514005ba2d69354cdb1ab9d to your computer and use it in GitHub Desktop.
Save Mitscherlich/ab41b4915514005ba2d69354cdb1ab9d to your computer and use it in GitHub Desktop.
Simple Countdown Component (Vue 2)
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