├── .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 | 
8 | 
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 |
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 |
--------------------------------------------------------------------------------