├── changelog.md ├── src ├── header.js ├── jq.js ├── auto.js ├── moment.ext.js ├── MinPubSub.js ├── input.js ├── util.js ├── main.js └── moment.js ├── LICENSE ├── Makefile ├── index.html └── readme.md /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.4.1 4 | 5 | ### Resolved Issues 6 | 7 | * \#83 Close button now works on IE9/IE10 8 | 9 | * \#68, \#76 Using ```config.format``` in blackout dates 10 | -------------------------------------------------------------------------------- /src/header.js: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | * Kalendae, a framework agnostic javascript date picker * 3 | * Copyright(c) 2013 Jarvis Badgley (chipersoft@gmail.com) * 4 | * http://github.com/ChiperSoft/Kalendae * 5 | * Version 0.4.1 * 6 | ********************************************************************/ 7 | 8 | -------------------------------------------------------------------------------- /src/jq.js: -------------------------------------------------------------------------------- 1 | 2 | if (typeof jQuery !== 'undefined' && (typeof document.addEventListener === 'function' || util.isIE8())) { 3 | jQuery.fn.kalendae = function (options) { 4 | this.each(function (i, e) { 5 | if (e.tagName === 'INPUT') { 6 | //if element is an input, bind a popup calendar to the input. 7 | $(e).data('kalendae', new Kalendae.Input(e, options)); 8 | } else { 9 | //otherwise, insert a flat calendar into the element. 10 | $(e).data('kalendae', new Kalendae($.extend({}, {attachTo:e}, options))); 11 | } 12 | }); 13 | return this; 14 | }; 15 | } 16 | 17 | -------------------------------------------------------------------------------- /src/auto.js: -------------------------------------------------------------------------------- 1 | 2 | //auto-initializaiton code 3 | if (typeof document.addEventListener === 'function') Kalendae.util.domReady(function () { 4 | var els = util.$$('.auto-kal'), 5 | i = els.length, 6 | e, 7 | options, 8 | optionsRaw; 9 | 10 | while (i--) { 11 | e = els[i]; 12 | optionsRaw = e.getAttribute('data-kal'); 13 | options = (optionsRaw == null || optionsRaw == "") ? {} : (new Function('return {' + optionsRaw + '};'))(); 14 | 15 | if (e.tagName === 'INPUT') { 16 | //if element is an input, bind a popup calendar to the input. 17 | new Kalendae.Input(e, options); 18 | } else { 19 | //otherwise, insert a flat calendar into the element. 20 | new Kalendae(util.merge(options, {attachTo:e})); 21 | } 22 | 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /src/moment.ext.js: -------------------------------------------------------------------------------- 1 | 2 | if (!Kalendae.moment) { 3 | if (window.moment) { 4 | Kalendae.moment = window.moment; 5 | } else { 6 | throw "Kalendae requires moment.js. You must use kalendae.standalone.js if moment is not available on the page."; 7 | } 8 | } 9 | 10 | moment = Kalendae.moment; 11 | 12 | //function to reset the date object to 00:00 GMT 13 | moment.fn.stripTime = function () { 14 | this._d = new Date(Math.floor(this._d.valueOf() / 86400000) * 86400000); 15 | return this; 16 | }; 17 | 18 | 19 | //function to get the total number of days since the epoch. 20 | moment.fn.yearDay = function (input) { 21 | var yearday = Math.floor(this._d / 86400000); 22 | return (typeof input === 'undefined') ? yearday : 23 | this.add({ d : input - yearday }); 24 | }; 25 | 26 | today = Kalendae.moment().stripTime(); 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Kalendae Date Picker Library 2 | Copyright (c) 2012, Jarvis Badgley - chiper[at]chipersoft[dot]com 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | kal=src/main.js \ 2 | src/util.js \ 3 | src/auto.js \ 4 | src/input.js \ 5 | src/MinPubSub.js \ 6 | src/moment.js \ 7 | src/moment.ext.js \ 8 | src/jq.js 9 | 10 | kmomentless=src/main.js \ 11 | src/util.js \ 12 | src/auto.js \ 13 | src/input.js \ 14 | src/MinPubSub.js \ 15 | src/moment.ext.js \ 16 | src/jq.js 17 | 18 | all: build/kalendae.js build/kalendae.standalone.js 19 | 20 | clean: 21 | rm -f build/*.js 22 | 23 | minified: build/kalendae.min.js build/kalendae.standalone.min.js 24 | 25 | minified-test: build/kalendae.min.errors 26 | 27 | build/kalendae.standalone.js: $(kal) src/header.js 28 | cat src/header.js > $@ 29 | echo "(function (undefined) {" >> $@ 30 | echo "" >> $@ 31 | cat $(kal) >> $@ 32 | echo "" >> $@ 33 | echo "})();" >> $@ 34 | 35 | build/kalendae.js: $(kmomentless) src/header.js 36 | cat src/header.js > $@ 37 | echo "(function (undefined) {" >> $@ 38 | echo "" >> $@ 39 | cat $(kmomentless) >> $@ 40 | echo "" >> $@ 41 | echo "})();" >> $@ 42 | 43 | build/kalendae.min.js: build/kalendae.js 44 | cat src/header.js > $@ 45 | curl -s \ 46 | --data-urlencode 'js_code@build/kalendae.js' \ 47 | --data-urlencode 'output_format=text' \ 48 | --data-urlencode 'output_info=compiled_code' \ 49 | http://closure-compiler.appspot.com/compile \ 50 | >> $@ 51 | gzip -c build/kalendae.min.js | wc -c 52 | 53 | build/kalendae.standalone.min.js: build/kalendae.standalone.js 54 | cat src/header.js > $@ 55 | curl -s \ 56 | --data-urlencode 'js_code@build/kalendae.standalone.js' \ 57 | --data-urlencode 'output_format=text' \ 58 | --data-urlencode 'output_info=compiled_code' \ 59 | http://closure-compiler.appspot.com/compile \ 60 | >> $@ 61 | gzip -c build/kalendae.min.js | wc -c 62 | 63 | 64 | build/kalendae.min.errors: build/kalendae.js 65 | curl -s \ 66 | --data-urlencode 'js_code@build/kalendae.js' \ 67 | --data-urlencode 'output_format=text' \ 68 | --data-urlencode 'output_info=errors' \ 69 | http://closure-compiler.appspot.com/compile 70 | -------------------------------------------------------------------------------- /src/MinPubSub.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * MinPubSub, modified for use on Kalendae 4 | * Copyright(c) 2011 Daniel Lamb 5 | * https://github.com/daniellmb/MinPubSub 6 | * MIT Licensed 7 | */ 8 | 9 | var MinPubSub = function(d){ 10 | 11 | if (!d) d = this; 12 | 13 | // the topic/subscription hash 14 | var cache = d.c_ || {}; //check for "c_" cache for unit testing 15 | 16 | d.publish = function(/* String */ topic, /* Object */ target, /* Array? */ args){ 17 | // summary: 18 | // Publish some data on a named topic. 19 | // topic: String 20 | // The channel to publish on 21 | // args: Array? 22 | // The data to publish. Each array item is converted into an ordered 23 | // arguments on the subscribed functions. 24 | // 25 | // example: 26 | // Publish stuff on '/some/topic'. Anything subscribed will be called 27 | // with a function signature like: function(a,b,c){ ... } 28 | // 29 | // publish("/some/topic", ["a","b","c"]); 30 | 31 | var subs = cache[topic], 32 | len = subs ? subs.length : 0, 33 | r; 34 | 35 | //can change loop or reverse array if the order matters 36 | while(len--){ 37 | r = subs[len].apply(target, args || []); 38 | if (typeof r === 'boolean') return r; 39 | } 40 | }; 41 | 42 | d.subscribe = function(/* String */ topic, /* Function */ callback, /* Boolean */ topPriority){ 43 | // summary: 44 | // Register a callback on a named topic. 45 | // topic: String 46 | // The channel to subscribe to 47 | // callback: Function 48 | // The handler event. Anytime something is publish'ed on a 49 | // subscribed channel, the callback will be called with the 50 | // published array as ordered arguments. 51 | // 52 | // returns: Array 53 | // A handle which can be used to unsubscribe this particular subscription. 54 | // 55 | // example: 56 | // subscribe("/some/topic", function(a, b, c){ /* handle data */ }); 57 | 58 | if(!cache[topic]){ 59 | cache[topic] = []; 60 | } 61 | if (topPriority) 62 | cache[topic].push(callback); 63 | else 64 | cache[topic].unshift(callback); 65 | return [topic, callback]; // Array 66 | }; 67 | 68 | d.unsubscribe = function(/* Array */ handle){ 69 | // summary: 70 | // Disconnect a subscribed function for a topic. 71 | // handle: Array 72 | // The return value from a subscribe call. 73 | // example: 74 | // var handle = subscribe("/some/topic", function(){}); 75 | // unsubscribe(handle); 76 | 77 | var subs = cache[handle[0]], 78 | callback = handle[1], 79 | len = subs ? subs.length : 0; 80 | 81 | while(len--){ 82 | if(subs[len] === callback){ 83 | subs.splice(len, 1); 84 | } 85 | } 86 | }; 87 | 88 | }; -------------------------------------------------------------------------------- /src/input.js: -------------------------------------------------------------------------------- 1 | Kalendae.Input = function (targetElement, options) { 2 | if (typeof document.addEventListener !== 'function' && !util.isIE8()) return; 3 | 4 | var $input = this.input = util.$(targetElement), 5 | overwriteInput, 6 | $closeButton; 7 | 8 | if (!$input || $input.tagName !== 'INPUT') throw "First argument for Kalendae.Input must be an element or a valid element id."; 9 | 10 | var self = this, 11 | classes = self.classes, 12 | opts = self.settings = util.merge(self.defaults, options); 13 | 14 | //force attachment to the body 15 | opts.attachTo = window.document.body; 16 | 17 | //if no override provided, use the input's contents 18 | if (!opts.selected) opts.selected = $input.value; 19 | else overwriteInput = true; 20 | 21 | //call our parent constructor 22 | Kalendae.call(self, opts); 23 | 24 | //create the close button 25 | if (opts.closeButton) { 26 | $closeButton = util.make('a', {'class':classes.closeButton}, self.container); 27 | util.addEvent($closeButton, 'click', function () { 28 | $input.blur(); 29 | }); 30 | } 31 | 32 | if (overwriteInput) $input.value = self.getSelected(); 33 | 34 | var $container = self.container, 35 | noclose = false; 36 | 37 | $container.style.display = 'none'; 38 | util.addClassName($container, classes.positioned); 39 | 40 | util.addEvent($container, 'mousedown', function (event, target) { 41 | noclose = true; //IE8 doesn't obey event blocking when it comes to focusing, so we have to do this shit. 42 | }); 43 | util.addEvent(window.document, 'mousedown', function (event, target) { 44 | noclose = false; 45 | }); 46 | 47 | util.addEvent($input, 'focus', function () { 48 | self.setSelected(this.value); 49 | self.show(); 50 | }); 51 | 52 | util.addEvent($input, 'blur', function () { 53 | if (noclose && util.isIE8()) { 54 | noclose = false; 55 | $input.focus(); 56 | } 57 | else self.hide(); 58 | }); 59 | util.addEvent($input, 'keyup', function (event) { 60 | self.setSelected(this.value); 61 | }); 62 | 63 | var $scrollContainer = util.scrollContainer($input); 64 | 65 | if( $scrollContainer ) { 66 | 67 | // Hide calendar when $scrollContainer is scrolled 68 | util.addEvent($scrollContainer, 'scroll', function (event) { 69 | $input.blur(); 70 | }); 71 | } 72 | 73 | self.subscribe('change', function () { 74 | $input.value = self.getSelected(); 75 | }); 76 | 77 | }; 78 | 79 | Kalendae.Input.prototype = util.merge(Kalendae.prototype, { 80 | defaults : util.merge(Kalendae.prototype.defaults, { 81 | format: 'MM/DD/YYYY', 82 | side: 'bottom', 83 | closeButton: true, 84 | offsetLeft: 0, 85 | offsetTop: 0 86 | }), 87 | classes : util.merge(Kalendae.prototype.classes, { 88 | positioned : 'k-floating', 89 | closeButton: 'k-btn-close' 90 | }), 91 | 92 | show : function () { 93 | var $container = this.container, 94 | style = $container.style, 95 | $input = this.input, 96 | pos = util.getPosition($input), 97 | $scrollContainer = util.scrollContainer($input), 98 | scrollTop = $scrollContainer ? $scrollContainer.scrollTop : 0, 99 | opts = this.settings; 100 | 101 | style.display = ''; 102 | switch (opts.side) { 103 | case 'left': 104 | style.left = (pos.left - util.getWidth($container) + opts.offsetLeft) + 'px'; 105 | style.top = (pos.top + opts.offsetTop - scrollTop) + 'px'; 106 | break; 107 | case 'right': 108 | style.left = (pos.left + util.getWidth($input)) + 'px'; 109 | style.top = (pos.top + opts.offsetTop - scrollTop) + 'px'; 110 | break; 111 | case 'top': 112 | style.left = (pos.left + opts.offsetLeft) + 'px'; 113 | style.top = (pos.top - util.getHeight($container) + opts.offsetTop - scrollTop) + 'px'; 114 | break; 115 | case 'bottom': 116 | /* falls through */ 117 | default: 118 | style.left = (pos.left + opts.offsetLeft) + 'px'; 119 | style.top = (pos.top + util.getHeight($input) + opts.offsetTop - scrollTop) + 'px'; 120 | break; 121 | } 122 | 123 | style.position = util.isFixed($input) ? 'fixed' : 'absolute'; 124 | 125 | this.publish('show', this); 126 | }, 127 | 128 | hide : function () { 129 | this.container.style.display = 'none'; 130 | this.publish('hide', this); 131 | } 132 | 133 | }); 134 | 135 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | 15 | 16 | 17 |

Kalendae

18 |

Single Select

19 | 40 | 41 |

Range Select

42 | 63 | 64 |

Multi-Select

65 | 88 | 89 |
90 |

Blackout and Direction Tests

91 | 92 | 121 | 122 | 123 | 124 |
125 |

Class Map Test

126 | 127 | 136 |
137 | 138 | Click this input element: 139 | 144 | 145 |
146 | This calendar is auto-created. 147 |
148 | 149 |
150 | So is the one on this input. 151 | 152 | 153 |
154 | This calendar is auto-created with months: 3 and direction: 'future'. 155 |
156 | 157 |
158 | This calander on this input is auto-created with direction: 'future'. 159 | 160 | 161 |
162 | This calendar allows clicks outside of the focused month (dayOutOfMonthClickable: true): 163 |
164 | 165 |
166 | This calendar has no year navigation (useYearNav: false). 167 |
168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | 2 | var util = Kalendae.util = { 3 | 4 | isIE8: function() { 5 | return !!( (/msie 8./i).test(navigator.appVersion) && !(/opera/i).test(navigator.userAgent) && window.ActiveXObject && XDomainRequest && !window.msPerformance ); 6 | }, 7 | 8 | // ELEMENT FUNCTIONS 9 | 10 | $: function (elem) { 11 | return (typeof elem == 'string') ? document.getElementById(elem) : elem; 12 | }, 13 | 14 | $$: function (selector) { 15 | return document.querySelectorAll(selector); 16 | }, 17 | 18 | make: function (tagName, attributes, attach) { 19 | var k, e = document.createElement(tagName); 20 | if (!!attributes) for (k in attributes) if (attributes.hasOwnProperty(k)) e.setAttribute(k, attributes[k]); 21 | if (!!attach) attach.appendChild(e); 22 | return e; 23 | }, 24 | 25 | // Returns true if the DOM element is visible, false if it's hidden. 26 | // Checks if display is anything other than none. 27 | isVisible: function (elem) { 28 | // shamelessly copied from jQuery 29 | return elem.offsetWidth > 0 || elem.offsetHeight > 0; 30 | }, 31 | 32 | getStyle: function (elem, styleProp) { 33 | var y; 34 | if (elem.currentStyle) { 35 | y = elem.currentStyle[styleProp]; 36 | } else if (window.getComputedStyle) { 37 | y = window.getComputedStyle(elem, null)[styleProp]; 38 | } 39 | return y; 40 | }, 41 | 42 | domReady:function (f){/in/.test(document.readyState) ? setTimeout(function() {util.domReady(f);},9) : f()}, 43 | 44 | // Adds a listener callback to a DOM element which is fired on a specified 45 | // event. Callback is sent the event object and the element that triggered the event 46 | addEvent: function (elem, eventName, callback) { 47 | var listener = function (event) { 48 | event = event || window.event; 49 | var target = event.target || event.srcElement; 50 | var block = callback.apply(elem, [event, target]); 51 | if (block === false) { 52 | if (!!event.preventDefault) event.preventDefault(); 53 | else { 54 | event.returnValue = false; 55 | event.cancelBubble = true; 56 | } 57 | } 58 | return block; 59 | }; 60 | if (elem.attachEvent) { // IE only. The "on" is mandatory. 61 | elem.attachEvent("on" + eventName, listener); 62 | } else { // Other browsers. 63 | elem.addEventListener(eventName, listener, false); 64 | } 65 | return listener; 66 | }, 67 | 68 | // Removes a listener callback from a DOM element which is fired on a specified 69 | // event. 70 | removeEvent: function (elem, event, listener) { 71 | if (elem.detachEvent) { // IE only. The "on" is mandatory. 72 | elem.detachEvent("on" + event, listener); 73 | } else { // Other browsers. 74 | elem.removeEventListener(event, listener, false); 75 | } 76 | }, 77 | 78 | hasClassName: function(elem, className) { //copied and modified from Prototype.js 79 | if (!(elem = util.$(elem))) return false; 80 | var eClassName = elem.className; 81 | return (eClassName.length > 0 && (eClassName == className || new RegExp("(^|\\s)" + className + "(\\s|$)").test(eClassName))); 82 | }, 83 | 84 | addClassName: function(elem, className) { //copied and modified from Prototype.js 85 | if (!(elem = util.$(elem))) return; 86 | if (!util.hasClassName(elem, className)) elem.className += (elem.className ? ' ' : '') + className; 87 | }, 88 | 89 | removeClassName: function(elem, className) { //copied and modified from Prototype.js 90 | if (!(elem = util.$(elem))) return; 91 | elem.className = util.trimString(elem.className.replace(new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ')); 92 | }, 93 | 94 | isFixed: function (elem) { 95 | do { 96 | if (util.getStyle(elem, 'position') === 'fixed') return true; 97 | } while ((elem = elem.offsetParent)); 98 | return false; 99 | }, 100 | 101 | scrollContainer: function (elem) { 102 | do { 103 | var overflow = util.getStyle(elem, 'overflow'); 104 | if (overflow === 'auto' || overflow === 'scroll') return elem; 105 | } while ((elem = elem.parentNode) && elem != window.document.body); 106 | return null; 107 | }, 108 | 109 | getPosition: function (elem, isInner) { 110 | var x = elem.offsetLeft, 111 | y = elem.offsetTop, 112 | r = {}; 113 | 114 | if (!isInner) { 115 | while ((elem = elem.offsetParent)) { 116 | x += elem.offsetLeft; 117 | y += elem.offsetTop; 118 | } 119 | } 120 | 121 | r[0] = r.left = x; 122 | r[1] = r.top = y; 123 | return r; 124 | }, 125 | 126 | getHeight: function (elem) { 127 | return elem.offsetHeight || elem.scrollHeight; 128 | }, 129 | 130 | getWidth: function (elem) { 131 | return elem.offsetWidth || elem.scrollWidth; 132 | }, 133 | 134 | 135 | // TEXT FUNCTIONS 136 | 137 | trimString: function (input) { 138 | return input.replace(/^\s+/, '').replace(/\s+$/, ''); 139 | }, 140 | 141 | 142 | // OBJECT FUNCTIONS 143 | 144 | merge: function () { 145 | /* Combines multiple objects into one. 146 | * Syntax: util.extend([true], object1, object2, ... objectN) 147 | * If first argument is true, function will merge recursively. 148 | */ 149 | 150 | var deep = (arguments[0]===true), 151 | d = {}, 152 | i = deep?1:0; 153 | 154 | var _c = function (a, b) { 155 | if (typeof b !== 'object') return; 156 | for (var k in b) if (b.hasOwnProperty(k)) { 157 | //if property is an object or array, merge the contents instead of overwriting, if extend() was called as such 158 | if (deep && typeof a[k] === 'object' && typeof b[k] === 'object') _update(a[k], b[k]); 159 | else a[k] = b[k]; 160 | } 161 | return a; 162 | }; 163 | 164 | for (; i < arguments.length; i++) { 165 | _c(d, arguments[i]); 166 | } 167 | return d; 168 | }, 169 | 170 | isArray: function (array) { 171 | return Object.prototype.toString.call(array) == "[object Array]" 172 | } 173 | }; 174 | 175 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | #Kalendae - A framework agnostic javascript date picker 2 | 3 | Kalendae is an attempt to do something that nobody has yet been able to do: make a date picker that doesn't suck. Kalendae provides the following features: 4 | 5 | 1. Fully portable, **no external dependencies**. No jQuery, no Prototype, no MooTools; just add the script and the stylesheet and you're good to go. 6 | 2. Fully and easily skinable. The default theme uses only one image file (a mask for the previous and next buttons), everything else is styled using CSS. 7 | 3. Supports all modern browsers and IE8. 8 | 4. Support single day, multiple day, or day range selection. 9 | 5. Configurable number of months to be displayed at once. 10 | 6. Can be displayed on the page as an inline widget, or attached to one or more input fields as a popup control. 11 | 7. Can be attached to **any** page element, not just named elements. 12 | 8. Configurable blackouts, defined either as an array of dates or via a callback function 13 | 9. Output selected dates in a variety of formats 14 | 10. Leverages [moment.js](http://www.momentjs.com) for smart and easy date parsing. 15 | 16 | ##Screenshots 17 | 18 | Default calendar, no options defined. 19 | ![screenshot](http://i.imgur.com/Ig52z.png) 20 | 21 | Two month calendar attached to an input element. 22 | ![screenshot](http://i.imgur.com/GIR3g.png) 23 | 24 | Two month, range selection, future dates only, with weekends blacked out: 25 | ![screenshot](http://i.imgur.com/JzBc7.png) 26 | 27 | ###[View The Demo Page](http://chipersoft.github.com/Kalendae/) 28 | 29 | 30 | ##Usage 31 | 32 | Copy the contents of the `build/` folder into wherever your website scripts are kept. Include the JS and CSS files in the head of your document like so: 33 | 34 | 35 | 36 | 37 | Once this is done you can initialize kalendae a number of ways. The easiest method is to simply add the "auto-kal" class onto the element you want to calendar attached to. The calendar will be created using the default settings. 38 | 39 |
40 | 41 | This works for input elements as well, providing a popup calendar. 42 | 43 | 44 | 45 | 46 | If you want to override the default settings, you can use the data-kal attribute. 47 | 48 |
49 | 50 | Again, this will work for input elements as well. 51 | 52 | You can also setup Kalendae manually via JavaScript code. This should be done either at the end of the page, or in the DOMReady/Load event. To do this you must instantiate one of two objects, the widget class `Kalendae`, or the input element popup class `Kalendae.Input`. Both objects take two arguments: 53 | 54 | 1. targetElement - This is either an Element object, or the element's ID as a string. 55 | 2. options - An object containing the new options. Any option omitted will revert to the default setting. 56 | 57 | See the included index.html file for usage examples. 58 | 59 | ###jQuery 60 | 61 | Kalendae does not require jQuery, but does provide a jQuery plugin when jQuery is available. jQuery users may create a Kalendae widget or popup by calling `$(selector).kalendae(options)`. 62 | 63 | ##moment.js 64 | 65 | To ease date handling processes, Kalendae bundles the [moment.js](http://www.momentjs.com) date handling library. This bundled library has been altered to prevent it from being added to the global context, but is still available if you wish to use it in your own code. Add the following directly after the ` 70 | 71 | ##Options 72 | 73 | The following options are available for configuration. 74 | 75 | - `attachTo`: The element that the calendar div will be appended to. 76 | - In `Kalendae` this defaults to the first argument on the constructor. 77 | - In `Kalendae.Input` this defaults to the document body. 78 | - Can be an Element or an element's string ID. 79 | 80 | - `format`: The format mask used when parsing date strings. 81 | - Uses moment.js notation (see http://momentjs.com/docs/#/display/format ) 82 | - If left undefined, will attempt to parse the date automatically. 83 | - Default is `null`. 84 | 85 | - `mode`: Selection mode. 86 | - `"single"`: Allows selection of only one day. Clicks change the selected day. This is the default. 87 | - `"multiple"`: Allows selection of multiple, non-sequential days. Clicks toggle a day's selection. 88 | - `"range"`: Selects multiple days in sequence. First click defines start of the range, second defines the end of the range. 89 | 90 | - `selected`: The date selected when the calendar is created. 91 | - Values my be a string, JavaScript Date object, Moment object, or an array containing any of the three. 92 | - In "multiple" mode, strings may contain multiple dates separated by commas. *ex: 2/3/2012, 3/15/2012, 4/2/2012* 93 | - In "range" mode, strings may contain two dates separated by a hyphen. *ex: 2/3/2012 - 3/15/2012* 94 | 95 | - `months`: The total number of months to display side by side on the calendar. 96 | - Default is `1`. 97 | 98 | - `weekStart`: The day to use for the start of the week. 99 | - 0 = Sunday, 1 = Monday, etc. 100 | - Default is `0`. 101 | 102 | - `direction`: Restricts date selectability to past or future. 103 | - Accepted values: past, today-past, any, today-future, future 104 | - Stacks with `blackout` 105 | - Default is `"any"` 106 | 107 | - `directionScrolling`: If `true` and a direction other than `any` is defined, Kalendae will not allow scrolling the view outside the direction. 108 | - Default is true. 109 | 110 | - `blackout`: Dates to be disallowed from selection. 111 | - Can be an array of dates formatted according to `format`, or a function taking a moment date object as the first argument and returning true to prevent selection. 112 | - Stacks with `direction` 113 | - Default is `null` 114 | 115 | - `viewStartDate`: Date defining the first month to display when created. 116 | - Uses the `format` definition. 117 | - Default is `null` (this month or month of first selected day). 118 | 119 | - `dateClassMap`: A key/value collection of css classes organized by date. String date keys found in this collection will have their value attached to the SPAN tag for the date. This allows for custom coloring for specific days. See the first example in index.html for usage. 120 | - Note that this property uses the `dayAttributeFormat` option, NOT the format option, for date strings. 121 | - Default is `null`. 122 | 123 | - `dayOutOfMonthClickable`: Allow clicks on days that fall outside of the currently focused month. Default is `false`. 124 | 125 | - `useYearNav`: Include the double-arrow year navigation. Default is `true`. 126 | 127 | ###Advanced Behavior Options 128 | 129 | The following settings alter the internal behavior of Kalendae and should only be changed by advanced users. 130 | 131 | 132 | - `columnHeaderFormat`: The format of moment data of the week day name to display in column headers. 133 | - Default is `dd` 134 | - `titleFormat`: Format string used in the calendar title. 135 | - Default is `"MMMM, YYYY"` 136 | 137 | - `dayNumberFormat`: Format string for individual day numbers. 138 | - Default is `"D"` 139 | 140 | - `dayAttributeFormat`: Format string for the `data-date` attribute set on every span 141 | - Default is `"YYYY-MM-DD"` 142 | 143 | - `parseSplitDelimiter`: RegExp used when splitting multiple dates from a passed string 144 | - Default is `/,\s*|\s*-\s*/` 145 | 146 | - `rangeDelimiter`: String used to delimit the start and end dates when outputting in range mode 147 | - Default is `' - '` 148 | 149 | - `multipleDelimiter`: String used to delimit dates when outputting in multiple mode 150 | - Default is `', '` 151 | 152 | ###Example Blackout Functions 153 | 154 | - Blackout weekends: `function (date) {return [1,0,0,0,0,0,1][Kalendae.moment(date).day()];}` 155 | - Blackout every other day: `function (date) {return Kalendae.moment(date).date() % 2;}` 156 | - Blackout every other week `function (date) {return Kalendae.moment(date).format('w') % 2;}` 157 | 158 | ##Member Functions 159 | 160 | The following functions are available on the instantiated `Kalendae` and `Kalendae.Input` objects. 161 | 162 | - `getSelected()`: Returns the selected dates as a formatted string. 163 | 164 | - `getSelectedAsText()`: Returns the selected dates as an array of formatted strings. 165 | 166 | - `getSelectedAsDates()`: Returns the selected dates as an array of JavaScript Date objects. 167 | 168 | - `getSelectedRaw()`: Returns the selected dates as an array of moment objects. 169 | 170 | - `isSelected(string|Date|moment)`: Returns a true or false indicating if the passed date is selected. 171 | 172 | - `setSelected(string|Date|moment|Array)`: Sets the currently selected dates. See the `selected` option for accepted input. 173 | 174 | - `addSelected(string|Date|moment)`: Adds the passed value to the selection. Behavior varies according to the `mode` option, but matches behavior of clicking on a day in the calendar. 175 | 176 | - `removeSelected(string|Date|moment)`: removes the passed value from the selection. 177 | 178 | - `draw()`: Forces a redraw of the calendar contents. 179 | 180 | ##Member Properties 181 | 182 | The following properties are exposed on the instantiated `Kalendae` and `Kalendae.Input` objects. 183 | 184 | - `settings`: The unified options object. 185 | 186 | - `container`: The calendar container div that is inserted into the page. 187 | 188 | - `calendars`: The individual month divs (see the `months` option). 189 | 190 | ##Kalendae Events 191 | 192 | Kalendae uses a publish/subscribe event system. To receive events from a Kalendae instance you can call the `subscribe()` function on the Kalendae instance, passing the event name and a callback function. Example: 193 | 194 | var k = new Kalendae('myDiv'); 195 | k.subscribe('change', function (date, action) { 196 | console.log(date, action, this.getSelected()); 197 | }); 198 | 199 | Callbacks can also be passed in the options object: 200 | 201 | new Kalendae('myDiv', { 202 | subscribe: { 203 | 'change': function (date, action) { 204 | console.log(date, action, this.getSelected()); 205 | } 206 | } 207 | }); 208 | 209 | Kalendae offers the following events: 210 | 211 | - `change` - Fires whenever the selected date changes, either from a user clicking or a call to `setSelected()` 212 | 213 | - `date-clicked` - Fires when a date has been clicked, but before the selection is changed. Receives the date clicked as a moment object in the first parameter. Returning false will prevent selection change. 214 | 215 | - `view-changed` - Fires when the user has clicked the next or previous month button, but before the calendar is redrawn. Returning false will prevent the change. 216 | 217 | Additionally, Kalendae.Input provides the following events: 218 | 219 | - `show` - Fires when the calendar appears due to the input gaining focus 220 | 221 | - `hide` - Fires when the calendar hides due to the input blurring 222 | 223 | 224 | ##Skinning Kalendae 225 | 226 | Coming Soon. 227 | 228 | 229 | ##Building Kalendae 230 | 231 | The Kalendae source code is assembled from multiple individual files. A standard GNU makefile is included to compile the files together into the finished product. 232 | 233 | To build Kalendae, navigate to the directory containing this readme file in the system terminal and run the `make` command. 234 | 235 | To create a minified version, run `make minified`. If the minified file is blank, run `make minified-test` to see what errors Google Closure Compiler is throwing. 236 | 237 | 238 | ##Contributing to Kalendae 239 | 240 | 1. Please submit all pull requests to the `dev` branch from your own named branch. 241 | 2. Please only include the changes within the `src/` directory, do not include new builds. 242 | 3. New code should match the existing code style, with hard tabs for indentation, spaces for alignment, and [BSD/KNF style bracketing](http://en.wikipedia.org/wiki/Indent_style#BSD_KNF_style). 243 | 4. Please be aware that I have family and work obligations and may take some time to respond to your Pull Request. 244 | 245 | ##License 246 | 247 | Kalendae is released under an MIT license and is freely distributable. 248 | 249 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | var today, moment; 2 | 3 | var Kalendae = function (targetElement, options) { 4 | if (typeof document.addEventListener !== 'function' && !util.isIE8()) return; 5 | 6 | //if the first argument isn't an element and isn't a string, assume that it is the options object 7 | var is_element = false; 8 | try { 9 | is_element = targetElement instanceof Element; 10 | } 11 | catch (err) { 12 | is_element = !!targetElement && is_element.nodeType === 1; 13 | } 14 | if (!(is_element || typeof(targetElement) === 'string')) options = targetElement; 15 | 16 | var self = this, 17 | classes = self.classes, 18 | opts = self.settings = util.merge(self.defaults, {attachTo:targetElement}, options || {}), 19 | $container = self.container = util.make('div', {'class':classes.container}), 20 | calendars = self.calendars = [], 21 | startDay = moment().day(opts.weekStart), 22 | vsd, 23 | columnHeaders = [], 24 | $cal, 25 | $title, 26 | $caption, 27 | $header, 28 | $days, dayNodes = [], 29 | $span, 30 | i = 0, 31 | j = opts.months; 32 | 33 | if (util.isIE8()) util.addClassName($container, 'ie8'); 34 | 35 | //generate the column headers (Su, Mo, Tu, etc) 36 | i = 7; 37 | while (i--) { 38 | columnHeaders.push( startDay.format(opts.columnHeaderFormat) ); 39 | startDay.add('days',1); 40 | } 41 | 42 | //setup publish/subscribe and apply any subscriptions passed in settings 43 | MinPubSub(self); 44 | if (typeof opts.subscribe === 'object') { 45 | for (i in opts.subscribe) if (opts.subscribe.hasOwnProperty(i)) { 46 | self.subscribe(i, opts.subscribe[i]); 47 | } 48 | } 49 | 50 | //process default selected dates 51 | self._sel = []; 52 | if (!!opts.selected) self.setSelected(opts.selected, false); 53 | 54 | //set the view month 55 | if (!!opts.viewStartDate) { 56 | vsd = moment(opts.viewStartDate, opts.format); 57 | } else if (self._sel.length > 0) { 58 | vsd = moment(self._sel[0]); 59 | } else { 60 | vsd = moment(); 61 | } 62 | self.viewStartDate = vsd.date(1); 63 | 64 | var viewDelta = ({ 65 | 'past' : opts.months-1, 66 | 'today-past' : opts.months-1, 67 | 'any' : opts.months>2?Math.floor(opts.months/2):0, 68 | 'today-future' : 0, 69 | 'future' : 0 70 | })[this.settings.direction]; 71 | 72 | 73 | if (viewDelta && moment().month()==moment(self.viewStartDate).month()){ 74 | self.viewStartDate = moment(self.viewStartDate).subtract({M:viewDelta}).date(1); 75 | } 76 | 77 | 78 | if (typeof opts.blackout === 'function') { 79 | self.blackout = opts.blackout; 80 | } else if (!!opts.blackout) { 81 | var bdates = parseDates(opts.blackout, opts.parseSplitDelimiter, opts.format); 82 | self.blackout = function (input) { 83 | input = moment(input).yearDay(); 84 | if (input < 1 || !self._sel) return false; 85 | var i = bdates.length; 86 | while (i--) if (bdates[i].yearDay() === input) return true; 87 | return false; 88 | }; 89 | } else { 90 | self.blackout = function () {return false;}; 91 | } 92 | 93 | 94 | self.direction = self.directions[opts.direction] ? self.directions[opts.direction] : self.directions['any']; 95 | 96 | 97 | //for the total months setting, generate N calendar views and add them to the container 98 | j = Math.max(opts.months,1); 99 | while (j--) { 100 | $cal = util.make('div', {'class':classes.calendar}, $container); 101 | 102 | $cal.setAttribute('data-cal-index', j); 103 | if (opts.months > 1) { 104 | if (j == Math.max(opts.months-1,1)) util.addClassName($cal, classes.monthFirst); 105 | else if (j === 0) util.addClassName($cal, classes.monthLast); 106 | else util.addClassName($cal, classes.monthMiddle); 107 | } 108 | 109 | //title bar 110 | $title = util.make('div', {'class':classes.title}, $cal); 111 | if(!opts.useYearNav){ 112 | util.addClassName($title, classes.disableYearNav); 113 | } 114 | util.make('a', {'class':classes.previousYear}, $title); //previous button 115 | util.make('a', {'class':classes.previousMonth}, $title); //previous button 116 | util.make('a', {'class':classes.nextYear}, $title); //next button 117 | util.make('a', {'class':classes.nextMonth}, $title); //next button 118 | $caption = util.make('span', {'class':classes.caption}, $title); //title caption 119 | 120 | //column headers 121 | $header = util.make('div', {'class':classes.header}, $cal); 122 | i = 0; 123 | do { 124 | $span = util.make('span', {}, $header); 125 | $span.innerHTML = columnHeaders[i]; 126 | } while (++i < 7); 127 | 128 | //individual day cells 129 | $days = util.make('div', {'class':classes.days}, $cal); 130 | i = 0; 131 | dayNodes = []; 132 | while (i++ < 42) { 133 | dayNodes.push(util.make('span', {}, $days)); 134 | } 135 | 136 | //store each calendar view for easy redrawing 137 | calendars.push({ 138 | caption:$caption, 139 | days:dayNodes 140 | }); 141 | 142 | if (j) util.make('div', {'class':classes.monthSeparator}, $container); 143 | } 144 | 145 | self.draw(); 146 | 147 | util.addEvent($container, 'mousedown', function (event, target) { 148 | var clickedDate; 149 | if (util.hasClassName(target, classes.nextMonth)) { 150 | //NEXT MONTH BUTTON 151 | if (!self.disableNext && self.publish('view-changed', self, ['next-month']) !== false) { 152 | self.viewStartDate.add('months',1); 153 | self.draw(); 154 | } 155 | return false; 156 | 157 | } else if (util.hasClassName(target, classes.previousMonth)) { 158 | //PREVIOUS MONTH BUTTON 159 | if (!self.disablePreviousMonth && self.publish('view-changed', self, ['previous-month']) !== false) { 160 | self.viewStartDate.subtract('months',1); 161 | self.draw(); 162 | } 163 | return false; 164 | 165 | } else if (util.hasClassName(target, classes.nextYear)) { 166 | //NEXT MONTH BUTTON 167 | if (!self.disableNext && self.publish('view-changed', self, ['next-year']) !== false) { 168 | self.viewStartDate.add('years',1); 169 | self.draw(); 170 | } 171 | return false; 172 | 173 | } else if (util.hasClassName(target, classes.previousYear)) { 174 | //PREVIOUS MONTH BUTTON 175 | if (!self.disablePreviousMonth && self.publish('view-changed', self, ['previous-year']) !== false) { 176 | self.viewStartDate.subtract('years',1); 177 | self.draw(); 178 | } 179 | return false; 180 | 181 | 182 | 183 | } else if (util.hasClassName(target.parentNode, classes.days) && util.hasClassName(target, classes.dayActive) && (clickedDate = target.getAttribute('data-date'))) { 184 | //DAY CLICK 185 | clickedDate = moment(clickedDate, opts.dayAttributeFormat).hours(12); 186 | if (self.publish('date-clicked', self, [clickedDate]) !== false) { 187 | 188 | switch (opts.mode) { 189 | case 'multiple': 190 | if (!self.addSelected(clickedDate)) self.removeSelected(clickedDate); 191 | break; 192 | case 'range': 193 | self.addSelected(clickedDate); 194 | break; 195 | case 'single': 196 | /* falls through */ 197 | default: 198 | self.addSelected(clickedDate); 199 | break; 200 | } 201 | 202 | } 203 | return false; 204 | 205 | } 206 | return false; 207 | }); 208 | 209 | 210 | if (!!(opts.attachTo = util.$(opts.attachTo))) { 211 | opts.attachTo.appendChild($container); 212 | } 213 | 214 | }; 215 | 216 | Kalendae.prototype = { 217 | defaults : { 218 | attachTo :null, /* the element to attach the root container to. can be string or DOMElement */ 219 | months :1, /* total number of months to display side by side */ 220 | weekStart :0, /* day to use for the start of the week. 0 is Sunday */ 221 | direction :'any', /* past, today-past, any, today-future, future */ 222 | directionScrolling :true, /* if a direction other than any is defined, prevent scrolling out of range */ 223 | viewStartDate :null, /* date in the month to display. When multiple months, this is the left most */ 224 | blackout :null, /* array of dates, or function to be passed a date */ 225 | selected :null, /* dates already selected. can be string, date, or array of strings or dates. */ 226 | mode :'single', /* single, multiple, range */ 227 | dayOutOfMonthClickable:false, 228 | format :null, /* string used for parsing dates. */ 229 | subscribe :null, /* object containing events to subscribe to */ 230 | 231 | columnHeaderFormat :'dd', /* number of characters to show in the column headers */ 232 | titleFormat :'MMMM, YYYY', /* format mask for month titles. See momentjs.com for rules */ 233 | dayNumberFormat :'D', /* format mask for individual days */ 234 | dayAttributeFormat :'YYYY-MM-DD', /* format mask for the data-date attribute set on every span */ 235 | parseSplitDelimiter : /,\s*|\s+-\s+/, /* regex to use for splitting multiple dates from a passed string */ 236 | rangeDelimiter :' - ', /* string to use between dates when outputting in range mode */ 237 | multipleDelimiter :', ', /* string to use between dates when outputting in multiple mode */ 238 | useYearNav :true, 239 | 240 | dateClassMap :{} 241 | }, 242 | classes : { 243 | container :'kalendae', 244 | calendar :'k-calendar', 245 | monthFirst :'k-first-month', 246 | monthMiddle :'k-middle-month', 247 | monthLast :'k-last-month', 248 | title :'k-title', 249 | previousMonth :'k-btn-previous-month', 250 | nextMonth :'k-btn-next-month', 251 | previousYear :'k-btn-previous-year', 252 | nextYear :'k-btn-next-year', 253 | caption :'k-caption', 254 | header :'k-header', 255 | days :'k-days', 256 | dayOutOfMonth :'k-out-of-month', 257 | dayInMonth :'k-in-month', 258 | dayActive :'k-active', 259 | daySelected :'k-selected', 260 | dayInRange :'k-range', 261 | dayToday :'k-today', 262 | monthSeparator :'k-separator', 263 | disablePreviousMonth :'k-disable-previous-month-btn', 264 | disableNextMonth :'k-disable-next-month-btn', 265 | disablePreviousYear :'k-disable-previous-year-btn', 266 | disableNextYear :'k-disable-next-year-btn', 267 | disableYearNav :'k-disable-year-nav' 268 | }, 269 | 270 | disablePreviousMonth: false, 271 | disableNextMonth: false, 272 | disablePreviousYear: false, 273 | disableNextYear: false, 274 | 275 | directions: { 276 | 'past' :function (date) {return moment(date).yearDay() >= today.yearDay();}, 277 | 'today-past' :function (date) {return moment(date).yearDay() > today.yearDay();}, 278 | 'any' :function (date) {return false;}, 279 | 'today-future' :function (date) {return moment(date).yearDay() < today.yearDay();}, 280 | 'future' :function (date) {return moment(date).yearDay() <= today.yearDay();} 281 | }, 282 | 283 | getSelectedAsDates : function () { 284 | var out = []; 285 | var i=0, c = this._sel.length; 286 | for (;i a && input < b) || (a b)) return -1; 340 | return false; 341 | 342 | case 'multiple': 343 | var i = this._sel.length; 344 | while (i--) { 345 | if (this._sel[i].yearDay() === input) { 346 | return true; 347 | } 348 | } 349 | return false; 350 | 351 | 352 | case 'single': 353 | /* falls through */ 354 | default: 355 | return (this._sel[0] && (this._sel[0].yearDay() === input)); 356 | } 357 | 358 | return false; 359 | }, 360 | 361 | setSelected : function (input, draw) { 362 | var i, 363 | new_dates = parseDates(input, this.settings.parseSplitDelimiter, this.settings.format), 364 | old_dates = parseDates(this.getSelected(), this.settings.parseSplitDelimiter, this.settings.format); 365 | 366 | i = old_dates.length; 367 | while(i--) { this.removeSelected(old_dates[i], draw); } 368 | 369 | i = new_dates.length; 370 | while(i--) { this.addSelected(new_dates[i], draw); } 371 | 372 | if (draw !== false) this.draw(); 373 | }, 374 | 375 | addSelected : function (date, draw) { 376 | date = moment(date, this.settings.format).hours(12); 377 | 378 | if(this.settings.dayOutOfMonthClickable && this.settings.mode !== 'range'){ this.makeSelectedDateVisible(date); } 379 | 380 | switch (this.settings.mode) { 381 | case 'multiple': 382 | if (!this.isSelected(date)) this._sel.push(date); 383 | else return false; 384 | break; 385 | case 'range': 386 | 387 | if (this._sel.length !== 1) this._sel = [date]; 388 | else { 389 | if (date.yearDay() > this._sel[0].yearDay()) this._sel[1] = date; 390 | else this._sel = [date, this._sel[0]]; 391 | } 392 | break; 393 | case 'single': 394 | /* falls through */ 395 | default: 396 | this._sel = [date]; 397 | break; 398 | } 399 | this._sel.sort(function (a,b) {return a.yearDay() - b.yearDay();}); 400 | this.publish('change', this, [date]); 401 | if (draw !== false) this.draw(); 402 | return true; 403 | }, 404 | 405 | makeSelectedDateVisible: function (date) { 406 | outOfViewMonth = moment(date).date('1').diff(this.viewStartDate,'months'); 407 | 408 | if(outOfViewMonth < 0){ 409 | this.viewStartDate.subtract('months',1); 410 | } 411 | else if(outOfViewMonth > 0 && outOfViewMonth >= this.settings.months){ 412 | this.viewStartDate.add('months',1); 413 | } 414 | }, 415 | 416 | removeSelected : function (date, draw) { 417 | date = moment(date, this.settings.format).hours(12); 418 | var i = this._sel.length; 419 | while (i--) { 420 | if (this._sel[i].yearDay() === date.yearDay()) { 421 | this._sel.splice(i,1); 422 | this.publish('change', this, [date]); 423 | if (draw !== false) this.draw(); 424 | return true; 425 | } 426 | } 427 | return false; 428 | }, 429 | 430 | draw : function draw() { 431 | // return; 432 | var month = moment(this.viewStartDate).hours(12), //force middle of the day to avoid any weird date shifts 433 | day, 434 | classes = this.classes, 435 | cal, 436 | $span, 437 | klass, 438 | i=0, c, 439 | j=0, k, 440 | s, 441 | dateString, 442 | opts = this.settings, 443 | diff; 444 | 445 | c = this.calendars.length; 446 | 447 | do { 448 | day = moment(month).date(1); 449 | day.day( day.day() < this.settings.weekStart ? this.settings.weekStart-7 : this.settings.weekStart); 450 | //if the first day of the month is less than our week start, back up a week 451 | 452 | cal = this.calendars[i]; 453 | cal.caption.innerHTML = month.format(this.settings.titleFormat); 454 | j = 0; 455 | do { 456 | $span = cal.days[j]; 457 | 458 | klass = []; 459 | 460 | s = this.isSelected(day); 461 | 462 | if (s) klass.push(({'-1':classes.dayInRange,'1':classes.daySelected, 'true':classes.daySelected})[s]); 463 | 464 | if (day.month() != month.month()) klass.push(classes.dayOutOfMonth); 465 | else klass.push(classes.dayInMonth); 466 | 467 | if (!(this.blackout(day) || this.direction(day) || (day.month() != month.month() && opts.dayOutOfMonthClickable === false)) || s>0) klass.push(classes.dayActive); 468 | 469 | if (day.yearDay() === today.yearDay()) klass.push(classes.dayToday); 470 | 471 | dateString = day.format(this.settings.dayAttributeFormat); 472 | if (opts.dateClassMap[dateString]) klass.push(opts.dateClassMap[dateString]); 473 | 474 | $span.innerHTML = day.format(opts.dayNumberFormat); 475 | $span.className = klass.join(' '); 476 | $span.setAttribute('data-date', dateString); 477 | 478 | 479 | day.add('days',1); 480 | } while (++j < 42); 481 | month.add('months',1); 482 | } while (++i < c); 483 | 484 | if (opts.directionScrolling) { 485 | if (opts.direction==='today-past' || opts.direction==='past') { 486 | diff = month.add({m:1}).diff(moment(), 'months', true); 487 | if (diff <= 0) { 488 | this.disableNextMonth = false; 489 | util.removeClassName(this.container, classes.disableNextMonth); 490 | } else { 491 | this.disableNextMonth = true; 492 | util.addClassName(this.container, classes.disableNextMonth); 493 | } 494 | 495 | } else if (opts.direction==='today-future' || opts.direction==='future') { 496 | diff = month.subtract({m:1}).diff(moment(), 'months', true); 497 | if (diff > opts.months) { 498 | this.disablePreviousMonth = false; 499 | util.removeClassName(this.container, classes.disablePreviousMonth); 500 | } else { 501 | this.disablePreviousMonth = true; 502 | util.addClassName(this.container, classes.disablePreviousMonth); 503 | } 504 | 505 | } 506 | 507 | 508 | if (opts.direction==='today-past' || opts.direction==='past') { 509 | diff = month.add({m:12}).diff(moment(), 'months', true); 510 | if (diff <= -11) { 511 | this.disableNextYear = false; 512 | util.removeClassName(this.container, classes.disableNextYear); 513 | } else { 514 | this.disableNextYear = true; 515 | util.addClassName(this.container, classes.disableNextYear); 516 | } 517 | 518 | } else if (opts.direction==='today-future' || opts.direction==='future') { 519 | diff = month.subtract({m:12}).diff(moment(), 'months', true); 520 | if (diff > (11 + opts.months)) { 521 | this.disablePreviousYear = false; 522 | util.removeClassName(this.container, classes.disablePreviousYear); 523 | } else { 524 | this.disablePreviousYear = true; 525 | util.addClassName(this.container, classes.disablePreviousYear); 526 | } 527 | 528 | } 529 | 530 | } 531 | } 532 | }; 533 | 534 | var parseDates = function (input, delimiter, format) { 535 | var output = []; 536 | 537 | if (typeof input === 'string') { 538 | input = input.split(delimiter); 539 | } else if (!util.isArray(input)) { 540 | input = [input]; 541 | } 542 | 543 | var c = input.length, 544 | i = 0; 545 | 546 | do { 547 | if (input[i]) output.push( moment(input[i], format).hours(12) ); 548 | } while (++i < c); 549 | 550 | return output; 551 | }; 552 | 553 | 554 | 555 | window.Kalendae = Kalendae; 556 | -------------------------------------------------------------------------------- /src/moment.js: -------------------------------------------------------------------------------- 1 | // moment.js 2 | // version : 2.0.0 3 | // author : Tim Wood 4 | // license : MIT 5 | // momentjs.com 6 | 7 | (function (undefined) { 8 | 9 | /************************************ 10 | Constants 11 | ************************************/ 12 | 13 | var moment, 14 | VERSION = "2.0.0", 15 | round = Math.round, i, 16 | // internal storage for language config files 17 | languages = {}, 18 | 19 | // check for nodeJS 20 | hasModule = (typeof module !== 'undefined' && module.exports), 21 | 22 | // ASP.NET json date format regex 23 | aspNetJsonRegex = /^\/?Date\((\-?\d+)/i, 24 | 25 | // format tokens 26 | formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYY|YYYY|YY|a|A|hh?|HH?|mm?|ss?|SS?S?|X|zz?|ZZ?|.)/g, 27 | localFormattingTokens = /(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g, 28 | 29 | // parsing tokens 30 | parseMultipleFormatChunker = /([0-9a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+)/gi, 31 | 32 | // parsing token regexes 33 | parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99 34 | parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999 35 | parseTokenThreeDigits = /\d{3}/, // 000 - 999 36 | parseTokenFourDigits = /\d{1,4}/, // 0 - 9999 37 | parseTokenSixDigits = /[+\-]?\d{1,6}/, // -999,999 - 999,999 38 | parseTokenWord = /[0-9]*[a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF]+\s*?[\u0600-\u06FF]+/i, // any word (or two) characters or numbers including two word month in arabic. 39 | parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/i, // +00:00 -00:00 +0000 -0000 or Z 40 | parseTokenT = /T/i, // T (ISO seperator) 41 | parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123 42 | 43 | // preliminary iso regex 44 | // 0000-00-00 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000 45 | isoRegex = /^\s*\d{4}-\d\d-\d\d((T| )(\d\d(:\d\d(:\d\d(\.\d\d?\d?)?)?)?)?([\+\-]\d\d:?\d\d)?)?/, 46 | isoFormat = 'YYYY-MM-DDTHH:mm:ssZ', 47 | 48 | // iso time formats and regexes 49 | isoTimes = [ 50 | ['HH:mm:ss.S', /(T| )\d\d:\d\d:\d\d\.\d{1,3}/], 51 | ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/], 52 | ['HH:mm', /(T| )\d\d:\d\d/], 53 | ['HH', /(T| )\d\d/] 54 | ], 55 | 56 | // timezone chunker "+10:00" > ["10", "00"] or "-1530" > ["-15", "30"] 57 | parseTimezoneChunker = /([\+\-]|\d\d)/gi, 58 | 59 | // getter and setter names 60 | proxyGettersAndSetters = 'Month|Date|Hours|Minutes|Seconds|Milliseconds'.split('|'), 61 | unitMillisecondFactors = { 62 | 'Milliseconds' : 1, 63 | 'Seconds' : 1e3, 64 | 'Minutes' : 6e4, 65 | 'Hours' : 36e5, 66 | 'Days' : 864e5, 67 | 'Months' : 2592e6, 68 | 'Years' : 31536e6 69 | }, 70 | 71 | // format function strings 72 | formatFunctions = {}, 73 | 74 | // tokens to ordinalize and pad 75 | ordinalizeTokens = 'DDD w W M D d'.split(' '), 76 | paddedTokens = 'M D H h m s w W'.split(' '), 77 | 78 | formatTokenFunctions = { 79 | M : function () { 80 | return this.month() + 1; 81 | }, 82 | MMM : function (format) { 83 | return this.lang().monthsShort(this, format); 84 | }, 85 | MMMM : function (format) { 86 | return this.lang().months(this, format); 87 | }, 88 | D : function () { 89 | return this.date(); 90 | }, 91 | DDD : function () { 92 | return this.dayOfYear(); 93 | }, 94 | d : function () { 95 | return this.day(); 96 | }, 97 | dd : function (format) { 98 | return this.lang().weekdaysMin(this, format); 99 | }, 100 | ddd : function (format) { 101 | return this.lang().weekdaysShort(this, format); 102 | }, 103 | dddd : function (format) { 104 | return this.lang().weekdays(this, format); 105 | }, 106 | w : function () { 107 | return this.week(); 108 | }, 109 | W : function () { 110 | return this.isoWeek(); 111 | }, 112 | YY : function () { 113 | return leftZeroFill(this.year() % 100, 2); 114 | }, 115 | YYYY : function () { 116 | return leftZeroFill(this.year(), 4); 117 | }, 118 | YYYYY : function () { 119 | return leftZeroFill(this.year(), 5); 120 | }, 121 | a : function () { 122 | return this.lang().meridiem(this.hours(), this.minutes(), true); 123 | }, 124 | A : function () { 125 | return this.lang().meridiem(this.hours(), this.minutes(), false); 126 | }, 127 | H : function () { 128 | return this.hours(); 129 | }, 130 | h : function () { 131 | return this.hours() % 12 || 12; 132 | }, 133 | m : function () { 134 | return this.minutes(); 135 | }, 136 | s : function () { 137 | return this.seconds(); 138 | }, 139 | S : function () { 140 | return ~~(this.milliseconds() / 100); 141 | }, 142 | SS : function () { 143 | return leftZeroFill(~~(this.milliseconds() / 10), 2); 144 | }, 145 | SSS : function () { 146 | return leftZeroFill(this.milliseconds(), 3); 147 | }, 148 | Z : function () { 149 | var a = -this.zone(), 150 | b = "+"; 151 | if (a < 0) { 152 | a = -a; 153 | b = "-"; 154 | } 155 | return b + leftZeroFill(~~(a / 60), 2) + ":" + leftZeroFill(~~a % 60, 2); 156 | }, 157 | ZZ : function () { 158 | var a = -this.zone(), 159 | b = "+"; 160 | if (a < 0) { 161 | a = -a; 162 | b = "-"; 163 | } 164 | return b + leftZeroFill(~~(10 * a / 6), 4); 165 | }, 166 | X : function () { 167 | return this.unix(); 168 | } 169 | }; 170 | 171 | function padToken(func, count) { 172 | return function (a) { 173 | return leftZeroFill(func.call(this, a), count); 174 | }; 175 | } 176 | function ordinalizeToken(func) { 177 | return function (a) { 178 | return this.lang().ordinal(func.call(this, a)); 179 | }; 180 | } 181 | 182 | while (ordinalizeTokens.length) { 183 | i = ordinalizeTokens.pop(); 184 | formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i]); 185 | } 186 | while (paddedTokens.length) { 187 | i = paddedTokens.pop(); 188 | formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2); 189 | } 190 | formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3); 191 | 192 | 193 | /************************************ 194 | Constructors 195 | ************************************/ 196 | 197 | function Language() { 198 | 199 | } 200 | 201 | // Moment prototype object 202 | function Moment(config) { 203 | extend(this, config); 204 | } 205 | 206 | // Duration Constructor 207 | function Duration(duration) { 208 | var data = this._data = {}, 209 | years = duration.years || duration.year || duration.y || 0, 210 | months = duration.months || duration.month || duration.M || 0, 211 | weeks = duration.weeks || duration.week || duration.w || 0, 212 | days = duration.days || duration.day || duration.d || 0, 213 | hours = duration.hours || duration.hour || duration.h || 0, 214 | minutes = duration.minutes || duration.minute || duration.m || 0, 215 | seconds = duration.seconds || duration.second || duration.s || 0, 216 | milliseconds = duration.milliseconds || duration.millisecond || duration.ms || 0; 217 | 218 | // representation for dateAddRemove 219 | this._milliseconds = milliseconds + 220 | seconds * 1e3 + // 1000 221 | minutes * 6e4 + // 1000 * 60 222 | hours * 36e5; // 1000 * 60 * 60 223 | // Because of dateAddRemove treats 24 hours as different from a 224 | // day when working around DST, we need to store them separately 225 | this._days = days + 226 | weeks * 7; 227 | // It is impossible translate months into days without knowing 228 | // which months you are are talking about, so we have to store 229 | // it separately. 230 | this._months = months + 231 | years * 12; 232 | 233 | // The following code bubbles up values, see the tests for 234 | // examples of what that means. 235 | data.milliseconds = milliseconds % 1000; 236 | seconds += absRound(milliseconds / 1000); 237 | 238 | data.seconds = seconds % 60; 239 | minutes += absRound(seconds / 60); 240 | 241 | data.minutes = minutes % 60; 242 | hours += absRound(minutes / 60); 243 | 244 | data.hours = hours % 24; 245 | days += absRound(hours / 24); 246 | 247 | days += weeks * 7; 248 | data.days = days % 30; 249 | 250 | months += absRound(days / 30); 251 | 252 | data.months = months % 12; 253 | years += absRound(months / 12); 254 | 255 | data.years = years; 256 | } 257 | 258 | 259 | /************************************ 260 | Helpers 261 | ************************************/ 262 | 263 | 264 | function extend(a, b) { 265 | for (var i in b) { 266 | if (b.hasOwnProperty(i)) { 267 | a[i] = b[i]; 268 | } 269 | } 270 | return a; 271 | } 272 | 273 | function absRound(number) { 274 | if (number < 0) { 275 | return Math.ceil(number); 276 | } else { 277 | return Math.floor(number); 278 | } 279 | } 280 | 281 | // left zero fill a number 282 | // see http://jsperf.com/left-zero-filling for performance comparison 283 | function leftZeroFill(number, targetLength) { 284 | var output = number + ''; 285 | while (output.length < targetLength) { 286 | output = '0' + output; 287 | } 288 | return output; 289 | } 290 | 291 | // helper function for _.addTime and _.subtractTime 292 | function addOrSubtractDurationFromMoment(mom, duration, isAdding) { 293 | var ms = duration._milliseconds, 294 | d = duration._days, 295 | M = duration._months, 296 | currentDate; 297 | 298 | if (ms) { 299 | mom._d.setTime(+mom + ms * isAdding); 300 | } 301 | if (d) { 302 | mom.date(mom.date() + d * isAdding); 303 | } 304 | if (M) { 305 | currentDate = mom.date(); 306 | mom.date(1) 307 | .month(mom.month() + M * isAdding) 308 | .date(Math.min(currentDate, mom.daysInMonth())); 309 | } 310 | } 311 | 312 | // check if is an array 313 | function isArray(input) { 314 | return Object.prototype.toString.call(input) === '[object Array]'; 315 | } 316 | 317 | // compare two arrays, return the number of differences 318 | function compareArrays(array1, array2) { 319 | var len = Math.min(array1.length, array2.length), 320 | lengthDiff = Math.abs(array1.length - array2.length), 321 | diffs = 0, 322 | i; 323 | for (i = 0; i < len; i++) { 324 | if (~~array1[i] !== ~~array2[i]) { 325 | diffs++; 326 | } 327 | } 328 | return diffs + lengthDiff; 329 | } 330 | 331 | 332 | /************************************ 333 | Languages 334 | ************************************/ 335 | 336 | 337 | Language.prototype = { 338 | set : function (config) { 339 | var prop, i; 340 | for (i in config) { 341 | prop = config[i]; 342 | if (typeof prop === 'function') { 343 | this[i] = prop; 344 | } else { 345 | this['_' + i] = prop; 346 | } 347 | } 348 | }, 349 | 350 | _months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"), 351 | months : function (m) { 352 | return this._months[m.month()]; 353 | }, 354 | 355 | _monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"), 356 | monthsShort : function (m) { 357 | return this._monthsShort[m.month()]; 358 | }, 359 | 360 | monthsParse : function (monthName) { 361 | var i, mom, regex, output; 362 | 363 | if (!this._monthsParse) { 364 | this._monthsParse = []; 365 | } 366 | 367 | for (i = 0; i < 12; i++) { 368 | // make the regex if we don't have it already 369 | if (!this._monthsParse[i]) { 370 | mom = moment([2000, i]); 371 | regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, ''); 372 | this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i'); 373 | } 374 | // test the regex 375 | if (this._monthsParse[i].test(monthName)) { 376 | return i; 377 | } 378 | } 379 | }, 380 | 381 | _weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"), 382 | weekdays : function (m) { 383 | return this._weekdays[m.day()]; 384 | }, 385 | 386 | _weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"), 387 | weekdaysShort : function (m) { 388 | return this._weekdaysShort[m.day()]; 389 | }, 390 | 391 | _weekdaysMin : "Su_Mo_Tu_We_Th_Fr_Sa".split("_"), 392 | weekdaysMin : function (m) { 393 | return this._weekdaysMin[m.day()]; 394 | }, 395 | 396 | _longDateFormat : { 397 | LT : "h:mm A", 398 | L : "MM/DD/YYYY", 399 | LL : "MMMM D YYYY", 400 | LLL : "MMMM D YYYY LT", 401 | LLLL : "dddd, MMMM D YYYY LT" 402 | }, 403 | longDateFormat : function (key) { 404 | var output = this._longDateFormat[key]; 405 | if (!output && this._longDateFormat[key.toUpperCase()]) { 406 | output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) { 407 | return val.slice(1); 408 | }); 409 | this._longDateFormat[key] = output; 410 | } 411 | return output; 412 | }, 413 | 414 | meridiem : function (hours, minutes, isLower) { 415 | if (hours > 11) { 416 | return isLower ? 'pm' : 'PM'; 417 | } else { 418 | return isLower ? 'am' : 'AM'; 419 | } 420 | }, 421 | 422 | _calendar : { 423 | sameDay : '[Today at] LT', 424 | nextDay : '[Tomorrow at] LT', 425 | nextWeek : 'dddd [at] LT', 426 | lastDay : '[Yesterday at] LT', 427 | lastWeek : '[last] dddd [at] LT', 428 | sameElse : 'L' 429 | }, 430 | calendar : function (key, mom) { 431 | var output = this._calendar[key]; 432 | return typeof output === 'function' ? output.apply(mom) : output; 433 | }, 434 | 435 | _relativeTime : { 436 | future : "in %s", 437 | past : "%s ago", 438 | s : "a few seconds", 439 | m : "a minute", 440 | mm : "%d minutes", 441 | h : "an hour", 442 | hh : "%d hours", 443 | d : "a day", 444 | dd : "%d days", 445 | M : "a month", 446 | MM : "%d months", 447 | y : "a year", 448 | yy : "%d years" 449 | }, 450 | relativeTime : function (number, withoutSuffix, string, isFuture) { 451 | var output = this._relativeTime[string]; 452 | return (typeof output === 'function') ? 453 | output(number, withoutSuffix, string, isFuture) : 454 | output.replace(/%d/i, number); 455 | }, 456 | pastFuture : function (diff, output) { 457 | var format = this._relativeTime[diff > 0 ? 'future' : 'past']; 458 | return typeof format === 'function' ? format(output) : format.replace(/%s/i, output); 459 | }, 460 | 461 | ordinal : function (number) { 462 | return this._ordinal.replace("%d", number); 463 | }, 464 | _ordinal : "%d", 465 | 466 | preparse : function (string) { 467 | return string; 468 | }, 469 | 470 | postformat : function (string) { 471 | return string; 472 | }, 473 | 474 | week : function (mom) { 475 | return weekOfYear(mom, this._week.dow, this._week.doy); 476 | }, 477 | _week : { 478 | dow : 0, // Sunday is the first day of the week. 479 | doy : 6 // The week that contains Jan 1st is the first week of the year. 480 | } 481 | }; 482 | 483 | // Loads a language definition into the `languages` cache. The function 484 | // takes a key and optionally values. If not in the browser and no values 485 | // are provided, it will load the language file module. As a convenience, 486 | // this function also returns the language values. 487 | function loadLang(key, values) { 488 | values.abbr = key; 489 | if (!languages[key]) { 490 | languages[key] = new Language(); 491 | } 492 | languages[key].set(values); 493 | return languages[key]; 494 | } 495 | 496 | // Determines which language definition to use and returns it. 497 | // 498 | // With no parameters, it will return the global language. If you 499 | // pass in a language key, such as 'en', it will return the 500 | // definition for 'en', so long as 'en' has already been loaded using 501 | // moment.lang. 502 | function getLangDefinition(key) { 503 | if (!key) { 504 | return moment.fn._lang; 505 | } 506 | if (!languages[key] && hasModule) { 507 | require('./lang/' + key); 508 | } 509 | return languages[key]; 510 | } 511 | 512 | 513 | /************************************ 514 | Formatting 515 | ************************************/ 516 | 517 | 518 | function removeFormattingTokens(input) { 519 | if (input.match(/\[.*\]/)) { 520 | return input.replace(/^\[|\]$/g, ""); 521 | } 522 | return input.replace(/\\/g, ""); 523 | } 524 | 525 | function makeFormatFunction(format) { 526 | var array = format.match(formattingTokens), i, length; 527 | 528 | for (i = 0, length = array.length; i < length; i++) { 529 | if (formatTokenFunctions[array[i]]) { 530 | array[i] = formatTokenFunctions[array[i]]; 531 | } else { 532 | array[i] = removeFormattingTokens(array[i]); 533 | } 534 | } 535 | 536 | return function (mom) { 537 | var output = ""; 538 | for (i = 0; i < length; i++) { 539 | output += typeof array[i].call === 'function' ? array[i].call(mom, format) : array[i]; 540 | } 541 | return output; 542 | }; 543 | } 544 | 545 | // format date using native date object 546 | function formatMoment(m, format) { 547 | var i = 5; 548 | 549 | function replaceLongDateFormatTokens(input) { 550 | return m.lang().longDateFormat(input) || input; 551 | } 552 | 553 | while (i-- && localFormattingTokens.test(format)) { 554 | format = format.replace(localFormattingTokens, replaceLongDateFormatTokens); 555 | } 556 | 557 | if (!formatFunctions[format]) { 558 | formatFunctions[format] = makeFormatFunction(format); 559 | } 560 | 561 | return formatFunctions[format](m); 562 | } 563 | 564 | 565 | /************************************ 566 | Parsing 567 | ************************************/ 568 | 569 | 570 | // get the regex to find the next token 571 | function getParseRegexForToken(token) { 572 | switch (token) { 573 | case 'DDDD': 574 | return parseTokenThreeDigits; 575 | case 'YYYY': 576 | return parseTokenFourDigits; 577 | case 'YYYYY': 578 | return parseTokenSixDigits; 579 | case 'S': 580 | case 'SS': 581 | case 'SSS': 582 | case 'DDD': 583 | return parseTokenOneToThreeDigits; 584 | case 'MMM': 585 | case 'MMMM': 586 | case 'dd': 587 | case 'ddd': 588 | case 'dddd': 589 | case 'a': 590 | case 'A': 591 | return parseTokenWord; 592 | case 'X': 593 | return parseTokenTimestampMs; 594 | case 'Z': 595 | case 'ZZ': 596 | return parseTokenTimezone; 597 | case 'T': 598 | return parseTokenT; 599 | case 'MM': 600 | case 'DD': 601 | case 'YY': 602 | case 'HH': 603 | case 'hh': 604 | case 'mm': 605 | case 'ss': 606 | case 'M': 607 | case 'D': 608 | case 'd': 609 | case 'H': 610 | case 'h': 611 | case 'm': 612 | case 's': 613 | return parseTokenOneOrTwoDigits; 614 | default : 615 | return new RegExp(token.replace('\\', '')); 616 | } 617 | } 618 | 619 | // function to convert string input to date 620 | function addTimeToArrayFromToken(token, input, config) { 621 | var a, b, 622 | datePartArray = config._a; 623 | 624 | switch (token) { 625 | // MONTH 626 | case 'M' : // fall through to MM 627 | case 'MM' : 628 | datePartArray[1] = (input == null) ? 0 : ~~input - 1; 629 | break; 630 | case 'MMM' : // fall through to MMMM 631 | case 'MMMM' : 632 | a = getLangDefinition(config._l).monthsParse(input); 633 | // if we didn't find a month name, mark the date as invalid. 634 | if (a != null) { 635 | datePartArray[1] = a; 636 | } else { 637 | config._isValid = false; 638 | } 639 | break; 640 | // DAY OF MONTH 641 | case 'D' : // fall through to DDDD 642 | case 'DD' : // fall through to DDDD 643 | case 'DDD' : // fall through to DDDD 644 | case 'DDDD' : 645 | if (input != null) { 646 | datePartArray[2] = ~~input; 647 | } 648 | break; 649 | // YEAR 650 | case 'YY' : 651 | datePartArray[0] = ~~input + (~~input > 68 ? 1900 : 2000); 652 | break; 653 | case 'YYYY' : 654 | case 'YYYYY' : 655 | datePartArray[0] = ~~input; 656 | break; 657 | // AM / PM 658 | case 'a' : // fall through to A 659 | case 'A' : 660 | config._isPm = ((input + '').toLowerCase() === 'pm'); 661 | break; 662 | // 24 HOUR 663 | case 'H' : // fall through to hh 664 | case 'HH' : // fall through to hh 665 | case 'h' : // fall through to hh 666 | case 'hh' : 667 | datePartArray[3] = ~~input; 668 | break; 669 | // MINUTE 670 | case 'm' : // fall through to mm 671 | case 'mm' : 672 | datePartArray[4] = ~~input; 673 | break; 674 | // SECOND 675 | case 's' : // fall through to ss 676 | case 'ss' : 677 | datePartArray[5] = ~~input; 678 | break; 679 | // MILLISECOND 680 | case 'S' : 681 | case 'SS' : 682 | case 'SSS' : 683 | datePartArray[6] = ~~ (('0.' + input) * 1000); 684 | break; 685 | // UNIX TIMESTAMP WITH MS 686 | case 'X': 687 | config._d = new Date(parseFloat(input) * 1000); 688 | break; 689 | // TIMEZONE 690 | case 'Z' : // fall through to ZZ 691 | case 'ZZ' : 692 | config._useUTC = true; 693 | a = (input + '').match(parseTimezoneChunker); 694 | if (a && a[1]) { 695 | config._tzh = ~~a[1]; 696 | } 697 | if (a && a[2]) { 698 | config._tzm = ~~a[2]; 699 | } 700 | // reverse offsets 701 | if (a && a[0] === '+') { 702 | config._tzh = -config._tzh; 703 | config._tzm = -config._tzm; 704 | } 705 | break; 706 | } 707 | 708 | // if the input is null, the date is not valid 709 | if (input == null) { 710 | config._isValid = false; 711 | } 712 | } 713 | 714 | // convert an array to a date. 715 | // the array should mirror the parameters below 716 | // note: all values past the year are optional and will default to the lowest possible value. 717 | // [year, month, day , hour, minute, second, millisecond] 718 | function dateFromArray(config) { 719 | var i, date, input = []; 720 | 721 | if (config._d) { 722 | return; 723 | } 724 | 725 | for (i = 0; i < 7; i++) { 726 | config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i]; 727 | } 728 | 729 | // add the offsets to the time to be parsed so that we can have a clean array for checking isValid 730 | input[3] += config._tzh || 0; 731 | input[4] += config._tzm || 0; 732 | 733 | date = new Date(0); 734 | 735 | if (config._useUTC) { 736 | date.setUTCFullYear(input[0], input[1], input[2]); 737 | date.setUTCHours(input[3], input[4], input[5], input[6]); 738 | } else { 739 | date.setFullYear(input[0], input[1], input[2]); 740 | date.setHours(input[3], input[4], input[5], input[6]); 741 | } 742 | 743 | config._d = date; 744 | } 745 | 746 | // date from string and format string 747 | function makeDateFromStringAndFormat(config) { 748 | // This array is used to make a Date, either with `new Date` or `Date.UTC` 749 | var tokens = config._f.match(formattingTokens), 750 | string = config._i, 751 | i, parsedInput; 752 | 753 | config._a = []; 754 | 755 | for (i = 0; i < tokens.length; i++) { 756 | parsedInput = (getParseRegexForToken(tokens[i]).exec(string) || [])[0]; 757 | if (parsedInput) { 758 | string = string.slice(string.indexOf(parsedInput) + parsedInput.length); 759 | } 760 | // don't parse if its not a known token 761 | if (formatTokenFunctions[tokens[i]]) { 762 | addTimeToArrayFromToken(tokens[i], parsedInput, config); 763 | } 764 | } 765 | // handle am pm 766 | if (config._isPm && config._a[3] < 12) { 767 | config._a[3] += 12; 768 | } 769 | // if is 12 am, change hours to 0 770 | if (config._isPm === false && config._a[3] === 12) { 771 | config._a[3] = 0; 772 | } 773 | // return 774 | dateFromArray(config); 775 | } 776 | 777 | // date from string and array of format strings 778 | function makeDateFromStringAndArray(config) { 779 | var tempConfig, 780 | tempMoment, 781 | bestMoment, 782 | 783 | scoreToBeat = 99, 784 | i, 785 | currentDate, 786 | currentScore; 787 | 788 | while (config._f.length) { 789 | tempConfig = extend({}, config); 790 | tempConfig._f = config._f.pop(); 791 | makeDateFromStringAndFormat(tempConfig); 792 | tempMoment = new Moment(tempConfig); 793 | 794 | if (tempMoment.isValid()) { 795 | bestMoment = tempMoment; 796 | break; 797 | } 798 | 799 | currentScore = compareArrays(tempConfig._a, tempMoment.toArray()); 800 | 801 | if (currentScore < scoreToBeat) { 802 | scoreToBeat = currentScore; 803 | bestMoment = tempMoment; 804 | } 805 | } 806 | 807 | extend(config, bestMoment); 808 | } 809 | 810 | // date from iso format 811 | function makeDateFromString(config) { 812 | var i, 813 | string = config._i; 814 | if (isoRegex.exec(string)) { 815 | config._f = 'YYYY-MM-DDT'; 816 | for (i = 0; i < 4; i++) { 817 | if (isoTimes[i][1].exec(string)) { 818 | config._f += isoTimes[i][0]; 819 | break; 820 | } 821 | } 822 | if (parseTokenTimezone.exec(string)) { 823 | config._f += " Z"; 824 | } 825 | makeDateFromStringAndFormat(config); 826 | } else { 827 | config._d = new Date(string); 828 | } 829 | } 830 | 831 | function makeDateFromInput(config) { 832 | var input = config._i, 833 | matched = aspNetJsonRegex.exec(input); 834 | 835 | if (input === undefined) { 836 | config._d = new Date(); 837 | } else if (matched) { 838 | config._d = new Date(+matched[1]); 839 | } else if (typeof input === 'string') { 840 | makeDateFromString(config); 841 | } else if (isArray(input)) { 842 | config._a = input.slice(0); 843 | dateFromArray(config); 844 | } else { 845 | config._d = input instanceof Date ? new Date(+input) : new Date(input); 846 | } 847 | } 848 | 849 | 850 | /************************************ 851 | Relative Time 852 | ************************************/ 853 | 854 | 855 | // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize 856 | function substituteTimeAgo(string, number, withoutSuffix, isFuture, lang) { 857 | return lang.relativeTime(number || 1, !!withoutSuffix, string, isFuture); 858 | } 859 | 860 | function relativeTime(milliseconds, withoutSuffix, lang) { 861 | var seconds = round(Math.abs(milliseconds) / 1000), 862 | minutes = round(seconds / 60), 863 | hours = round(minutes / 60), 864 | days = round(hours / 24), 865 | years = round(days / 365), 866 | args = seconds < 45 && ['s', seconds] || 867 | minutes === 1 && ['m'] || 868 | minutes < 45 && ['mm', minutes] || 869 | hours === 1 && ['h'] || 870 | hours < 22 && ['hh', hours] || 871 | days === 1 && ['d'] || 872 | days <= 25 && ['dd', days] || 873 | days <= 45 && ['M'] || 874 | days < 345 && ['MM', round(days / 30)] || 875 | years === 1 && ['y'] || ['yy', years]; 876 | args[2] = withoutSuffix; 877 | args[3] = milliseconds > 0; 878 | args[4] = lang; 879 | return substituteTimeAgo.apply({}, args); 880 | } 881 | 882 | 883 | /************************************ 884 | Week of Year 885 | ************************************/ 886 | 887 | 888 | // firstDayOfWeek 0 = sun, 6 = sat 889 | // the day of the week that starts the week 890 | // (usually sunday or monday) 891 | // firstDayOfWeekOfYear 0 = sun, 6 = sat 892 | // the first week is the week that contains the first 893 | // of this day of the week 894 | // (eg. ISO weeks use thursday (4)) 895 | function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) { 896 | var end = firstDayOfWeekOfYear - firstDayOfWeek, 897 | daysToDayOfWeek = firstDayOfWeekOfYear - mom.day(); 898 | 899 | 900 | if (daysToDayOfWeek > end) { 901 | daysToDayOfWeek -= 7; 902 | } 903 | 904 | if (daysToDayOfWeek < end - 7) { 905 | daysToDayOfWeek += 7; 906 | } 907 | 908 | return Math.ceil(moment(mom).add('d', daysToDayOfWeek).dayOfYear() / 7); 909 | } 910 | 911 | 912 | /************************************ 913 | Top Level Functions 914 | ************************************/ 915 | 916 | function makeMoment(config) { 917 | var input = config._i, 918 | format = config._f; 919 | 920 | if (input === null || input === '') { 921 | return null; 922 | } 923 | 924 | if (typeof input === 'string') { 925 | config._i = input = getLangDefinition().preparse(input); 926 | } 927 | 928 | if (moment.isMoment(input)) { 929 | config = extend({}, input); 930 | config._d = new Date(+input._d); 931 | } else if (format) { 932 | if (isArray(format)) { 933 | makeDateFromStringAndArray(config); 934 | } else { 935 | makeDateFromStringAndFormat(config); 936 | } 937 | } else { 938 | makeDateFromInput(config); 939 | } 940 | 941 | return new Moment(config); 942 | } 943 | 944 | moment = function (input, format, lang) { 945 | return makeMoment({ 946 | _i : input, 947 | _f : format, 948 | _l : lang, 949 | _isUTC : false 950 | }); 951 | }; 952 | 953 | // creating with utc 954 | moment.utc = function (input, format, lang) { 955 | return makeMoment({ 956 | _useUTC : true, 957 | _isUTC : true, 958 | _l : lang, 959 | _i : input, 960 | _f : format 961 | }); 962 | }; 963 | 964 | // creating with unix timestamp (in seconds) 965 | moment.unix = function (input) { 966 | return moment(input * 1000); 967 | }; 968 | 969 | // duration 970 | moment.duration = function (input, key) { 971 | var isDuration = moment.isDuration(input), 972 | isNumber = (typeof input === 'number'), 973 | duration = (isDuration ? input._data : (isNumber ? {} : input)), 974 | ret; 975 | 976 | if (isNumber) { 977 | if (key) { 978 | duration[key] = input; 979 | } else { 980 | duration.milliseconds = input; 981 | } 982 | } 983 | 984 | ret = new Duration(duration); 985 | 986 | if (isDuration && input.hasOwnProperty('_lang')) { 987 | ret._lang = input._lang; 988 | } 989 | 990 | return ret; 991 | }; 992 | 993 | // version number 994 | moment.version = VERSION; 995 | 996 | // default format 997 | moment.defaultFormat = isoFormat; 998 | 999 | // This function will load languages and then set the global language. If 1000 | // no arguments are passed in, it will simply return the current global 1001 | // language key. 1002 | moment.lang = function (key, values) { 1003 | var i; 1004 | 1005 | if (!key) { 1006 | return moment.fn._lang._abbr; 1007 | } 1008 | if (values) { 1009 | loadLang(key, values); 1010 | } else if (!languages[key]) { 1011 | getLangDefinition(key); 1012 | } 1013 | moment.duration.fn._lang = moment.fn._lang = getLangDefinition(key); 1014 | }; 1015 | 1016 | // returns language data 1017 | moment.langData = function (key) { 1018 | if (key && key._lang && key._lang._abbr) { 1019 | key = key._lang._abbr; 1020 | } 1021 | return getLangDefinition(key); 1022 | }; 1023 | 1024 | // compare moment object 1025 | moment.isMoment = function (obj) { 1026 | return obj instanceof Moment; 1027 | }; 1028 | 1029 | // for typechecking Duration objects 1030 | moment.isDuration = function (obj) { 1031 | return obj instanceof Duration; 1032 | }; 1033 | 1034 | 1035 | /************************************ 1036 | Moment Prototype 1037 | ************************************/ 1038 | 1039 | 1040 | moment.fn = Moment.prototype = { 1041 | 1042 | clone : function () { 1043 | return moment(this); 1044 | }, 1045 | 1046 | valueOf : function () { 1047 | return +this._d; 1048 | }, 1049 | 1050 | unix : function () { 1051 | return Math.floor(+this._d / 1000); 1052 | }, 1053 | 1054 | toString : function () { 1055 | return this.format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ"); 1056 | }, 1057 | 1058 | toDate : function () { 1059 | return this._d; 1060 | }, 1061 | 1062 | toJSON : function () { 1063 | return moment.utc(this).format('YYYY-MM-DD[T]HH:mm:ss.SSS[Z]'); 1064 | }, 1065 | 1066 | toArray : function () { 1067 | var m = this; 1068 | return [ 1069 | m.year(), 1070 | m.month(), 1071 | m.date(), 1072 | m.hours(), 1073 | m.minutes(), 1074 | m.seconds(), 1075 | m.milliseconds() 1076 | ]; 1077 | }, 1078 | 1079 | isValid : function () { 1080 | if (this._isValid == null) { 1081 | if (this._a) { 1082 | this._isValid = !compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray()); 1083 | } else { 1084 | this._isValid = !isNaN(this._d.getTime()); 1085 | } 1086 | } 1087 | return !!this._isValid; 1088 | }, 1089 | 1090 | utc : function () { 1091 | this._isUTC = true; 1092 | return this; 1093 | }, 1094 | 1095 | local : function () { 1096 | this._isUTC = false; 1097 | return this; 1098 | }, 1099 | 1100 | format : function (inputString) { 1101 | var output = formatMoment(this, inputString || moment.defaultFormat); 1102 | return this.lang().postformat(output); 1103 | }, 1104 | 1105 | add : function (input, val) { 1106 | var dur; 1107 | // switch args to support add('s', 1) and add(1, 's') 1108 | if (typeof input === 'string') { 1109 | dur = moment.duration(+val, input); 1110 | } else { 1111 | dur = moment.duration(input, val); 1112 | } 1113 | addOrSubtractDurationFromMoment(this, dur, 1); 1114 | return this; 1115 | }, 1116 | 1117 | subtract : function (input, val) { 1118 | var dur; 1119 | // switch args to support subtract('s', 1) and subtract(1, 's') 1120 | if (typeof input === 'string') { 1121 | dur = moment.duration(+val, input); 1122 | } else { 1123 | dur = moment.duration(input, val); 1124 | } 1125 | addOrSubtractDurationFromMoment(this, dur, -1); 1126 | return this; 1127 | }, 1128 | 1129 | diff : function (input, units, asFloat) { 1130 | var that = this._isUTC ? moment(input).utc() : moment(input).local(), 1131 | zoneDiff = (this.zone() - that.zone()) * 6e4, 1132 | diff, output; 1133 | 1134 | if (units) { 1135 | // standardize on singular form 1136 | units = units.replace(/s$/, ''); 1137 | } 1138 | 1139 | if (units === 'year' || units === 'month') { 1140 | diff = (this.daysInMonth() + that.daysInMonth()) * 432e5; // 24 * 60 * 60 * 1000 / 2 1141 | output = ((this.year() - that.year()) * 12) + (this.month() - that.month()); 1142 | output += ((this - moment(this).startOf('month')) - (that - moment(that).startOf('month'))) / diff; 1143 | if (units === 'year') { 1144 | output = output / 12; 1145 | } 1146 | } else { 1147 | diff = (this - that) - zoneDiff; 1148 | output = units === 'second' ? diff / 1e3 : // 1000 1149 | units === 'minute' ? diff / 6e4 : // 1000 * 60 1150 | units === 'hour' ? diff / 36e5 : // 1000 * 60 * 60 1151 | units === 'day' ? diff / 864e5 : // 1000 * 60 * 60 * 24 1152 | units === 'week' ? diff / 6048e5 : // 1000 * 60 * 60 * 24 * 7 1153 | diff; 1154 | } 1155 | return asFloat ? output : absRound(output); 1156 | }, 1157 | 1158 | from : function (time, withoutSuffix) { 1159 | return moment.duration(this.diff(time)).lang(this.lang()._abbr).humanize(!withoutSuffix); 1160 | }, 1161 | 1162 | fromNow : function (withoutSuffix) { 1163 | return this.from(moment(), withoutSuffix); 1164 | }, 1165 | 1166 | calendar : function () { 1167 | var diff = this.diff(moment().startOf('day'), 'days', true), 1168 | format = diff < -6 ? 'sameElse' : 1169 | diff < -1 ? 'lastWeek' : 1170 | diff < 0 ? 'lastDay' : 1171 | diff < 1 ? 'sameDay' : 1172 | diff < 2 ? 'nextDay' : 1173 | diff < 7 ? 'nextWeek' : 'sameElse'; 1174 | return this.format(this.lang().calendar(format, this)); 1175 | }, 1176 | 1177 | isLeapYear : function () { 1178 | var year = this.year(); 1179 | return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; 1180 | }, 1181 | 1182 | isDST : function () { 1183 | return (this.zone() < moment([this.year()]).zone() || 1184 | this.zone() < moment([this.year(), 5]).zone()); 1185 | }, 1186 | 1187 | day : function (input) { 1188 | var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay(); 1189 | return input == null ? day : 1190 | this.add({ d : input - day }); 1191 | }, 1192 | 1193 | startOf: function (units) { 1194 | units = units.replace(/s$/, ''); 1195 | // the following switch intentionally omits break keywords 1196 | // to utilize falling through the cases. 1197 | switch (units) { 1198 | case 'year': 1199 | this.month(0); 1200 | /* falls through */ 1201 | case 'month': 1202 | this.date(1); 1203 | /* falls through */ 1204 | case 'week': 1205 | case 'day': 1206 | this.hours(0); 1207 | /* falls through */ 1208 | case 'hour': 1209 | this.minutes(0); 1210 | /* falls through */ 1211 | case 'minute': 1212 | this.seconds(0); 1213 | /* falls through */ 1214 | case 'second': 1215 | this.milliseconds(0); 1216 | /* falls through */ 1217 | } 1218 | 1219 | // weeks are a special case 1220 | if (units === 'week') { 1221 | this.day(0); 1222 | } 1223 | 1224 | return this; 1225 | }, 1226 | 1227 | endOf: function (units) { 1228 | return this.startOf(units).add(units.replace(/s?$/, 's'), 1).subtract('ms', 1); 1229 | }, 1230 | 1231 | isAfter: function (input, units) { 1232 | units = typeof units !== 'undefined' ? units : 'millisecond'; 1233 | return +this.clone().startOf(units) > +moment(input).startOf(units); 1234 | }, 1235 | 1236 | isBefore: function (input, units) { 1237 | units = typeof units !== 'undefined' ? units : 'millisecond'; 1238 | return +this.clone().startOf(units) < +moment(input).startOf(units); 1239 | }, 1240 | 1241 | isSame: function (input, units) { 1242 | units = typeof units !== 'undefined' ? units : 'millisecond'; 1243 | return +this.clone().startOf(units) === +moment(input).startOf(units); 1244 | }, 1245 | 1246 | zone : function () { 1247 | return this._isUTC ? 0 : this._d.getTimezoneOffset(); 1248 | }, 1249 | 1250 | daysInMonth : function () { 1251 | return moment.utc([this.year(), this.month() + 1, 0]).date(); 1252 | }, 1253 | 1254 | dayOfYear : function (input) { 1255 | var dayOfYear = round((moment(this).startOf('day') - moment(this).startOf('year')) / 864e5) + 1; 1256 | return input == null ? dayOfYear : this.add("d", (input - dayOfYear)); 1257 | }, 1258 | 1259 | isoWeek : function (input) { 1260 | var week = weekOfYear(this, 1, 4); 1261 | return input == null ? week : this.add("d", (input - week) * 7); 1262 | }, 1263 | 1264 | week : function (input) { 1265 | var week = this.lang().week(this); 1266 | return input == null ? week : this.add("d", (input - week) * 7); 1267 | }, 1268 | 1269 | // If passed a language key, it will set the language for this 1270 | // instance. Otherwise, it will return the language configuration 1271 | // variables for this instance. 1272 | lang : function (key) { 1273 | if (key === undefined) { 1274 | return this._lang; 1275 | } else { 1276 | this._lang = getLangDefinition(key); 1277 | return this; 1278 | } 1279 | } 1280 | }; 1281 | 1282 | // helper for adding shortcuts 1283 | function makeGetterAndSetter(name, key) { 1284 | moment.fn[name] = moment.fn[name + 's'] = function (input) { 1285 | var utc = this._isUTC ? 'UTC' : ''; 1286 | if (input != null) { 1287 | this._d['set' + utc + key](input); 1288 | return this; 1289 | } else { 1290 | return this._d['get' + utc + key](); 1291 | } 1292 | }; 1293 | } 1294 | 1295 | // loop through and add shortcuts (Month, Date, Hours, Minutes, Seconds, Milliseconds) 1296 | for (i = 0; i < proxyGettersAndSetters.length; i ++) { 1297 | makeGetterAndSetter(proxyGettersAndSetters[i].toLowerCase().replace(/s$/, ''), proxyGettersAndSetters[i]); 1298 | } 1299 | 1300 | // add shortcut for year (uses different syntax than the getter/setter 'year' == 'FullYear') 1301 | makeGetterAndSetter('year', 'FullYear'); 1302 | 1303 | // add plural methods 1304 | moment.fn.days = moment.fn.day; 1305 | moment.fn.weeks = moment.fn.week; 1306 | moment.fn.isoWeeks = moment.fn.isoWeek; 1307 | 1308 | /************************************ 1309 | Duration Prototype 1310 | ************************************/ 1311 | 1312 | 1313 | moment.duration.fn = Duration.prototype = { 1314 | weeks : function () { 1315 | return absRound(this.days() / 7); 1316 | }, 1317 | 1318 | valueOf : function () { 1319 | return this._milliseconds + 1320 | this._days * 864e5 + 1321 | this._months * 2592e6; 1322 | }, 1323 | 1324 | humanize : function (withSuffix) { 1325 | var difference = +this, 1326 | output = relativeTime(difference, !withSuffix, this.lang()); 1327 | 1328 | if (withSuffix) { 1329 | output = this.lang().pastFuture(difference, output); 1330 | } 1331 | 1332 | return this.lang().postformat(output); 1333 | }, 1334 | 1335 | lang : moment.fn.lang 1336 | }; 1337 | 1338 | function makeDurationGetter(name) { 1339 | moment.duration.fn[name] = function () { 1340 | return this._data[name]; 1341 | }; 1342 | } 1343 | 1344 | function makeDurationAsGetter(name, factor) { 1345 | moment.duration.fn['as' + name] = function () { 1346 | return +this / factor; 1347 | }; 1348 | } 1349 | 1350 | for (i in unitMillisecondFactors) { 1351 | if (unitMillisecondFactors.hasOwnProperty(i)) { 1352 | makeDurationAsGetter(i, unitMillisecondFactors[i]); 1353 | makeDurationGetter(i.toLowerCase()); 1354 | } 1355 | } 1356 | 1357 | makeDurationAsGetter('Weeks', 6048e5); 1358 | 1359 | 1360 | /************************************ 1361 | Default Lang 1362 | ************************************/ 1363 | 1364 | 1365 | // Set default language, other languages will inherit from English. 1366 | moment.lang('en', { 1367 | ordinal : function (number) { 1368 | var b = number % 10, 1369 | output = (~~ (number % 100 / 10) === 1) ? 'th' : 1370 | (b === 1) ? 'st' : 1371 | (b === 2) ? 'nd' : 1372 | (b === 3) ? 'rd' : 'th'; 1373 | return number + output; 1374 | } 1375 | }); 1376 | 1377 | 1378 | /************************************ 1379 | Exposing Moment 1380 | ************************************/ 1381 | 1382 | this['moment'] = moment; 1383 | 1384 | }).call(typeof Kalendae === 'undefined' ? window : Kalendae); 1385 | --------------------------------------------------------------------------------