Last active
March 20, 2019 08:58
-
-
Save xtrasmal/9f56aa278237c2e507cd145071a9c9d5 to your computer and use it in GitHub Desktop.
VueJS Flip countdown flip clock using momentjs
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 characters
<script> | |
import * as moment from 'moment-timezone' | |
export default { | |
props: { | |
date: { | |
required: true, | |
default: moment().format() | |
}, | |
locale: { | |
required: false, | |
default: 'en' | |
} | |
}, | |
data: () => ({ | |
now: null, | |
interval: null, | |
running: true | |
}), | |
mounted() { | |
this.now = moment(); | |
if ( window['requestAnimationFrame'] ) { | |
this.update() | |
} else { | |
this.interval = setInterval(() => { | |
this.update() | |
}, 1000); | |
} | |
}, | |
beforeDestroy () { | |
this.clear(); | |
}, | |
methods: { | |
update() { | |
this.now = moment(); | |
if ( window['requestAnimationFrame'] ) { | |
this.interval = requestAnimationFrame(this.update.bind(this)); | |
} | |
}, | |
clear() { | |
this.running = false | |
if ( window['cancelAnimationFrame'] ) { | |
cancelAnimationFrame(this.interval); | |
} else { | |
clearInterval(this.interval) | |
} | |
}, | |
toArray(number) { | |
let output = []; | |
if(typeof number !== 'string') { | |
number = number.toString(); | |
} | |
for (var i = 0, len = number.length; i < len; i += 1) { | |
output.push(+number.charAt(i)); | |
} | |
return output | |
}, | |
twoDigits(number) { | |
return ( number < 10 && number > -1 ? '0' : '' ) + number; | |
} | |
}, | |
computed: { | |
timeLeft() { | |
let seconds = moment(this.date).locale(this.locale).diff(this.now, 'seconds') | |
if(seconds <= 0) { | |
this.clear() | |
return 0 | |
} | |
return seconds | |
}, | |
time() { | |
return { | |
running: this.running, | |
seconds: this.timeLeft % 60 || 0, | |
minutes: Math.trunc(this.timeLeft / 60) % 60 || 0, | |
hours: Math.trunc(this.timeLeft / 60 / 60) % 24 || 0, | |
days: Math.trunc(this.timeLeft / 60 / 60 / 24 || 0) | |
} | |
} | |
}, | |
render() { | |
return this.$scopedSlots.default({ | |
toArray: number => this.toArray(number), | |
twoDigits: number => this.twoDigits(number), | |
timePart: part => this.time[part], | |
time: this.time | |
}) | |
} | |
} | |
</script> |
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 characters
.flip-clock { | |
text-align: center; | |
-webkit-perspective: 600px; | |
perspective: 600px; | |
margin: 0 auto; | |
} | |
.flip-clock *, | |
.flip-clock *:before, | |
.flip-clock *:after { | |
box-sizing: border-box; | |
} | |
.flip-clock__piece { | |
display: inline-block; | |
margin: 0 0.2vw; | |
} | |
@media (min-width: 1000px) { | |
.flip-clock__piece { | |
margin: 0 5px; | |
} | |
} | |
.flip-clock__slot { | |
font-size: 1rem; | |
line-height: 1.5; | |
display: block; | |
} | |
.flip-card { | |
display: block; | |
position: relative; | |
padding-bottom: 0.72em; | |
font-size: 2.25rem; | |
line-height: 0.95; | |
} | |
@media (min-width: 1000px) { | |
.flip-clock__slot { | |
font-size: 1.2rem; | |
} | |
.flip-card { | |
font-size: 3rem; | |
} | |
} | |
.flip-card__top, | |
.flip-card__bottom, | |
.flip-card__back-bottom, | |
.flip-card__back::before, | |
.flip-card__back::after { | |
display: block; | |
height: 0.72em; | |
color: #ccc; | |
background: #222; | |
padding: 0.23em 0.25em 0.4em; | |
border-radius: 0.15em 0.15em 0 0; | |
backface-visibility: hidden; | |
-webkit-transform-style: preserve-3d; | |
transform-style: preserve-3d; | |
width: 1.8em; | |
} | |
.flip-card__bottom, | |
.flip-card__back-bottom { | |
color: #FFF; | |
position: absolute; | |
top: 50%; | |
left: 0; | |
border-top: solid 1px #000; | |
background: #393939; | |
border-radius: 0 0 0.15em 0.15em; | |
pointer-events: none; | |
overflow: hidden; | |
z-index: 2; | |
} | |
.flip-card__back-bottom { | |
z-index: 1; | |
} | |
.flip-card__bottom::after, | |
.flip-card__back-bottom::after { | |
display: block; | |
margin-top: -0.72em; | |
} | |
.flip-card__back::before, | |
.flip-card__bottom::after, | |
.flip-card__back-bottom::after { | |
content: attr(data-value); | |
} | |
.flip-card__back { | |
position: absolute; | |
top: 0; | |
height: 100%; | |
left: 0%; | |
pointer-events: none; | |
} | |
.flip-card__back::before { | |
position: relative; | |
overflow: hidden; | |
z-index: -1; | |
} | |
.flip .flip-card__back::before { | |
z-index: 1; | |
-webkit-animation: flipTop 0.3s cubic-bezier(0.37, 0.01, 0.94, 0.35); | |
animation: flipTop 0.3s cubic-bezier(0.37, 0.01, 0.94, 0.35); | |
-webkit-animation-fill-mode: both; | |
animation-fill-mode: both; | |
-webkit-transform-origin: center bottom; | |
transform-origin: center bottom; | |
} | |
.flip .flip-card__bottom { | |
-webkit-transform-origin: center top; | |
transform-origin: center top; | |
-webkit-animation-fill-mode: both; | |
animation-fill-mode: both; | |
-webkit-animation: flipBottom 0.6s cubic-bezier(0.15, 0.45, 0.28, 1); | |
animation: flipBottom 0.6s cubic-bezier(0.15, 0.45, 0.28, 1); | |
} | |
@-webkit-keyframes flipTop { | |
0% { | |
-webkit-transform: rotateX(0deg); | |
transform: rotateX(0deg); | |
z-index: 2; | |
} | |
0%, | |
99% { | |
opacity: 1; | |
} | |
100% { | |
-webkit-transform: rotateX(-90deg); | |
transform: rotateX(-90deg); | |
opacity: 0; | |
} | |
} | |
@keyframes flipTop { | |
0% { | |
-webkit-transform: rotateX(0deg); | |
transform: rotateX(0deg); | |
z-index: 2; | |
} | |
0%, | |
99% { | |
opacity: 1; | |
} | |
100% { | |
-webkit-transform: rotateX(-90deg); | |
transform: rotateX(-90deg); | |
opacity: 0; | |
} | |
} | |
@-webkit-keyframes flipBottom { | |
0%, | |
50% { | |
z-index: -1; | |
-webkit-transform: rotateX(90deg); | |
transform: rotateX(90deg); | |
opacity: 0; | |
} | |
51% { | |
opacity: 1; | |
} | |
100% { | |
opacity: 1; | |
-webkit-transform: rotateX(0deg); | |
transform: rotateX(0deg); | |
z-index: 5; | |
} | |
} | |
@keyframes flipBottom { | |
0%, | |
50% { | |
z-index: -1; | |
-webkit-transform: rotateX(90deg); | |
transform: rotateX(90deg); | |
opacity: 0; | |
} | |
51% { | |
opacity: 1; | |
} | |
100% { | |
opacity: 1; | |
-webkit-transform: rotateX(0deg); | |
transform: rotateX(0deg); | |
z-index: 5; | |
} | |
} |
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 characters
<template> | |
<countdown class="flip-clock" :date="date" :locale="locale" @completed="completed"> | |
<div slot-scope="{time}"> | |
<FlipCountdownPart v-for="(part, index) in parts" :key="index" :part="part" :time="time" ref="parts"></FlipCountdownPart> | |
</div> | |
</countdown> | |
</template> | |
<script> | |
import Countdown from '~/components/utils/countdown/Countdown' | |
import FlipCountdownPart from '~/components/utils/countdown/FlipCountdownPart' | |
export default { | |
props: { | |
parts: { | |
required: true | |
}, | |
date: { | |
required: true, | |
}, | |
locale: { | |
required: false, | |
default: 'en' | |
} | |
}, | |
methods: { | |
completed(status) { | |
this.$emit('completed', status) | |
} | |
}, | |
components: { | |
Countdown, FlipCountdownPart | |
} | |
} | |
</script> |
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 characters
<template> | |
<span v-show="show" ref="flipclock" class="flip-clock__piece"> | |
<span class="flip-clock__card flip-card"> | |
<b class="flip-card__top">{{twoDigits(current)}}</b> | |
<b class="flip-card__bottom" :data-value="twoDigits(current)"></b> | |
<b class="flip-card__back" :data-value="twoDigits(previous)"></b> | |
<b class="flip-card__back-bottom" :data-value="twoDigits(previous)"></b> | |
</span> | |
<span class="flip-clock__slot">{{part}}</span> | |
</span> | |
</template> | |
<script> | |
export default { | |
props: ['time', 'part'], | |
data: () => ({ | |
current: 0, | |
previous: 0, | |
show: true | |
}), | |
methods: { | |
twoDigits(value) { | |
return ( value < 10 && value > -1 ? '0' : '' ) + value; | |
} | |
}, | |
watch: { | |
time(newValue) { | |
if ( newValue[this.part] === undefined ) { | |
this.show = false; | |
return; | |
} | |
let val = newValue[this.part]; | |
this.show = true; | |
val = ( val < 0 ? 0 : val ); | |
if ( val !== this.current ) { | |
this.previous = this.current; | |
this.current = val; | |
this.$refs.flipclock.classList.remove('flip'); | |
void this.$refs.flipclock.offsetWidth; | |
this.$refs.flipclock.classList.add('flip'); | |
} | |
} | |
} | |
} | |
</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment