├── .bowerrc ├── .gitignore ├── .jshintignore ├── .jshintrc ├── .travis.yml ├── Gruntfile.js ├── LICENSE.md ├── README.md ├── bower.json ├── countdown.jquery.json ├── dist ├── jquery.countdown.js └── jquery.countdown.min.js ├── karma.conf.js ├── package.json ├── src ├── .jshintrc └── countdown.js └── test ├── integration ├── karma.conf.js ├── requirejs.config.js └── requirejs_test.js ├── qunit.config.js ├── scenarios.json └── unit ├── date_casting_test.js ├── event_test.js ├── functionality_test.js ├── math_test.js ├── sanity_test.js └── strftime_test.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory" : "test/vendor" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | 3 | lib-cov 4 | *.seed 5 | *.log 6 | *.csv 7 | *.dat 8 | *.out 9 | *.pid 10 | *.gz 11 | 12 | pids 13 | logs 14 | results 15 | build 16 | 17 | node_modules 18 | 19 | .DS_Store 20 | 21 | test/vendor 22 | _site/ 23 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | test/vendor/**/*.js 2 | lib/**/*.js 3 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": true, 3 | "camelcase": true, 4 | "curly": true, 5 | "eqeqeq": true, 6 | "es3": true, 7 | "forin": true, 8 | "freeze": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "noempty": true, 15 | "nonbsp": true, 16 | "nonew": false, 17 | "plusplus": false, 18 | "quotmark": "single", 19 | "undef": true, 20 | "unused": false, 21 | "strict": false, 22 | "trailing": true, 23 | "maxparams": false, 24 | "maxdepth": 3, 25 | "maxstatements": false, 26 | "maxcomplexity": 8, 27 | "maxlen": 80, 28 | "gcl": false, 29 | "browser": true, 30 | "devel": true, 31 | "node": true, 32 | "jquery": true, 33 | "globals": { 34 | "asyncTest": false, 35 | "deepEqual": false, 36 | "equal": false, 37 | "expect": false, 38 | "module": false, 39 | "notDeepEqual": false, 40 | "notEqual": false, 41 | "notStrictEqual": false, 42 | "ok": false, 43 | "QUnit": false, 44 | "raises": false, 45 | "throws": false, 46 | "start": false, 47 | "stop": false, 48 | "strictEqual": false, 49 | "test": false, 50 | "sinon": false, 51 | "require": false, 52 | "define": false, 53 | "$dom": true, 54 | "$clock": true 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | before_script: 5 | - npm install -g grunt-cli bower 6 | - npm install 7 | - bower install 8 | branches: 9 | except: 10 | - /^v1\..*/ 11 | - /^1\..*/ 12 | - gh-pages 13 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 'use strict'; 3 | // Project configuration. 4 | grunt.initConfig({ 5 | pkg: grunt.file.readJSON('package.json'), 6 | dirs: { 7 | src: './src', 8 | lib: './lib', 9 | bin: './build', 10 | test: './test', 11 | dest: './dist' 12 | }, 13 | // project utils 14 | pluginName: '<%= pkg.name.replace(/-/, ".") %>', 15 | license: grunt.file.read('LICENSE.md').split('\n').splice(3).join('\n'), 16 | banner: '/*!\n' + 17 | ' * <%= pkg.description %> v<%= pkg.version %> ' + 18 | '(<%= pkg.homepage %>)\n' + 19 | ' * Copyright (c) <%= grunt.template.today("yyyy") %> ' + 20 | '<%= pkg.author %>\n' + 21 | ' * <%= license.replace(/\\n/gm, "\\n * ") %>\n' + 22 | ' */\n', 23 | // contrib-clean 24 | clean: ['<%= dirs.dest %>'], 25 | // contrib-compress 26 | compress: { 27 | release: { 28 | options: { 29 | archive: '<%= dirs.bin %>/<%= pluginName %>-<%= pkg.version %>.zip' 30 | }, 31 | expand: true, 32 | cwd: '<%= dirs.dest %>', 33 | src: ['**/*'], 34 | dest: '<%= pluginName %>-<%= pkg.version %>' 35 | } 36 | }, 37 | // contrib-jshint 38 | jshint: { 39 | options: { 40 | jshintrc: true 41 | }, 42 | all: [ 43 | 'Gruntfile.js', 44 | '<%= dirs.src %>/**/*.js', 45 | '<%= dirs.test %>/**/*.js' 46 | ] 47 | }, 48 | // contrib-uglify 49 | uglify: { 50 | dev: { 51 | files: { 52 | '<%= dirs.dest %>/<%= pluginName %>.js': 53 | ['<%= dirs.src %>/**/*.js'] 54 | }, 55 | options: { 56 | beautify: true, 57 | compress: false, 58 | mangle: false, 59 | preserveComments: false 60 | } 61 | }, 62 | min: { 63 | files: { 64 | '<%= dirs.dest %>/<%= pluginName %>.min.js': 65 | ['<%= dirs.src %>/**/*.js'] 66 | }, 67 | options: { 68 | report: 'min' 69 | } 70 | }, 71 | options: { 72 | banner: '<%= banner %>' 73 | } 74 | }, 75 | jsonlint: { 76 | all: { 77 | src: ['*.json'] 78 | } 79 | }, 80 | // karma 81 | karma: { 82 | options: { 83 | configFile: 'karma.conf.js', 84 | autoWatch: true, 85 | singleRun: false 86 | }, 87 | watch: { 88 | 89 | }, 90 | unit: { 91 | autoWatch: false, 92 | singleRun: true 93 | } 94 | }, 95 | // version 96 | version: { 97 | release: { 98 | src: ['*.json'] 99 | } 100 | } 101 | }); 102 | // Load grunt tasks 103 | grunt.loadNpmTasks('grunt-contrib-clean'); 104 | grunt.loadNpmTasks('grunt-contrib-compress'); 105 | grunt.loadNpmTasks('grunt-contrib-jshint'); 106 | grunt.loadNpmTasks('grunt-contrib-uglify'); 107 | grunt.loadNpmTasks('grunt-jsonlint'); 108 | grunt.loadNpmTasks('grunt-karma'); 109 | grunt.loadNpmTasks('grunt-version'); 110 | // Test 111 | grunt.registerTask('test', ['jshint', 'jsonlint', 'test:scenarios']); 112 | grunt.registerTask('test:unit', ['jshint', 'jsonlint', 'karma:unit']); 113 | // Test scenarios 114 | grunt.registerTask('test:scenarios', 115 | 'Test multiple scenarios', function() { 116 | 117 | var scenariosConf = require('./test/scenarios.json'), 118 | scenarios = Object.keys(scenariosConf), 119 | scenariosTasks = [], 120 | karmaConf = {}, 121 | karmaFiles; 122 | // Fetchs the values of karma conf file. 123 | require('./karma.conf.js')({set: function(values) { 124 | karmaConf = values; 125 | }}); 126 | 127 | karmaFiles = karmaConf.files.filter(function(file) { 128 | return !(/vendor/.test(file)); 129 | }); 130 | 131 | grunt.log.write('Configuring scenarios:'.cyan + 132 | ' %s found...'.replace(/%s/, scenarios.length)); 133 | 134 | scenarios.forEach(function(scenario) { 135 | var value = scenariosConf[scenario], 136 | confName = 'karma.scenario_' + scenario.replace(/[._-]/gi, '_'), 137 | conf = { 138 | autoWatch: false, 139 | singleRun: true, 140 | logLevel: 'OFF', 141 | reporters: 'dots' 142 | }; 143 | 144 | if (Array.isArray(value)) { 145 | conf.options = { 146 | files: value.concat(karmaFiles) 147 | }; 148 | } else { 149 | conf.configFile = value; 150 | } 151 | // Register the scenario 152 | grunt.config.set(confName, conf); 153 | scenariosTasks.push(confName.replace(/\./, ':')); 154 | }); 155 | grunt.log.ok(); 156 | // Run all scenarios tasks 157 | grunt.task.run(scenariosTasks); 158 | }); 159 | // Build 160 | grunt.registerTask('build', ['uglify', 'test:all', 'version', 'compress']); 161 | // Develop 162 | grunt.registerTask('default', ['karma:watch']); 163 | }; 164 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Edson Hilios 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [The Final Countdown](http://hilios.github.io/jQuery.countdown/) [![Build Status](https://travis-ci.org/hilios/jQuery.countdown.svg)](https://travis-ci.org/hilios/jQuery.countdown) 2 | ===================== 3 | 4 | #### A simple and html agnostic date countdown plugin for jQuery #### 5 | 6 | To get started, check-it out: http://hilios.github.io/jQuery.countdown/ 7 | 8 | The ultimate countdown plugin designed to fit in any coupon, auction site or product launch. Read our [Documentation](http://hilios.github.io/jQuery.countdown/documentation.html) and follow our [Examples](http://hilios.github.io/jQuery.countdown/examples.html) to see what suits your particular needs. 9 | 10 | #### [Download](https://github.com/hilios/jQuery.countdown/releases/download/2.1.0/jquery.countdown-2.1.0.zip) #### 11 | 12 | [Click here to download the latest version](https://github.com/hilios/jQuery.countdown/releases/download/2.1.0/jquery.countdown-2.1.0.zip) 13 | 14 | If you want to clone the repo always use the files under [dist](https://github.com/hilios/jQuery.countdown/tree/master/dist) folder, they are optimized for production and development. 15 | 16 | #### Install via Bower #### 17 | 18 | ``` 19 | bower install jquery.countdown 20 | ``` 21 | 22 | Add a `script` to your html: 23 | 24 | ```html 25 | 26 | ``` 27 | 28 | #### Install via NPM 29 | 30 | ``` 31 | npm install --save jquery-countdown 32 | ``` 33 | 34 | Require the script: 35 | 36 | ```js 37 | require('jquery-countdown'); 38 | ``` 39 | 40 | Getting started 41 | --------------- 42 | 43 | ```html 44 |
45 | 50 | ``` 51 | 52 | ### Requirements ### 53 | 54 | Since version 2.0.0 we only support jQuery above **1.7** (including **2.x** and **3.x**). For legacy **1.6** support please use the version [1.0.2](https://github.com/hilios/jQuery.countdown/releases/download/1.0.2/jquery.countdown-1.0.2.zip). 55 | 56 | ### [Documentation](http://hilios.github.io/jQuery.countdown/documentation.html) ### 57 | 58 | Our documentation is powered by [Jekyll](http://jekyllrb.com/) (see `gh-page` branch) and hosted in GitHub Pages at [http://hilios.github.io/jQuery.countdown/](http://hilios.github.io/jQuery.countdown/documentation.html). 59 | 60 | ### [Examples](http://hilios.github.io/jQuery.countdown/examples.html) ### 61 | 62 | There are few ways to get started, from the most simple example to advanced, we support many different countdown styles, see wich one fits your scenario, and if anyone doesn't it's a good starting point to customize your output. 63 | 64 | - [Basic coupon site with format N days hr:min:sec](http://hilios.github.io/jQuery.countdown/examples/basic-coupon-site.html) 65 | - [Advance coupon with conditionals and pluralization, format N weeks N days hr:min:sec](http://hilios.github.io/jQuery.countdown/examples/advanced-coupon-site.html) 66 | - [Product launch in... (callback style)](http://hilios.github.io/jQuery.countdown/examples/website-launch.html) 67 | - [New year's eve (legacy style)](http://hilios.github.io/jQuery.countdown/examples/legacy-style.html) 68 | - [Multiple instances on the same page](http://hilios.github.io/jQuery.countdown/examples/multiple-instances.html) 69 | - [Calculate the countdown total hours](http://hilios.github.io/jQuery.countdown/examples/show-total-hours.html) 70 | 71 | [Release notes](https://github.com/hilios/jQuery.countdown/releases) 72 | --------------- 73 | 74 | Current version is **2.2.0**, to follow our change log please visit the [release notes](https://github.com/hilios/jQuery.countdown/releases). 75 | 76 | #### What's new in 2.2.0? #### 77 | 78 | * Total count for hours `%I`, minutes `%N` and seconds `%T`; 79 | * Count to weeks left to complete a month `%W`; 80 | * Deferred initialization, allows to control the exact start moment; 81 | * Fix pluralization bug when return is zero; 82 | 83 | #### What's new in 2.1.0? #### 84 | 85 | * Add proper offset for days left to a month and to a week; 86 | * Fix bower amd install; 87 | 88 | Contributing 89 | ------------ 90 | 91 | The Final Countdown uses **Grunt** and **Bower** with convenient methods for developing the plugin. It's how we compile our code and run tests. To get started install [NodeJS](http://nodejs.org/), [Bower](http://bower.io/), and then run some Grunt/Bower commands. 92 | 93 | ```shell 94 | bower install 95 | npm install 96 | grunt test # Lint code and run test suite 97 | grunt build # Generate the release files (dev, min and zip) 98 | grunt # Watch for updates than test and build 99 | ``` 100 | 101 | This plugin is tested with [QUnit](http://qunitjs.com/), under jQuery 1.7 up to 3.1, Bootstrap 3.0 and RequireJS. 102 | 103 | The functional tests made against: 104 | 105 | * Chrome >= 12 106 | * Safari >= 5 107 | * Firefox >= 5.0 108 | * IE 7/8/9 109 | 110 | Code coverage: 111 | 112 | ```sh 113 | ---------------|----------|----------|----------|----------|----------------| 114 | File | % Stmts | % Branch | % Funcs | % Lines |Uncovered Lines | 115 | ---------------|----------|----------|----------|----------|----------------| 116 | src/ | 98.4 | 90.63 | 100 | 98.4 | | 117 | countdown.js | 98.4 | 90.63 | 100 | 98.4 | 6,283 | 118 | ---------------|----------|----------|----------|----------|----------------| 119 | All files | 98.4 | 90.63 | 100 | 98.4 | | 120 | ---------------|----------|----------|----------|----------|----------------| 121 | ``` 122 | 123 | ### Contributors ### 124 | 125 | Thanks for bug reporting and fixes: 126 | 127 | * Daniel Leavitt (@dleavitt) 128 | * Fagner Brack (@FagnerMartinsBrack) 129 | * Matthew Sigley (@msigley) 130 | * Roman Shterenzon (@romanbsd) 131 | * Marios (@assiotis) 132 | * Zane Yao (@yaoazhen) 133 | * Ricardo Calvo (@ricardocalvo) 134 | * Daniel Z. (@nietonfir) 135 | 136 | ### License ### 137 | 138 | Copyright (c) 2011-2015 Edson Hilios. This is a free software is licensed under the MIT License. 139 | 140 | * [Edson Hilios](http://edson.hilios.com.br). Mail me: edson (at) hilios (dot) com (dot) br 141 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery.countdown", 3 | "version": "2.2.0", 4 | "description": "jQuery plugin to display a countdown clock on your page", 5 | "keywords": [ 6 | "countdown", 7 | "coupons", 8 | "auction", 9 | "clock", 10 | "datetime", 11 | "date" 12 | ], 13 | "author": "Edson Hilios", 14 | "homepage": "http://hilios.github.io/jQuery.countdown/", 15 | "license": [ 16 | { 17 | "type": "MIT", 18 | "url": "https://github.com/hilios/jQuery.countdown/blob/2.0.4/LICENSE.md" 19 | } 20 | ], 21 | "main": [ 22 | "dist/jquery.countdown.js" 23 | ], 24 | "ignore": [ 25 | "**/.*", 26 | "src", 27 | "test", 28 | "package.json", 29 | "Gruntfile.js", 30 | "node_modules", 31 | "bower_components" 32 | ], 33 | "dependencies": { 34 | "jquery": ">=1.7" 35 | }, 36 | "devDependencies": { 37 | "jquery-3.1": "jquery#~3.1", 38 | "jquery-3.0": "jquery#~3.0", 39 | "jquery-2.2": "jquery#~2.2", 40 | "jquery-2.1": "jquery#~2.1", 41 | "jquery-2.0": "jquery#~2.0", 42 | "jquery-1.9": "jquery#~1.9", 43 | "jquery-1.8": "jquery#~1.8", 44 | "jquery-1.7": "jquery#~1.7", 45 | "jquery-1.10": "jquery#~1.10", 46 | "jquery-1.11": "jquery#~1.11", 47 | "bootstrap": "~3" 48 | }, 49 | "moduleType": [ 50 | "amd" 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /countdown.jquery.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "countdown", 3 | "version": "2.2.0", 4 | "title": "The Final Countdown", 5 | "description": "jQuery plugin to display a countdown clock on your page.", 6 | "keywords": [ 7 | "countdown", 8 | "coupons", 9 | "auction", 10 | "clock", 11 | "datetime", 12 | "date" 13 | ], 14 | "author": { 15 | "name": "Edson Hilios", 16 | "url": "http://edson.hilios.com.br" 17 | }, 18 | "homepage": "http://hilios.github.io/jQuery.countdown/", 19 | "docs": "http://hilios.github.io/jQuery.countdown/documentation.html", 20 | "demo": "http://hilios.github.io/jQuery.countdown/examples.html", 21 | "bugs": "https://github.com/hilios/jQuery.countdown/issues", 22 | "download": "https://github.com/hilios/jQuery.countdown/releases/download/2.0.4/jquery.countdown-2.0.4.zip", 23 | "licenses": [ 24 | { 25 | "type": "MIT", 26 | "url": "https://github.com/hilios/jQuery.countdown/blob/2.0.4/LICENSE.md" 27 | } 28 | ], 29 | "dependencies": { 30 | "jquery": ">=1.7" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /dist/jquery.countdown.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * The Final Countdown for jQuery v2.2.0 (http://hilios.github.io/jQuery.countdown/) 3 | * Copyright (c) 2016 Edson Hilios 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | * this software and associated documentation files (the "Software"), to deal in 7 | * the Software without restriction, including without limitation the rights to 8 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | * the Software, and to permit persons to whom the Software is furnished to do so, 10 | * 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, FITNESS 17 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | */ 22 | (function(factory) { 23 | "use strict"; 24 | if (typeof define === "function" && define.amd) { 25 | define([ "jquery" ], factory); 26 | } else { 27 | factory(jQuery); 28 | } 29 | })(function($) { 30 | "use strict"; 31 | var instances = [], matchers = [], defaultOptions = { 32 | precision: 100, 33 | elapse: false, 34 | defer: false 35 | }; 36 | matchers.push(/^[0-9]*$/.source); 37 | matchers.push(/([0-9]{1,2}\/){2}[0-9]{4}( [0-9]{1,2}(:[0-9]{2}){2})?/.source); 38 | matchers.push(/[0-9]{4}([\/\-][0-9]{1,2}){2}( [0-9]{1,2}(:[0-9]{2}){2})?/.source); 39 | matchers = new RegExp(matchers.join("|")); 40 | function parseDateString(dateString) { 41 | if (dateString instanceof Date) { 42 | return dateString; 43 | } 44 | if (String(dateString).match(matchers)) { 45 | if (String(dateString).match(/^[0-9]*$/)) { 46 | dateString = Number(dateString); 47 | } 48 | if (String(dateString).match(/\-/)) { 49 | dateString = String(dateString).replace(/\-/g, "/"); 50 | } 51 | return new Date(dateString); 52 | } else { 53 | throw new Error("Couldn't cast `" + dateString + "` to a date object."); 54 | } 55 | } 56 | var DIRECTIVE_KEY_MAP = { 57 | Y: "years", 58 | m: "months", 59 | n: "daysToMonth", 60 | d: "daysToWeek", 61 | w: "weeks", 62 | W: "weeksToMonth", 63 | H: "hours", 64 | M: "minutes", 65 | S: "seconds", 66 | D: "totalDays", 67 | I: "totalHours", 68 | N: "totalMinutes", 69 | T: "totalSeconds" 70 | }; 71 | function escapedRegExp(str) { 72 | var sanitize = str.toString().replace(/([.?*+^$[\]\\(){}|-])/g, "\\$1"); 73 | return new RegExp(sanitize); 74 | } 75 | function strftime(offsetObject) { 76 | return function(format) { 77 | var directives = format.match(/%(-|!)?[A-Z]{1}(:[^;]+;)?/gi); 78 | if (directives) { 79 | for (var i = 0, len = directives.length; i < len; ++i) { 80 | var directive = directives[i].match(/%(-|!)?([a-zA-Z]{1})(:[^;]+;)?/), regexp = escapedRegExp(directive[0]), modifier = directive[1] || "", plural = directive[3] || "", value = null; 81 | directive = directive[2]; 82 | if (DIRECTIVE_KEY_MAP.hasOwnProperty(directive)) { 83 | value = DIRECTIVE_KEY_MAP[directive]; 84 | value = Number(offsetObject[value]); 85 | } 86 | if (value !== null) { 87 | if (modifier === "!") { 88 | value = pluralize(plural, value); 89 | } 90 | if (modifier === "") { 91 | if (value < 10) { 92 | value = "0" + value.toString(); 93 | } 94 | } 95 | format = format.replace(regexp, value.toString()); 96 | } 97 | } 98 | } 99 | format = format.replace(/%%/, "%"); 100 | return format; 101 | }; 102 | } 103 | function pluralize(format, count) { 104 | var plural = "s", singular = ""; 105 | if (format) { 106 | format = format.replace(/(:|;|\s)/gi, "").split(/\,/); 107 | if (format.length === 1) { 108 | plural = format[0]; 109 | } else { 110 | singular = format[0]; 111 | plural = format[1]; 112 | } 113 | } 114 | if (Math.abs(count) > 1) { 115 | return plural; 116 | } else { 117 | return singular; 118 | } 119 | } 120 | var Countdown = function(el, finalDate, options) { 121 | this.el = el; 122 | this.$el = $(el); 123 | this.interval = null; 124 | this.offset = {}; 125 | this.options = $.extend({}, defaultOptions); 126 | this.firstTick = true; 127 | this.instanceNumber = instances.length; 128 | instances.push(this); 129 | this.$el.data("countdown-instance", this.instanceNumber); 130 | if (options) { 131 | if (typeof options === "function") { 132 | this.$el.on("update.countdown", options); 133 | this.$el.on("stoped.countdown", options); 134 | this.$el.on("finish.countdown", options); 135 | } else { 136 | this.options = $.extend({}, defaultOptions, options); 137 | } 138 | } 139 | this.setFinalDate(finalDate); 140 | if (this.options.defer === false) { 141 | this.start(); 142 | } 143 | }; 144 | $.extend(Countdown.prototype, { 145 | start: function() { 146 | if (this.interval !== null) { 147 | clearInterval(this.interval); 148 | } 149 | var self = this; 150 | this.update(); 151 | this.interval = setInterval(function() { 152 | self.update.call(self); 153 | }, this.options.precision); 154 | }, 155 | stop: function() { 156 | clearInterval(this.interval); 157 | this.interval = null; 158 | this.dispatchEvent("stoped"); 159 | }, 160 | toggle: function() { 161 | if (this.interval) { 162 | this.stop(); 163 | } else { 164 | this.start(); 165 | } 166 | }, 167 | pause: function() { 168 | this.stop(); 169 | }, 170 | resume: function() { 171 | this.start(); 172 | }, 173 | remove: function() { 174 | this.stop.call(this); 175 | instances[this.instanceNumber] = null; 176 | delete this.$el.data().countdownInstance; 177 | }, 178 | setFinalDate: function(value) { 179 | this.finalDate = parseDateString(value); 180 | }, 181 | update: function() { 182 | if (this.$el.closest("html").length === 0) { 183 | this.remove(); 184 | return; 185 | } 186 | var now = new Date(), newTotalSecsLeft; 187 | newTotalSecsLeft = this.finalDate.getTime() - now.getTime(); 188 | newTotalSecsLeft = Math.ceil(newTotalSecsLeft / 1e3); 189 | newTotalSecsLeft = !this.options.elapse && newTotalSecsLeft < 0 ? 0 : Math.abs(newTotalSecsLeft); 190 | if (this.totalSecsLeft === newTotalSecsLeft || this.firstTick) { 191 | this.firstTick = false; 192 | return; 193 | } else { 194 | this.totalSecsLeft = newTotalSecsLeft; 195 | } 196 | this.elapsed = now >= this.finalDate; 197 | this.offset = { 198 | seconds: this.totalSecsLeft % 60, 199 | minutes: Math.floor(this.totalSecsLeft / 60) % 60, 200 | hours: Math.floor(this.totalSecsLeft / 60 / 60) % 24, 201 | days: Math.floor(this.totalSecsLeft / 60 / 60 / 24) % 7, 202 | daysToWeek: Math.floor(this.totalSecsLeft / 60 / 60 / 24) % 7, 203 | daysToMonth: Math.floor(this.totalSecsLeft / 60 / 60 / 24 % 30.4368), 204 | weeks: Math.floor(this.totalSecsLeft / 60 / 60 / 24 / 7), 205 | weeksToMonth: Math.floor(this.totalSecsLeft / 60 / 60 / 24 / 7) % 4, 206 | months: Math.floor(this.totalSecsLeft / 60 / 60 / 24 / 30.4368), 207 | years: Math.abs(this.finalDate.getFullYear() - now.getFullYear()), 208 | totalDays: Math.floor(this.totalSecsLeft / 60 / 60 / 24), 209 | totalHours: Math.floor(this.totalSecsLeft / 60 / 60), 210 | totalMinutes: Math.floor(this.totalSecsLeft / 60), 211 | totalSeconds: this.totalSecsLeft 212 | }; 213 | if (!this.options.elapse && this.totalSecsLeft === 0) { 214 | this.stop(); 215 | this.dispatchEvent("finish"); 216 | } else { 217 | this.dispatchEvent("update"); 218 | } 219 | }, 220 | dispatchEvent: function(eventName) { 221 | var event = $.Event(eventName + ".countdown"); 222 | event.finalDate = this.finalDate; 223 | event.elapsed = this.elapsed; 224 | event.offset = $.extend({}, this.offset); 225 | event.strftime = strftime(this.offset); 226 | this.$el.trigger(event); 227 | } 228 | }); 229 | $.fn.countdown = function() { 230 | var argumentsArray = Array.prototype.slice.call(arguments, 0); 231 | return this.each(function() { 232 | var instanceNumber = $(this).data("countdown-instance"); 233 | if (instanceNumber !== undefined) { 234 | var instance = instances[instanceNumber], method = argumentsArray[0]; 235 | if (Countdown.prototype.hasOwnProperty(method)) { 236 | instance[method].apply(instance, argumentsArray.slice(1)); 237 | } else if (String(method).match(/^[$A-Z_][0-9A-Z_$]*$/i) === null) { 238 | instance.setFinalDate.call(instance, method); 239 | instance.start(); 240 | } else { 241 | $.error("Method %s does not exist on jQuery.countdown".replace(/\%s/gi, method)); 242 | } 243 | } else { 244 | new Countdown(this, argumentsArray[0], argumentsArray[1]); 245 | } 246 | }); 247 | }; 248 | }); -------------------------------------------------------------------------------- /dist/jquery.countdown.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * The Final Countdown for jQuery v2.2.0 (http://hilios.github.io/jQuery.countdown/) 3 | * Copyright (c) 2016 Edson Hilios 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | * this software and associated documentation files (the "Software"), to deal in 7 | * the Software without restriction, including without limitation the rights to 8 | * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | * the Software, and to permit persons to whom the Software is furnished to do so, 10 | * 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, FITNESS 17 | * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | */ 22 | !function(a){"use strict";"function"==typeof define&&define.amd?define(["jquery"],a):a(jQuery)}(function(a){"use strict";function b(a){if(a instanceof Date)return a;if(String(a).match(g))return String(a).match(/^[0-9]*$/)&&(a=Number(a)),String(a).match(/\-/)&&(a=String(a).replace(/\-/g,"/")),new Date(a);throw new Error("Couldn't cast `"+a+"` to a date object.")}function c(a){var b=a.toString().replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1");return new RegExp(b)}function d(a){return function(b){var d=b.match(/%(-|!)?[A-Z]{1}(:[^;]+;)?/gi);if(d)for(var f=0,g=d.length;f1?c:d}var f=[],g=[],h={precision:100,elapse:!1,defer:!1};g.push(/^[0-9]*$/.source),g.push(/([0-9]{1,2}\/){2}[0-9]{4}( [0-9]{1,2}(:[0-9]{2}){2})?/.source),g.push(/[0-9]{4}([\/\-][0-9]{1,2}){2}( [0-9]{1,2}(:[0-9]{2}){2})?/.source),g=new RegExp(g.join("|"));var i={Y:"years",m:"months",n:"daysToMonth",d:"daysToWeek",w:"weeks",W:"weeksToMonth",H:"hours",M:"minutes",S:"seconds",D:"totalDays",I:"totalHours",N:"totalMinutes",T:"totalSeconds"},j=function(b,c,d){this.el=b,this.$el=a(b),this.interval=null,this.offset={},this.options=a.extend({},h),this.firstTick=!0,this.instanceNumber=f.length,f.push(this),this.$el.data("countdown-instance",this.instanceNumber),d&&("function"==typeof d?(this.$el.on("update.countdown",d),this.$el.on("stoped.countdown",d),this.$el.on("finish.countdown",d)):this.options=a.extend({},h,d)),this.setFinalDate(c),this.options.defer===!1&&this.start()};a.extend(j.prototype,{start:function(){null!==this.interval&&clearInterval(this.interval);var a=this;this.update(),this.interval=setInterval(function(){a.update.call(a)},this.options.precision)},stop:function(){clearInterval(this.interval),this.interval=null,this.dispatchEvent("stoped")},toggle:function(){this.interval?this.stop():this.start()},pause:function(){this.stop()},resume:function(){this.start()},remove:function(){this.stop.call(this),f[this.instanceNumber]=null,delete this.$el.data().countdownInstance},setFinalDate:function(a){this.finalDate=b(a)},update:function(){if(0===this.$el.closest("html").length)return void this.remove();var a,b=new Date;return a=this.finalDate.getTime()-b.getTime(),a=Math.ceil(a/1e3),a=!this.options.elapse&&a<0?0:Math.abs(a),this.totalSecsLeft===a||this.firstTick?void(this.firstTick=!1):(this.totalSecsLeft=a,this.elapsed=b>=this.finalDate,this.offset={seconds:this.totalSecsLeft%60,minutes:Math.floor(this.totalSecsLeft/60)%60,hours:Math.floor(this.totalSecsLeft/60/60)%24,days:Math.floor(this.totalSecsLeft/60/60/24)%7,daysToWeek:Math.floor(this.totalSecsLeft/60/60/24)%7,daysToMonth:Math.floor(this.totalSecsLeft/60/60/24%30.4368),weeks:Math.floor(this.totalSecsLeft/60/60/24/7),weeksToMonth:Math.floor(this.totalSecsLeft/60/60/24/7)%4,months:Math.floor(this.totalSecsLeft/60/60/24/30.4368),years:Math.abs(this.finalDate.getFullYear()-b.getFullYear()),totalDays:Math.floor(this.totalSecsLeft/60/60/24),totalHours:Math.floor(this.totalSecsLeft/60/60),totalMinutes:Math.floor(this.totalSecsLeft/60),totalSeconds:this.totalSecsLeft},void(this.options.elapse||0!==this.totalSecsLeft?this.dispatchEvent("update"):(this.stop(),this.dispatchEvent("finish"))))},dispatchEvent:function(b){var c=a.Event(b+".countdown");c.finalDate=this.finalDate,c.elapsed=this.elapsed,c.offset=a.extend({},this.offset),c.strftime=d(this.offset),this.$el.trigger(c)}}),a.fn.countdown=function(){var b=Array.prototype.slice.call(arguments,0);return this.each(function(){var c=a(this).data("countdown-instance");if(void 0!==c){var d=f[c],e=b[0];j.prototype.hasOwnProperty(e)?d[e].apply(d,b.slice(1)):null===String(e).match(/^[$A-Z_][0-9A-Z_$]*$/i)?(d.setFinalDate.call(d,e),d.start()):a.error("Method %s does not exist on jQuery.countdown".replace(/\%s/gi,e))}else new j(this,b[0],b[1])})}}); -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | module.exports = function(config) { 3 | config.set({ 4 | basePath: '', 5 | frameworks: [ 6 | 'qunit', 7 | 'sinon' 8 | ], 9 | files: [ 10 | 'test/vendor/jquery-2.1/dist/jquery.js', 11 | 'test/*.config.js', 12 | 'src/**/*.js', 13 | 'test/unit/**/*test.js' 14 | ], 15 | exclude: [ 16 | 'karma.conf.js' 17 | ], 18 | reporters: [ 19 | 'dots', 20 | 'coverage' 21 | ], 22 | preprocessors: { 23 | 'src/**/*.js': 'coverage' 24 | }, 25 | coverageReporter: { 26 | type: 'text', 27 | dir: 'test/coverage/' 28 | }, 29 | port: 9876, 30 | colors: true, 31 | logLevel: config.LOG_DISABLE, 32 | browsers: [ 33 | 'PhantomJS' 34 | ], 35 | autoWatch: false, 36 | singleRun: true 37 | }); 38 | }; 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-countdown", 3 | "version": "2.2.0", 4 | "main": "dist/jquery.countdown.js", 5 | "description": "The Final Countdown for jQuery", 6 | "author": "Edson Hilios", 7 | "homepage": "http://hilios.github.io/jQuery.countdown/", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/hilios/jQuery.countdown" 11 | }, 12 | "devDependencies": { 13 | "grunt": "~0.4", 14 | "grunt-contrib-clean": "~0.5", 15 | "grunt-contrib-compress": "~0.6", 16 | "grunt-contrib-jshint": "~0.10", 17 | "grunt-contrib-uglify": "~0.4", 18 | "grunt-jsonlint": "~1.0", 19 | "grunt-karma": "~0.8", 20 | "grunt-version": "~0.3", 21 | "karma": "~0.12", 22 | "karma-coverage": "~0.2", 23 | "karma-phantomjs-launcher": "~0.1", 24 | "karma-qunit": "~0.1", 25 | "karma-requirejs": "~0.2", 26 | "karma-sinon": "~1.0", 27 | "requirejs": "~2.1" 28 | }, 29 | "scripts": { 30 | "test": "grunt test" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": true, 3 | "camelcase": true, 4 | "curly": true, 5 | "eqeqeq": true, 6 | "es3": true, 7 | "forin": true, 8 | "freeze": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "noempty": true, 15 | "nonbsp": true, 16 | "nonew": false, 17 | "plusplus": false, 18 | "quotmark": "single", 19 | "undef": true, 20 | "unused": true, 21 | "strict": true, 22 | "trailing": true, 23 | "maxparams": false, 24 | "maxstatements": false, 25 | "maxcomplexity": 10, 26 | "maxlen": 80, 27 | "gcl": true, 28 | "browser": true, 29 | "devel": false, 30 | "jquery": true, 31 | "globals": { 32 | "require": false, 33 | "define": false 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/countdown.js: -------------------------------------------------------------------------------- 1 | // AMD support (Thanks to @FagnerMartinsBrack) 2 | ;(function(factory) { 3 | 'use strict'; 4 | 5 | if (typeof define === 'function' && define.amd) { 6 | define(['jquery'], factory); 7 | } else { 8 | factory(jQuery); 9 | } 10 | })(function($){ 11 | 'use strict'; 12 | 13 | var instances = [], 14 | matchers = [], 15 | defaultOptions = { 16 | precision: 100, // 0.1 seconds, used to update the DOM 17 | elapse: false, 18 | defer: false 19 | }; 20 | // Miliseconds 21 | matchers.push(/^[0-9]*$/.source); 22 | // Month/Day/Year [hours:minutes:seconds] 23 | matchers.push(/([0-9]{1,2}\/){2}[0-9]{4}( [0-9]{1,2}(:[0-9]{2}){2})?/ 24 | .source); 25 | // Year/Day/Month [hours:minutes:seconds] and 26 | // Year-Day-Month [hours:minutes:seconds] 27 | matchers.push(/[0-9]{4}([\/\-][0-9]{1,2}){2}( [0-9]{1,2}(:[0-9]{2}){2})?/ 28 | .source); 29 | // Cast the matchers to a regular expression object 30 | matchers = new RegExp(matchers.join('|')); 31 | // Parse a Date formatted has String to a native object 32 | function parseDateString(dateString) { 33 | // Pass through when a native object is sent 34 | if(dateString instanceof Date) { 35 | return dateString; 36 | } 37 | // Caste string to date object 38 | if(String(dateString).match(matchers)) { 39 | // If looks like a milisecond value cast to number before 40 | // final casting (Thanks to @msigley) 41 | if(String(dateString).match(/^[0-9]*$/)) { 42 | dateString = Number(dateString); 43 | } 44 | // Replace dashes to slashes 45 | if(String(dateString).match(/\-/)) { 46 | dateString = String(dateString).replace(/\-/g, '/'); 47 | } 48 | return new Date(dateString); 49 | } else { 50 | throw new Error('Couldn\'t cast `' + dateString + 51 | '` to a date object.'); 52 | } 53 | } 54 | // Map to convert from a directive to offset object property 55 | var DIRECTIVE_KEY_MAP = { 56 | 'Y': 'years', 57 | 'm': 'months', 58 | 'n': 'daysToMonth', 59 | 'd': 'daysToWeek', 60 | 'w': 'weeks', 61 | 'W': 'weeksToMonth', 62 | 'H': 'hours', 63 | 'M': 'minutes', 64 | 'S': 'seconds', 65 | 'D': 'totalDays', 66 | 'I': 'totalHours', 67 | 'N': 'totalMinutes', 68 | 'T': 'totalSeconds' 69 | }; 70 | // Returns an escaped regexp from the string 71 | function escapedRegExp(str) { 72 | var sanitize = str.toString().replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); 73 | return new RegExp(sanitize); 74 | } 75 | // Time string formatter 76 | function strftime(offsetObject) { 77 | return function(format) { 78 | var directives = format.match(/%(-|!)?[A-Z]{1}(:[^;]+;)?/gi); 79 | if(directives) { 80 | for(var i = 0, len = directives.length; i < len; ++i) { 81 | var directive = directives[i] 82 | .match(/%(-|!)?([a-zA-Z]{1})(:[^;]+;)?/), 83 | regexp = escapedRegExp(directive[0]), 84 | modifier = directive[1] || '', 85 | plural = directive[3] || '', 86 | value = null; 87 | // Get the key 88 | directive = directive[2]; 89 | // Swap shot-versions directives 90 | if(DIRECTIVE_KEY_MAP.hasOwnProperty(directive)) { 91 | value = DIRECTIVE_KEY_MAP[directive]; 92 | value = Number(offsetObject[value]); 93 | } 94 | if(value !== null) { 95 | // Pluralize 96 | if(modifier === '!') { 97 | value = pluralize(plural, value); 98 | } 99 | // Add zero-padding 100 | if(modifier === '') { 101 | if(value < 10) { 102 | value = '0' + value.toString(); 103 | } 104 | } 105 | // Replace the directive 106 | format = format.replace(regexp, value.toString()); 107 | } 108 | } 109 | } 110 | format = format.replace(/%%/, '%'); 111 | return format; 112 | }; 113 | } 114 | // Pluralize 115 | function pluralize(format, count) { 116 | var plural = 's', singular = ''; 117 | if(format) { 118 | format = format.replace(/(:|;|\s)/gi, '').split(/\,/); 119 | if(format.length === 1) { 120 | plural = format[0]; 121 | } else { 122 | singular = format[0]; 123 | plural = format[1]; 124 | } 125 | } 126 | // Fix #187 127 | if(Math.abs(count) > 1) { 128 | return plural; 129 | } else { 130 | return singular; 131 | } 132 | } 133 | // The Final Countdown 134 | var Countdown = function(el, finalDate, options) { 135 | this.el = el; 136 | this.$el = $(el); 137 | this.interval = null; 138 | this.offset = {}; 139 | this.options = $.extend({}, defaultOptions); 140 | // console.log(this.options); 141 | // This helper variable is necessary to mimick the previous check for an 142 | // event listener on this.$el. Because of the event loop there might not 143 | // be a registered event listener during the first tick. In order to work 144 | // as expected a second tick is necessary, so that the events can be fired 145 | // and handled properly. 146 | this.firstTick = true; 147 | // Register this instance 148 | this.instanceNumber = instances.length; 149 | instances.push(this); 150 | // Save the reference 151 | this.$el.data('countdown-instance', this.instanceNumber); 152 | // Handle options or callback 153 | if (options) { 154 | // Register the callbacks when supplied 155 | if(typeof options === 'function') { 156 | this.$el.on('update.countdown', options); 157 | this.$el.on('stoped.countdown', options); 158 | this.$el.on('finish.countdown', options); 159 | } else { 160 | this.options = $.extend({}, defaultOptions, options); 161 | } 162 | } 163 | // Set the final date and start 164 | this.setFinalDate(finalDate); 165 | // Starts the countdown automatically unless it's defered, 166 | // Issue #198 167 | if (this.options.defer === false) { 168 | this.start(); 169 | } 170 | }; 171 | $.extend(Countdown.prototype, { 172 | start: function() { 173 | if(this.interval !== null) { 174 | clearInterval(this.interval); 175 | } 176 | var self = this; 177 | this.update(); 178 | this.interval = setInterval(function() { 179 | self.update.call(self); 180 | }, this.options.precision); 181 | }, 182 | stop: function() { 183 | clearInterval(this.interval); 184 | this.interval = null; 185 | this.dispatchEvent('stoped'); 186 | }, 187 | toggle: function() { 188 | if (this.interval) { 189 | this.stop(); 190 | } else { 191 | this.start(); 192 | } 193 | }, 194 | pause: function() { 195 | this.stop(); 196 | }, 197 | resume: function() { 198 | this.start(); 199 | }, 200 | remove: function() { 201 | this.stop.call(this); 202 | instances[this.instanceNumber] = null; 203 | // Reset the countdown instance under data attr (Thanks to @assiotis) 204 | delete this.$el.data().countdownInstance; 205 | }, 206 | setFinalDate: function(value) { 207 | this.finalDate = parseDateString(value); // Cast the given date 208 | }, 209 | update: function() { 210 | // Stop if dom is not in the html (Thanks to @dleavitt) 211 | if(this.$el.closest('html').length === 0) { 212 | this.remove(); 213 | return; 214 | } 215 | var now = new Date(), 216 | newTotalSecsLeft; 217 | // Create an offset date object 218 | newTotalSecsLeft = this.finalDate.getTime() - now.getTime(); // Millisecs 219 | // Calculate the remaining time 220 | newTotalSecsLeft = Math.ceil(newTotalSecsLeft / 1000); // Secs 221 | // If is not have to elapse set the finish 222 | newTotalSecsLeft = !this.options.elapse && newTotalSecsLeft < 0 ? 0 : 223 | Math.abs(newTotalSecsLeft); 224 | // Do not proceed to calculation if the seconds have not changed or 225 | // during the first tick 226 | if (this.totalSecsLeft === newTotalSecsLeft || this.firstTick) { 227 | this.firstTick = false; 228 | return; 229 | } else { 230 | this.totalSecsLeft = newTotalSecsLeft; 231 | } 232 | // Check if the countdown has elapsed 233 | this.elapsed = (now >= this.finalDate); 234 | // Calculate the offsets 235 | this.offset = { 236 | seconds : this.totalSecsLeft % 60, 237 | minutes : Math.floor(this.totalSecsLeft / 60) % 60, 238 | hours : Math.floor(this.totalSecsLeft / 60 / 60) % 24, 239 | days : Math.floor(this.totalSecsLeft / 60 / 60 / 24) % 7, 240 | daysToWeek : Math.floor(this.totalSecsLeft / 60 / 60 / 24) % 7, 241 | daysToMonth : Math.floor(this.totalSecsLeft / 60 / 60 / 24 % 30.4368), 242 | weeks : Math.floor(this.totalSecsLeft / 60 / 60 / 24 / 7), 243 | weeksToMonth: Math.floor(this.totalSecsLeft / 60 / 60 / 24 / 7) % 4, 244 | months : Math.floor(this.totalSecsLeft / 60 / 60 / 24 / 30.4368), 245 | years : Math.abs(this.finalDate.getFullYear()-now.getFullYear()), 246 | totalDays : Math.floor(this.totalSecsLeft / 60 / 60 / 24), 247 | totalHours : Math.floor(this.totalSecsLeft / 60 / 60), 248 | totalMinutes: Math.floor(this.totalSecsLeft / 60), 249 | totalSeconds: this.totalSecsLeft 250 | }; 251 | // Dispatch an event 252 | if(!this.options.elapse && this.totalSecsLeft === 0) { 253 | this.stop(); 254 | this.dispatchEvent('finish'); 255 | } else { 256 | this.dispatchEvent('update'); 257 | } 258 | }, 259 | dispatchEvent: function(eventName) { 260 | var event = $.Event(eventName + '.countdown'); 261 | event.finalDate = this.finalDate; 262 | event.elapsed = this.elapsed; 263 | event.offset = $.extend({}, this.offset); 264 | event.strftime = strftime(this.offset); 265 | this.$el.trigger(event); 266 | } 267 | }); 268 | // Register the jQuery selector actions 269 | $.fn.countdown = function() { 270 | var argumentsArray = Array.prototype.slice.call(arguments, 0); 271 | return this.each(function() { 272 | // If no data was set, jQuery.data returns undefined 273 | var instanceNumber = $(this).data('countdown-instance'); 274 | // Verify if we already have a countdown for this node ... 275 | // Fix issue #22 (Thanks to @romanbsd) 276 | if (instanceNumber !== undefined) { 277 | var instance = instances[instanceNumber], 278 | method = argumentsArray[0]; 279 | // If method exists in the prototype execute 280 | if(Countdown.prototype.hasOwnProperty(method)) { 281 | instance[method].apply(instance, argumentsArray.slice(1)); 282 | // If method look like a date try to set a new final date 283 | } else if(String(method).match(/^[$A-Z_][0-9A-Z_$]*$/i) === null) { 284 | instance.setFinalDate.call(instance, method); 285 | // Allow plugin to restart after finished 286 | // Fix issue #38 (thanks to @yaoazhen) 287 | instance.start(); 288 | } else { 289 | $.error('Method %s does not exist on jQuery.countdown' 290 | .replace(/\%s/gi, method)); 291 | } 292 | } else { 293 | // ... if not we create an instance 294 | new Countdown(this, argumentsArray[0], argumentsArray[1]); 295 | } 296 | }); 297 | }; 298 | }); 299 | -------------------------------------------------------------------------------- /test/integration/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration for RequireJS integration 2 | module.exports = function(config) { 3 | config.set({ 4 | basePath: '../../', 5 | frameworks: [ 6 | 'requirejs', 7 | 'qunit', 8 | 'sinon' 9 | ], 10 | files: [ 11 | { 12 | pattern: 'test/vendor/jquery-2.1/dist/jquery.js', 13 | included: false 14 | }, 15 | { 16 | pattern: 'src/**/*.js', 17 | included: false 18 | }, 19 | 'test/integration/*.config.js', 20 | 'test/integration/*test.js' 21 | ], 22 | exclude: [ 23 | 'karma.conf.js' 24 | ], 25 | reporters: [ 26 | 'dots' 27 | ], 28 | port: 9876, 29 | colors: true, 30 | logLevel: config.LOG_INFO, 31 | browsers: [ 32 | 'PhantomJS' 33 | ], 34 | autoWatch: false, 35 | singleRun: true 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /test/integration/requirejs.config.js: -------------------------------------------------------------------------------- 1 | var testFiles = []; 2 | var TEST_REGEXP = /(spec|test)\.js$/i; 3 | 4 | var pathToModule = function(path) { 5 | return path.replace(/^\/base\//, '').replace(/\.js$/, ''); 6 | }; 7 | 8 | Object.keys(window.__karma__.files).forEach(function(file) { 9 | if (TEST_REGEXP.test(file)) { 10 | // Normalize paths to RequireJS module names. 11 | testFiles.push(pathToModule(file)); 12 | } 13 | }); 14 | 15 | require.config({ 16 | baseUrl: '/base', 17 | paths: { 18 | 'jquery': 'test/vendor/jquery-2.1/dist/jquery', 19 | 'jquery.countdown': 'src/countdown' 20 | }, 21 | shim: { 22 | 'jquery': { 23 | exports: '$' 24 | }, 25 | 'jquery.countdown': { 26 | deps: ['jquery'] 27 | } 28 | }, 29 | deps: testFiles, 30 | callback: window.__karma__.start 31 | }); 32 | -------------------------------------------------------------------------------- /test/integration/requirejs_test.js: -------------------------------------------------------------------------------- 1 | define('test/integration/requirejs_test', 2 | ['jquery', 'jquery.countdown'], function($) { 3 | 4 | test('integration with requirejs', function() { 5 | ok(!!$.fn.countdown, 'Should load the plugin via requireJS'); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /test/qunit.config.js: -------------------------------------------------------------------------------- 1 | module('jQuery The Final Countdown'); 2 | 3 | // Helpers 4 | var uid = 0, $dom, $clock; 5 | 6 | QUnit.testStart(function() { 7 | $clock = sinon.useFakeTimers(); 8 | $dom = $('
').appendTo('body'); 9 | }); 10 | 11 | QUnit.testDone(function() { 12 | $clock.restore(); 13 | try{ 14 | $dom.countdown('remove'); 15 | $dom.remove(); 16 | $dom = null; 17 | } catch(e) { 18 | // ... countdown wasn't created 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /test/scenarios.json: -------------------------------------------------------------------------------- 1 | { 2 | "jquery-1.10": ["test/vendor/jquery-1.10/jquery.js"], 3 | "jquery-1.11": ["test/vendor/jquery-1.11/dist/jquery.js"], 4 | "jquery-1.7": ["test/vendor/jquery-1.7/jquery.js"], 5 | "jquery-1.8": ["test/vendor/jquery-1.8/jquery.js"], 6 | "jquery-1.9": ["test/vendor/jquery-1.9/jquery.js"], 7 | "jquery-2.0": ["test/vendor/jquery-2.0/jquery.js"], 8 | "jquery-2.1": ["test/vendor/jquery-2.1/dist/jquery.js"], 9 | "jquery-2.2": ["test/vendor/jquery-2.2/dist/jquery.js"], 10 | "jquery-3.0": ["test/vendor/jquery-3.0/dist/jquery.js"], 11 | "jquery-3.1": ["test/vendor/jquery-3.1/dist/jquery.js"], 12 | "bootstrap": [ 13 | "test/vendor/jquery-2.1/dist/jquery.js", 14 | "test/vendor/bootstrap/dist/js/bootstrap.js" 15 | ], 16 | "requirejs": "test/integration/karma.conf.js" 17 | } 18 | -------------------------------------------------------------------------------- /test/unit/date_casting_test.js: -------------------------------------------------------------------------------- 1 | module('Date casting'); 2 | 3 | test('throw an error when string cannot be parsed', function() { 4 | throws(function() { 5 | $dom.countdown('Foo Bar!'); 6 | }); 7 | }); 8 | 9 | test('accept a native Date object', function() { 10 | // Remember that the month starts at zero 0 11 | var date = new Date(2020, 7, 8, 9, 10, 11); 12 | $dom.countdown(date).on('update.countdown', function(event) { 13 | ok(event.finalDate.toString().match(/Aug 08 2020 09:10:11/)); 14 | }); 15 | $clock.tick(500); 16 | }); 17 | 18 | test('cast miliseconds as a string (issue #17)', function() { 19 | // Remember that the month starts at zero 0 20 | var ms = new Date(2020, 8, 9, 10, 11, 12).getTime(); 21 | $dom.countdown(String(ms)).on('update.countdown', function(event) { 22 | ok(event.finalDate.toString().match(/Sep 09 2020 10:11:12/)); 23 | }); 24 | $clock.tick(500); 25 | }); 26 | 27 | test('cast miliseconds as a number', function() { 28 | // Remember that the month starts at zero 0 29 | var ms = new Date(2020, 8, 9, 10, 11, 12).getTime(); 30 | $dom.countdown(Number(ms)).on('update.countdown', function(event) { 31 | ok(event.finalDate.toString().match(/Sep 09 2020 10:11:12/)); 32 | }); 33 | $clock.tick(500); 34 | }); 35 | 36 | test('cast YYYY/MM/DD hh:mm:ss', function() { 37 | $dom.countdown('2020/10/20 12:34:56').on('update.countdown', function(event) { 38 | ok(event.finalDate.toString().match(/Oct 20 2020 12:34:56/)); 39 | }); 40 | $clock.tick(500); 41 | }); 42 | 43 | test('cast YYYY-MM-DD hh:mm:ss', function() { 44 | $dom.countdown('2020-10-20 12:34:56').on('update.countdown', function(event) { 45 | ok(event.finalDate.toString().match(/Oct 20 2020 12:34:56/)); 46 | }); 47 | $clock.tick(500); 48 | }); 49 | 50 | test('cast YYYY/MM/DD', function() { 51 | $dom.countdown('2020/10/20').on('update.countdown', function(event) { 52 | ok(event.finalDate.toString().match(/Oct 20 2020 00:00:00/)); 53 | }); 54 | $clock.tick(500); 55 | }); 56 | 57 | test('cast MM/DD/YYYY', function() { 58 | $dom.countdown('11/22/2020').on('update.countdown', function(event) { 59 | ok(event.finalDate.toString().match(/Nov 22 2020 00:00:00/)); 60 | }); 61 | $clock.tick(500); 62 | }); 63 | 64 | test('cast MM/DD/YYYY', function() { 65 | $dom.countdown('11/22/2020').on('update.countdown', function(event) { 66 | ok(event.finalDate.toString().match(/Nov 22 2020 00:00:00/)); 67 | }); 68 | $clock.tick(500); 69 | }); 70 | -------------------------------------------------------------------------------- /test/unit/event_test.js: -------------------------------------------------------------------------------- 1 | module('Events'); 2 | 3 | test('trigger the update event', function() { 4 | var callback = sinon.spy(); 5 | $dom.countdown('2020/10/20').on('update.countdown', callback); 6 | $clock.tick(500); 7 | ok(callback.callCount > 0); 8 | }); 9 | 10 | test('trigger the finish event', function() { 11 | var now = new Date(); 12 | var callback = sinon.spy(); 13 | $dom.countdown(now).on('finish.countdown', callback); 14 | // Expect the callback to be called once (Issue #82) 15 | $clock.tick(2000); 16 | ok(callback.callCount === 1); 17 | }); 18 | 19 | test('event object has {type, strftime, finalDate, offset, elapsed} ' + 20 | 'properties', function() { 21 | $dom.countdown('2020/10/20').on('update.countdown', function(event) { 22 | ok(event.hasOwnProperty('type')); 23 | ok(event.hasOwnProperty('strftime')); 24 | ok(event.hasOwnProperty('finalDate')); 25 | ok(event.hasOwnProperty('offset')); 26 | ok(event.hasOwnProperty('elapsed')); 27 | }); 28 | $clock.tick(500); 29 | }); 30 | 31 | test('event.offset object has {seconds, minutes, hours, days, ' + 32 | 'totalDays, weeks, years} properties', function() { 33 | $dom.countdown('2020/10/20').on('update.countdown', function(event) { 34 | ok(event.offset.hasOwnProperty('seconds')); 35 | ok(event.offset.hasOwnProperty('minutes')); 36 | ok(event.offset.hasOwnProperty('hours')); 37 | ok(event.offset.hasOwnProperty('days')); 38 | ok(event.offset.hasOwnProperty('daysToWeek')); 39 | ok(event.offset.hasOwnProperty('daysToMonth')); 40 | ok(event.offset.hasOwnProperty('weeks')); 41 | ok(event.offset.hasOwnProperty('weeksToMonth')); 42 | ok(event.offset.hasOwnProperty('years')); 43 | ok(event.offset.hasOwnProperty('totalDays')); 44 | ok(event.offset.hasOwnProperty('totalHours')); 45 | ok(event.offset.hasOwnProperty('totalMinutes')); 46 | ok(event.offset.hasOwnProperty('totalSeconds')); 47 | }); 48 | $clock.tick(500); 49 | }); 50 | 51 | test('allow the callback be setted uppon initialization (legacy ' + 52 | 'style)', function() { 53 | $dom.countdown('2020/10/20', function(event) { 54 | ok(true); // Up to this point the event was dispatched 55 | }); 56 | $clock.tick(500); 57 | }); 58 | 59 | test('event leaking and collision', function() { 60 | var event; 61 | var future = new Date().getTime() + 5000; 62 | var clickHandler = sinon.spy(); 63 | var updateHandler = sinon.spy(); 64 | 65 | $dom.countdown(future) 66 | .on('click', clickHandler) 67 | .on('update.countdown', updateHandler); 68 | $clock.tick(500); 69 | 70 | ok(!clickHandler.called); 71 | ok(updateHandler.called); 72 | 73 | event = updateHandler.lastCall.args[0]; 74 | ok(event.namespace === 'countdown'); 75 | 76 | $dom.trigger('click'); 77 | updateHandler.reset(); 78 | 79 | ok(clickHandler.called); 80 | ok(!updateHandler.called); 81 | 82 | // Trigger event without the namespace 83 | $dom.trigger('update'); 84 | ok(updateHandler.called); 85 | 86 | event = updateHandler.lastCall.args[0]; 87 | ok(event.namespace === ''); 88 | }); 89 | 90 | test('event bubbling with event listeners registered after ' + 91 | 'invocation', function() { 92 | var $doc = $(document), 93 | event, 94 | future = new Date().getTime() + 5000, 95 | updateHandler = sinon.spy(), 96 | finishHandler = sinon.spy(); 97 | 98 | $dom.countdown(future); 99 | 100 | $doc 101 | .on('update.countdown', updateHandler) 102 | .on('finish.countdown', finishHandler); 103 | $clock.tick(500); 104 | 105 | ok(updateHandler.called); 106 | 107 | event = updateHandler.lastCall.args[0]; 108 | ok(event.namespace === 'countdown'); 109 | ok(event.type === 'update'); 110 | ok(!event.elapsed); 111 | 112 | $clock.tick(5000); 113 | ok(updateHandler.called); 114 | 115 | event = finishHandler.lastCall.args[0]; 116 | ok(event.namespace === 'countdown'); 117 | ok(event.type === 'finish'); 118 | ok(event.elapsed); 119 | 120 | // cleanup 121 | $doc = null; 122 | }); 123 | 124 | test('event bubbling with event listeners registered before ' + 125 | 'invocation', function() { 126 | var $doc = $(document), 127 | event, 128 | future = new Date().getTime() + 5000, 129 | updateHandler = sinon.spy(), 130 | finishHandler = sinon.spy(); 131 | 132 | $doc 133 | .on('update.countdown', updateHandler) 134 | .on('finish.countdown', finishHandler); 135 | 136 | $dom.countdown(future); 137 | $clock.tick(500); 138 | 139 | ok(updateHandler.called); 140 | 141 | event = updateHandler.lastCall.args[0]; 142 | ok(event.namespace === 'countdown'); 143 | ok(event.type === 'update'); 144 | ok(!event.elapsed); 145 | 146 | $clock.tick(5000); 147 | ok(updateHandler.called); 148 | 149 | event = finishHandler.lastCall.args[0]; 150 | ok(event.namespace === 'countdown'); 151 | ok(event.type === 'finish'); 152 | ok(event.elapsed); 153 | 154 | // cleanup 155 | $doc = null; 156 | }); 157 | 158 | test('event listeners on parent and deferred invocation', function() { 159 | var $doc = $(document), 160 | event, 161 | future = new Date().getTime() + 5000, 162 | updateHandler = sinon.spy(), 163 | finishHandler = sinon.spy(); 164 | 165 | $dom.countdown(future, {defer: true}); 166 | 167 | $doc 168 | .on('update.countdown', updateHandler) 169 | .on('finish.countdown', finishHandler); 170 | 171 | $clock.tick(500); 172 | 173 | ok(!updateHandler.called); 174 | ok(!finishHandler.called); 175 | 176 | $dom.countdown('start'); 177 | $clock.tick(500); 178 | 179 | ok(updateHandler.called); 180 | 181 | event = updateHandler.lastCall.args[0]; 182 | ok(event.namespace === 'countdown'); 183 | ok(event.type === 'update'); 184 | ok(!event.elapsed); 185 | 186 | $clock.tick(5000); 187 | ok(updateHandler.called); 188 | 189 | event = finishHandler.lastCall.args[0]; 190 | ok(event.namespace === 'countdown'); 191 | ok(event.type === 'finish'); 192 | ok(event.elapsed); 193 | 194 | // cleanup 195 | $doc = null; 196 | }); 197 | -------------------------------------------------------------------------------- /test/unit/functionality_test.js: -------------------------------------------------------------------------------- 1 | module('Functionality'); 2 | 3 | test('stop/start the countdown', function() { 4 | var callback = sinon.spy(); 5 | $dom.countdown('2020/10/20').on('update.countdown', callback); 6 | // Stop after 0.55 sec 7 | $clock.tick(550); 8 | $dom.countdown('stop'); 9 | // Should have been called once 10 | ok(callback.calledOnce); 11 | // Resume after 1 sec 12 | $clock.tick(1000); 13 | $dom.countdown('start'); 14 | // Verify if update event was called once more after 1.2 sec 15 | $clock.tick(5000); 16 | ok(callback.callCount > 5); 17 | }); 18 | 19 | test('pause/resume the countdown', function() { 20 | var callback = sinon.spy(); 21 | $dom.countdown('2020/10/20').on('update.countdown', callback); 22 | // Stop after 0.55 sec 23 | $clock.tick(550); 24 | $dom.countdown('pause'); 25 | // Should have been called once 26 | ok(callback.calledOnce); 27 | // Resume after 1 sec 28 | $clock.tick(1000); 29 | $dom.countdown('resume'); 30 | // Verify if update event was called once more after 1.2 sec 31 | $clock.tick(5000); 32 | ok(callback.callCount > 5); 33 | }); 34 | 35 | test('toggle countdown', function() { 36 | var callback = sinon.spy(); 37 | $dom.countdown('2020/10/20').on('update.countdown', callback); 38 | // Let it count once 39 | $clock.tick(100); 40 | ok(callback.callCount === 1); 41 | // Toggle the countdown to pause 42 | $dom.countdown('toggle'); 43 | // Test if callback wasn't invoked and toggle gain 44 | $clock.tick(1000); 45 | ok(callback.callCount === 1); 46 | $dom.countdown('toggle'); 47 | // Ensure it's running again 48 | $clock.tick(5000); 49 | ok(callback.callCount > 5); 50 | }); 51 | 52 | test('remove the countdown instance', function() { 53 | var callback = sinon.spy(); 54 | $dom.countdown('2020/10/20').on('update.countdown', callback); 55 | $dom.countdown('remove'); 56 | // See if noting was executed after 0.5 sec 57 | ok(callback.callCount === 0); 58 | $clock.tick(500); 59 | }); 60 | 61 | test('remove the countdown if dom was removed', function() { 62 | var callback = sinon.spy(); 63 | $dom.countdown('2020/10/20').on('update.countdown', callback); 64 | $dom.remove(); 65 | // See if noting was executed after 0.5 sec 66 | ok(callback.callCount === 0); 67 | $clock.tick(500); 68 | }); 69 | 70 | test('set countdown-instance data attr to undefined uppon remove', function() { 71 | var callback = sinon.spy(); 72 | $dom.countdown('2020/10/20').on('update.countdown', callback); 73 | ok($dom.data('countdown-instance') !== undefined); 74 | 75 | $dom.countdown('remove'); 76 | ok($dom.data('countdown-instance') === undefined); 77 | }); 78 | 79 | test('set a new final date calling the countdown again', function() { 80 | $dom.countdown('2020/10/20').on('update.countdown', function(event) { 81 | ok(event.finalDate.toString().match(/Oct 20 2020/)); 82 | }); 83 | $clock.tick(500); 84 | // Unset the event and set a new date 85 | $dom.off('update.countdown').countdown('2021/10/20') 86 | .on('update.countdown', function(event) { 87 | ok(event.finalDate.toString().match(/Oct 20 2021/)); 88 | }); 89 | $clock.tick(500); 90 | }); 91 | 92 | test('starts the countdown again after being finished (issue #38)', function() { 93 | function twoSecondsFromNow() { 94 | return new Date().getTime() + 2000; 95 | } 96 | // Count the number of times 97 | var i = 0; 98 | // Start a countdown for 2secs from now and uppon finish start it again from 99 | // within the callback recursively 100 | $dom.countdown(twoSecondsFromNow()).on('finish.countdown', function() { 101 | ok(true, 'Countdown iteraction #' + i++); 102 | if (i < 2) { 103 | $dom.countdown(twoSecondsFromNow()); 104 | } 105 | }); 106 | // Expect 2 assertions after 4 seconds 107 | expect(2); 108 | $clock.tick(4000); 109 | }); 110 | 111 | // Instance check against undefined, as 0 is a falsy value 112 | // Issue #24 113 | test('control a single instance correctly (issue #24)', function() { 114 | var callback = sinon.spy(), 115 | $single = $('
').appendTo('body'); 116 | 117 | $single.countdown('2020/10/20').on('update.countdown', callback); 118 | // Let it count once 119 | $clock.tick(100); 120 | ok(callback.callCount === 1); 121 | // Toggle the countdown to pause 122 | $single.countdown('toggle'); 123 | // Test if callback wasn't invoked and toggle gain 124 | $clock.tick(1000); 125 | ok(callback.callCount === 1); 126 | $single.countdown('toggle'); 127 | // Ensure it's running again 128 | $clock.tick(5000); 129 | ok(callback.callCount > 5); 130 | // Remove the node 131 | $single.remove(); 132 | }); 133 | 134 | test('configure the dom update rate', function() { 135 | var callback = sinon.spy(); 136 | // Setup a different update rate precision 137 | $dom.countdown('2020/10/20', { 138 | precision: 2000 139 | }).on('update.countdown', callback); 140 | 141 | // Should execute just once in 2s 142 | $clock.tick(2000); 143 | ok(callback.callCount === 1); 144 | }); 145 | 146 | test('continue after pass the final date', function() { 147 | var oneSecAgo = new Date().getTime() * 1000; 148 | var callback = sinon.spy(); 149 | // Setup a different update rate precision 150 | $dom.countdown(oneSecAgo, { 151 | elapse: true 152 | }).on('update.countdown', callback); 153 | // Should execute just once in 500ms 154 | $clock.tick(500); 155 | ok(callback.callCount === 1); 156 | // Check if event was set has elapsed 157 | var eventObject = callback.lastCall.args[0]; 158 | ok(eventObject.elapsed === true); 159 | }); 160 | 161 | test('defer the start with lazy initialization option', function() { 162 | var callback = sinon.spy(); 163 | // Setup a different update rate precision 164 | $dom.countdown('2020/10/20', { 165 | defer: true 166 | }).on('update.countdown', callback); 167 | 168 | // Should not start automatically 169 | $clock.tick(500); 170 | ok(!callback.called); 171 | 172 | // Start the countdown 173 | $dom.countdown('start'); 174 | $clock.tick(500); 175 | ok(callback.called); 176 | }); 177 | -------------------------------------------------------------------------------- /test/unit/math_test.js: -------------------------------------------------------------------------------- 1 | module('Math'); 2 | 3 | test('time offset calculation', function() { 4 | var testDate = new Date().getTime(); 5 | testDate += 7 * 24 * 60 * 60 * 1000; // 1 week 6 | testDate += 2 * 24 * 60 * 60 * 1000; // 2 days 7 | testDate += 3 * 60 * 60 * 1000; // 3 hours 8 | testDate += 4 * 60 * 1000; // 4 minutes 9 | testDate += 5 * 1000; // 5 seconds 10 | 11 | $dom.countdown(testDate).on('update.countdown', function(event) { 12 | ok(event.offset.weeks === 1); 13 | ok(event.offset.days === 2); 14 | ok(event.offset.hours === 3); 15 | ok(event.offset.minutes === 4); 16 | ok(event.offset.seconds === 5); 17 | // Offset values 18 | ok(event.offset.daysToWeek === 2); 19 | ok(event.offset.daysToMonth === 9); 20 | ok(event.offset.weeksToMonth === 1); 21 | // Test total count 22 | ok(event.offset.totalDays === 9); 23 | ok(event.offset.totalHours === 9 * 24 + 3); 24 | ok(event.offset.totalMinutes === 9 * 24 * 60 + 3 * 60 + 4); 25 | ok(event.offset.totalSeconds === (9 * 24 * 60 + 3 * 60 + 4) * 60 + 5); 26 | }); 27 | 28 | $clock.tick(500); 29 | }); 30 | 31 | test('days differencence for week and month offset (Issue #125)', function() { 32 | var now = new Date(2015, 6, 16).getTime(); 33 | $clock = sinon.useFakeTimers(now); 34 | 35 | var testDate = new Date(2015, 8, 25).getTime(); 36 | 37 | $dom.countdown(testDate).on('update.countdown', function(event) { 38 | ok(event.offset.months === 2); 39 | ok(event.offset.daysToMonth === 10); 40 | }); 41 | 42 | $clock.tick(500); 43 | }); 44 | 45 | test('years offset calculation (Issue #125)', function() { 46 | var now = new Date(2014, 0, 1).getTime(); 47 | $clock = sinon.useFakeTimers(now); 48 | 49 | var testDate = new Date(2015, 0, 1).getTime(); 50 | 51 | $dom.countdown(testDate).on('update.countdown', function(event) { 52 | ok(event.offset.years === 1); 53 | }); 54 | 55 | $clock.tick(500); 56 | }); 57 | -------------------------------------------------------------------------------- /test/unit/sanity_test.js: -------------------------------------------------------------------------------- 1 | module('Sanity tests'); 2 | 3 | test('ensure DOM for testing is created', function() { 4 | ok($dom !== null); 5 | ok($dom !== undefined); 6 | ok($dom.length === 1); 7 | }); 8 | 9 | test('countdown method exists within jquery selector', function() { 10 | ok($.fn.countdown !== undefined); 11 | ok($.fn.countdown !== null); 12 | ok($.fn.countdown); 13 | }); 14 | 15 | test('throws an error when initiated with wrong number of args', function() { 16 | throws(function() { 17 | $('
').countdown(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /test/unit/strftime_test.js: -------------------------------------------------------------------------------- 1 | module('strftime'); 2 | 3 | test('strftime is a function that returns a string', function() { 4 | $dom.countdown('11/22/2020').on('update.countdown', function(event) { 5 | ok(typeof event.strftime === 'function'); 6 | ok(typeof event.strftime('foo') === 'string'); 7 | }); 8 | $clock.tick(500); 9 | }); 10 | 11 | test('strftime returns the same string if no replaces were made', function() { 12 | $dom.countdown('11/22/2020').on('update.countdown', function(event) { 13 | ok(event.strftime('Foo Bar') === 'Foo Bar'); 14 | }); 15 | $clock.tick(500); 16 | }); 17 | 18 | test('escaping percentage character %% ', function() { 19 | $dom.countdown('2020/11/10 09:08:07').on('update.countdown', function(event) { 20 | ok(event.strftime('%%') === '%'); 21 | }); 22 | $clock.tick(500); 23 | }); 24 | 25 | /* 26 | | Directive | Blank-padded | Pluralization | Description | 27 | |---------------|---------------|---------------|---------------------------| 28 | | %Y | %-Y | %!Y | Years left | 29 | | %m | %-m | %!m | Months left | 30 | | %n | %-n | %!n | Days left to a month | 31 | | %w | %-w | %!w | Weeks left | 32 | | %W | %-W | %!W | Weeks left to a month | 33 | | %d | %-d | %!d | Days left to a week | 34 | | %H | %-H | %!H | Hours left | 35 | | %M | %-M | %!M | Minutes left | 36 | | %S | %-S | %!S | Seconds left | 37 | | %D | %-D | %!D | Total amount of days left | 38 | | %I | %-I | %!I | Total hours left | 39 | | %N | %-N | %!N | Total minutest left | 40 | | %T | %-T | %!T | Total seconds left | 41 | |---------------------------------------------------------------------------| 42 | */ 43 | 44 | test('all directives', function() { 45 | $dom.countdown('2020/11/10 09:08:07').on('update.countdown', function(event) { 46 | ok(event.strftime('%Y %m %n %w %W %d %D %H %M %S %D %I %N %T') 47 | .match(/^([0-9]{1,}\s?){14}$/) !== null); 48 | }); 49 | $clock.tick(500); 50 | }); 51 | 52 | test('blank-padded directives', function() { 53 | $dom.countdown('2020/11/10 09:08:07').on('update.countdown', function(event) { 54 | ok(event.strftime('%-Y %-m %-n %-w %-W %-d %-H %-M %-S %-D %-I %-N %-T') 55 | .match(/^([0-9]{1,}\s?){14}$/) !== null); 56 | }); 57 | $clock.tick(500); 58 | }); 59 | 60 | test('return an empty character when plural', function() { 61 | $dom.countdown(new Date().valueOf() + 1000) 62 | .on('update.countdown', function(event) { 63 | ok(event.offset.seconds === 1); 64 | ok(event.strftime('%!S') === ''); 65 | }); 66 | $clock.tick(500); 67 | }); 68 | 69 | test('return a `s` when when plural', function() { 70 | $dom.countdown(new Date().valueOf() + 2000) 71 | .on('update.countdown', function(event) { 72 | ok(event.offset.seconds === 2); 73 | ok(event.strftime('%!S') === 's'); 74 | }); 75 | $clock.tick(500); 76 | }); 77 | 78 | test('return the given character when plural', function() { 79 | $dom.countdown(new Date().valueOf() + 2000) 80 | .on('update.countdown', function(event) { 81 | ok(event.offset.seconds === 2); 82 | ok(event.strftime('sekunde%!S:n;') === 'sekunden'); 83 | }); 84 | $clock.tick(500); 85 | }); 86 | 87 | test('return the plural when given pair of arguments', function() { 88 | $dom.countdown(new Date().valueOf() + 2000) 89 | .on('update.countdown', function(event) { 90 | ok(event.offset.seconds === 2); 91 | ok(event.strftime('%!S:day,days;') === 'days'); 92 | }); 93 | $clock.tick(500); 94 | }); 95 | 96 | test('return the singular when given pair of arguments', function() { 97 | $dom.countdown(new Date().valueOf() + 1000) 98 | .on('update.countdown', function(event) { 99 | ok(event.offset.seconds === 1); 100 | ok(event.strftime('%!S:day,days;') === 'day'); 101 | }); 102 | $clock.tick(500); 103 | }); 104 | 105 | test('return the singular for zero (issue #187)', function() { 106 | $dom.countdown(new Date().valueOf() + 1000) 107 | .on('update.countdown', function(event) { 108 | ok(event.offset.seconds === 1); 109 | ok(event.strftime('%m %!m:min,minutes;') === '00 min'); 110 | }); 111 | $clock.tick(500); 112 | }); 113 | 114 | test('return the correct plural even with odd looking (issue #70)', function() { 115 | $dom.countdown(new Date().valueOf() + 2 * 60 * 1000) 116 | .on('update.countdown', function(event) { 117 | ok(event.offset.minutes === 2); 118 | ok(event.strftime('%!M:Minuta,Minute(e);') === 'Minute(e)'); 119 | }); 120 | $clock.tick(500); 121 | }); 122 | --------------------------------------------------------------------------------