Aeron Glemamn Calendar.js fixed for IE9 vs Mootools 1.2.4


/ Published in: JavaScript
Save to your folder(s)



Copy this code and paste it in your HTML
  1. // Calendar: a Javascript class for Mootools that adds accessible and unobtrusive date pickers to your form elements <http://electricprism.com/aeron/calendar>
  2. // Calendar RC4, Copyright (c) 2007 Aeron Glemann <http://electricprism.com/aeron>, MIT Style License.
  3. // Mootools 1.2 compatibility by Davorin Ã… ego
  4.  
  5. var Calendar = new Class({
  6.  
  7. Implements: Options,
  8.  
  9. options: {
  10. blocked: [], // blocked dates
  11. classes: [], // ['calendar', 'prev', 'next', 'month', 'year', 'today', 'invalid', 'valid', 'inactive', 'active', 'hover', 'hilite']
  12. days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], // days of the week starting at sunday
  13. direction: 0, // -1 past, 0 past + future, 1 future
  14. draggable: true,
  15. months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
  16. navigation: 1, // 0 = no nav; 1 = single nav for month; 2 = dual nav for month and year
  17. offset: 0, // first day of the week: 0 = sunday, 1 = monday, etc..
  18. onHideStart: Class.empty,
  19. onHideComplete: Class.empty,
  20. onShowStart: Class.empty,
  21. onShowComplete: Class.empty,
  22. pad: 1, // padding between multiple calendars
  23. tweak: {x: 0, y: 0} // tweak calendar positioning
  24. },
  25.  
  26. // initialize: calendar constructor
  27. // @param obj (obj) a js object containing the form elements and format strings { id: 'format', id: 'format' etc }
  28. // @param props (obj) optional properties
  29.  
  30. initialize: function(obj, options) {
  31. // basic error checking
  32. if (!obj) { return false; }
  33.  
  34. this.setOptions(options);
  35.  
  36. // create our classes array
  37. var keys = ['calendar', 'prev', 'next', 'month', 'year', 'today', 'invalid', 'valid', 'inactive', 'active', 'hover', 'hilite'];
  38.  
  39. var values = keys.map(function(key, i) {
  40. if (this.options.classes[i]) {
  41. if (this.options.classes[i].length) { key = this.options.classes[i]; }
  42. }
  43. return key;
  44. }, this);
  45.  
  46. this.classes = values.associate(keys);
  47.  
  48. // create cal element with css styles required for proper cal functioning
  49. this.calendar = new Element('div', {
  50. 'styles': { left: '-1000px', opacity: 0, position: 'absolute', top: '-1000px', zIndex: 1000 }
  51. }).addClass(this.classes.calendar).injectInside(document.body);
  52.  
  53. // iex 6 needs a transparent iframe underneath the calendar in order to not allow select elements to render through
  54. if (window.ie6) {
  55. this.iframe = new Element('iframe', {
  56. 'styles': { left: '-1000px', position: 'absolute', top: '-1000px', zIndex: 999 }
  57. }).injectInside(document.body);
  58. this.iframe.style.filter = 'progid:DXImageTransform.Microsoft.Alpha(style=0,opacity=0)';
  59. }
  60.  
  61. // initialize fade method
  62. this.fx = new Fx.Tween(this.calendar, {
  63. onStart: function() {
  64. if (this.calendar.getStyle('opacity') == 0) { // show
  65. if (window.ie6) { this.iframe.setStyle('display', 'block'); }
  66. this.calendar.setStyle('display', 'block');
  67. this.fireEvent('onShowStart', this.element);
  68. }
  69. else { // hide
  70. this.fireEvent('onHideStart', this.element);
  71. }
  72. }.bind(this),
  73. onComplete: function() {
  74. if (this.calendar.getStyle('opacity') == 0) { // hidden
  75. this.calendar.setStyle('display', 'none');
  76. if (window.ie6) { this.iframe.setStyle('display', 'none'); }
  77. this.fireEvent('onHideComplete', this.element);
  78. }
  79. else { // shown
  80. this.fireEvent('onShowComplete', this.element);
  81. }
  82. }.bind(this)
  83. });
  84.  
  85. // initialize drag method
  86. if (window.Drag && this.options.draggable) {
  87. this.drag = new Drag.Move(this.calendar, {
  88. onDrag: function() {
  89. if (window.ie6) { this.iframe.setStyles({ left: this.calendar.style.left, top: this.calendar.style.top }); }
  90. }.bind(this)
  91. });
  92. }
  93.  
  94. // create calendars array
  95. this.calendars = [];
  96.  
  97. var id = 0;
  98. var d = new Date(); // today
  99.  
  100. d.setDate(d.getDate() + this.options.direction.toInt()); // correct today for directional offset
  101.  
  102. for (var i in obj) {
  103. var cal = {
  104. button: new Element('a', { 'type': 'button' }),
  105. el: $(i),
  106. els: [],
  107. id: id++,
  108. month: d.getMonth(),
  109. visible: false,
  110. year: d.getFullYear()
  111. };
  112.  
  113. // fix for bad element (naughty, naughty element!)
  114. if (!this.element(i, obj[i], cal)) { continue; }
  115.  
  116. cal.el.addClass(this.classes.calendar);
  117.  
  118. // create cal button
  119. cal.button.addClass(this.classes.calendar).addEvent('click', function(cal) { this.toggle(cal); }.pass(cal, this)).injectAfter(cal.el);
  120.  
  121. // read in default value
  122. cal.val = this.read(cal);
  123.  
  124. $extend(cal, this.bounds(cal)); // abs bounds of calendar
  125.  
  126. $extend(cal, this.values(cal)); // valid days, months, years
  127.  
  128. this.rebuild(cal);
  129.  
  130. this.calendars.push(cal); // add to cals array
  131. }
  132. },
  133.  
  134.  
  135. // blocked: returns an array of blocked days for the month / year
  136. // @param cal (obj)
  137. // @returns blocked days (array)
  138.  
  139. blocked: function(cal) {
  140. var blocked = [];
  141. var offset = new Date(cal.year, cal.month, 1).getDay(); // day of the week (offset)
  142. var last = new Date(cal.year, cal.month + 1, 0).getDate(); // last day of this month
  143.  
  144. this.options.blocked.each(function(date){
  145. var values = date.split(' ');
  146.  
  147. // preparation
  148. for (var i = 0; i <= 3; i++){
  149. if (!values[i]){ values[i] = (i == 3) ? '' : '*'; } // make sure blocked date contains values for at least d, m and y
  150. values[i] = values[i].contains(',') ? values[i].split(',') : new Array(values[i]); // split multiple values
  151. var count = values[i].length - 1;
  152. for (var j = count; j >= 0; j--){
  153. if (values[i][j].contains('-')){ // a range
  154. var val = values[i][j].split('-');
  155. for (var k = val[0]; k <= val[1]; k++){
  156. if (!values[i].contains(k)){ values[i].push(k + ''); }
  157. }
  158. values[i].splice(j, 1);
  159. }
  160. }
  161. }
  162.  
  163. // execution
  164. if (values[2].contains(cal.year + '') || values[2].contains('*')){
  165. if (values[1].contains(cal.month + 1 + '') || values[1].contains('*')){
  166. values[0].each(function(val){ // if blocked value indicates this month / year
  167. if (val > 0){ blocked.push(val.toInt()); } // add date to blocked array
  168. });
  169.  
  170. if (values[3]){ // optional value for day of week
  171. for (var i = 0; i < last; i++){
  172. var day = (i + offset) % 7;
  173.  
  174. if (values[3].contains(day + '')){
  175. blocked.push(i + 1); // add every date that corresponds to the blocked day of the week to the blocked array
  176. }
  177. }
  178. }
  179. }
  180. }
  181. }, this);
  182.  
  183. return blocked;
  184. },
  185.  
  186.  
  187. // bounds: returns the start / end bounds of the calendar
  188. // @param cal (obj)
  189. // @returns obj
  190.  
  191. bounds: function(cal) {
  192. // 1. first we assume the calendar has no bounds (or a thousand years in either direction)
  193.  
  194. // by default the calendar will accept a millennium in either direction
  195. var start = new Date(1000, 0, 1); // jan 1, 1000
  196. var end = new Date(2999, 11, 31); // dec 31, 2999
  197.  
  198. // 2. but if the cal is one directional we adjust accordingly
  199. var date = new Date().getDate() + this.options.direction.toInt();
  200.  
  201. if (this.options.direction > 0) {
  202. start = new Date();
  203. start.setDate(date + this.options.pad * cal.id);
  204. }
  205.  
  206. if (this.options.direction < 0) {
  207. end = new Date();
  208. end.setDate(date - this.options.pad * (this.calendars.length - cal.id - 1));
  209. }
  210.  
  211. // 3. then we can further filter the limits by using the pre-existing values in the selects
  212. cal.els.each(function(el) {
  213. if (el.get('tag') == 'select') {
  214. if (el.format.test('(y|Y)')) { // search for a year select
  215. var years = [];
  216.  
  217. el.getChildren().each(function(option) { // get options
  218. var values = this.unformat(option.value, el.format);
  219.  
  220. if (!years.contains(values[0])) { years.push(values[0]); } // add to years array
  221. }, this);
  222.  
  223. years.sort(this.sort);
  224.  
  225. if (years[0] > start.getFullYear()) {
  226. d = new Date(years[0], start.getMonth() + 1, 0); // last day of new month
  227.  
  228. if (start.getDate() > d.getDate()) { start.setDate(d.getDate()); }
  229.  
  230. start.setYear(years[0]);
  231. }
  232.  
  233. if (years.getLast() < end.getFullYear()) {
  234. d = new Date(years.getLast(), end.getMonth() + 1, 0); // last day of new month
  235.  
  236. if (end.getDate() > d.getDate()) { end.setDate(d.getDate()); }
  237.  
  238. end.setYear(years.getLast());
  239. }
  240. }
  241.  
  242. if (el.format.test('(F|m|M|n)')) { // search for a month select
  243. var months_start = [];
  244. var months_end = [];
  245.  
  246. el.getChildren().each(function(option) { // get options
  247. var values = this.unformat(option.value, el.format);
  248.  
  249. if ($type(values[0]) != 'number' || values[0] == years[0]) { // if it's a year / month combo for curr year, or simply a month select
  250. if (!months_start.contains(values[1])) { months_start.push(values[1]); } // add to months array
  251. }
  252.  
  253. if ($type(values[0]) != 'number' || values[0] == years.getLast()) { // if it's a year / month combo for curr year, or simply a month select
  254. if (!months_end.contains(values[1])) { months_end.push(values[1]); } // add to months array
  255. }
  256. }, this);
  257.  
  258. months_start.sort(this.sort);
  259. months_end.sort(this.sort);
  260.  
  261. if (months_start[0] > start.getMonth()) {
  262. d = new Date(start.getFullYear(), months_start[0] + 1, 0); // last day of new month
  263.  
  264. if (start.getDate() > d.getDate()) { start.setDate(d.getDate()); }
  265.  
  266. start.setMonth(months_start[0]);
  267. }
  268.  
  269. if (months_end.getLast() < end.getMonth()) {
  270. d = new Date(start.getFullYear(), months_end.getLast() + 1, 0); // last day of new month
  271.  
  272. if (end.getDate() > d.getDate()) { end.setDate(d.getDate()); }
  273.  
  274. end.setMonth(months_end.getLast());
  275. }
  276. }
  277. }
  278. }, this);
  279.  
  280. return { 'start': start, 'end': end };
  281. },
  282.  
  283.  
  284. // caption: returns the caption element with header and navigation
  285. // @param cal (obj)
  286. // @returns caption (element)
  287.  
  288. caption: function(cal) {
  289. // start by assuming navigation is allowed
  290. var navigation = {
  291. prev: { 'month': true, 'year': true },
  292. next: { 'month': true, 'year': true }
  293. };
  294.  
  295. // if we're in an out of bounds year
  296. if (cal.year == cal.start.getFullYear()) {
  297. navigation.prev.year = false;
  298. if (cal.month == cal.start.getMonth() && this.options.navigation == 1) {
  299. navigation.prev.month = false;
  300. }
  301. }
  302. if (cal.year == cal.end.getFullYear()) {
  303. navigation.next.year = false;
  304. if (cal.month == cal.end.getMonth() && this.options.navigation == 1) {
  305. navigation.next.month = false;
  306. }
  307. }
  308.  
  309. // special case of improved navigation but months array with only 1 month we can disable all month navigation
  310. if ($type(cal.months) == 'array') {
  311. if (cal.months.length == 1 && this.options.navigation == 2) {
  312. navigation.prev.month = navigation.next.month = false;
  313. }
  314. }
  315.  
  316. var caption = new Element('caption');
  317.  
  318. var prev = new Element('a').addClass(this.classes.prev).appendText('\x3c'); // <
  319. var next = new Element('a').addClass(this.classes.next).appendText('\x3e'); // >
  320.  
  321. if (this.options.navigation == 2) {
  322. var month = new Element('span').addClass(this.classes.month).injectInside(caption);
  323.  
  324. if (navigation.prev.month) { prev.clone().addEvent('click', function(cal) { this.navigate(cal, 'm', -1); }.pass(cal, this)).injectInside(month); }
  325.  
  326. month.adopt(new Element('span').appendText(this.options.months[cal.month]));
  327.  
  328. if (navigation.next.month) { next.clone().addEvent('click', function(cal) { this.navigate(cal, 'm', 1); }.pass(cal, this)).injectInside(month); }
  329.  
  330. var year = new Element('span').addClass(this.classes.year).injectInside(caption);
  331.  
  332. if (navigation.prev.year) { prev.clone().addEvent('click', function(cal) { this.navigate(cal, 'y', -1); }.pass(cal, this)).injectInside(year); }
  333.  
  334. year.adopt(new Element('span').appendText(cal.year));
  335.  
  336. if (navigation.next.year) { next.clone().addEvent('click', function(cal) { this.navigate(cal, 'y', 1); }.pass(cal, this)).injectInside(year); }
  337. }
  338. else { // 1 or 0
  339. if (navigation.prev.month && this.options.navigation) { prev.clone().addEvent('click', function(cal) { this.navigate(cal, 'm', -1); }.pass(cal, this)).injectInside(caption); }
  340.  
  341. caption.adopt(new Element('span').addClass(this.classes.month).appendText(this.options.months[cal.month]));
  342.  
  343. caption.adopt(new Element('span').addClass(this.classes.year).appendText(cal.year));
  344.  
  345. if (navigation.next.month && this.options.navigation) { next.clone().addEvent('click', function(cal) { this.navigate(cal, 'm', 1); }.pass(cal, this)).injectInside(caption); }
  346.  
  347. }
  348.  
  349. return caption;
  350. },
  351.  
  352.  
  353. // changed: run when a select value is changed
  354. // @param cal (obj)
  355.  
  356. changed: function(cal) {
  357. cal.val = this.read(cal); // update calendar val from inputs
  358.  
  359. $extend(cal, this.values(cal)); // update bounds - based on curr month
  360.  
  361. this.rebuild(cal); // rebuild days select
  362.  
  363. if (!cal.val) { return; } // in case the same date was clicked the cal has no set date we should exit
  364.  
  365. if (cal.val.getDate() < cal.days[0]) { cal.val.setDate(cal.days[0]); }
  366. if (cal.val.getDate() > cal.days.getLast()) { cal.val.setDate(cal.days.getLast()); }
  367.  
  368. cal.els.each(function(el) { // then we can set the value to the field
  369. el.value = this.format(cal.val, el.format);
  370. }, this);
  371.  
  372. this.check(cal); // checks other cals
  373.  
  374. this.calendars.each(function(kal) { // update cal graphic if visible
  375. if (kal.visible) { this.display(kal); }
  376. }, this);
  377. },
  378.  
  379.  
  380. // check: checks other calendars to make sure no overlapping values
  381. // @param cal (obj)
  382.  
  383. check: function(cal) {
  384. this.calendars.each(function(kal, i) {
  385. if (kal.val) { // if calendar has value set
  386. var change = false;
  387.  
  388. if (i < cal.id) { // preceding calendar
  389. var bound = new Date(Date.parse(cal.val));
  390.  
  391. bound.setDate(bound.getDate() - (this.options.pad * (cal.id - i)));
  392.  
  393. if (bound < kal.val) { change = true; }
  394. }
  395. if (i > cal.id) { // following calendar
  396. var bound = new Date(Date.parse(cal.val));
  397.  
  398. bound.setDate(bound.getDate() + (this.options.pad * (i - cal.id)));
  399.  
  400. if (bound > kal.val) { change = true; }
  401. }
  402.  
  403. if (change) {
  404. if (kal.start > bound) { bound = kal.start; }
  405. if (kal.end < bound) { bound = kal.end; }
  406.  
  407. kal.month = bound.getMonth();
  408. kal.year = bound.getFullYear();
  409.  
  410. $extend(kal, this.values(kal));
  411.  
  412. // TODO - IN THE CASE OF SELECT MOVE TO NEAREST VALID VALUE
  413. // IN THE CASE OF INPUT DISABLE
  414.  
  415. // if new date is not valid better unset cal value
  416. // otherwise it would mean incrementally checking to find the nearest valid date which could be months / years away
  417. kal.val = kal.days.contains(bound.getDate()) ? bound : null;
  418.  
  419. this.write(kal);
  420.  
  421. if (kal.visible) { this.display(kal); } // update cal graphic if visible
  422. }
  423. }
  424. else {
  425. kal.month = cal.month;
  426. kal.year = cal.year;
  427. }
  428. }, this);
  429. },
  430.  
  431.  
  432. // clicked: run when a valid day is clicked in the calendar
  433. // @param cal (obj)
  434.  
  435. clicked: function(td, day, cal) {
  436. cal.val = (this.value(cal) == day) ? null : new Date(cal.year, cal.month, day); // set new value - if same then disable
  437.  
  438. this.write(cal);
  439.  
  440. // ok - in the special case that it's all selects and there's always a date no matter what (at least as far as the form is concerned)
  441. // we can't let the calendar undo a date selection - it's just not possible!!
  442. if (!cal.val) { cal.val = this.read(cal); }
  443.  
  444. if (cal.val) {
  445. this.check(cal); // checks other cals
  446. this.toggle(cal); // hide cal
  447. }
  448. else { // remove active class and replace with valid
  449. td.addClass(this.classes.valid);
  450. td.removeClass(this.classes.active);
  451. }
  452. },
  453.  
  454.  
  455. // display: create calendar element
  456. // @param cal (obj)
  457.  
  458. display: function(cal) {
  459. // 1. header and navigation
  460. this.calendar.empty(); // init div
  461.  
  462. this.calendar.className = this.classes.calendar + ' ' + this.options.months[cal.month].toLowerCase();
  463.  
  464. var div = new Element('div').injectInside(this.calendar); // a wrapper div to help correct browser css problems with the caption element
  465.  
  466. var table = new Element('table').injectInside(div).adopt(this.caption(cal));
  467.  
  468. // 2. day names
  469. var thead = new Element('thead').injectInside(table);
  470.  
  471. var tr = new Element('tr').injectInside(thead);
  472.  
  473. for (var i = 0; i <= 6; i++) {
  474. var th = this.options.days[(i + this.options.offset) % 7];
  475.  
  476. tr.adopt(new Element('th', { 'title': th }).appendText(th.substr(0, 1)));
  477. }
  478.  
  479. // 3. day numbers
  480. var tbody = new Element('tbody').injectInside(table);
  481. var tr = new Element('tr').injectInside(tbody);
  482.  
  483. var d = new Date(cal.year, cal.month, 1);
  484. var offset = ((d.getDay() - this.options.offset) + 7) % 7; // day of the week (offset)
  485. var last = new Date(cal.year, cal.month + 1, 0).getDate(); // last day of this month
  486. var prev = new Date(cal.year, cal.month, 0).getDate(); // last day of previous month
  487. var active = this.value(cal); // active date (if set and within curr month)
  488. var valid = cal.days; // valid days for curr month
  489. var inactive = []; // active dates set by other calendars
  490. var hilited = [];
  491. this.calendars.each(function(kal, i) {
  492. if (kal != cal && kal.val) {
  493. if (cal.year == kal.val.getFullYear() && cal.month == kal.val.getMonth()) { inactive.push(kal.val.getDate()); }
  494.  
  495. if (cal.val) {
  496. for (var day = 1; day <= last; day++) {
  497. d.setDate(day);
  498.  
  499. if ((i < cal.id && d > kal.val && d < cal.val) || (i > cal.id && d > cal.val && d < kal.val)) {
  500. if (!hilited.contains(day)) { hilited.push(day); }
  501. }
  502. }
  503. }
  504. }
  505. }, this);
  506. var d = new Date();
  507. var today = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime(); // today obv
  508.  
  509. for (var i = 1; i < 43; i++) { // 1 to 42 (6 x 7 or 6 weeks)
  510. if ((i - 1) % 7 == 0) { tr = new Element('tr').injectInside(tbody); } // each week is it's own table row
  511.  
  512. var td = new Element('td').injectInside(tr);
  513.  
  514. var day = i - offset;
  515. var date = new Date(cal.year, cal.month, day);
  516.  
  517. var cls = '';
  518.  
  519. if (day === active) { cls = this.classes.active; } // active
  520. else if (inactive.contains(day)) { cls = this.classes.inactive; } // inactive
  521. else if (valid.contains(day)) { cls = this.classes.valid; } // valid
  522. else if (day >= 1 && day <= last) { cls = this.classes.invalid; } // invalid
  523.  
  524. if (date.getTime() == today) { cls = cls + ' ' + this.classes.today; } // adds class for today
  525.  
  526. if (hilited.contains(day)) { cls = cls + ' ' + this.classes.hilite; } // adds class if hilited
  527.  
  528. td.addClass(cls);
  529.  
  530. if (valid.contains(day)) { // if it's a valid - clickable - day we add interaction
  531. td.setProperty('title', this.format(date, 'D M jS Y'));
  532.  
  533. td.addEvents({
  534. 'click': function(td, day, cal) {
  535. this.clicked(td, day, cal);
  536. }.pass([td, day, cal], this),
  537. 'mouseover': function(td, cls) {
  538. td.addClass(cls);
  539. }.pass([td, this.classes.hover]),
  540. 'mouseout': function(td, cls) {
  541. td.removeClass(cls);
  542. }.pass([td, this.classes.hover])
  543. });
  544. }
  545.  
  546. // pad calendar with last days of prev month and first days of next month
  547. if (day < 1) { day = prev + day; }
  548. else if (day > last) { day = day - last; }
  549.  
  550. td.appendText(day);
  551. }
  552. },
  553.  
  554.  
  555. // element: helper function
  556. // @param el (string) element id
  557. // @param f (string) format string
  558. // @param cal (obj)
  559.  
  560. element: function(el, f, cal) {
  561. if ($type(f) == 'object') { // in the case of multiple inputs per calendar
  562. for (var i in f) {
  563. if (!this.element(i, f[i], cal)) { return false; }
  564. }
  565.  
  566. return true;
  567. }
  568.  
  569. el = $(el);
  570.  
  571. if (!el) { return false; }
  572.  
  573. el.format = f;
  574.  
  575. if (el.get('tag') == 'select') { // select elements allow the user to manually set the date via select option
  576. el.addEvent('change', function(cal) { this.changed(cal); }.pass(cal, this));
  577. }
  578. else { // input (type text) elements restrict the user to only setting the date via the calendar
  579. el.readOnly = true;
  580. el.addEvent('focus', function(cal) { this.toggle(cal); }.pass(cal, this));
  581. }
  582.  
  583. cal.els.push(el);
  584.  
  585. return true;
  586. },
  587.  
  588.  
  589. // format: formats a date object according to passed in instructions
  590. // @param date (obj)
  591. // @param f (string) any combination of punctuation / separators and d, j, D, l, S, m, n, F, M, y, Y
  592. // @returns string
  593.  
  594. format: function(date, format) {
  595. var str = '';
  596.  
  597. if (date) {
  598. var j = date.getDate(); // 1 - 31
  599. var w = date.getDay(); // 0 - 6
  600. var l = this.options.days[w]; // Sunday - Saturday
  601. var n = date.getMonth() + 1; // 1 - 12
  602. var f = this.options.months[n - 1]; // January - December
  603. var y = date.getFullYear() + ''; // 19xx - 20xx
  604.  
  605. for (var i = 0, len = format.length; i < len; i++) {
  606. var cha = format.charAt(i); // format char
  607.  
  608. switch(cha) {
  609. // year cases
  610. case 'y': // xx - xx
  611. y = y.substr(2);
  612. case 'Y': // 19xx - 20xx
  613. str += y;
  614. break;
  615.  
  616. // month cases
  617. case 'm': // 01 - 12
  618. if (n < 10) { n = '0' + n; }
  619. case 'n': // 1 - 12
  620. str += n;
  621. break;
  622.  
  623. case 'M': // Jan - Dec
  624. f = f.substr(0, 3);
  625. case 'F': // January - December
  626. str += f;
  627. break;
  628.  
  629. // day cases
  630. case 'd': // 01 - 31
  631. if (j < 10) { j = '0' + j; }
  632. case 'j': // 1 - 31
  633. str += j;
  634. break;
  635.  
  636. case 'D': // Sun - Sat
  637. l = l.substr(0, 3);
  638. case 'l': // Sunday - Saturday
  639. str += l;
  640. break;
  641.  
  642. case 'N': // 1 - 7
  643. w += 1;
  644. case 'w': // 0 - 6
  645. str += w;
  646. break;
  647.  
  648. case 'S': // st, nd, rd or th (works well with j)
  649. if (j % 10 == 1 && j != '11') { str += 'st'; }
  650. else if (j % 10 == 2 && j != '12') { str += 'nd'; }
  651. else if (j % 10 == 3 && j != '13') { str += 'rd'; }
  652. else { str += 'th'; }
  653. break;
  654.  
  655. default:
  656. str += cha;
  657. }
  658. }
  659. }
  660.  
  661. return str; // return format with values replaced
  662. },
  663.  
  664.  
  665. // navigate: calendar navigation
  666. // @param cal (obj)
  667. // @param type (str) m or y for month or year
  668. // @param n (int) + or - for next or prev
  669.  
  670. navigate: function(cal, type, n) {
  671. switch (type) {
  672. case 'm': // month
  673. if ($type(cal.months) == 'array') {
  674. var i = cal.months.indexOf(cal.month) + n; // index of current month
  675.  
  676. if (i < 0 || i == cal.months.length) { // out of range
  677. if (this.options.navigation == 1) { // if type 1 nav we'll need to increment the year
  678. this.navigate(cal, 'y', n);
  679. }
  680.  
  681. i = (i < 0) ? cal.months.length - 1 : 0;
  682. }
  683.  
  684. cal.month = cal.months[i];
  685. }
  686. else {
  687. var i = cal.month + n;
  688.  
  689. if (i < 0 || i == 12) {
  690. if (this.options.navigation == 1) {
  691. this.navigate(cal, 'y', n);
  692. }
  693.  
  694. i = (i < 0) ? 11 : 0;
  695. }
  696.  
  697. cal.month = i;
  698. }
  699. break;
  700.  
  701. case 'y': // year
  702. if ($type(cal.years) == 'array') {
  703. var i = cal.years.indexOf(cal.year) + n;
  704.  
  705. cal.year = cal.years[i];
  706. }
  707. else {
  708. cal.year += n;
  709. }
  710. break;
  711. }
  712.  
  713. $extend(cal, this.values(cal));
  714.  
  715. if ($type(cal.months) == 'array') { // if the calendar has a months select
  716. var i = cal.months.indexOf(cal.month); // and make sure the curr months exists for the new year
  717.  
  718. if (i < 0) { cal.month = cal.months[0]; } // otherwise we'll reset the month
  719. }
  720.  
  721.  
  722. this.display(cal);
  723. },
  724.  
  725.  
  726. // read: compiles cal value based on array of inputs passed in
  727. // @param cal (obj)
  728. // @returns date (obj) or (null)
  729.  
  730. read: function(cal) {
  731. var arr = [null, null, null];
  732.  
  733. cal.els.each(function(el) {
  734. // returns an array which may contain empty values
  735. var values = this.unformat(el.value, el.format);
  736.  
  737. values.each(function(val, i) {
  738. if ($type(val) == 'number') { arr[i] = val; }
  739. });
  740. }, this);
  741.  
  742. // we can update the cals month and year values
  743. if ($type(arr[0]) == 'number') { cal.year = arr[0]; }
  744. if ($type(arr[1]) == 'number') { cal.month = arr[1]; }
  745.  
  746. var val = null;
  747.  
  748. if (arr.every(function(i) { return $type(i) == 'number'; })) { // if valid date
  749. var last = new Date(arr[0], arr[1] + 1, 0).getDate(); // last day of month
  750.  
  751. if (arr[2] > last) { arr[2] = last; } // make sure we stay within the month (ex in case default day of select is 31 and month is feb)
  752.  
  753. val = new Date(arr[0], arr[1], arr[2]);
  754. }
  755.  
  756. return (cal.val == val) ? null : val; // if new date matches old return null (same date clicked twice = disable)
  757. },
  758.  
  759.  
  760. // rebuild: rebuilds days + months selects
  761. // @param cal (obj)
  762.  
  763. rebuild: function(cal) {
  764. cal.els.each(function(el) {
  765. /*
  766. if (el.get('tag') == 'select' && el.format.test('^(F|m|M|n)$')) { // special case for months-only select
  767. if (!cal.options) { cal.options = el.clone(); } // clone a copy of months select
  768.  
  769. var val = (cal.val) ? cal.val.getMonth() : el.value.toInt();
  770.  
  771. el.empty(); // initialize select
  772.  
  773. cal.months.each(function(month) {
  774. // create an option element
  775. var option = new Element('option', {
  776. 'selected': (val == month),
  777. 'value': this.format(new Date(1, month, 1), el.format);
  778. }).appendText(day).injectInside(el);
  779. }, this);
  780. }
  781. */
  782.  
  783. if (el.get('tag') == 'select' && el.format.test('^(d|j)$')) { // special case for days-only select
  784. var d = this.value(cal);
  785.  
  786. if (!d) { d = el.value.toInt(); } // if the calendar doesn't have a set value, try to use value from select
  787.  
  788. el.empty(); // initialize select
  789.  
  790. cal.days.each(function(day) {
  791. // create an option element
  792. var option = new Element('option', {
  793. 'selected': (d == day),
  794. 'value': ((el.format == 'd' && day < 10) ? '0' + day : day)
  795. }).appendText(day).injectInside(el);
  796. }, this);
  797. }
  798. }, this);
  799. },
  800.  
  801.  
  802. // sort: helper function for numerical sorting
  803.  
  804. sort: function(a, b) {
  805. return a - b;
  806. },
  807.  
  808.  
  809. // toggle: show / hide calendar
  810. // @param cal (obj)
  811.  
  812. toggle: function(cal) {
  813. document.removeEvent('mousedown', this.fn); // always remove the current mousedown script first
  814.  
  815. if (cal.visible) { // simply hide curr cal
  816. cal.visible = false;
  817. cal.button.removeClass(this.classes.active); // active
  818.  
  819. this.fx.start('opacity', 1, 0);
  820. }
  821. else { // otherwise show (may have to hide others)
  822. // hide cal on out-of-bounds click
  823. this.fn = function(e, cal) {
  824. var e = new Event(e);
  825.  
  826. var el = e.target;
  827.  
  828. var stop = false;
  829.  
  830. while (el != document.body && el.nodeType == 1) {
  831. if (el == this.calendar) { stop = true; }
  832. this.calendars.each(function(kal) {
  833. if (kal.button == el || kal.els.contains(el)) { stop = true; }
  834. });
  835.  
  836. if (stop) {
  837. e.stop();
  838. return false;
  839. }
  840. else { el = el.parentNode; }
  841. }
  842.  
  843. this.toggle(cal);
  844. }.create({ 'arguments': cal, 'bind': this, 'event': true });
  845.  
  846. document.addEvent('mousedown', this.fn);
  847.  
  848. this.calendars.each(function(kal) {
  849. if (kal == cal) {
  850. kal.visible = true;
  851. kal.button.addClass(this.classes.active); // css c-icon-active
  852. }
  853. else {
  854. kal.visible = false;
  855. kal.button.removeClass(this.classes.active); // css c-icon-active
  856. }
  857. }, this);
  858.  
  859. var size = window.getScrollSize();
  860.  
  861. var coord = cal.button.getCoordinates();
  862.  
  863. var x = coord.right + this.options.tweak.x;
  864. var y = coord.top + this.options.tweak.y;
  865.  
  866. // make sure the calendar doesn't open off screen
  867. if (!this.calendar.coord) { this.calendar.coord = this.calendar.getCoordinates(); }
  868.  
  869. if (x + this.calendar.coord.width > size.x) { x -= (x + this.calendar.coord.width - size.x); }
  870. if (y + this.calendar.coord.height > size.y) { y -= (y + this.calendar.coord.height - size.y); }
  871.  
  872. this.calendar.setStyles({ left: x + 'px', top: y + 'px' });
  873.  
  874. if (window.ie6) {
  875. this.iframe.setStyles({ height: this.calendar.coord.height + 'px', left: x + 'px', top: y + 'px', width: this.calendar.coord.width + 'px' });
  876. }
  877.  
  878. this.display(cal);
  879.  
  880. this.fx.start('opacity', 0, 1);
  881. }
  882. },
  883.  
  884.  
  885. // unformat: takes a value from an input and parses the d, m and y elements
  886. // @param val (string)
  887. // @param f (string) any combination of punctuation / separators and d, j, D, l, S, m, n, F, M, y, Y
  888. // @returns array
  889.  
  890. unformat: function(val, f) {
  891. f = f.escapeRegExp();
  892.  
  893. var re = {
  894. d: '([0-9]{2})',
  895. j: '([0-9]{1,2})',
  896. D: '(' + this.options.days.map(function(day) { return day.substr(0, 3); }).join('|') + ')',
  897. l: '(' + this.options.days.join('|') + ')',
  898. S: '(st|nd|rd|th)',
  899. F: '(' + this.options.months.join('|') + ')',
  900. m: '([0-9]{2})',
  901. M: '(' + this.options.months.map(function(month) { return month.substr(0, 3); }).join('|') + ')',
  902. n: '([0-9]{1,2})',
  903. Y: '([0-9]{4})',
  904. y: '([0-9]{2})'
  905. }
  906.  
  907. var arr = []; // array of indexes
  908.  
  909. var g = '';
  910.  
  911. // convert our format string to regexp
  912. for (var i = 0; i < f.length; i++) {
  913. var c = f.charAt(i);
  914.  
  915. if (re[c]) {
  916. arr.push(c);
  917.  
  918. g += re[c];
  919. }
  920. else {
  921. g += c;
  922. }
  923. }
  924.  
  925. // match against date
  926. var matches = val.match('^' + g + '$');
  927.  
  928. var dates = new Array(3);
  929.  
  930. if (matches) {
  931. matches = matches.slice(1); // remove first match which is the date
  932.  
  933. arr.each(function(c, i) {
  934. i = matches[i];
  935.  
  936. switch(c) {
  937. // year cases
  938. case 'y':
  939. i = '19' + i; // 2 digit year assumes 19th century (same as JS)
  940. case 'Y':
  941. dates[0] = i.toInt();
  942. break;
  943.  
  944. // month cases
  945. case 'F':
  946. i = i.substr(0, 3);
  947. case 'M':
  948. i = this.options.months.map(function(month) { return month.substr(0, 3); }).indexOf(i) + 1;
  949. case 'm':
  950. case 'n':
  951. dates[1] = i.toInt() - 1;
  952. break;
  953.  
  954. // day cases
  955. case 'd':
  956. case 'j':
  957. dates[2] = i.toInt();
  958. break;
  959. }
  960. }, this);
  961. }
  962.  
  963. return dates;
  964. },
  965.  
  966.  
  967. // value: returns day value of calendar if set
  968. // @param cal (obj)
  969. // @returns day (int) or null
  970.  
  971. value: function(cal) {
  972. var day = null;
  973.  
  974. if (cal.val) {
  975. if (cal.year == cal.val.getFullYear() && cal.month == cal.val.getMonth()) { day = cal.val.getDate(); }
  976. }
  977.  
  978. return day;
  979. },
  980.  
  981.  
  982. // values: returns the years, months (for curr year) and days (for curr month and year) for the calendar
  983. // @param cal (obj)
  984. // @returns obj
  985.  
  986. values: function(cal) {
  987. var years, months, days;
  988.  
  989. cal.els.each(function(el) {
  990. if (el.get('tag') == 'select') {
  991. if (el.format.test('(y|Y)')) { // search for a year select
  992. years = [];
  993.  
  994. el.getChildren().each(function(option) { // get options
  995. var values = this.unformat(option.value, el.format);
  996.  
  997. if (!years.contains(values[0])) { years.push(values[0]); } // add to years array
  998. }, this);
  999.  
  1000. years.sort(this.sort);
  1001. }
  1002.  
  1003. if (el.format.test('(F|m|M|n)')) { // search for a month select
  1004. months = []; // 0 - 11 should be
  1005.  
  1006. el.getChildren().each(function(option) { // get options
  1007. var values = this.unformat(option.value, el.format);
  1008.  
  1009. if ($type(values[0]) != 'number' || values[0] == cal.year) { // if it's a year / month combo for curr year, or simply a month select
  1010. if (!months.contains(values[1])) { months.push(values[1]); } // add to months array
  1011. }
  1012. }, this);
  1013.  
  1014. months.sort(this.sort);
  1015. }
  1016.  
  1017. if (el.format.test('(d|j)') && !el.format.test('^(d|j)$')) { // search for a day select, but NOT a days only select
  1018. days = []; // 1 - 31
  1019.  
  1020. el.getChildren().each(function(option) { // get options
  1021. var values = this.unformat(option.value, el.format);
  1022.  
  1023. // in the special case of days we dont want the value if its a days only select
  1024. // otherwise that will screw up the options rebuilding
  1025. // we will take the values if they are exact dates though
  1026. if (values[0] == cal.year && values[1] == cal.month) {
  1027. if (!days.contains(values[2])) { days.push(values[2]); } // add to days array
  1028. }
  1029. }, this);
  1030. }
  1031. }
  1032. }, this);
  1033.  
  1034. // we start with what would be the first and last days were there no restrictions
  1035. var first = 1;
  1036. var last = new Date(cal.year, cal.month + 1, 0).getDate(); // last day of the month
  1037.  
  1038. // if we're in an out of bounds year
  1039. if (cal.year == cal.start.getFullYear()) {
  1040. // in the special case of improved navigation but no months array, we'll need to construct one
  1041. if (months == null && this.options.navigation == 2) {
  1042. months = [];
  1043.  
  1044. for (var i = 0; i < 12; i ++) {
  1045. if (i >= cal.start.getMonth()) { months.push(i); }
  1046. }
  1047. }
  1048.  
  1049. // if we're in an out of bounds month
  1050. if (cal.month == cal.start.getMonth()) {
  1051. first = cal.start.getDate(); // first day equals day of bound
  1052. }
  1053. }
  1054. if (cal.year == cal.end.getFullYear()) {
  1055. // in the special case of improved navigation but no months array, we'll need to construct one
  1056. if (months == null && this.options.navigation == 2) {
  1057. months = [];
  1058.  
  1059. for (var i = 0; i < 12; i ++) {
  1060. if (i <= cal.end.getMonth()) { months.push(i); }
  1061. }
  1062. }
  1063.  
  1064. if (cal.month == cal.end.getMonth()) {
  1065. last = cal.end.getDate(); // last day equals day of bound
  1066. }
  1067. }
  1068.  
  1069. // let's get our invalid days
  1070. var blocked = this.blocked(cal);
  1071.  
  1072. // finally we can prepare all the valid days in a neat little array
  1073. if ($type(days) == 'array') { // somewhere there was a days select
  1074. days = days.filter(function(day) {
  1075. if (day >= first && day <= last && !blocked.contains(day)) { return day; }
  1076. });
  1077. }
  1078. else { // no days select we'll need to construct a valid days array
  1079. days = [];
  1080.  
  1081. for (var i = first; i <= last; i++) {
  1082. if (!blocked.contains(i)) { days.push(i); }
  1083. }
  1084. }
  1085.  
  1086. days.sort(this.sort); // sorting our days will give us first and last of month
  1087.  
  1088. return { 'days': days, 'months': months, 'years': years };
  1089. },
  1090.  
  1091.  
  1092. // write: sets calendars value to form elements
  1093. // @param cal (obj)
  1094.  
  1095. write: function(cal) {
  1096. this.rebuild(cal); // in the case of options, we'll need to make sure we have the correct number of days available
  1097.  
  1098. cal.els.each(function(el) { // then we can set the value to the field
  1099. el.value = this.format(cal.val, el.format);
  1100. }, this);
  1101. }
  1102. });
  1103.  
  1104. Calendar.implement(new Events, new Options);

Report this snippet


Comments

RSS Icon Subscribe to comments

You need to login to post a comment.