pikaday-gem 1.0.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2013 UC Berkeley - ETS
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # Pikaday Gem
2
+ [![Gem Version](https://badge.fury.io/rb/pikaday-gem.png)](http://badge.fury.io/rb/pikaday-gem) [![Dependency Status](https://gemnasium.com/ets-berkeley-edu/pikaday-gem.png)](https://gemnasium.com/ets-berkeley-edu/pikaday-gem) [![Code Climate](https://codeclimate.com/github/ets-berkeley-edu/pikaday-gem.png)](https://codeclimate.com/github/ets-berkeley-edu/pikaday-gem)
3
+
4
+ [Pikaday datepicker][pikaday] as a Ruby gem.
5
+
6
+ ## Getting Started
7
+
8
+ Add the gem to your Gemfile:
9
+
10
+ ```ruby
11
+ gem "pikaday-gem"
12
+ ```
13
+
14
+ And run
15
+
16
+ ```bash
17
+ bundle install
18
+ ```
19
+ in the terminal to download the resources.
20
+
21
+ ### Adding the files to your projects
22
+
23
+ In order for the files to load, you'll need to do add them.
24
+
25
+ `application.js`:
26
+
27
+ ```javascript
28
+ //= require pikaday
29
+ ```
30
+
31
+ `application.css`:
32
+
33
+ ```css
34
+ *= require pikaday
35
+ ```
36
+
37
+ and you should be good to go.
38
+
39
+ ## Updating this plug-in
40
+
41
+ If you would like to update this gem you should take the following steps:
42
+
43
+ 1. `rake download VERSION=X.X.X`. If you don't specify the version, it will get the latest one.
44
+ 1. `rake tag VERSION=X.X.X` will tag the version you've specified as the standard version.
45
+ 1. Make a Pull request
46
+
47
+ Then the maintainer of the gem will need to do the following steps:
48
+
49
+ 1. Update the version [lib/pikaday-gem/version.rb](lib/pikaday-gem/version.rb)
50
+ 1. Run ``gem build pikaday-gem.gemspec`` to package the gem
51
+ 1. Once satisfied, push the gem up to RubyGems.org with ``gem push pikaday-gem-<VERSION>.gem``
52
+
53
+ [pikaday]: https://github.com/dbushell/Pikaday
data/Rakefile ADDED
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+
8
+ Bundler::GemHelper.install_tasks
9
+
10
+ require 'rake'
11
+ require 'open-uri'
12
+ require 'json'
13
+
14
+ dir_assets = 'vendor/assets/'
15
+ dir_js = dir_assets + 'javascripts'
16
+ dir_css = dir_assets + 'stylesheets'
17
+
18
+ desc 'Downloads the pikaday CSS and JavaScript files from GitHub'
19
+ task :download do |t|
20
+
21
+ def create_dir dir, version
22
+ dir_name = File.join(dir, version)
23
+ Dir.mkdir(dir_name) unless Dir.exist?(dir_name)
24
+ end
25
+
26
+ def download_file url, dir, filename, version
27
+ Dir.chdir(File.join(dir, version)) do
28
+ if File.exists?(filename)
29
+ puts " #{dir + '/' + version + '/' + filename} already exists"
30
+ next
31
+ end
32
+ puts " #{url}"
33
+ open(filename, "w") { |file| file.write(open(url).read)}
34
+ end
35
+ end
36
+
37
+ # Specify which version you want
38
+ version = ENV['VERSION']
39
+ version ||= 'latest'
40
+ puts "Target version: #{version.chomp('/')}"
41
+
42
+ # Get the different versions
43
+ tags_url = 'https://api.github.com/repos/dbushell/Pikaday/tags'
44
+ result = JSON.parse(open(tags_url).read)
45
+ versions = result.map { |item| item['name'] }
46
+
47
+ puts "Available versions: #{versions.inspect}"
48
+
49
+ # Figuring out which version to get
50
+ if versions.include? version
51
+ get_version = version
52
+ else
53
+ get_version = versions.first
54
+
55
+ if !(versions.include? version) && version != 'latest'
56
+ puts "Warning: The version you've specified: '#{version}' wasn't found, using the latest version instead: '#{get_version}'"
57
+ end
58
+ end
59
+
60
+ # Get the right commit
61
+ commit = result.select { |item| item['name'] == get_version }.first['commit']['sha']
62
+ puts "We'll use the following commit to get the files: #{commit}"
63
+
64
+ # Creating the necessary directories
65
+ create_dir dir_js, get_version
66
+ create_dir dir_css, get_version
67
+
68
+ # Download the right files
69
+ url_root = 'https://raw.github.com/dbushell/Pikaday/' + commit + '/'
70
+ url_js = url_root + 'pikaday.js'
71
+ url_css = url_root + '/css/pikaday.css'
72
+
73
+ puts "Downloading... \n"
74
+ download_file url_js, dir_js, 'pikaday.js', get_version
75
+ download_file url_css, dir_css, 'pikaday.css', get_version
76
+
77
+ end
78
+
79
+ desc 'Tag the default file versions for asset helpers'
80
+ task :tag do |t|
81
+ Rake::Task['tag_default'].invoke
82
+ end
83
+
84
+ task :tag_default do |t|
85
+
86
+ def copy_files dir, version
87
+ Dir.entries(File.join(dir, version)).each do |file|
88
+ file_source = File.join(dir, version, file)
89
+ file_destination = File.join(dir, file)
90
+ if File.file?(file_source)
91
+ FileUtils.cp file_source, file_destination, { verbose: true }
92
+ end
93
+ end
94
+ end
95
+
96
+ version = ENV['VERSION']
97
+ version ||= 'latest'
98
+
99
+ puts "Target version: #{version.chomp('/')}"
100
+
101
+ Dir.chdir(dir_js) do
102
+ version_directories = Dir.glob("*").select { |fn| File.directory?(fn) }.sort.reverse
103
+
104
+ puts "Available versions: #{version_directories.inspect}"
105
+ if !(version_directories.include? version)
106
+ if version != 'latest'
107
+ puts "WARN: Specified version='#{version}' not found, setting to latest version: '#{version_directories.first}'"
108
+ end
109
+ version = version_directories.first
110
+ end
111
+ end
112
+
113
+ copy_files dir_js, version
114
+ copy_files dir_css, version
115
+ end
@@ -0,0 +1,6 @@
1
+ require "rails/engine"
2
+
3
+ module PikadayGem
4
+ class Engine < Rails::Engine
5
+ end
6
+ end
@@ -0,0 +1,3 @@
1
+ module PikadayGem
2
+ VERSION = "1.0.0.0"
3
+ end
@@ -0,0 +1,816 @@
1
+ /*!
2
+ * Pikaday
3
+ * Copyright © 2012 David Bushell | BSD & MIT license | http://dbushell.com/
4
+ */
5
+
6
+ (function(window, document, undefined)
7
+ {
8
+ 'use strict';
9
+
10
+ /**
11
+ * feature detection and helper functions
12
+ */
13
+ var hasMoment = typeof window.moment === 'function',
14
+
15
+ hasEventListeners = !!window.addEventListener,
16
+
17
+ sto = window.setTimeout,
18
+
19
+ addEvent = function(el, e, callback, capture)
20
+ {
21
+ if (hasEventListeners) {
22
+ el.addEventListener(e, callback, !!capture);
23
+ } else {
24
+ el.attachEvent('on' + e, callback);
25
+ }
26
+ },
27
+
28
+ removeEvent = function(el, e, callback, capture)
29
+ {
30
+ if (hasEventListeners) {
31
+ el.removeEventListener(e, callback, !!capture);
32
+ } else {
33
+ el.detachEvent('on' + e, callback);
34
+ }
35
+ },
36
+
37
+ fireEvent = function(el, eventName, data)
38
+ {
39
+ var ev;
40
+
41
+ if (document.createEvent) {
42
+ ev = document.createEvent('HTMLEvents');
43
+ ev.initEvent(eventName, true, false);
44
+ ev = extend(ev, data);
45
+ el.dispatchEvent(ev);
46
+ } else if (document.createEventObject) {
47
+ ev = document.createEventObject();
48
+ ev = extend(ev, data);
49
+ el.fireEvent('on' + eventName, ev);
50
+ }
51
+ },
52
+
53
+ trim = function(str)
54
+ {
55
+ return str.trim ? str.trim() : str.replace(/^\s+|\s+$/g,'');
56
+ },
57
+
58
+ hasClass = function(el, cn)
59
+ {
60
+ return (' ' + el.className + ' ').indexOf(' ' + cn + ' ') !== -1;
61
+ },
62
+
63
+ addClass = function(el, cn)
64
+ {
65
+ if (!hasClass(el, cn)) {
66
+ el.className = (el.className === '') ? cn : el.className + ' ' + cn;
67
+ }
68
+ },
69
+
70
+ removeClass = function(el, cn)
71
+ {
72
+ el.className = trim((' ' + el.className + ' ').replace(' ' + cn + ' ', ' '));
73
+ },
74
+
75
+ isArray = function(obj)
76
+ {
77
+ return (/Array/).test(Object.prototype.toString.call(obj));
78
+ },
79
+
80
+ isDate = function(obj)
81
+ {
82
+ return (/Date/).test(Object.prototype.toString.call(obj)) && !isNaN(obj.getTime());
83
+ },
84
+
85
+ isLeapYear = function(year)
86
+ {
87
+ // solution by Matti Virkkunen: http://stackoverflow.com/a/4881951
88
+ return year % 4 === 0 && year % 100 !== 0 || year % 400 === 0;
89
+ },
90
+
91
+ getDaysInMonth = function(year, month)
92
+ {
93
+ return [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month];
94
+ },
95
+
96
+ setToStartOfDay = function(date)
97
+ {
98
+ if (isDate(date)) date.setHours(0,0,0,0);
99
+ },
100
+
101
+ compareDates = function(a,b)
102
+ {
103
+ // weak date comparison (use setToStartOfDay(date) to ensure correct result)
104
+ return a.getTime() === b.getTime();
105
+ },
106
+
107
+ extend = function(to, from, overwrite)
108
+ {
109
+ var prop, hasProp;
110
+ for (prop in from) {
111
+ hasProp = to[prop] !== undefined;
112
+ if (hasProp && typeof from[prop] === 'object' && from[prop].nodeName === undefined) {
113
+ if (isDate(from[prop])) {
114
+ if (overwrite) {
115
+ to[prop] = new Date(from[prop].getTime());
116
+ }
117
+ }
118
+ else if (isArray(from[prop])) {
119
+ if (overwrite) {
120
+ to[prop] = from[prop].slice(0);
121
+ }
122
+ } else {
123
+ to[prop] = extend({}, from[prop], overwrite);
124
+ }
125
+ } else if (overwrite || !hasProp) {
126
+ to[prop] = from[prop];
127
+ }
128
+ }
129
+ return to;
130
+ },
131
+
132
+
133
+ /**
134
+ * defaults and localisation
135
+ */
136
+ defaults = {
137
+
138
+ // bind the picker to a form field
139
+ field: null,
140
+
141
+ // automatically show/hide the picker on `field` focus (default `true` if `field` is set)
142
+ bound: undefined,
143
+
144
+ // the default output format for `.toString()` and `field` value
145
+ format: 'YYYY-MM-DD',
146
+
147
+ // the initial date to view when first opened
148
+ defaultDate: null,
149
+
150
+ // make the `defaultDate` the initial selected value
151
+ setDefaultDate: false,
152
+
153
+ // first day of week (0: Sunday, 1: Monday etc)
154
+ firstDay: 0,
155
+
156
+ // the minimum/earliest date that can be selected
157
+ minDate: null,
158
+ // the maximum/latest date that can be selected
159
+ maxDate: null,
160
+
161
+ // number of years either side, or array of upper/lower range
162
+ yearRange: 10,
163
+
164
+ // used internally (don't config outside)
165
+ minYear: 0,
166
+ maxYear: 9999,
167
+ minMonth: undefined,
168
+ maxMonth: undefined,
169
+
170
+ isRTL: false,
171
+
172
+ // how many months are visible (not implemented yet)
173
+ numberOfMonths: 1,
174
+
175
+ // internationalization
176
+ i18n: {
177
+ previousMonth : 'Previous Month',
178
+ nextMonth : 'Next Month',
179
+ months : ['January','February','March','April','May','June','July','August','September','October','November','December'],
180
+ //monthsShort : ['Jan_Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'],
181
+ weekdays : ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'],
182
+ weekdaysShort : ['Sun','Mon','Tue','Wed','Thu','Fri','Sat']
183
+ },
184
+
185
+ // callback function
186
+ onSelect: null,
187
+ onOpen: null,
188
+ onClose: null,
189
+ onDraw: null
190
+ },
191
+
192
+
193
+ /**
194
+ * templating functions to abstract HTML rendering
195
+ */
196
+ renderDayName = function(opts, day, abbr)
197
+ {
198
+ day += opts.firstDay;
199
+ while (day >= 7) {
200
+ day -= 7;
201
+ }
202
+ return abbr ? opts.i18n.weekdaysShort[day] : opts.i18n.weekdays[day];
203
+ },
204
+
205
+ renderDay = function(i, isSelected, isToday, isDisabled, isEmpty)
206
+ {
207
+ if (isEmpty) {
208
+ return '<td class="is-empty"></td>';
209
+ }
210
+ var arr = [];
211
+ if (isDisabled) {
212
+ arr.push('is-disabled');
213
+ }
214
+ if (isToday) {
215
+ arr.push('is-today');
216
+ }
217
+ if (isSelected) {
218
+ arr.push('is-selected');
219
+ }
220
+ return '<td data-day="' + i + '" class="' + arr.join(' ') + '"><button class="pika-button" type="button">' + i + '</button>' + '</td>';
221
+ },
222
+
223
+ renderRow = function(days, isRTL)
224
+ {
225
+ return '<tr>' + (isRTL ? days.reverse() : days).join('') + '</tr>';
226
+ },
227
+
228
+ renderBody = function(rows)
229
+ {
230
+ return '<tbody>' + rows.join('') + '</tbody>';
231
+ },
232
+
233
+ renderHead = function(opts)
234
+ {
235
+ var i, arr = [];
236
+ for (i = 0; i < 7; i++) {
237
+ arr.push('<th scope="col"><abbr title="' + renderDayName(opts, i) + '">' + renderDayName(opts, i, true) + '</abbr></th>');
238
+ }
239
+ return '<thead>' + (opts.isRTL ? arr.reverse() : arr).join('') + '</thead>';
240
+ },
241
+
242
+ renderTitle = function(instance)
243
+ {
244
+ var i, j, arr,
245
+ opts = instance._o,
246
+ month = instance._m,
247
+ year = instance._y,
248
+ isMinYear = year === opts.minYear,
249
+ isMaxYear = year === opts.maxYear,
250
+ html = '<div class="pika-title">',
251
+ prev = true,
252
+ next = true;
253
+
254
+ for (arr = [], i = 0; i < 12; i++) {
255
+ arr.push('<option value="' + i + '"' +
256
+ (i === month ? ' selected': '') +
257
+ ((isMinYear && i < opts.minMonth) || (isMaxYear && i > opts.maxMonth) ? 'disabled' : '') + '>' +
258
+ opts.i18n.months[i] + '</option>');
259
+ }
260
+ html += '<div class="pika-label">' + opts.i18n.months[month] + '<select class="pika-select pika-select-month">' + arr.join('') + '</select></div>';
261
+
262
+ if (isArray(opts.yearRange)) {
263
+ i = opts.yearRange[0];
264
+ j = opts.yearRange[1] + 1;
265
+ } else {
266
+ i = year - opts.yearRange;
267
+ j = 1 + year + opts.yearRange;
268
+ }
269
+
270
+ for (arr = []; i < j && i <= opts.maxYear; i++) {
271
+ if (i >= opts.minYear) {
272
+ arr.push('<option value="' + i + '"' + (i === year ? ' selected': '') + '>' + (i) + '</option>');
273
+ }
274
+ }
275
+ html += '<div class="pika-label">' + year + '<select class="pika-select pika-select-year">' + arr.join('') + '</select></div>';
276
+
277
+ if (isMinYear && (month === 0 || opts.minMonth >= month)) {
278
+ prev = false;
279
+ }
280
+
281
+ if (isMaxYear && (month === 11 || opts.maxMonth <= month)) {
282
+ next = false;
283
+ }
284
+
285
+ html += '<button class="pika-prev' + (prev ? '' : ' is-disabled') + '" type="button">' + opts.i18n.previousMonth + '</button>';
286
+ html += '<button class="pika-next' + (next ? '' : ' is-disabled') + '" type="button">' + opts.i18n.nextMonth + '</button>';
287
+
288
+ return html += '</div>';
289
+ },
290
+
291
+ renderTable = function(opts, data)
292
+ {
293
+ return '<table cellpadding="0" cellspacing="0" class="pika-table">' + renderHead(opts) + renderBody(data) + '</table>';
294
+ };
295
+
296
+
297
+ /**
298
+ * Pikaday constructor
299
+ */
300
+ window.Pikaday = function(options)
301
+ {
302
+ var self = this,
303
+ opts = self.config(options);
304
+
305
+ self._onMouseDown = function(e)
306
+ {
307
+ if (!self._v) {
308
+ return;
309
+ }
310
+ e = e || window.event;
311
+ var target = e.target || e.srcElement;
312
+ if (!target) {
313
+ return;
314
+ }
315
+
316
+ if (!hasClass(target, 'is-disabled')) {
317
+ if (hasClass(target, 'pika-button') && !hasClass(target, 'is-empty')) {
318
+ self.setDate(new Date(self._y, self._m, parseInt(target.innerHTML, 10)));
319
+ if (opts.bound) {
320
+ sto(function() {
321
+ self.hide();
322
+ }, 100);
323
+ }
324
+ return;
325
+ }
326
+ else if (hasClass(target, 'pika-prev')) {
327
+ self.prevMonth();
328
+ }
329
+ else if (hasClass(target, 'pika-next')) {
330
+ self.nextMonth();
331
+ }
332
+ }
333
+ if (!hasClass(target, 'pika-select')) {
334
+ if (e.preventDefault) {
335
+ e.preventDefault();
336
+ } else {
337
+ return e.returnValue = false;
338
+ }
339
+ } else {
340
+ self._c = true;
341
+ }
342
+ };
343
+
344
+ self._onChange = function(e)
345
+ {
346
+ e = e || window.event;
347
+ var target = e.target || e.srcElement;
348
+ if (!target) {
349
+ return;
350
+ }
351
+ if (hasClass(target, 'pika-select-month')) {
352
+ self.gotoMonth(target.value);
353
+ }
354
+ else if (hasClass(target, 'pika-select-year')) {
355
+ self.gotoYear(target.value);
356
+ }
357
+ };
358
+
359
+ self._onInputChange = function(e)
360
+ {
361
+ var date;
362
+
363
+ if (e.firedBy === self) {
364
+ return;
365
+ }
366
+ if (hasMoment) {
367
+ date = window.moment(opts.field.value, opts.format);
368
+ date = date ? date.toDate() : null;
369
+ }
370
+ else {
371
+ date = new Date(Date.parse(opts.field.value));
372
+ }
373
+ self.setDate(isDate(date) ? date : null);
374
+ if (!self._v) {
375
+ self.show();
376
+ }
377
+ };
378
+
379
+ self._onInputFocus = function(e)
380
+ {
381
+ self.show();
382
+ };
383
+
384
+ self._onInputClick = function(e)
385
+ {
386
+ self.show();
387
+ };
388
+
389
+ self._onInputBlur = function(e)
390
+ {
391
+ if (!self._c) {
392
+ self._b = sto(function() {
393
+ self.hide();
394
+ }, 50);
395
+ }
396
+ self._c = false;
397
+ };
398
+
399
+ self._onClick = function(e)
400
+ {
401
+ e = e || window.event;
402
+ var target = e.target || e.srcElement,
403
+ pEl = target;
404
+ if (!target) {
405
+ return;
406
+ }
407
+ if (!hasEventListeners && hasClass(target, 'pika-select')) {
408
+ if (!target.onchange) {
409
+ target.setAttribute('onchange', 'return;');
410
+ addEvent(target, 'change', self._onChange);
411
+ }
412
+ }
413
+ do {
414
+ if (hasClass(pEl, 'pika-single')) {
415
+ return;
416
+ }
417
+ }
418
+ while ((pEl = pEl.parentNode));
419
+ if (self._v && target !== opts.field) {
420
+ self.hide();
421
+ }
422
+ };
423
+
424
+ self.el = document.createElement('div');
425
+ self.el.className = 'pika-single' + (opts.isRTL ? ' is-rtl' : '');
426
+
427
+ addEvent(self.el, 'mousedown', self._onMouseDown, true);
428
+ addEvent(self.el, 'change', self._onChange);
429
+
430
+ if (opts.field) {
431
+ if (opts.bound) {
432
+ document.body.appendChild(self.el);
433
+ } else {
434
+ opts.field.parentNode.insertBefore(self.el, opts.field.nextSibling);
435
+ }
436
+ addEvent(opts.field, 'change', self._onInputChange);
437
+
438
+ if (!opts.defaultDate) {
439
+ if (hasMoment && opts.field.value) {
440
+ opts.defaultDate = window.moment(opts.field.value, opts.format).toDate();
441
+ } else {
442
+ opts.defaultDate = new Date(Date.parse(opts.field.value));
443
+ }
444
+ opts.setDefaultDate = true;
445
+ }
446
+ }
447
+
448
+ var defDate = opts.defaultDate;
449
+
450
+ if (isDate(defDate)) {
451
+ if (opts.setDefaultDate) {
452
+ self.setDate(defDate, true);
453
+ } else {
454
+ self.gotoDate(defDate);
455
+ }
456
+ } else {
457
+ self.gotoDate(new Date());
458
+ }
459
+
460
+ if (opts.bound) {
461
+ this.hide();
462
+ self.el.className += ' is-bound';
463
+ addEvent(opts.field, 'click', self._onInputClick);
464
+ addEvent(opts.field, 'focus', self._onInputFocus);
465
+ addEvent(opts.field, 'blur', self._onInputBlur);
466
+ } else {
467
+ this.show();
468
+ }
469
+
470
+ };
471
+
472
+
473
+ /**
474
+ * public Pikaday API
475
+ */
476
+ window.Pikaday.prototype = {
477
+
478
+
479
+ /**
480
+ * configure functionality
481
+ */
482
+ config: function(options)
483
+ {
484
+ if (!this._o) {
485
+ this._o = extend({}, defaults, true);
486
+ }
487
+
488
+ var opts = extend(this._o, options, true);
489
+
490
+ opts.isRTL = !!opts.isRTL;
491
+
492
+ opts.field = (opts.field && opts.field.nodeName) ? opts.field : null;
493
+
494
+ opts.bound = !!(opts.bound !== undefined ? opts.field && opts.bound : opts.field);
495
+
496
+ var nom = parseInt(opts.numberOfMonths, 10) || 1;
497
+ opts.numberOfMonths = nom > 4 ? 4 : nom;
498
+
499
+ if (!isDate(opts.minDate)) {
500
+ opts.minDate = false;
501
+ }
502
+ if (!isDate(opts.maxDate)) {
503
+ opts.maxDate = false;
504
+ }
505
+ if ((opts.minDate && opts.maxDate) && opts.maxDate < opts.minDate) {
506
+ opts.maxDate = opts.minDate = false;
507
+ }
508
+ if (opts.minDate) {
509
+ setToStartOfDay(opts.minDate);
510
+ opts.minYear = opts.minDate.getFullYear();
511
+ opts.minMonth = opts.minDate.getMonth();
512
+ }
513
+ if (opts.maxDate) {
514
+ setToStartOfDay(opts.maxDate);
515
+ opts.maxYear = opts.maxDate.getFullYear();
516
+ opts.maxMonth = opts.maxDate.getMonth();
517
+ }
518
+
519
+ if (isArray(opts.yearRange)) {
520
+ var fallback = new Date().getFullYear() - 10;
521
+ opts.yearRange[0] = parseInt(opts.yearRange[0], 10) || fallback;
522
+ opts.yearRange[1] = parseInt(opts.yearRange[1], 10) || fallback;
523
+ } else {
524
+ opts.yearRange = Math.abs(parseInt(opts.yearRange, 10)) || defaults.yearRange;
525
+ if (opts.yearRange > 100) {
526
+ opts.yearRange = 100;
527
+ }
528
+ }
529
+
530
+ return opts;
531
+ },
532
+
533
+ /**
534
+ * return a formatted string of the current selection (using Moment.js if available)
535
+ */
536
+ toString: function(format)
537
+ {
538
+ return !isDate(this._d) ? '' : hasMoment ? window.moment(this._d).format(format || this._o.format) : this._d.toDateString();
539
+ },
540
+
541
+ /**
542
+ * return a Moment.js object of the current selection (if available)
543
+ */
544
+ getMoment: function()
545
+ {
546
+ return hasMoment ? window.moment(this._d) : null;
547
+ },
548
+
549
+ /**
550
+ * set the current selection from a Moment.js object (if available)
551
+ */
552
+ setMoment: function(date)
553
+ {
554
+ if (hasMoment && window.moment.isMoment(date)) {
555
+ this.setDate(date.toDate());
556
+ }
557
+ },
558
+
559
+ /**
560
+ * return a Date object of the current selection
561
+ */
562
+ getDate: function()
563
+ {
564
+ return isDate(this._d) ? new Date(this._d.getTime()) : null;
565
+ },
566
+
567
+ /**
568
+ * set the current selection
569
+ */
570
+ setDate: function(date, preventOnSelect)
571
+ {
572
+ if (!date) {
573
+ this._d = null;
574
+ return this.draw();
575
+ }
576
+ if (typeof date === 'string') {
577
+ date = new Date(Date.parse(date));
578
+ }
579
+ if (!isDate(date)) {
580
+ return;
581
+ }
582
+
583
+ var min = this._o.minDate,
584
+ max = this._o.maxDate;
585
+
586
+ if (isDate(min) && date < min) {
587
+ date = min;
588
+ } else if (isDate(max) && date > max) {
589
+ date = max;
590
+ }
591
+
592
+ this._d = new Date(date.getTime());
593
+ setToStartOfDay(this._d);
594
+ this.gotoDate(this._d);
595
+
596
+ if (this._o.field) {
597
+ this._o.field.value = this.toString();
598
+ fireEvent(this._o.field, "change", { firedBy: this });
599
+ }
600
+ if (!preventOnSelect && typeof this._o.onSelect === 'function') {
601
+ this._o.onSelect.call(this, this.getDate());
602
+ }
603
+ },
604
+
605
+ /**
606
+ * change view to a specific date
607
+ */
608
+ gotoDate: function(date)
609
+ {
610
+ if (!isDate(date)) {
611
+ return;
612
+ }
613
+ this._y = date.getFullYear();
614
+ this._m = date.getMonth();
615
+ this.draw();
616
+ },
617
+
618
+ gotoToday: function()
619
+ {
620
+ this.gotoDate(new Date());
621
+ },
622
+
623
+ /**
624
+ * change view to a specific month (zero-index, e.g. 0: January)
625
+ */
626
+ gotoMonth: function(month)
627
+ {
628
+ if (!isNaN( (month = parseInt(month, 10)) )) {
629
+ this._m = month < 0 ? 0 : month > 11 ? 11 : month;
630
+ this.draw();
631
+ }
632
+ },
633
+
634
+ nextMonth: function()
635
+ {
636
+ if (++this._m > 11) {
637
+ this._m = 0;
638
+ this._y++;
639
+ }
640
+ this.draw();
641
+ },
642
+
643
+ prevMonth: function()
644
+ {
645
+ if (--this._m < 0) {
646
+ this._m = 11;
647
+ this._y--;
648
+ }
649
+ this.draw();
650
+ },
651
+
652
+ /**
653
+ * change view to a specific full year (e.g. "2012")
654
+ */
655
+ gotoYear: function(year)
656
+ {
657
+ if (!isNaN(year)) {
658
+ this._y = parseInt(year, 10);
659
+ this.draw();
660
+ }
661
+ },
662
+
663
+ /**
664
+ * refresh the HTML
665
+ */
666
+ draw: function(force)
667
+ {
668
+ if (!this._v && !force) {
669
+ return;
670
+ }
671
+ var opts = this._o,
672
+ minYear = opts.minYear,
673
+ maxYear = opts.maxYear,
674
+ minMonth = opts.minMonth,
675
+ maxMonth = opts.maxMonth;
676
+
677
+ if (this._y <= minYear) {
678
+ this._y = minYear;
679
+ if (!isNaN(minMonth) && this._m < minMonth) {
680
+ this._m = minMonth;
681
+ }
682
+ }
683
+ if (this._y >= maxYear) {
684
+ this._y = maxYear;
685
+ if (!isNaN(maxMonth) && this._m > maxMonth) {
686
+ this._m = maxMonth;
687
+ }
688
+ }
689
+
690
+ this.el.innerHTML = renderTitle(this) + this.render(this._y, this._m);
691
+
692
+ if (opts.bound) {
693
+ var pEl = opts.field,
694
+ left = pEl.offsetLeft,
695
+ top = pEl.offsetTop + pEl.offsetHeight;
696
+ while((pEl = pEl.offsetParent)) {
697
+ left += pEl.offsetLeft;
698
+ top += pEl.offsetTop;
699
+ }
700
+ this.el.style.cssText = 'position:absolute;left:' + left + 'px;top:' + top + 'px;';
701
+ sto(function() {
702
+ opts.field.focus();
703
+ }, 1);
704
+ }
705
+
706
+ if (typeof this._o.onDraw === 'function') {
707
+ var self = this;
708
+ sto(function() {
709
+ self._o.onDraw.call(self);
710
+ }, 0);
711
+ }
712
+ },
713
+
714
+ /**
715
+ * render HTML for a particular month
716
+ */
717
+ render: function(year, month)
718
+ {
719
+ var opts = this._o,
720
+ now = new Date(),
721
+ days = getDaysInMonth(year, month),
722
+ before = new Date(year, month, 1).getDay(),
723
+ data = [],
724
+ row = [];
725
+ setToStartOfDay(now);
726
+ if (opts.firstDay > 0) {
727
+ before -= opts.firstDay;
728
+ if (before < 0) {
729
+ before += 7;
730
+ }
731
+ }
732
+ var cells = days + before,
733
+ after = cells;
734
+ while(after > 7) {
735
+ after -= 7;
736
+ }
737
+ cells += 7 - after;
738
+ for (var i = 0, r = 0; i < cells; i++)
739
+ {
740
+ var day = new Date(year, month, 1 + (i - before)),
741
+ isDisabled = (opts.minDate && day < opts.minDate) || (opts.maxDate && day > opts.maxDate),
742
+ isSelected = isDate(this._d) ? compareDates(day, this._d) : false,
743
+ isToday = compareDates(day, now),
744
+ isEmpty = i < before || i >= (days + before);
745
+
746
+ row.push(renderDay(1 + (i - before), isSelected, isToday, isDisabled, isEmpty));
747
+
748
+ if (++r === 7) {
749
+ data.push(renderRow(row, opts.isRTL));
750
+ row = [];
751
+ r = 0;
752
+ }
753
+ }
754
+ return renderTable(opts, data);
755
+ },
756
+
757
+ isVisible: function()
758
+ {
759
+ return this._v;
760
+ },
761
+
762
+ show: function()
763
+ {
764
+ if (!this._v) {
765
+ if (this._o.bound) {
766
+ addEvent(document, 'click', this._onClick);
767
+ }
768
+ removeClass(this.el, 'is-hidden');
769
+ this._v = true;
770
+ this.draw();
771
+ if (typeof this._o.onOpen === 'function') {
772
+ this._o.onOpen.call(this);
773
+ }
774
+ }
775
+ },
776
+
777
+ hide: function()
778
+ {
779
+ var v = this._v;
780
+ if (v !== false) {
781
+ if (this._o.bound) {
782
+ removeEvent(document, 'click', this._onClick);
783
+ }
784
+ this.el.style.cssText = '';
785
+ addClass(this.el, 'is-hidden');
786
+ this._v = false;
787
+ if (v !== undefined && typeof this._o.onClose === 'function') {
788
+ this._o.onClose.call(this);
789
+ }
790
+ }
791
+ },
792
+
793
+ /**
794
+ * GAME OVER
795
+ */
796
+ destroy: function()
797
+ {
798
+ this.hide();
799
+ removeEvent(this.el, 'mousedown', this._onMouseDown, true);
800
+ removeEvent(this.el, 'change', this._onChange);
801
+ if (this._o.field) {
802
+ removeEvent(this._o.field, 'change', this._onInputChange);
803
+ if (this._o.bound) {
804
+ removeEvent(this._o.field, 'click', this._onInputClick);
805
+ removeEvent(this._o.field, 'focus', this._onInputFocus);
806
+ removeEvent(this._o.field, 'blur', this._onInputBlur);
807
+ }
808
+ }
809
+ if (this.el.parentNode) {
810
+ this.el.parentNode.removeChild(this.el);
811
+ }
812
+ }
813
+
814
+ };
815
+
816
+ })(window, window.document);