├── .babelrc ├── .gitignore ├── LICENSE ├── README.md ├── dist ├── flipdown.css ├── flipdown.js ├── flipdown.min.css └── flipdown.min.js ├── example ├── css │ ├── flipdown │ │ └── flipdown.css │ └── style.css ├── index.html └── js │ ├── flipdown │ └── flipdown.js │ └── main.js ├── package.json └── src ├── flipdown.css └── flipdown.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Peter Butcher 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # FlipDown 4 | 5 | ⏰ A lightweight and performant flip styled countdown clock. 6 | 7 | ![NPM Version](https://img.shields.io/npm/v/flipdown?style=flat-square) 8 | ![NPM Downloads](https://img.shields.io/npm/dt/flipdown?style=flat-square) 9 | 10 | ## Features 11 | 12 | - 💡 Lightweight - No jQuery! <11KB minified bundle 13 | - ⚡ Performant - Animations powered by CSS transitions 14 | - 📱 Responsive - Works great on screens of all sizes 15 | - 🎨 Themeable - Choose from built-in themes, or add your own 16 | - 🌍 i18n - Customisable headings for your language 17 | 18 | ## Example 19 | 20 | Example live at: https://pbutcher.uk/flipdown/ 21 | 22 | Remix FlipDown on CodePen: https://codepen.io/PButcher/pen/dzvMzZ 23 | 24 | ## Basic Usage 25 | 26 | To get started, either clone this repo or install with `npm install flipdown` or `yarn add flipdown`. 27 | 28 | For basic usage, FlipDown takes a unix timestamp (in seconds) as an argument. 29 | 30 | ```javascript 31 | new FlipDown(1538137672).start(); 32 | ``` 33 | 34 | Include the [CSS and JS](https://github.com/PButcher/flipdown/tree/master/dist) in `` and include the following line in your HTML. 35 | 36 | ```html 37 |
38 | ``` 39 | 40 | See a full example [here](https://github.com/PButcher/flipdown/tree/master/example). 41 | 42 | ## Multiple Instances 43 | 44 | To use multiple instances of FlipDown on the same page, specify a DOM element ID as the second argument in FlipDown's constructor: 45 | 46 | ```javascript 47 | new FlipDown(1588017373, "registerBy").start(); 48 | new FlipDown(1593561600, "eventStart").start(); 49 | ``` 50 | 51 | ```html 52 |
53 |
54 | ``` 55 | 56 | ## Themes 57 | 58 | FlipDown comes with 2 themes as standard: 59 | 60 | - dark [default] 61 | - light 62 | 63 | To change the theme, you can supply the `theme` property in the `opt` object in the constructor with the theme name as a string: 64 | 65 | ```javascript 66 | { 67 | theme: "light"; 68 | } 69 | ``` 70 | 71 | For example, to instantiate FlipDown using the light theme instead: 72 | 73 | ```javascript 74 | new FlipDown(1538137672, { 75 | theme: "light", 76 | }).start(); 77 | ``` 78 | 79 | ### Custom Themes 80 | 81 | Custom themes can be added by adding a new stylesheet using the FlipDown [theme template](https://github.com/PButcher/flipdown/blob/master/src/flipdown.css#L3-L34). 82 | 83 | FlipDown themes must have the class name prefix of: `.flipdown__theme-` followed by the name of your theme. For example, the standard theme class names are: 84 | 85 | - `.flipdown__theme-dark` 86 | - `.flipdown__theme-light` 87 | 88 | You can then load your theme by specifying the `theme` property in the `opt` object of the constructor (see [Themes](#Themes)). 89 | 90 | ## Headings 91 | 92 | You can add your own rotor group headings by passing an array as part of the `opt` object. Bear in mind this won't change the functionality of the rotors (eg: the 'days' rotor won't magically start counting months because you passed it 'Months' as a heading). 93 | 94 | Suggested use is for i18n. Usage as follows: 95 | 96 | ```javascript 97 | new FlipDown(1538137672, { 98 | headings: ["Nap", "Óra", "Perc", "Másodperc"], 99 | }).start(); 100 | ``` 101 | 102 | Note that headings will default to English if not provided: `["Days", "Hours", "Minutes", "Seconds"]` 103 | 104 | ## API 105 | 106 | ### `FlipDown.prototype.constructor(uts, [el], [opts])` 107 | 108 | Create a new FlipDown instance. 109 | 110 | #### Parameters 111 | 112 | ##### `uts` 113 | 114 | Type: _number_ 115 | 116 | The unix timestamp to count down to (in seconds). 117 | 118 | ##### `[el]` 119 | 120 | **Optional** 121 | Type: _string_ (default: `flipdown`) 122 | 123 | The DOM element ID to attach this FlipDown instance to. Defaults to `flipdown`. 124 | 125 | ##### `[opts]` 126 | 127 | **Optional** 128 | Type: _object_ (default: `{}`) 129 | 130 | Optionally specify additional configuration settings. Currently supported settings include: 131 | 132 | - [`theme`](#Themes) 133 | - [`headings`](#Headings) 134 | 135 | ### `FlipDown.prototype.start()` 136 | 137 | Start the countdown. 138 | 139 | ### `FlipDown.prototype.ifEnded(callback)` 140 | 141 | Call a function once the countdown has ended. 142 | 143 | #### Parameters 144 | 145 | ##### `callback` 146 | 147 | Type: _function_ 148 | 149 | Function to execute once the countdown has ended. 150 | 151 | #### Example 152 | 153 | ```javascript 154 | var flipdown = new FlipDown(1538137672) 155 | 156 | // Start the countdown 157 | .start() 158 | 159 | // Do something when the countdown ends 160 | .ifEnded(() => { 161 | console.log("The countdown has ended!"); 162 | }); 163 | ``` 164 | 165 | ## Acknowledgements 166 | 167 | Thanks to the following people for their suggestions/fixes: 168 | 169 | - [@chuckbergeron](https://github.com/chuckbergeron) for his help with making FlipDown responsive. 170 | - [@vasiliki-b](https://github.com/vasiliki-b) for spotting and fixing the Safari backface-visibility issue. 171 | - [@joeinnes](https://github.com/joeinnes) for adding i18n to rotor group headings. 172 | -------------------------------------------------------------------------------- /dist/flipdown.css: -------------------------------------------------------------------------------- 1 | /* THEMES */ 2 | 3 | /********** Theme: dark **********/ 4 | /* Font styles */ 5 | .flipdown.flipdown__theme-dark { 6 | font-family: sans-serif; 7 | font-weight: bold; 8 | } 9 | /* Rotor group headings */ 10 | .flipdown.flipdown__theme-dark .rotor-group-heading:before { 11 | color: #000000; 12 | } 13 | /* Delimeters */ 14 | .flipdown.flipdown__theme-dark .rotor-group:nth-child(n+2):nth-child(-n+3):before, 15 | .flipdown.flipdown__theme-dark .rotor-group:nth-child(n+2):nth-child(-n+3):after { 16 | background-color: #151515; 17 | } 18 | /* Rotor tops */ 19 | .flipdown.flipdown__theme-dark .rotor, 20 | .flipdown.flipdown__theme-dark .rotor-top, 21 | .flipdown.flipdown__theme-dark .rotor-leaf-front { 22 | color: #FFFFFF; 23 | background-color: #151515; 24 | } 25 | /* Rotor bottoms */ 26 | .flipdown.flipdown__theme-dark .rotor-bottom, 27 | .flipdown.flipdown__theme-dark .rotor-leaf-rear { 28 | color: #EFEFEF; 29 | background-color: #202020; 30 | } 31 | /* Hinge */ 32 | .flipdown.flipdown__theme-dark .rotor:after { 33 | border-top: solid 1px #151515; 34 | } 35 | 36 | /********** Theme: light **********/ 37 | /* Font styles */ 38 | .flipdown.flipdown__theme-light { 39 | font-family: sans-serif; 40 | font-weight: bold; 41 | } 42 | /* Rotor group headings */ 43 | .flipdown.flipdown__theme-light .rotor-group-heading:before { 44 | color: #EEEEEE; 45 | } 46 | /* Delimeters */ 47 | .flipdown.flipdown__theme-light .rotor-group:nth-child(n+2):nth-child(-n+3):before, 48 | .flipdown.flipdown__theme-light .rotor-group:nth-child(n+2):nth-child(-n+3):after { 49 | background-color: #DDDDDD; 50 | } 51 | /* Rotor tops */ 52 | .flipdown.flipdown__theme-light .rotor, 53 | .flipdown.flipdown__theme-light .rotor-top, 54 | .flipdown.flipdown__theme-light .rotor-leaf-front { 55 | color: #222222; 56 | background-color: #DDDDDD; 57 | } 58 | /* Rotor bottoms */ 59 | .flipdown.flipdown__theme-light .rotor-bottom, 60 | .flipdown.flipdown__theme-light .rotor-leaf-rear { 61 | color: #333333; 62 | background-color: #EEEEEE; 63 | } 64 | /* Hinge */ 65 | .flipdown.flipdown__theme-light .rotor:after { 66 | border-top: solid 1px #222222; 67 | } 68 | 69 | /* END OF THEMES */ 70 | 71 | .flipdown { 72 | overflow: visible; 73 | width: 510px; 74 | height: 110px; 75 | } 76 | 77 | .flipdown .rotor-group { 78 | position: relative; 79 | float: left; 80 | padding-right: 30px; 81 | } 82 | 83 | .flipdown .rotor-group:last-child { 84 | padding-right: 0; 85 | } 86 | 87 | .flipdown .rotor-group-heading:before { 88 | display: block; 89 | height: 30px; 90 | line-height: 30px; 91 | text-align: center; 92 | } 93 | 94 | .flipdown .rotor-group:nth-child(1) .rotor-group-heading:before { 95 | content: attr(data-before); 96 | } 97 | 98 | .flipdown .rotor-group:nth-child(2) .rotor-group-heading:before { 99 | content: attr(data-before); 100 | } 101 | 102 | .flipdown .rotor-group:nth-child(3) .rotor-group-heading:before { 103 | content: attr(data-before); 104 | } 105 | 106 | .flipdown .rotor-group:nth-child(4) .rotor-group-heading:before { 107 | content: attr(data-before); 108 | } 109 | 110 | .flipdown .rotor-group:nth-child(n+2):nth-child(-n+3):before { 111 | content: ''; 112 | position: absolute; 113 | bottom: 20px; 114 | left: 115px; 115 | width: 10px; 116 | height: 10px; 117 | border-radius: 50%; 118 | } 119 | 120 | .flipdown .rotor-group:nth-child(n+2):nth-child(-n+3):after { 121 | content: ''; 122 | position: absolute; 123 | bottom: 50px; 124 | left: 115px; 125 | width: 10px; 126 | height: 10px; 127 | border-radius: 50%; 128 | } 129 | 130 | .flipdown .rotor { 131 | position: relative; 132 | float: left; 133 | width: 50px; 134 | height: 80px; 135 | margin: 0px 5px 0px 0px; 136 | border-radius: 4px; 137 | font-size: 4rem; 138 | text-align: center; 139 | perspective: 200px; 140 | } 141 | 142 | .flipdown .rotor:last-child { 143 | margin-right: 0; 144 | } 145 | 146 | .flipdown .rotor-top, 147 | .flipdown .rotor-bottom { 148 | overflow: hidden; 149 | position: absolute; 150 | width: 50px; 151 | height: 40px; 152 | } 153 | 154 | .flipdown .rotor-leaf { 155 | z-index: 1; 156 | position: absolute; 157 | width: 50px; 158 | height: 80px; 159 | transform-style: preserve-3d; 160 | transition: transform 0s; 161 | } 162 | 163 | .flipdown .rotor-leaf.flipped { 164 | transform: rotateX(-180deg); 165 | transition: all 0.5s ease-in-out; 166 | } 167 | 168 | .flipdown .rotor-leaf-front, 169 | .flipdown .rotor-leaf-rear { 170 | overflow: hidden; 171 | position: absolute; 172 | width: 50px; 173 | height: 40px; 174 | margin: 0; 175 | transform: rotateX(0deg); 176 | backface-visibility: hidden; 177 | -webkit-backface-visibility: hidden; 178 | } 179 | 180 | .flipdown .rotor-leaf-front { 181 | line-height: 80px; 182 | border-radius: 4px 4px 0px 0px; 183 | } 184 | 185 | .flipdown .rotor-leaf-rear { 186 | line-height: 0px; 187 | border-radius: 0px 0px 4px 4px; 188 | transform: rotateX(-180deg); 189 | } 190 | 191 | .flipdown .rotor-top { 192 | line-height: 80px; 193 | border-radius: 4px 4px 0px 0px; 194 | } 195 | 196 | .flipdown .rotor-bottom { 197 | bottom: 0; 198 | line-height: 0px; 199 | border-radius: 0px 0px 4px 4px; 200 | } 201 | 202 | .flipdown .rotor:after { 203 | content: ''; 204 | z-index: 2; 205 | position: absolute; 206 | bottom: 0px; 207 | left: 0px; 208 | width: 50px; 209 | height: 40px; 210 | border-radius: 0px 0px 4px 4px; 211 | } 212 | 213 | @media (max-width: 550px) { 214 | 215 | .flipdown { 216 | width: 312px; 217 | height: 70px; 218 | } 219 | 220 | .flipdown .rotor { 221 | font-size: 2.2rem; 222 | margin-right: 3px; 223 | } 224 | 225 | .flipdown .rotor, 226 | .flipdown .rotor-leaf, 227 | .flipdown .rotor-leaf-front, 228 | .flipdown .rotor-leaf-rear, 229 | .flipdown .rotor-top, 230 | .flipdown .rotor-bottom, 231 | .flipdown .rotor:after { 232 | width: 30px; 233 | } 234 | 235 | .flipdown .rotor-group { 236 | padding-right: 20px; 237 | } 238 | 239 | .flipdown .rotor-group:last-child { 240 | padding-right: 0px; 241 | } 242 | 243 | .flipdown .rotor-group-heading:before { 244 | font-size: 0.8rem; 245 | height: 20px; 246 | line-height: 20px; 247 | } 248 | 249 | .flipdown .rotor-group:nth-child(n+2):nth-child(-n+3):before, 250 | .flipdown .rotor-group:nth-child(n+2):nth-child(-n+3):after { 251 | left: 69px; 252 | } 253 | 254 | .flipdown .rotor-group:nth-child(n+2):nth-child(-n+3):before { 255 | bottom: 13px; 256 | height: 8px; 257 | width: 8px; 258 | } 259 | 260 | .flipdown .rotor-group:nth-child(n+2):nth-child(-n+3):after { 261 | bottom: 29px; 262 | height: 8px; 263 | width: 8px; 264 | } 265 | 266 | .flipdown .rotor-leaf-front, 267 | .flipdown .rotor-top { 268 | line-height: 50px; 269 | } 270 | 271 | .flipdown .rotor-leaf, 272 | .flipdown .rotor { 273 | height: 50px; 274 | } 275 | 276 | .flipdown .rotor-leaf-front, 277 | .flipdown .rotor-leaf-rear, 278 | .flipdown .rotor-top, 279 | .flipdown .rotor-bottom, 280 | .flipdown .rotor:after { 281 | height: 25px; 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /dist/flipdown.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 4 | 5 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 6 | 7 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } 8 | 9 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } 10 | 11 | var FlipDown = function () { 12 | function FlipDown(uts) { 13 | var el = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "flipdown"; 14 | var opt = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; 15 | 16 | _classCallCheck(this, FlipDown); 17 | 18 | if (typeof uts !== "number") { 19 | throw new Error("FlipDown: Constructor expected unix timestamp, got ".concat(_typeof(uts), " instead.")); 20 | } 21 | 22 | if (_typeof(el) === "object") { 23 | opt = el; 24 | el = "flipdown"; 25 | } 26 | 27 | this.version = "0.3.2"; 28 | this.initialised = false; 29 | this.now = this._getTime(); 30 | this.epoch = uts; 31 | this.countdownEnded = false; 32 | this.hasEndedCallback = null; 33 | this.element = document.getElementById(el); 34 | this.rotors = []; 35 | this.rotorLeafFront = []; 36 | this.rotorLeafRear = []; 37 | this.rotorTops = []; 38 | this.rotorBottoms = []; 39 | this.countdown = null; 40 | this.daysRemaining = 0; 41 | this.clockValues = {}; 42 | this.clockStrings = {}; 43 | this.clockValuesAsString = []; 44 | this.prevClockValuesAsString = []; 45 | this.opts = this._parseOptions(opt); 46 | 47 | this._setOptions(); 48 | 49 | console.log("FlipDown ".concat(this.version, " (Theme: ").concat(this.opts.theme, ")")); 50 | } 51 | 52 | _createClass(FlipDown, [{ 53 | key: "start", 54 | value: function start() { 55 | if (!this.initialised) this._init(); 56 | this.countdown = setInterval(this._tick.bind(this), 1000); 57 | return this; 58 | } 59 | }, { 60 | key: "ifEnded", 61 | value: function ifEnded(cb) { 62 | this.hasEndedCallback = function () { 63 | cb(); 64 | this.hasEndedCallback = null; 65 | }; 66 | 67 | return this; 68 | } 69 | }, { 70 | key: "_getTime", 71 | value: function _getTime() { 72 | return new Date().getTime() / 1000; 73 | } 74 | }, { 75 | key: "_hasCountdownEnded", 76 | value: function _hasCountdownEnded() { 77 | if (this.epoch - this.now < 0) { 78 | this.countdownEnded = true; 79 | 80 | if (this.hasEndedCallback != null) { 81 | this.hasEndedCallback(); 82 | this.hasEndedCallback = null; 83 | } 84 | 85 | return true; 86 | } else { 87 | this.countdownEnded = false; 88 | return false; 89 | } 90 | } 91 | }, { 92 | key: "_parseOptions", 93 | value: function _parseOptions(opt) { 94 | var headings = ["Days", "Hours", "Minutes", "Seconds"]; 95 | 96 | if (opt.headings && opt.headings.length === 4) { 97 | headings = opt.headings; 98 | } 99 | 100 | return { 101 | theme: opt.hasOwnProperty("theme") ? opt.theme : "dark", 102 | headings: headings 103 | }; 104 | } 105 | }, { 106 | key: "_setOptions", 107 | value: function _setOptions() { 108 | this.element.classList.add("flipdown__theme-".concat(this.opts.theme)); 109 | } 110 | }, { 111 | key: "_init", 112 | value: function _init() { 113 | this.initialised = true; 114 | 115 | if (this._hasCountdownEnded()) { 116 | this.daysremaining = 0; 117 | } else { 118 | this.daysremaining = Math.floor((this.epoch - this.now) / 86400).toString().length; 119 | } 120 | 121 | var dayRotorCount = this.daysremaining <= 2 ? 2 : this.daysremaining; 122 | 123 | for (var i = 0; i < dayRotorCount + 6; i++) { 124 | this.rotors.push(this._createRotor(0)); 125 | } 126 | 127 | var dayRotors = []; 128 | 129 | for (var i = 0; i < dayRotorCount; i++) { 130 | dayRotors.push(this.rotors[i]); 131 | } 132 | 133 | this.element.appendChild(this._createRotorGroup(dayRotors, 0)); 134 | var count = dayRotorCount; 135 | 136 | for (var i = 0; i < 3; i++) { 137 | var otherRotors = []; 138 | 139 | for (var j = 0; j < 2; j++) { 140 | otherRotors.push(this.rotors[count]); 141 | count++; 142 | } 143 | 144 | this.element.appendChild(this._createRotorGroup(otherRotors, i + 1)); 145 | } 146 | 147 | this.rotorLeafFront = Array.prototype.slice.call(this.element.getElementsByClassName("rotor-leaf-front")); 148 | this.rotorLeafRear = Array.prototype.slice.call(this.element.getElementsByClassName("rotor-leaf-rear")); 149 | this.rotorTop = Array.prototype.slice.call(this.element.getElementsByClassName("rotor-top")); 150 | this.rotorBottom = Array.prototype.slice.call(this.element.getElementsByClassName("rotor-bottom")); 151 | 152 | this._tick(); 153 | 154 | this._updateClockValues(true); 155 | 156 | return this; 157 | } 158 | }, { 159 | key: "_createRotorGroup", 160 | value: function _createRotorGroup(rotors, rotorIndex) { 161 | var rotorGroup = document.createElement("div"); 162 | rotorGroup.className = "rotor-group"; 163 | var dayRotorGroupHeading = document.createElement("div"); 164 | dayRotorGroupHeading.className = "rotor-group-heading"; 165 | dayRotorGroupHeading.setAttribute("data-before", this.opts.headings[rotorIndex]); 166 | rotorGroup.appendChild(dayRotorGroupHeading); 167 | appendChildren(rotorGroup, rotors); 168 | return rotorGroup; 169 | } 170 | }, { 171 | key: "_createRotor", 172 | value: function _createRotor() { 173 | var v = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; 174 | var rotor = document.createElement("div"); 175 | var rotorLeaf = document.createElement("div"); 176 | var rotorLeafRear = document.createElement("figure"); 177 | var rotorLeafFront = document.createElement("figure"); 178 | var rotorTop = document.createElement("div"); 179 | var rotorBottom = document.createElement("div"); 180 | rotor.className = "rotor"; 181 | rotorLeaf.className = "rotor-leaf"; 182 | rotorLeafRear.className = "rotor-leaf-rear"; 183 | rotorLeafFront.className = "rotor-leaf-front"; 184 | rotorTop.className = "rotor-top"; 185 | rotorBottom.className = "rotor-bottom"; 186 | rotorLeafRear.textContent = v; 187 | rotorTop.textContent = v; 188 | rotorBottom.textContent = v; 189 | appendChildren(rotor, [rotorLeaf, rotorTop, rotorBottom]); 190 | appendChildren(rotorLeaf, [rotorLeafRear, rotorLeafFront]); 191 | return rotor; 192 | } 193 | }, { 194 | key: "_tick", 195 | value: function _tick() { 196 | this.now = this._getTime(); 197 | var diff = this.epoch - this.now <= 0 ? 0 : this.epoch - this.now; 198 | this.clockValues.d = Math.floor(diff / 86400); 199 | diff -= this.clockValues.d * 86400; 200 | this.clockValues.h = Math.floor(diff / 3600); 201 | diff -= this.clockValues.h * 3600; 202 | this.clockValues.m = Math.floor(diff / 60); 203 | diff -= this.clockValues.m * 60; 204 | this.clockValues.s = Math.floor(diff); 205 | 206 | this._updateClockValues(); 207 | 208 | this._hasCountdownEnded(); 209 | } 210 | }, { 211 | key: "_updateClockValues", 212 | value: function _updateClockValues() { 213 | var _this = this; 214 | 215 | var init = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; 216 | this.clockStrings.d = pad(this.clockValues.d, 2); 217 | this.clockStrings.h = pad(this.clockValues.h, 2); 218 | this.clockStrings.m = pad(this.clockValues.m, 2); 219 | this.clockStrings.s = pad(this.clockValues.s, 2); 220 | this.clockValuesAsString = (this.clockStrings.d + this.clockStrings.h + this.clockStrings.m + this.clockStrings.s).split(""); 221 | this.rotorLeafFront.forEach(function (el, i) { 222 | el.textContent = _this.prevClockValuesAsString[i]; 223 | }); 224 | this.rotorBottom.forEach(function (el, i) { 225 | el.textContent = _this.prevClockValuesAsString[i]; 226 | }); 227 | 228 | function rotorTopFlip() { 229 | var _this2 = this; 230 | 231 | this.rotorTop.forEach(function (el, i) { 232 | if (el.textContent != _this2.clockValuesAsString[i]) { 233 | el.textContent = _this2.clockValuesAsString[i]; 234 | } 235 | }); 236 | } 237 | 238 | function rotorLeafRearFlip() { 239 | var _this3 = this; 240 | 241 | this.rotorLeafRear.forEach(function (el, i) { 242 | if (el.textContent != _this3.clockValuesAsString[i]) { 243 | el.textContent = _this3.clockValuesAsString[i]; 244 | el.parentElement.classList.add("flipped"); 245 | var flip = setInterval(function () { 246 | el.parentElement.classList.remove("flipped"); 247 | clearInterval(flip); 248 | }.bind(_this3), 500); 249 | } 250 | }); 251 | } 252 | 253 | if (!init) { 254 | setTimeout(rotorTopFlip.bind(this), 500); 255 | setTimeout(rotorLeafRearFlip.bind(this), 500); 256 | } else { 257 | rotorTopFlip.call(this); 258 | rotorLeafRearFlip.call(this); 259 | } 260 | 261 | this.prevClockValuesAsString = this.clockValuesAsString; 262 | } 263 | }]); 264 | 265 | return FlipDown; 266 | }(); 267 | 268 | function pad(n, len) { 269 | n = n.toString(); 270 | return n.length < len ? pad("0" + n, len) : n; 271 | } 272 | 273 | function appendChildren(parent, children) { 274 | children.forEach(function (el) { 275 | parent.appendChild(el); 276 | }); 277 | } 278 | -------------------------------------------------------------------------------- /dist/flipdown.min.css: -------------------------------------------------------------------------------- 1 | .flipdown.flipdown__theme-dark{font-family:sans-serif;font-weight:bold}.flipdown.flipdown__theme-dark .rotor-group-heading:before{color:#000}.flipdown.flipdown__theme-dark .rotor-group:nth-child(n+2):nth-child(-n+3):before,.flipdown.flipdown__theme-dark .rotor-group:nth-child(n+2):nth-child(-n+3):after{background-color:#151515}.flipdown.flipdown__theme-dark .rotor,.flipdown.flipdown__theme-dark .rotor-top,.flipdown.flipdown__theme-dark .rotor-leaf-front{color:#fff;background-color:#151515}.flipdown.flipdown__theme-dark .rotor-bottom,.flipdown.flipdown__theme-dark .rotor-leaf-rear{color:#efefef;background-color:#202020}.flipdown.flipdown__theme-dark .rotor:after{border-top:solid 1px #151515}.flipdown.flipdown__theme-light{font-family:sans-serif;font-weight:bold}.flipdown.flipdown__theme-light .rotor-group-heading:before{color:#eee}.flipdown.flipdown__theme-light .rotor-group:nth-child(n+2):nth-child(-n+3):before,.flipdown.flipdown__theme-light .rotor-group:nth-child(n+2):nth-child(-n+3):after{background-color:#ddd}.flipdown.flipdown__theme-light .rotor,.flipdown.flipdown__theme-light .rotor-top,.flipdown.flipdown__theme-light .rotor-leaf-front{color:#222;background-color:#ddd}.flipdown.flipdown__theme-light .rotor-bottom,.flipdown.flipdown__theme-light .rotor-leaf-rear{color:#333;background-color:#eee}.flipdown.flipdown__theme-light .rotor:after{border-top:solid 1px #222}.flipdown{overflow:visible;width:510px;height:110px}.flipdown .rotor-group{position:relative;float:left;padding-right:30px}.flipdown .rotor-group:last-child{padding-right:0}.flipdown .rotor-group-heading:before{display:block;height:30px;line-height:30px;text-align:center}.flipdown .rotor-group:nth-child(1) .rotor-group-heading:before{content:attr(data-before)}.flipdown .rotor-group:nth-child(2) .rotor-group-heading:before{content:attr(data-before)}.flipdown .rotor-group:nth-child(3) .rotor-group-heading:before{content:attr(data-before)}.flipdown .rotor-group:nth-child(4) .rotor-group-heading:before{content:attr(data-before)}.flipdown .rotor-group:nth-child(n+2):nth-child(-n+3):before{content:'';position:absolute;bottom:20px;left:115px;width:10px;height:10px;border-radius:50%}.flipdown .rotor-group:nth-child(n+2):nth-child(-n+3):after{content:'';position:absolute;bottom:50px;left:115px;width:10px;height:10px;border-radius:50%}.flipdown .rotor{position:relative;float:left;width:50px;height:80px;margin:0 5px 0 0;border-radius:4px;font-size:4rem;text-align:center;perspective:200px}.flipdown .rotor:last-child{margin-right:0}.flipdown .rotor-top,.flipdown .rotor-bottom{overflow:hidden;position:absolute;width:50px;height:40px}.flipdown .rotor-leaf{z-index:1;position:absolute;width:50px;height:80px;transform-style:preserve-3d;transition:transform 0s}.flipdown .rotor-leaf.flipped{transform:rotateX(-180deg);transition:all .5s ease-in-out}.flipdown .rotor-leaf-front,.flipdown .rotor-leaf-rear{overflow:hidden;position:absolute;width:50px;height:40px;margin:0;transform:rotateX(0);backface-visibility:hidden;-webkit-backface-visibility:hidden}.flipdown .rotor-leaf-front{line-height:80px;border-radius:4px 4px 0 0}.flipdown .rotor-leaf-rear{line-height:0;border-radius:0 0 4px 4px;transform:rotateX(-180deg)}.flipdown .rotor-top{line-height:80px;border-radius:4px 4px 0 0}.flipdown .rotor-bottom{bottom:0;line-height:0;border-radius:0 0 4px 4px}.flipdown .rotor:after{content:'';z-index:2;position:absolute;bottom:0;left:0;width:50px;height:40px;border-radius:0 0 4px 4px}@media(max-width:550px){.flipdown{width:312px;height:70px}.flipdown .rotor{font-size:2.2rem;margin-right:3px}.flipdown .rotor,.flipdown .rotor-leaf,.flipdown .rotor-leaf-front,.flipdown .rotor-leaf-rear,.flipdown .rotor-top,.flipdown .rotor-bottom,.flipdown .rotor:after{width:30px}.flipdown .rotor-group{padding-right:20px}.flipdown .rotor-group:last-child{padding-right:0}.flipdown .rotor-group-heading:before{font-size:.8rem;height:20px;line-height:20px}.flipdown .rotor-group:nth-child(n+2):nth-child(-n+3):before,.flipdown .rotor-group:nth-child(n+2):nth-child(-n+3):after{left:69px}.flipdown .rotor-group:nth-child(n+2):nth-child(-n+3):before{bottom:13px;height:8px;width:8px}.flipdown .rotor-group:nth-child(n+2):nth-child(-n+3):after{bottom:29px;height:8px;width:8px}.flipdown .rotor-leaf-front,.flipdown .rotor-top{line-height:50px}.flipdown .rotor-leaf,.flipdown .rotor{height:50px}.flipdown .rotor-leaf-front,.flipdown .rotor-leaf-rear,.flipdown .rotor-top,.flipdown .rotor-bottom,.flipdown .rotor:after{height:25px}} 2 | -------------------------------------------------------------------------------- /dist/flipdown.min.js: -------------------------------------------------------------------------------- 1 | "use strict";function _typeof(a){return _typeof="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(a){return typeof a}:function(a){return a&&"function"==typeof Symbol&&a.constructor===Symbol&&a!==Symbol.prototype?"symbol":typeof a},_typeof(a)}function _classCallCheck(a,b){if(!(a instanceof b))throw new TypeError("Cannot call a class as a function")}function _defineProperties(a,b){for(var c,d=0;dthis.epoch-this.now?(this.countdownEnded=!0,null!=this.hasEndedCallback&&(this.hasEndedCallback(),this.hasEndedCallback=null),!0):(this.countdownEnded=!1,!1)}},{key:"_parseOptions",value:function c(a){var b=["Days","Hours","Minutes","Seconds"];return a.headings&&4===a.headings.length&&(b=a.headings),{theme:a.hasOwnProperty("theme")?a.theme:"dark",headings:b}}},{key:"_setOptions",value:function a(){this.element.classList.add("flipdown__theme-".concat(this.opts.theme))}},{key:"_init",value:function h(){this.initialised=!0,this.daysremaining=this._hasCountdownEnded()?0:b((this.epoch-this.now)/86400).toString().length;for(var a=2>=this.daysremaining?2:this.daysremaining,c=0;cc;c++){e=[];for(var g=0;2>g;g++)e.push(this.rotors[f]),f++;this.element.appendChild(this._createRotorGroup(e,c+1))}return this.rotorLeafFront=Array.prototype.slice.call(this.element.getElementsByClassName("rotor-leaf-front")),this.rotorLeafRear=Array.prototype.slice.call(this.element.getElementsByClassName("rotor-leaf-rear")),this.rotorTop=Array.prototype.slice.call(this.element.getElementsByClassName("rotor-top")),this.rotorBottom=Array.prototype.slice.call(this.element.getElementsByClassName("rotor-bottom")),this._tick(),this._updateClockValues(!0),this}},{key:"_createRotorGroup",value:function e(a,b){var c=document.createElement("div");c.className="rotor-group";var d=document.createElement("div");return d.className="rotor-group-heading",d.setAttribute("data-before",this.opts.headings[b]),c.appendChild(d),appendChildren(c,a),c}},{key:"_createRotor",value:function h(){var a=0=this.epoch-this.now?0:this.epoch-this.now;this.clockValues.d=b(a/86400),a-=86400*this.clockValues.d,this.clockValues.h=b(a/3600),a-=3600*this.clockValues.h,this.clockValues.m=b(a/60),a-=60*this.clockValues.m,this.clockValues.s=b(a),this._updateClockValues(),this._hasCountdownEnded()}},{key:"_updateClockValues",value:function e(){function a(){var a=this;this.rotorTop.forEach(function(b,c){b.textContent!=a.clockValuesAsString[c]&&(b.textContent=a.clockValuesAsString[c])})}function b(){var a=this;this.rotorLeafRear.forEach(function(b,c){if(b.textContent!=a.clockValuesAsString[c]){b.textContent=a.clockValuesAsString[c],b.parentElement.classList.add("flipped");var d=setInterval(function(){b.parentElement.classList.remove("flipped"),clearInterval(d)}.bind(a),500)}})}var c=this,d=!!(0 2 | 3 | 4 | 5 | 6 | FlipDown Example 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |

FlipDown.js

17 |

⏰ A lightweight and performant flip styled countdown clock

18 |
19 |
20 |

Version: (<11KB minified)

21 | Get started 22 |
23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /example/js/flipdown/flipdown.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 4 | 5 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 6 | 7 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } 8 | 9 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } 10 | 11 | var FlipDown = function () { 12 | function FlipDown(uts) { 13 | var el = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : "flipdown"; 14 | var opt = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; 15 | 16 | _classCallCheck(this, FlipDown); 17 | 18 | if (typeof uts !== "number") { 19 | throw new Error("FlipDown: Constructor expected unix timestamp, got ".concat(_typeof(uts), " instead.")); 20 | } 21 | 22 | if (_typeof(el) === "object") { 23 | opt = el; 24 | el = "flipdown"; 25 | } 26 | 27 | this.version = "0.3.2"; 28 | this.initialised = false; 29 | this.now = this._getTime(); 30 | this.epoch = uts; 31 | this.countdownEnded = false; 32 | this.hasEndedCallback = null; 33 | this.element = document.getElementById(el); 34 | this.rotors = []; 35 | this.rotorLeafFront = []; 36 | this.rotorLeafRear = []; 37 | this.rotorTops = []; 38 | this.rotorBottoms = []; 39 | this.countdown = null; 40 | this.daysRemaining = 0; 41 | this.clockValues = {}; 42 | this.clockStrings = {}; 43 | this.clockValuesAsString = []; 44 | this.prevClockValuesAsString = []; 45 | this.opts = this._parseOptions(opt); 46 | 47 | this._setOptions(); 48 | 49 | console.log("FlipDown ".concat(this.version, " (Theme: ").concat(this.opts.theme, ")")); 50 | } 51 | 52 | _createClass(FlipDown, [{ 53 | key: "start", 54 | value: function start() { 55 | if (!this.initialised) this._init(); 56 | this.countdown = setInterval(this._tick.bind(this), 1000); 57 | return this; 58 | } 59 | }, { 60 | key: "ifEnded", 61 | value: function ifEnded(cb) { 62 | this.hasEndedCallback = function () { 63 | cb(); 64 | this.hasEndedCallback = null; 65 | }; 66 | 67 | return this; 68 | } 69 | }, { 70 | key: "_getTime", 71 | value: function _getTime() { 72 | return new Date().getTime() / 1000; 73 | } 74 | }, { 75 | key: "_hasCountdownEnded", 76 | value: function _hasCountdownEnded() { 77 | if (this.epoch - this.now < 0) { 78 | this.countdownEnded = true; 79 | 80 | if (this.hasEndedCallback != null) { 81 | this.hasEndedCallback(); 82 | this.hasEndedCallback = null; 83 | } 84 | 85 | return true; 86 | } else { 87 | this.countdownEnded = false; 88 | return false; 89 | } 90 | } 91 | }, { 92 | key: "_parseOptions", 93 | value: function _parseOptions(opt) { 94 | var headings = ["Days", "Hours", "Minutes", "Seconds"]; 95 | 96 | if (opt.headings && opt.headings.length === 4) { 97 | headings = opt.headings; 98 | } 99 | 100 | return { 101 | theme: opt.hasOwnProperty("theme") ? opt.theme : "dark", 102 | headings: headings 103 | }; 104 | } 105 | }, { 106 | key: "_setOptions", 107 | value: function _setOptions() { 108 | this.element.classList.add("flipdown__theme-".concat(this.opts.theme)); 109 | } 110 | }, { 111 | key: "_init", 112 | value: function _init() { 113 | this.initialised = true; 114 | 115 | if (this._hasCountdownEnded()) { 116 | this.daysremaining = 0; 117 | } else { 118 | this.daysremaining = Math.floor((this.epoch - this.now) / 86400).toString().length; 119 | } 120 | 121 | var dayRotorCount = this.daysremaining <= 2 ? 2 : this.daysremaining; 122 | 123 | for (var i = 0; i < dayRotorCount + 6; i++) { 124 | this.rotors.push(this._createRotor(0)); 125 | } 126 | 127 | var dayRotors = []; 128 | 129 | for (var i = 0; i < dayRotorCount; i++) { 130 | dayRotors.push(this.rotors[i]); 131 | } 132 | 133 | this.element.appendChild(this._createRotorGroup(dayRotors, 0)); 134 | var count = dayRotorCount; 135 | 136 | for (var i = 0; i < 3; i++) { 137 | var otherRotors = []; 138 | 139 | for (var j = 0; j < 2; j++) { 140 | otherRotors.push(this.rotors[count]); 141 | count++; 142 | } 143 | 144 | this.element.appendChild(this._createRotorGroup(otherRotors, i + 1)); 145 | } 146 | 147 | this.rotorLeafFront = Array.prototype.slice.call(this.element.getElementsByClassName("rotor-leaf-front")); 148 | this.rotorLeafRear = Array.prototype.slice.call(this.element.getElementsByClassName("rotor-leaf-rear")); 149 | this.rotorTop = Array.prototype.slice.call(this.element.getElementsByClassName("rotor-top")); 150 | this.rotorBottom = Array.prototype.slice.call(this.element.getElementsByClassName("rotor-bottom")); 151 | 152 | this._tick(); 153 | 154 | this._updateClockValues(true); 155 | 156 | return this; 157 | } 158 | }, { 159 | key: "_createRotorGroup", 160 | value: function _createRotorGroup(rotors, rotorIndex) { 161 | var rotorGroup = document.createElement("div"); 162 | rotorGroup.className = "rotor-group"; 163 | var dayRotorGroupHeading = document.createElement("div"); 164 | dayRotorGroupHeading.className = "rotor-group-heading"; 165 | dayRotorGroupHeading.setAttribute("data-before", this.opts.headings[rotorIndex]); 166 | rotorGroup.appendChild(dayRotorGroupHeading); 167 | appendChildren(rotorGroup, rotors); 168 | return rotorGroup; 169 | } 170 | }, { 171 | key: "_createRotor", 172 | value: function _createRotor() { 173 | var v = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0; 174 | var rotor = document.createElement("div"); 175 | var rotorLeaf = document.createElement("div"); 176 | var rotorLeafRear = document.createElement("figure"); 177 | var rotorLeafFront = document.createElement("figure"); 178 | var rotorTop = document.createElement("div"); 179 | var rotorBottom = document.createElement("div"); 180 | rotor.className = "rotor"; 181 | rotorLeaf.className = "rotor-leaf"; 182 | rotorLeafRear.className = "rotor-leaf-rear"; 183 | rotorLeafFront.className = "rotor-leaf-front"; 184 | rotorTop.className = "rotor-top"; 185 | rotorBottom.className = "rotor-bottom"; 186 | rotorLeafRear.textContent = v; 187 | rotorTop.textContent = v; 188 | rotorBottom.textContent = v; 189 | appendChildren(rotor, [rotorLeaf, rotorTop, rotorBottom]); 190 | appendChildren(rotorLeaf, [rotorLeafRear, rotorLeafFront]); 191 | return rotor; 192 | } 193 | }, { 194 | key: "_tick", 195 | value: function _tick() { 196 | this.now = this._getTime(); 197 | var diff = this.epoch - this.now <= 0 ? 0 : this.epoch - this.now; 198 | this.clockValues.d = Math.floor(diff / 86400); 199 | diff -= this.clockValues.d * 86400; 200 | this.clockValues.h = Math.floor(diff / 3600); 201 | diff -= this.clockValues.h * 3600; 202 | this.clockValues.m = Math.floor(diff / 60); 203 | diff -= this.clockValues.m * 60; 204 | this.clockValues.s = Math.floor(diff); 205 | 206 | this._updateClockValues(); 207 | 208 | this._hasCountdownEnded(); 209 | } 210 | }, { 211 | key: "_updateClockValues", 212 | value: function _updateClockValues() { 213 | var _this = this; 214 | 215 | var init = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; 216 | this.clockStrings.d = pad(this.clockValues.d, 2); 217 | this.clockStrings.h = pad(this.clockValues.h, 2); 218 | this.clockStrings.m = pad(this.clockValues.m, 2); 219 | this.clockStrings.s = pad(this.clockValues.s, 2); 220 | this.clockValuesAsString = (this.clockStrings.d + this.clockStrings.h + this.clockStrings.m + this.clockStrings.s).split(""); 221 | this.rotorLeafFront.forEach(function (el, i) { 222 | el.textContent = _this.prevClockValuesAsString[i]; 223 | }); 224 | this.rotorBottom.forEach(function (el, i) { 225 | el.textContent = _this.prevClockValuesAsString[i]; 226 | }); 227 | 228 | function rotorTopFlip() { 229 | var _this2 = this; 230 | 231 | this.rotorTop.forEach(function (el, i) { 232 | if (el.textContent != _this2.clockValuesAsString[i]) { 233 | el.textContent = _this2.clockValuesAsString[i]; 234 | } 235 | }); 236 | } 237 | 238 | function rotorLeafRearFlip() { 239 | var _this3 = this; 240 | 241 | this.rotorLeafRear.forEach(function (el, i) { 242 | if (el.textContent != _this3.clockValuesAsString[i]) { 243 | el.textContent = _this3.clockValuesAsString[i]; 244 | el.parentElement.classList.add("flipped"); 245 | var flip = setInterval(function () { 246 | el.parentElement.classList.remove("flipped"); 247 | clearInterval(flip); 248 | }.bind(_this3), 500); 249 | } 250 | }); 251 | } 252 | 253 | if (!init) { 254 | setTimeout(rotorTopFlip.bind(this), 500); 255 | setTimeout(rotorLeafRearFlip.bind(this), 500); 256 | } else { 257 | rotorTopFlip.call(this); 258 | rotorLeafRearFlip.call(this); 259 | } 260 | 261 | this.prevClockValuesAsString = this.clockValuesAsString; 262 | } 263 | }]); 264 | 265 | return FlipDown; 266 | }(); 267 | 268 | function pad(n, len) { 269 | n = n.toString(); 270 | return n.length < len ? pad("0" + n, len) : n; 271 | } 272 | 273 | function appendChildren(parent, children) { 274 | children.forEach(function (el) { 275 | parent.appendChild(el); 276 | }); 277 | } 278 | -------------------------------------------------------------------------------- /example/js/main.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', () => { 2 | 3 | // Unix timestamp (in seconds) to count down to 4 | var twoDaysFromNow = (new Date().getTime() / 1000) + (86400 * 2) + 1; 5 | 6 | // Set up FlipDown 7 | var flipdown = new FlipDown(twoDaysFromNow) 8 | 9 | // Start the countdown 10 | .start() 11 | 12 | // Do something when the countdown ends 13 | .ifEnded(() => { 14 | console.log('The countdown has ended!'); 15 | }); 16 | 17 | // Toggle theme 18 | var interval = setInterval(() => { 19 | let body = document.body; 20 | body.classList.toggle('light-theme'); 21 | body.querySelector('#flipdown').classList.toggle('flipdown__theme-dark'); 22 | body.querySelector('#flipdown').classList.toggle('flipdown__theme-light'); 23 | }, 5000); 24 | 25 | // Show version number 26 | var ver = document.getElementById('ver'); 27 | ver.innerHTML = flipdown.version; 28 | }); 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flipdown", 3 | "version": "0.3.2", 4 | "description": "A lightweight and performant flip styled countdown clock", 5 | "main": "src/flipdown.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "clean": "rm -rf dist && mkdir -p dist && rm -rf example/css/flipdown && mkdir -p example/css/flipdown && rm -rf example/js/flipdown && mkdir -p example/js/flipdown", 9 | "dist": "npx babel src/flipdown.js -o dist/flipdown.js --no-comments && npx babel src/flipdown.js --presets=minify --no-comments -o dist/flipdown.min.js && cp src/flipdown.css dist/flipdown.css && uglifycss src/flipdown.css > dist/flipdown.min.css", 10 | "example": "npx babel src/flipdown.js -o example/js/flipdown/flipdown.js --no-comments && cp dist/flipdown.css example/css/flipdown/flipdown.css", 11 | "build": "npm run clean && npm run dist && npm run example" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+ssh://git@github.com/PButcher/flipdown.git" 16 | }, 17 | "author": "Peter Butcher ", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/PButcher/flipdown/issues" 21 | }, 22 | "homepage": "https://github.com/PButcher/flipdown#readme", 23 | "dependencies": {}, 24 | "devDependencies": { 25 | "@babel/cli": "^7.1.0", 26 | "@babel/core": "^7.1.0", 27 | "@babel/preset-env": "^7.1.5", 28 | "babel-preset-minify": "^0.5.0", 29 | "uglifycss": "0.0.29" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/flipdown.css: -------------------------------------------------------------------------------- 1 | /* THEMES */ 2 | 3 | /********** Theme: dark **********/ 4 | /* Font styles */ 5 | .flipdown.flipdown__theme-dark { 6 | font-family: sans-serif; 7 | font-weight: bold; 8 | } 9 | /* Rotor group headings */ 10 | .flipdown.flipdown__theme-dark .rotor-group-heading:before { 11 | color: #000000; 12 | } 13 | /* Delimeters */ 14 | .flipdown.flipdown__theme-dark .rotor-group:nth-child(n+2):nth-child(-n+3):before, 15 | .flipdown.flipdown__theme-dark .rotor-group:nth-child(n+2):nth-child(-n+3):after { 16 | background-color: #151515; 17 | } 18 | /* Rotor tops */ 19 | .flipdown.flipdown__theme-dark .rotor, 20 | .flipdown.flipdown__theme-dark .rotor-top, 21 | .flipdown.flipdown__theme-dark .rotor-leaf-front { 22 | color: #FFFFFF; 23 | background-color: #151515; 24 | } 25 | /* Rotor bottoms */ 26 | .flipdown.flipdown__theme-dark .rotor-bottom, 27 | .flipdown.flipdown__theme-dark .rotor-leaf-rear { 28 | color: #EFEFEF; 29 | background-color: #202020; 30 | } 31 | /* Hinge */ 32 | .flipdown.flipdown__theme-dark .rotor:after { 33 | border-top: solid 1px #151515; 34 | } 35 | 36 | /********** Theme: light **********/ 37 | /* Font styles */ 38 | .flipdown.flipdown__theme-light { 39 | font-family: sans-serif; 40 | font-weight: bold; 41 | } 42 | /* Rotor group headings */ 43 | .flipdown.flipdown__theme-light .rotor-group-heading:before { 44 | color: #EEEEEE; 45 | } 46 | /* Delimeters */ 47 | .flipdown.flipdown__theme-light .rotor-group:nth-child(n+2):nth-child(-n+3):before, 48 | .flipdown.flipdown__theme-light .rotor-group:nth-child(n+2):nth-child(-n+3):after { 49 | background-color: #DDDDDD; 50 | } 51 | /* Rotor tops */ 52 | .flipdown.flipdown__theme-light .rotor, 53 | .flipdown.flipdown__theme-light .rotor-top, 54 | .flipdown.flipdown__theme-light .rotor-leaf-front { 55 | color: #222222; 56 | background-color: #DDDDDD; 57 | } 58 | /* Rotor bottoms */ 59 | .flipdown.flipdown__theme-light .rotor-bottom, 60 | .flipdown.flipdown__theme-light .rotor-leaf-rear { 61 | color: #333333; 62 | background-color: #EEEEEE; 63 | } 64 | /* Hinge */ 65 | .flipdown.flipdown__theme-light .rotor:after { 66 | border-top: solid 1px #222222; 67 | } 68 | 69 | /* END OF THEMES */ 70 | 71 | .flipdown { 72 | overflow: visible; 73 | width: 510px; 74 | height: 110px; 75 | } 76 | 77 | .flipdown .rotor-group { 78 | position: relative; 79 | float: left; 80 | padding-right: 30px; 81 | } 82 | 83 | .flipdown .rotor-group:last-child { 84 | padding-right: 0; 85 | } 86 | 87 | .flipdown .rotor-group-heading:before { 88 | display: block; 89 | height: 30px; 90 | line-height: 30px; 91 | text-align: center; 92 | } 93 | 94 | .flipdown .rotor-group:nth-child(1) .rotor-group-heading:before { 95 | content: attr(data-before); 96 | } 97 | 98 | .flipdown .rotor-group:nth-child(2) .rotor-group-heading:before { 99 | content: attr(data-before); 100 | } 101 | 102 | .flipdown .rotor-group:nth-child(3) .rotor-group-heading:before { 103 | content: attr(data-before); 104 | } 105 | 106 | .flipdown .rotor-group:nth-child(4) .rotor-group-heading:before { 107 | content: attr(data-before); 108 | } 109 | 110 | .flipdown .rotor-group:nth-child(n+2):nth-child(-n+3):before { 111 | content: ''; 112 | position: absolute; 113 | bottom: 20px; 114 | left: 115px; 115 | width: 10px; 116 | height: 10px; 117 | border-radius: 50%; 118 | } 119 | 120 | .flipdown .rotor-group:nth-child(n+2):nth-child(-n+3):after { 121 | content: ''; 122 | position: absolute; 123 | bottom: 50px; 124 | left: 115px; 125 | width: 10px; 126 | height: 10px; 127 | border-radius: 50%; 128 | } 129 | 130 | .flipdown .rotor { 131 | position: relative; 132 | float: left; 133 | width: 50px; 134 | height: 80px; 135 | margin: 0px 5px 0px 0px; 136 | border-radius: 4px; 137 | font-size: 4rem; 138 | text-align: center; 139 | perspective: 200px; 140 | } 141 | 142 | .flipdown .rotor:last-child { 143 | margin-right: 0; 144 | } 145 | 146 | .flipdown .rotor-top, 147 | .flipdown .rotor-bottom { 148 | overflow: hidden; 149 | position: absolute; 150 | width: 50px; 151 | height: 40px; 152 | } 153 | 154 | .flipdown .rotor-leaf { 155 | z-index: 1; 156 | position: absolute; 157 | width: 50px; 158 | height: 80px; 159 | transform-style: preserve-3d; 160 | transition: transform 0s; 161 | } 162 | 163 | .flipdown .rotor-leaf.flipped { 164 | transform: rotateX(-180deg); 165 | transition: all 0.5s ease-in-out; 166 | } 167 | 168 | .flipdown .rotor-leaf-front, 169 | .flipdown .rotor-leaf-rear { 170 | overflow: hidden; 171 | position: absolute; 172 | width: 50px; 173 | height: 40px; 174 | margin: 0; 175 | transform: rotateX(0deg); 176 | backface-visibility: hidden; 177 | -webkit-backface-visibility: hidden; 178 | } 179 | 180 | .flipdown .rotor-leaf-front { 181 | line-height: 80px; 182 | border-radius: 4px 4px 0px 0px; 183 | } 184 | 185 | .flipdown .rotor-leaf-rear { 186 | line-height: 0px; 187 | border-radius: 0px 0px 4px 4px; 188 | transform: rotateX(-180deg); 189 | } 190 | 191 | .flipdown .rotor-top { 192 | line-height: 80px; 193 | border-radius: 4px 4px 0px 0px; 194 | } 195 | 196 | .flipdown .rotor-bottom { 197 | bottom: 0; 198 | line-height: 0px; 199 | border-radius: 0px 0px 4px 4px; 200 | } 201 | 202 | .flipdown .rotor:after { 203 | content: ''; 204 | z-index: 2; 205 | position: absolute; 206 | bottom: 0px; 207 | left: 0px; 208 | width: 50px; 209 | height: 40px; 210 | border-radius: 0px 0px 4px 4px; 211 | } 212 | 213 | @media (max-width: 550px) { 214 | 215 | .flipdown { 216 | width: 312px; 217 | height: 70px; 218 | } 219 | 220 | .flipdown .rotor { 221 | font-size: 2.2rem; 222 | margin-right: 3px; 223 | } 224 | 225 | .flipdown .rotor, 226 | .flipdown .rotor-leaf, 227 | .flipdown .rotor-leaf-front, 228 | .flipdown .rotor-leaf-rear, 229 | .flipdown .rotor-top, 230 | .flipdown .rotor-bottom, 231 | .flipdown .rotor:after { 232 | width: 30px; 233 | } 234 | 235 | .flipdown .rotor-group { 236 | padding-right: 20px; 237 | } 238 | 239 | .flipdown .rotor-group:last-child { 240 | padding-right: 0px; 241 | } 242 | 243 | .flipdown .rotor-group-heading:before { 244 | font-size: 0.8rem; 245 | height: 20px; 246 | line-height: 20px; 247 | } 248 | 249 | .flipdown .rotor-group:nth-child(n+2):nth-child(-n+3):before, 250 | .flipdown .rotor-group:nth-child(n+2):nth-child(-n+3):after { 251 | left: 69px; 252 | } 253 | 254 | .flipdown .rotor-group:nth-child(n+2):nth-child(-n+3):before { 255 | bottom: 13px; 256 | height: 8px; 257 | width: 8px; 258 | } 259 | 260 | .flipdown .rotor-group:nth-child(n+2):nth-child(-n+3):after { 261 | bottom: 29px; 262 | height: 8px; 263 | width: 8px; 264 | } 265 | 266 | .flipdown .rotor-leaf-front, 267 | .flipdown .rotor-top { 268 | line-height: 50px; 269 | } 270 | 271 | .flipdown .rotor-leaf, 272 | .flipdown .rotor { 273 | height: 50px; 274 | } 275 | 276 | .flipdown .rotor-leaf-front, 277 | .flipdown .rotor-leaf-rear, 278 | .flipdown .rotor-top, 279 | .flipdown .rotor-bottom, 280 | .flipdown .rotor:after { 281 | height: 25px; 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /src/flipdown.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @name FlipDown 3 | * @description Flip styled countdown clock 4 | * @author Peter Butcher (PButcher) 5 | * @param {number} uts - Time to count down to as unix timestamp 6 | * @param {string} el - DOM element to attach FlipDown to 7 | * @param {object} opt - Optional configuration settings 8 | **/ 9 | class FlipDown { 10 | constructor(uts, el = "flipdown", opt = {}) { 11 | // If uts is not specified 12 | if (typeof uts !== "number") { 13 | throw new Error( 14 | `FlipDown: Constructor expected unix timestamp, got ${typeof uts} instead.` 15 | ); 16 | } 17 | 18 | // If opt is specified, but not el 19 | if (typeof el === "object") { 20 | opt = el; 21 | el = "flipdown"; 22 | } 23 | 24 | // FlipDown version 25 | this.version = "0.3.2"; 26 | 27 | // Initialised? 28 | this.initialised = false; 29 | 30 | // Time at instantiation in seconds 31 | this.now = this._getTime(); 32 | 33 | // UTS to count down to 34 | this.epoch = uts; 35 | 36 | // UTS passed to FlipDown is in the past 37 | this.countdownEnded = false; 38 | 39 | // User defined callback for countdown end 40 | this.hasEndedCallback = null; 41 | 42 | // FlipDown DOM element 43 | this.element = document.getElementById(el); 44 | 45 | // Rotor DOM elements 46 | this.rotors = []; 47 | this.rotorLeafFront = []; 48 | this.rotorLeafRear = []; 49 | this.rotorTops = []; 50 | this.rotorBottoms = []; 51 | 52 | // Interval 53 | this.countdown = null; 54 | 55 | // Number of days remaining 56 | this.daysRemaining = 0; 57 | 58 | // Clock values as numbers 59 | this.clockValues = {}; 60 | 61 | // Clock values as strings 62 | this.clockStrings = {}; 63 | 64 | // Clock values as array 65 | this.clockValuesAsString = []; 66 | this.prevClockValuesAsString = []; 67 | 68 | // Parse options 69 | this.opts = this._parseOptions(opt); 70 | 71 | // Set options 72 | this._setOptions(); 73 | 74 | // Print Version 75 | console.log(`FlipDown ${this.version} (Theme: ${this.opts.theme})`); 76 | } 77 | 78 | /** 79 | * @name start 80 | * @description Start the countdown 81 | * @author PButcher 82 | **/ 83 | start() { 84 | // Initialise the clock 85 | if (!this.initialised) this._init(); 86 | 87 | // Set up the countdown interval 88 | this.countdown = setInterval(this._tick.bind(this), 1000); 89 | 90 | // Chainable 91 | return this; 92 | } 93 | 94 | /** 95 | * @name ifEnded 96 | * @description Call a function once the countdown ends 97 | * @author PButcher 98 | * @param {function} cb - Callback 99 | **/ 100 | ifEnded(cb) { 101 | this.hasEndedCallback = function () { 102 | cb(); 103 | this.hasEndedCallback = null; 104 | }; 105 | 106 | // Chainable 107 | return this; 108 | } 109 | 110 | /** 111 | * @name _getTime 112 | * @description Get the time in seconds (unix timestamp) 113 | * @author PButcher 114 | **/ 115 | _getTime() { 116 | return new Date().getTime() / 1000; 117 | } 118 | 119 | /** 120 | * @name _hasCountdownEnded 121 | * @description Has the countdown ended? 122 | * @author PButcher 123 | **/ 124 | _hasCountdownEnded() { 125 | // Countdown has ended 126 | if (this.epoch - this.now < 0) { 127 | this.countdownEnded = true; 128 | 129 | // Fire the ifEnded callback once if it was set 130 | if (this.hasEndedCallback != null) { 131 | // Call ifEnded callback 132 | this.hasEndedCallback(); 133 | 134 | // Remove the callback 135 | this.hasEndedCallback = null; 136 | } 137 | 138 | return true; 139 | 140 | // Countdown has not ended 141 | } else { 142 | this.countdownEnded = false; 143 | return false; 144 | } 145 | } 146 | 147 | /** 148 | * @name _parseOptions 149 | * @description Parse any passed options 150 | * @param {object} opt - Optional configuration settings 151 | * @author PButcher 152 | **/ 153 | _parseOptions(opt) { 154 | let headings = ["Days", "Hours", "Minutes", "Seconds"]; 155 | if (opt.headings && opt.headings.length === 4) { 156 | headings = opt.headings; 157 | } 158 | return { 159 | // Theme 160 | theme: opt.hasOwnProperty("theme") ? opt.theme : "dark", 161 | headings, 162 | }; 163 | } 164 | 165 | /** 166 | * @name _setOptions 167 | * @description Set optional configuration settings 168 | * @author PButcher 169 | **/ 170 | _setOptions() { 171 | // Apply theme 172 | this.element.classList.add(`flipdown__theme-${this.opts.theme}`); 173 | } 174 | 175 | /** 176 | * @name _init 177 | * @description Initialise the countdown 178 | * @author PButcher 179 | **/ 180 | _init() { 181 | this.initialised = true; 182 | 183 | // Check whether countdown has ended and calculate how many digits the day counter needs 184 | if (this._hasCountdownEnded()) { 185 | this.daysremaining = 0; 186 | } else { 187 | this.daysremaining = Math.floor( 188 | (this.epoch - this.now) / 86400 189 | ).toString().length; 190 | } 191 | var dayRotorCount = this.daysremaining <= 2 ? 2 : this.daysremaining; 192 | 193 | // Create and store rotors 194 | for (var i = 0; i < dayRotorCount + 6; i++) { 195 | this.rotors.push(this._createRotor(0)); 196 | } 197 | 198 | // Create day rotor group 199 | var dayRotors = []; 200 | for (var i = 0; i < dayRotorCount; i++) { 201 | dayRotors.push(this.rotors[i]); 202 | } 203 | this.element.appendChild(this._createRotorGroup(dayRotors, 0)); 204 | 205 | // Create other rotor groups 206 | var count = dayRotorCount; 207 | for (var i = 0; i < 3; i++) { 208 | var otherRotors = []; 209 | for (var j = 0; j < 2; j++) { 210 | otherRotors.push(this.rotors[count]); 211 | count++; 212 | } 213 | this.element.appendChild(this._createRotorGroup(otherRotors, i + 1)); 214 | } 215 | 216 | // Store and convert rotor nodelists to arrays 217 | this.rotorLeafFront = Array.prototype.slice.call( 218 | this.element.getElementsByClassName("rotor-leaf-front") 219 | ); 220 | this.rotorLeafRear = Array.prototype.slice.call( 221 | this.element.getElementsByClassName("rotor-leaf-rear") 222 | ); 223 | this.rotorTop = Array.prototype.slice.call( 224 | this.element.getElementsByClassName("rotor-top") 225 | ); 226 | this.rotorBottom = Array.prototype.slice.call( 227 | this.element.getElementsByClassName("rotor-bottom") 228 | ); 229 | 230 | // Set initial values; 231 | this._tick(); 232 | this._updateClockValues(true); 233 | 234 | return this; 235 | } 236 | 237 | /** 238 | * @name _createRotorGroup 239 | * @description Add rotors to the DOM 240 | * @author PButcher 241 | * @param {array} rotors - A set of rotors 242 | **/ 243 | _createRotorGroup(rotors, rotorIndex) { 244 | var rotorGroup = document.createElement("div"); 245 | rotorGroup.className = "rotor-group"; 246 | var dayRotorGroupHeading = document.createElement("div"); 247 | dayRotorGroupHeading.className = "rotor-group-heading"; 248 | dayRotorGroupHeading.setAttribute( 249 | "data-before", 250 | this.opts.headings[rotorIndex] 251 | ); 252 | rotorGroup.appendChild(dayRotorGroupHeading); 253 | appendChildren(rotorGroup, rotors); 254 | return rotorGroup; 255 | } 256 | 257 | /** 258 | * @name _createRotor 259 | * @description Create a rotor DOM element 260 | * @author PButcher 261 | * @param {number} v - Initial rotor value 262 | **/ 263 | _createRotor(v = 0) { 264 | var rotor = document.createElement("div"); 265 | var rotorLeaf = document.createElement("div"); 266 | var rotorLeafRear = document.createElement("figure"); 267 | var rotorLeafFront = document.createElement("figure"); 268 | var rotorTop = document.createElement("div"); 269 | var rotorBottom = document.createElement("div"); 270 | rotor.className = "rotor"; 271 | rotorLeaf.className = "rotor-leaf"; 272 | rotorLeafRear.className = "rotor-leaf-rear"; 273 | rotorLeafFront.className = "rotor-leaf-front"; 274 | rotorTop.className = "rotor-top"; 275 | rotorBottom.className = "rotor-bottom"; 276 | rotorLeafRear.textContent = v; 277 | rotorTop.textContent = v; 278 | rotorBottom.textContent = v; 279 | appendChildren(rotor, [rotorLeaf, rotorTop, rotorBottom]); 280 | appendChildren(rotorLeaf, [rotorLeafRear, rotorLeafFront]); 281 | return rotor; 282 | } 283 | 284 | /** 285 | * @name _tick 286 | * @description Calculate current tick 287 | * @author PButcher 288 | **/ 289 | _tick() { 290 | // Get time now 291 | this.now = this._getTime(); 292 | 293 | // Between now and epoch 294 | var diff = this.epoch - this.now <= 0 ? 0 : this.epoch - this.now; 295 | 296 | // Days remaining 297 | this.clockValues.d = Math.floor(diff / 86400); 298 | diff -= this.clockValues.d * 86400; 299 | 300 | // Hours remaining 301 | this.clockValues.h = Math.floor(diff / 3600); 302 | diff -= this.clockValues.h * 3600; 303 | 304 | // Minutes remaining 305 | this.clockValues.m = Math.floor(diff / 60); 306 | diff -= this.clockValues.m * 60; 307 | 308 | // Seconds remaining 309 | this.clockValues.s = Math.floor(diff); 310 | 311 | // Update clock values 312 | this._updateClockValues(); 313 | 314 | // Has the countdown ended? 315 | this._hasCountdownEnded(); 316 | } 317 | 318 | /** 319 | * @name _updateClockValues 320 | * @description Update the clock face values 321 | * @author PButcher 322 | * @param {boolean} init - True if calling for initialisation 323 | **/ 324 | _updateClockValues(init = false) { 325 | // Build clock value strings 326 | this.clockStrings.d = pad(this.clockValues.d, 2); 327 | this.clockStrings.h = pad(this.clockValues.h, 2); 328 | this.clockStrings.m = pad(this.clockValues.m, 2); 329 | this.clockStrings.s = pad(this.clockValues.s, 2); 330 | 331 | // Concat clock value strings 332 | this.clockValuesAsString = ( 333 | this.clockStrings.d + 334 | this.clockStrings.h + 335 | this.clockStrings.m + 336 | this.clockStrings.s 337 | ).split(""); 338 | 339 | // Update rotor values 340 | // Note that the faces which are initially visible are: 341 | // - rotorLeafFront (top half of current rotor) 342 | // - rotorBottom (bottom half of current rotor) 343 | // Note that the faces which are initially hidden are: 344 | // - rotorTop (top half of next rotor) 345 | // - rotorLeafRear (bottom half of next rotor) 346 | this.rotorLeafFront.forEach((el, i) => { 347 | el.textContent = this.prevClockValuesAsString[i]; 348 | }); 349 | 350 | this.rotorBottom.forEach((el, i) => { 351 | el.textContent = this.prevClockValuesAsString[i]; 352 | }); 353 | 354 | function rotorTopFlip() { 355 | this.rotorTop.forEach((el, i) => { 356 | if (el.textContent != this.clockValuesAsString[i]) { 357 | el.textContent = this.clockValuesAsString[i]; 358 | } 359 | }); 360 | } 361 | 362 | function rotorLeafRearFlip() { 363 | this.rotorLeafRear.forEach((el, i) => { 364 | if (el.textContent != this.clockValuesAsString[i]) { 365 | el.textContent = this.clockValuesAsString[i]; 366 | el.parentElement.classList.add("flipped"); 367 | var flip = setInterval( 368 | function () { 369 | el.parentElement.classList.remove("flipped"); 370 | clearInterval(flip); 371 | }.bind(this), 372 | 500 373 | ); 374 | } 375 | }); 376 | } 377 | 378 | // Init 379 | if (!init) { 380 | setTimeout(rotorTopFlip.bind(this), 500); 381 | setTimeout(rotorLeafRearFlip.bind(this), 500); 382 | } else { 383 | rotorTopFlip.call(this); 384 | rotorLeafRearFlip.call(this); 385 | } 386 | 387 | // Save a copy of clock values for next tick 388 | this.prevClockValuesAsString = this.clockValuesAsString; 389 | } 390 | } 391 | 392 | /** 393 | * @name pad 394 | * @description Prefix a number with zeroes 395 | * @author PButcher 396 | * @param {string} n - Number to pad 397 | * @param {number} len - Desired length of number 398 | **/ 399 | function pad(n, len) { 400 | n = n.toString(); 401 | return n.length < len ? pad("0" + n, len) : n; 402 | } 403 | 404 | /** 405 | * @name appendChildren 406 | * @description Add multiple children to an element 407 | * @author PButcher 408 | * @param {object} parent - Parent 409 | **/ 410 | function appendChildren(parent, children) { 411 | children.forEach((el) => { 412 | parent.appendChild(el); 413 | }); 414 | } 415 | --------------------------------------------------------------------------------