From 02d45cc8434f97ec88030c7077e0a88d213e3da1 Mon Sep 17 00:00:00 2001 From: Florent Dionisi Date: Fri, 27 Nov 2009 17:45:59 +0100 Subject: [PATCH] #24 : added calendar class for mootools from http://www.electricprism.com/aeron/calendar/. - some css stuff as well. - selector view implementation for stats plugin (from admin menu) --- extensions/stats/main.py | 26 +- extensions/stats/templates/adminindex.html | 14 +- extensions/stats/templates/calendar.html | 38 + extensions/stats/urls.py | 1 + static/js/calendar.js | 1344 ++++++++++++++++++++ static/stylesheets/calendar.css | 99 ++ 6 files changed, 1517 insertions(+), 5 deletions(-) create mode 100644 extensions/stats/templates/calendar.html create mode 100644 static/js/calendar.js create mode 100644 static/stylesheets/calendar.css diff --git a/extensions/stats/main.py b/extensions/stats/main.py index a87ccb30f..d25825bb4 100644 --- a/extensions/stats/main.py +++ b/extensions/stats/main.py @@ -7,6 +7,7 @@ This module provides rrdtool support to retreive statistics from postfix log : sent, received, bounced, rejected """ +import calendar from django.core.urlresolvers import reverse from django.utils.translation import ugettext as _ from django.conf.urls.defaults import * @@ -89,6 +90,8 @@ def graph_display(request,dom_id,graph_t=graph_types): @login_required @user_passes_test(lambda u: u.is_superuser) def adminindex(request): + CH = calendar.HTMLCalendar(calendar.MONDAY) + CH_print = CH.formatmonth(2009,11) period = request.GET.has_key("period") and request.GET["period"] or "day" domain = request.GET.has_key("domain") and request.GET["domain"] or "global" if domain != "global": @@ -100,8 +103,25 @@ def adminindex(request): "graphs" : [{"name" : "traffic", "label" : _("Average normal traffic")}, {"name" : "badtraffic", "label" : _("Average bad traffic")}, {"name" : "size", "label" : _("Average normal traffic size")}], - "periods" : ["day", "week", "month", "year"], - "period" : period + "periods" : ["day", "week", "month", "year","Custom"], + "period" : period, "cal" : CH_print + }) + +@login_required +@user_passes_test(lambda u: u.is_superuser) +def custom_period(request): + print "[stats] json get" + start = request.GET.has_key("start") + if not start: + print "[stats] no start selected" + return + end = request.GET.has_key("end") + return _render(request, 'stats/graphs.html', { + "domain" : domain, + "graphs" : [{"name" : "traffic", "label" : _("Average normal traffic")}, + {"name" : "badtraffic", "label" : _("Average bad traffic")}, + {"name" : "size", "label" : _("Average normal traffic size")}], + "period" : "%s_%s" %(start,end) }) @login_required @@ -118,6 +138,6 @@ def index(request, dom_id=None): "graphs" : [{"name" : "traffic", "label" : _("Average normal traffic")}, {"name" : "badtraffic", "label" : _("Average bad traffic")}, {"name" : "size", "label" : _("Average normal traffic size")}], - "periods" : ["day", "week", "month", "year"], + "periods" : ["day", "week", "month", "year","Custom"], "period" : period }) diff --git a/extensions/stats/templates/adminindex.html b/extensions/stats/templates/adminindex.html index 25cf9ff8e..1f170898f 100644 --- a/extensions/stats/templates/adminindex.html +++ b/extensions/stats/templates/adminindex.html @@ -4,6 +4,7 @@ {% block html_head %} + @@ -27,6 +28,7 @@ window.addEvent("domready", function() { }); }); + {% endblock %} {% block middletitle %}{% trans "Global statistics" %}{% endblock %} {% block content %} @@ -46,7 +48,15 @@ window.addEvent("domready", function() { {% endfor %} + {% ifequal period "Custom" %} + {% include "stats/calendar.html" %} + {% url extensions.stats.main.adminindex as c_stats_url %} + {% endifequal %} - {% include "stats/graphs.html" %} + + {% include "stats/graphs.html" %} {% endblock %} diff --git a/extensions/stats/templates/calendar.html b/extensions/stats/templates/calendar.html new file mode 100644 index 000000000..d48d2bfff --- /dev/null +++ b/extensions/stats/templates/calendar.html @@ -0,0 +1,38 @@ +{% load i18n %} +
+
+ {% trans "Start" %}: + {% trans "End" %}: + +
+
+ + + diff --git a/extensions/stats/urls.py b/extensions/stats/urls.py index 904b014c4..b72709229 100644 --- a/extensions/stats/urls.py +++ b/extensions/stats/urls.py @@ -2,6 +2,7 @@ from django.conf.urls.defaults import * urlpatterns = patterns('mailng.extensions.stats.main', url(r'^$', 'adminindex', name='fullindex'), + url(r'^custom_period$', 'custom_period'), url(r'^(?P\d+)/$', 'index', name='domindex'), (r'^display/(?P\d+)/(?P\w+)/$', 'graph_display'), (r'^domain/(?P\d+)/$', 'domain'), diff --git a/static/js/calendar.js b/static/js/calendar.js new file mode 100644 index 000000000..ab4935248 --- /dev/null +++ b/static/js/calendar.js @@ -0,0 +1,1344 @@ +/** + * Calendar + * + * version 1.1.2 + * - added option onValuesStart + * version 1.1.1 + * - added bounds option + * - fixes chrome toggle bug + * version 1.1 + * - fix: events in fx not work bug + * - fix: ie6 iframe bug + * - added displayHead and displayFooter abstract render function + * version 1.0 + * - new option dayLabelLength + * - IE6 Fix works with variable content height + */ + +/** + * Calendar: a Javascript class for Mootools + * + * adds accessible and unobtrusive date pickers to your form elements + * based on a work by Aeron Glemann + * modifyed by Jens Klose + * + * @licence MIT Style License + */ +var Calendar = new Class({ + + Implements: [Events,Options], + + options: { + dayLabelLength: 2, + blocked: [], // blocked dates + bounds: {start:null , end:null}, // + classes: { + calendar:'calendar', + prev:'prev', + next:'next', + month:'month', + year:'year', + today:'today', + invalid:'invalid', + valid:'valid', + inactive:'inactive', + active:'active', + hover:'hover', + hilite:'hilite' + }, + days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], // days of the week starting at sunday + direction: 0, // -1 past, 0 past + future, 1 future + draggable: true, + months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], + navigation: 1, // 0 = no nav; 1 = single nav for month; 2 = dual nav for month and year + offset: 0, // first day of the week: 0 = sunday, 1 = monday, etc.. + onHideStart: $empty, + onHideComplete: $empty, + onShowStart: $empty, + onShowComplete: $empty, + onValuesStart: $empty, + pad: 1, // padding between multiple calendars + tweak: { + x: 0, + y: 0 + } // tweak calendar positioning + }, + + /** + * initialize: calendar constructor + * + * @param obj (obj) a js object containing the form elements and format strings { id: 'format', id: 'format' etc } + * @param props (obj) optional properties + */ + initialize: function(obj, options) { + // basic error checking + if (!obj) { + return false; + } + + this.setOptions(options); + this.classes = this.options.classes; + + // create cal element with css styles required for proper cal functioning + this.calendar = new Element('div', { + 'class':this.classes.calendar, + 'styles': { + left: '-1000px', + opacity: 0, + position: 'absolute', + top: '-1000px', + zIndex: 1000 + } + }).inject(document.body); + + this.calendar.coord = this.calendar.getCoordinates(); + + // iex 6 needs a transparent iframe underneath the calendar in order to not allow select elements to render through + if (Browser.Engine.trident4) { + this.iframe = new IFrame({ + 'styles': { + left: '-1000px', + position: 'absolute', + top: '-1000px', + zIndex: 999 + } + }).inject(document.body); + this.iframe.style.filter = 'progid:DXImageTransform.Microsoft.Alpha(style=0,opacity=0)'; + } + + // initialize fade method + this.fx = new Fx.Tween(this.calendar,{ + link : 'chain', + duration : 'short', + onStart: function() { + if (this.calendar.getStyle('opacity') == 0) { // show + //this.calendar.setStyle('display', 'block'); + this.fireEvent('onShowStart', this.element); + } + else { // hide + this.fireEvent('onHideStart', this.element); + } + }.bind(this), + onComplete: function() { + if (this.calendar.getStyle('opacity') == 0) { // hidden + //this.calendar.setStyle('display', 'none'); + if (Browser.Engine.trident4) { + this.iframe.setStyles({ + left: '-1000px', + top: '-1000px' + }); + } + this.fireEvent('onHideComplete', this.element); + } + else { // shown + this.fireEvent('onShowComplete', this.element); + } + }.bind(this) + }); + + // initialize drag method + if (window.Drag && this.options.draggable) { + this.drag = new Drag.Move(this.calendar, { + onDrag: function() { + if (Browser.Engine.trident4) { + this.iframe.setStyles({ + left: this.calendar.getStyle('left'), + top: this.calendar.getStyle('top') + }); + } + }.bind(this) + }); + } + + // create calendars array + this.calendars = []; + var id = 0; + var d = new Date(); // today + d.setDate(d.getDate() + this.options.direction.toInt()); // correct today for directional offset + + for (var i in obj) { + var cal = { + button: new Element('button', { + 'type': 'button' + }), + el: $(i), + els: [], + id: id++, + month: d.getMonth(), + visible: false, + year: d.getFullYear() + }; + + // fix for bad element (naughty, naughty element!) + if (!this.element(i, obj[i], cal)) { + continue; + } + // show cal on click + this.fn = function(e, cal) { + e.stop(); + this.toggle(cal); + }.create({ + 'arguments': cal, + 'bind': this, + 'event': true + }); + + cal.el.addClass(this.classes.calendar); + // create cal button + cal.button.addClass(this.classes.calendar) + .addEvent('click', this.fn) + .inject(cal.el,'after'); + // read in default value + cal.val = this.read(cal); + $extend(cal, this.bounds(cal)); // abs bounds of calendar + $extend(cal, this.values(cal)); // valid days, months, years + this.rebuild(cal); + this.calendars.push(cal); // add to cals array + } + return this; + }, + + /** + * blocked: returns an array of blocked days for the month / year + * + * @param cal (obj) + * @returns blocked days (array) + */ + blocked: function(cal) { + var blocked = []; + var offset = new Date(cal.year, cal.month, 1).getDay(); // day of the week (offset) + var last = new Date(cal.year, cal.month + 1, 0).getDate(); // last day of this month + + this.options.blocked.each(function(date) { + var values = date.split(' '); + + for (var i = 0; i < 3; i++) { + if (!values[i]) { + values[i] = '*'; + } // make sure blocked date contains values for at least d, m and y + values[i] = values[i].contains(',') ? values[i].split(',') : new Array(values[i]); // split multiple values + } + + if (values[2].contains(cal.year + '') || values[2].contains('*')) { + if (values[1].contains(cal.month + 1 + '') || values[1].contains('*')) { + values[0].each(function(val) { // if blocked value indicates this month / year + if (val > 0) { + blocked.push(val.toInt()); + } // add date to blocked array + }); + + if (values[3]) { // optional value for day of week + values[3] = values[3].contains(',') ? values[3].split(',') : new Array(values[3]); + + for (var i = 0; i < last; i++) { + var day = (i + offset) % 7; + + if (values[3].contains(day + '')) { + blocked.push(i + 1); // add every date that corresponds to the blocked day of the week to the blocked array + } + } + } + } + } + }, this); + + return blocked; + }, + + /** + * bounds: returns the start / end bounds of the calendar + * + * @param cal (obj) + * @returns obj + */ + bounds: function(cal) { + // check external settings + if ($defined(this.options.bounds.start)&&$defined(this.options.bounds.end)) { + if ($type(this.options.bounds.end) == 'date' && + $type(this.options.bounds.start) == 'date' && + this.options.bounds.end>=this.options.bounds.start) { + return { + 'start': this.options.bounds.start, + 'end': this.options.bounds.end + } + } + } + + // 1. first we assume the calendar has no bounds (or a thousand years in either direction) + + // by default the calendar will accept a millennium in either direction + var start = new Date(1000, 0, 1); // jan 1, 1000 + var end = new Date(2999, 11, 31); // dec 31, 2999 + + // 2. but if the cal is one directional we adjust accordingly + var date = new Date().getDate() + this.options.direction.toInt(); + + if (this.options.direction > 0) { + start = new Date(); + start.setDate(date + this.options.pad * cal.id); + } + + if (this.options.direction < 0) { + end = new Date(); + end.setDate(date - this.options.pad * (this.calendars.length - cal.id - 1)); + } + + // 3. then we can further filter the limits by using the pre-existing values in the selects + cal.els.each(function(el) { + if (el.get('tag') == 'select') { + if (el.retrieve('format').test('(y|Y)')) { // search for a year select + var years = []; + + el.getChildren().each(function(option) { // get options + var values = this.unformat(option.get('value'), el.retrieve('format')); + + if (!years.contains(values[0])) { + years.push(values[0]); + } // add to years array + }, this); + + years.sort(this.sort); + + if (years[0] > start.getFullYear()) { + d = new Date(years[0], start.getMonth() + 1, 0); // last day of new month + + if (start.getDate() > d.getDate()) { + start.setDate(d.getDate()); + } + + start.setYear(years[0]); + } + + if (years.getLast() < end.getFullYear()) { + d = new Date(years.getLast(), end.getMonth() + 1, 0); // last day of new month + + if (end.getDate() > d.getDate()) { + end.setDate(d.getDate()); + } + + end.setYear(years.getLast()); + } + } + + if (el.retrieve('format').test('(F|m|M|n)')) { // search for a month select + var months_start = []; + var months_end = []; + + el.getChildren().each(function(option) { // get options + var values = this.unformat(option.get('value'), el.retrieve('format')); + + if ($type(values[0]) != 'number' || values[0] == years[0]) { // if it's a year / month combo for curr year, or simply a month select + if (!months_start.contains(values[1])) { + months_start.push(values[1]); + } // add to months array + } + + if ($type(values[0]) != 'number' || values[0] == years.getLast()) { // if it's a year / month combo for curr year, or simply a month select + if (!months_end.contains(values[1])) { + months_end.push(values[1]); + } // add to months array + } + }, this); + + months_start.sort(this.sort); + months_end.sort(this.sort); + + if (months_start[0] > start.getMonth()) { + d = new Date(start.getFullYear(), months_start[0] + 1, 0); // last day of new month + + if (start.getDate() > d.getDate()) { + start.setDate(d.getDate()); + } + + start.setMonth(months_start[0]); + } + + if (months_end.getLast() < end.getMonth()) { + d = new Date(start.getFullYear(), months_end.getLast() + 1, 0); // last day of new month + + if (end.getDate() > d.getDate()) { + end.setDate(d.getDate()); + } + + end.setMonth(months_end.getLast()); + } + } + } + }, this); + + return { + 'start': start, + 'end': end + }; + }, + + /** + * caption: returns the caption element with header and navigation + * + * @param cal (obj) + * @returns caption (element) + */ + caption: function(cal) { + // start by assuming navigation is allowed + var navigation = { + prev: { + 'month': true, + 'year': true + }, + next: { + 'month': true, + 'year': true + } + }; + + // if we're in an out of bounds year + if (cal.year == cal.start.getFullYear()) { + navigation.prev.year = false; + if (cal.month == cal.start.getMonth() && this.options.navigation == 1) { + navigation.prev.month = false; + } + } + if (cal.year == cal.end.getFullYear()) { + navigation.next.year = false; + if (cal.month == cal.end.getMonth() && this.options.navigation == 1) { + navigation.next.month = false; + } + } + + // special case of improved navigation but months array with only 1 month we can disable all month navigation + if ($type(cal.months) == 'array') { + if (cal.months.length == 1 && this.options.navigation == 2) { + navigation.prev.month = navigation.next.month = false; + } + } + + var caption = new Element('caption'); + + var prev = new Element('a').addClass(this.classes.prev).appendText('\x3c'); // < + var next = new Element('a').addClass(this.classes.next).appendText('\x3e'); // > + + if (this.options.navigation == 2) { + var month = new Element('span').addClass(this.classes.month).inject(caption); + + if (navigation.prev.month) { + prev.clone().addEvent('click', function(cal) { + this.navigate(cal, 'm', -1); + }.pass(cal, this)).inject(month); + } + + month.adopt(new Element('span').appendText(this.options.months[cal.month])); + + if (navigation.next.month) { + next.clone().addEvent('click', function(cal) { + this.navigate(cal, 'm', 1); + }.pass(cal, this)).inject(month); + } + + var year = new Element('span').addClass(this.classes.year).inject(caption); + + if (navigation.prev.year) { + prev.clone().addEvent('click', function(cal) { + this.navigate(cal, 'y', -1); + }.pass(cal, this)).inject(year); + } + + year.adopt(new Element('span').appendText(cal.year)); + + if (navigation.next.year) { + next.clone().addEvent('click', function(cal) { + this.navigate(cal, 'y', 1); + }.pass(cal, this)).inject(year); + } + } + else { // 1 or 0 + if (navigation.prev.month && this.options.navigation) { + prev.clone().addEvent('click', function(cal) { + this.navigate(cal, 'm', -1); + }.pass(cal, this)).inject(caption); + } + + caption.adopt(new Element('span').addClass(this.classes.month).appendText(this.options.months[cal.month])); + + caption.adopt(new Element('span').addClass(this.classes.year).appendText(cal.year)); + + if (navigation.next.month && this.options.navigation) { + next.clone().addEvent('click', function(cal) { + this.navigate(cal, 'm', 1); + }.pass(cal, this)).inject(caption); + } + + } + + return caption; + }, + + /** + * changed: run when a select value is changed + * + * @param cal (obj) + * @returns caption (element) + */ + changed: function(cal) { + cal.val = this.read(cal); // update calendar val from inputs + + $extend(cal, this.values(cal)); // update bounds - based on curr month + + this.rebuild(cal); // rebuild days select + + if (!cal.val) { + return; + } // in case the same date was clicked the cal has no set date we should exit + + if (cal.val.getDate() < cal.days[0]) { + cal.val.setDate(cal.days[0]); + } + if (cal.val.getDate() > cal.days.getLast()) { + cal.val.setDate(cal.days.getLast()); + } + + cal.els.each(function(el) { // then we can set the value to the field + el.set('value',this.format(cal.val, el.retrieve('format'))); + }, this); + + this.check(cal); // checks other cals + + this.calendars.each(function(kal) { // update cal graphic if visible + if (kal.visible) { + this.display(kal); + } + }, this); + }, + + /** + * check: checks other calendars to make sure no overlapping values + * + * @param cal (obj) + */ + check: function(cal) { + this.calendars.each(function(kal, i) { + if (kal.val) { // if calendar has value set + var change = false; + + if (i < cal.id) { // preceding calendar + var bound = new Date(Date.parse(cal.val)); + + bound.setDate(bound.getDate() - (this.options.pad * (cal.id - i))); + + if (bound < kal.val) { + change = true; + } + } + if (i > cal.id) { // following calendar + var bound = new Date(Date.parse(cal.val)); + + bound.setDate(bound.getDate() + (this.options.pad * (i - cal.id))); + + if (bound > kal.val) { + change = true; + } + } + + if (change) { + if (kal.start > bound) { + bound = kal.start; + } + if (kal.end < bound) { + bound = kal.end; + } + + kal.month = bound.getMonth(); + kal.year = bound.getFullYear(); + + $extend(kal, this.values(kal)); + + // TODO - IN THE CASE OF SELECT MOVE TO NEAREST VALID VALUE + // IN THE CASE OF INPUT DISABLE + + // if new date is not valid better unset cal value + // otherwise it would mean incrementally checking to find the nearest valid date which could be months / years away + kal.val = kal.days.contains(bound.getDate()) ? bound : null; + + this.write(kal); + + if (kal.visible) { + this.display(kal); + } // update cal graphic if visible + } + } + }, this); + }, + + /** + * clicked: run when a valid day is clicked in the calendar + * + * @param td (obj) + * @param day (int) + * @param cal (obj) + */ + clicked: function(td, day, cal) { + cal.val = (this.value(cal) == day) ? null : new Date(cal.year, cal.month, day); // set new value - if same then disable + + this.write(cal); + + // 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) + // we can't let the calendar undo a date selection - it's just not possible!! + if (!cal.val) { + cal.val = this.read(cal); + } + + if (cal.val) { + this.check(cal); // checks other cals + this.toggle(cal); // hide cal + } + else { // remove active class and replace with valid + td.addClass(this.classes.valid); + td.removeClass(this.classes.active); + } + }, + + /** + * display: create calendar element + * + * @param cal (obj) + */ + display: function(cal) { + // 1. header and navigation + this.calendar.empty(); // init div + this.calendar.className = this.classes.calendar + ' ' + this.options.months[cal.month].toLowerCase(); + // render optional head content + var extra = this.displayHead(cal); + if ($defined(extra) && $defined(extra.inject)) extra.inject(this.calendar); + //this.displayHead(cal).inject(this.calendar, 'top'); + + var div = new Element('div').inject(this.calendar); // a wrapper div to help correct browser css problems with the caption element + var table = new Element('table').inject(div).adopt(this.caption(cal)); + // 2. day names + var thead = new Element('thead').inject(table); + var tr = new Element('tr').inject(thead); + + for (var i = 0; i <= 6; i++) { + var th = this.options.days[(i + this.options.offset) % 7]; + + tr.adopt(new Element('th', { + 'title': th + }).appendText(th.substr(0, this.options.dayLabelLength))); + } + + // 3. day numbers + var tbody = new Element('tbody').inject(table); + var tr = new Element('tr').inject(tbody); + var d = new Date(cal.year, cal.month, 1); + var offset = ((d.getDay() - this.options.offset) + 7) % 7; // day of the week (offset) + var last = new Date(cal.year, cal.month + 1, 0).getDate(); // last day of this month + var prev = new Date(cal.year, cal.month, 0).getDate(); // last day of previous month + var active = this.value(cal); // active date (if set and within curr month) + var valid = cal.days; // valid days for curr month + var inactive = []; // active dates set by other calendars + var hilited = []; + this.calendars.each(function(kal, i) { + if (kal != cal && kal.val) { + if (cal.year == kal.val.getFullYear() && cal.month == kal.val.getMonth()) { + inactive.push(kal.val.getDate()); + } + if (cal.val) { + for (var day = 1; day <= last; day++) { + d.setDate(day); + if ((i < cal.id && d > kal.val && d < cal.val) || (i > cal.id && d > cal.val && d < kal.val)) { + if (!hilited.contains(day)) { + hilited.push(day); + } + } + } + } + } + }, this); + var d = new Date(); + var today = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime(); // today obv + + for (var i = 1; i < 43; i++) { // 1 to 42 (6 x 7 or 6 weeks) + if ((i - 1) % 7 == 0) { + tr = new Element('tr').inject(tbody); + } // each week is it's own table row + var td = new Element('td').inject(tr); + var day = i - offset; + var date = new Date(cal.year, cal.month, day); + var cls = ''; + + if (day === active) { + cls = this.classes.active; + } // active + else if (inactive.contains(day)) { + cls = this.classes.inactive; + } // inactive + else if (valid.contains(day)) { + cls = this.classes.valid; + } // valid + else if (day >= 1 && day <= last) { + cls = this.classes.invalid; + } // invalid + + if (date.getTime() == today) { + cls = cls + ' ' + this.classes.today; + } // adds class for today + if (hilited.contains(day)) { + cls = cls + ' ' + this.classes.hilite; + } // adds class if hilited + td.addClass(cls); + + if (valid.contains(day)) { // if it's a valid - clickable - day we add interaction + td.set('title', this.format(date, 'D M jS Y')); + td.addEvents({ + 'click': function(td, day, cal) { + this.clicked(td, day, cal); + }.pass([td, day, cal], this), + 'mouseover': function(td, cls) { + td.addClass(cls); + }.pass([td, this.classes.hover]), + 'mouseout': function(td, cls) { + td.removeClass(cls); + }.pass([td, this.classes.hover]) + }); + } + + // pad calendar with last days of prev month and first days of next month + if (day < 1) { + day = prev + day; + } + else if (day > last) { + day = day - last; + } + td.appendText(day); + } + // render optional footer content + extra = this.displayFooter(cal) + if ($defined(extra) && $defined(extra.inject)) extra.inject(this.calendar); + + this.calendar.coord = this.calendar.getCoordinates(); + }, + + /** + * displayHead + * + * @abstract + */ + displayHead: function(cal) {}, + + /** + * displayFooter + * + * @abstract + */ + displayFooter: function(cal) {}, + + /** + * element: helper function + * + * @param el (string) element id + * @param f (string) format string + * @param cal (obj) + */ + element: function(el, f, cal) { + if ($type(f) == 'object') { // in the case of multiple inputs per calendar + for (var i in f) { + if (!this.element(i, f[i], cal)) { + return false; + } + } + + return true; + } + + el = $(el); + + if (!el) { + return false; + } + + el.store('format',f); + + if (el.get('tag') == 'select') { // select elements allow the user to manually set the date via select option + el.addEvent('change', function(cal) { + this.changed(cal); + }.pass(cal, this)); + } + else { // input (type text) elements restrict the user to only setting the date via the calendar + el.set('readonly','readonly'); + el.addEvent('focus', function(cal) { + this.toggle(cal); + }.pass(cal, this)); + } + + cal.els.push(el); + + return true; + }, + + /** + * format: formats a date object according to passed in instructions + * + * @param date (obj) + * @param f (string) any combination of punctuation / separators and d, j, D, l, S, m, n, F, M, y, Y + * @returns string + */ + format: function(date, f) { + var g = ''; + + if (date) { + var d = date.getDate(); // 1 - 31 + var day = this.options.days[date.getDay()]; // Sunday - Saturday + var m = date.getMonth() + 1; // 1 - 12 + var month = this.options.months[date.getMonth()]; // January - December + var y = date.getFullYear() + ''; // 19xx - 20xx + + for (var i = 0; i < f.length; i++) { + var c = f.charAt(i); // format char + switch(c) { + // year cases + case 'y': // xx - xx + y = y.substr(2); + case 'Y': // 19xx - 20xx + g += y; + break; + + // month cases + case 'm': // 01 - 12 + if (m < 10) { + m = '0' + m; + } + case 'n': // 1 - 12 + g += m; + break; + + case 'M': // Jan - Dec + month = month.substr(0, 3); + case 'F': // January - December + g += month; + break; + + // day cases + case 'd': // 01 - 31 + if (d < 10) { + d = '0' + d; + } + case 'j': // 1 - 31 + g += d; + break; + + case 'D': // Sun - Sat + day = day.substr(0, 3); + case 'l': // Sunday - Saturday + g += day; + break; + + case 'S': // st, nd, rd or th (works well with j) + if (d % 10 == 1 && d != '11') { + g += 'st'; + } + else if (d % 10 == 2 && d != '12') { + g += 'nd'; + } + else if (d % 10 == 3 && d != '13') { + g += 'rd'; + } + else { + g += 'th'; + } + break; + + default: + g += c; + } + } + } + return g; // return format with values replaced + }, + + /** + * navigate: calendar navigation + * + * @param cal (obj) + * @param type (str) m or y for month or year + * @param n (int) + or - for next or prev + */ + navigate: function(cal, type, n) { + switch (type) { + case 'm': // month + if ($type(cal.months) == 'array') { + var i = cal.months.indexOf(cal.month) + n; // index of current month + + if (i < 0 || i == cal.months.length) { // out of range + if (this.options.navigation == 1) { // if type 1 nav we'll need to increment the year + this.navigate(cal, 'y', n); + } + + i = (i < 0) ? cal.months.length - 1 : 0; + } + + cal.month = cal.months[i]; + } + else { + var i = cal.month + n; + + if (i < 0 || i == 12) { + if (this.options.navigation == 1) { + this.navigate(cal, 'y', n); + } + + i = (i < 0) ? 11 : 0; + } + + cal.month = i; + } + break; + + case 'y': // year + if ($type(cal.years) == 'array') { + var i = cal.years.indexOf(cal.year) + n; + + cal.year = cal.years[i]; + } + else { + cal.year += n; + } + break; + } + $extend(cal, this.values(cal)); + if ($type(cal.months) == 'array') { // if the calendar has a months select + var i = cal.months.indexOf(cal.month); // and make sure the curr months exists for the new year + + if (i < 0) { + cal.month = cal.months[0]; + } // otherwise we'll reset the month + } + this.display(cal); + }, + + /** + * read: compiles cal value based on array of inputs passed in + * + * @param cal (obj) + * @returns date (obj) or (null) + */ + read: function(cal) { + var arr = [null, null, null]; + cal.els.each(function(el) { + // returns an array which may contain empty values + var values = this.unformat(el.get('value'), el.retrieve('format')); + + values.each(function(val, i) { + if ($type(val) == 'number') { + arr[i] = val; + } + }); + }, this); + // we can update the cals month and year values + if ($type(arr[0]) == 'number') { + cal.year = arr[0]; + } + if ($type(arr[1]) == 'number') { + cal.month = arr[1]; + } + var val = null; + if (arr.every(function(i) { + return $type(i) == 'number'; + })) { // if valid date + var last = new Date(arr[0], arr[1] + 1, 0).getDate(); // last day of month + + 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) + val = new Date(arr[0], arr[1], arr[2]); + } + return (cal.val == val) ? null : val; // if new date matches old return null (same date clicked twice = disable) + }, + + /** + * rebuild: rebuilds days + months selects + * + * @param cal (obj) + */ + rebuild: function(cal) { + cal.els.each(function(el) { + /* + if (el.getTag() == 'select' && el.format.test('^(F|m|M|n)$')) { // special case for months-only select + if (!cal.options) { cal.options = el.clone(); } // clone a copy of months select + + var val = (cal.val) ? cal.val.getMonth() : el.value.toInt(); + + el.empty(); // initialize select + + cal.months.each(function(month) { + // create an option element + var option = new Element('option', { + 'selected': (val == month), + 'value': this.format(new Date(1, month, 1), el.format); + }).appendText(day).injectInside(el); + }, this); + } + */ + if (el.get('tag') == 'select' && el.retrieve('format').test('^(d|j)$')) { // special case for days-only select + var d = this.value(cal); + + if (!d) { + d = el.get('value').toInt(); + } // if the calendar doesn't have a set value, try to use value from select + + el.empty(); // initialize select + + cal.days.each(function(day) { + // create an option element + var option = new Element('option', { + 'value': ((el.retrieve('format') == 'd' && day < 10) ? '0' + day : day) + }).appendText(day).inject(el); + if(d == day){ + option.set('selected','selected'); + } + }, this); + } + }, this); + }, + + /** + * sort: helper function for numerical sorting + * + * @param a (int) + * @param b (int) + */ + sort: function(a, b) { + return a - b; + }, + + /** + * toggle: show / hide calendar + * + * @param cal (obj) + */ + toggle: function(cal) { + document.removeEvent('mousedown', this.fn); // always remove the current mousedown script first + + if (cal.visible) { // simply hide curr cal + cal.visible = false; + cal.button.removeClass(this.classes.active); // active + this.fx.start('opacity', 0); //*RAL - Fx.Tween.start([property], from, [to]) + } + else { // otherwise show (may have to hide others) + // hide cal on out-of-bounds click + this.fn = function(e, cal) { + var event = new Event(e); + var el = $(event.target); + var stop = false; + + while (el != document.body && el.nodeType == 1) { + if (el == this.calendar) { + stop = true; + } + this.calendars.each(function(kal) { + if (kal.button == el || kal.els.contains(el)) { + stop = true; + } + }); + + if (stop) { + e.stop(); + return false; + } + else { + el = el.getParent(); + } + } + + this.toggle(cal); + }.create({ + 'arguments': cal, + 'bind': this, + 'event': true + }); + + document.addEvent('mousedown', this.fn); + + this.calendars.each(function(kal) { + if (kal == cal) { + kal.visible = true; + kal.button.addClass(this.classes.active); // css c-icon-active + } + else { + kal.visible = false; + kal.button.removeClass(this.classes.active); // css c-icon-active + } + }, this); + + this.display(cal); + + var size = window.getScrollSize(); + var coord = cal.button.getCoordinates(); + var x = coord.right + this.options.tweak.x; + var y = coord.top + this.options.tweak.y; + + // make sure the calendar doesn't open off screen (does't work in safari!?) + if (x + this.calendar.coord.width > size.x) { + x -= (x + this.calendar.coord.width - size.x); + } + if (y + this.calendar.coord.height > size.y) { + y -= (y + this.calendar.coord.height - size.y); + } + + this.calendar.setStyles({ + left: x + 'px', + top: y + 'px' + }); + // IE6 Iframe Fix + if (Browser.Engine.trident4) { + this.iframe.setStyles({ + left: x + 'px', + top: y + 'px', + height: this.calendar.coord.height + 'px', + width: this.calendar.coord.width + 'px' + }); + } + this.fx.start('opacity', 1); //*RAL - Fx.Tween.start([property], from, [to]) + } + }, + + /** + * unformat: takes a value from an input and parses the d, m and y elements + * + * @param val (string) + * @param f (string) any combination of punctuation / separators and d, j, D, l, S, m, n, F, M, y, Y + * @returns array + */ + unformat: function(val, f) { + f = f.escapeRegExp(); + + var re = { + d: '([0-9]{2})', + j: '([0-9]{1,2})', + D: '(' + this.options.days.map(function(day) { + return day.substr(0, 3); + }).join('|') + ')', + l: '(' + this.options.days.join('|') + ')', + S: '(st|nd|rd|th)', + F: '(' + this.options.months.join('|') + ')', + m: '([0-9]{2})', + M: '(' + this.options.months.map(function(month) { + return month.substr(0, 3); + }).join('|') + ')', + N: '([0-9]{1,2})', + n: '([0-9]{1,2})', + Y: '([0-9]{4})', + y: '([0-9]{2})' + }; + + var arr = []; // array of indexes + var g = ''; + + // convert our format string to regexp + for (var i = 0; i < f.length; i++) { + var c = f.charAt(i); + if (re[c]) { + arr.push(c); + g += re[c]; + } + else { + g += c; + } + } + + // match against date + var matches = val.match('^' + g + '$'); + var dates = new Array(3); + if (matches) { + matches = matches.slice(1); // remove first match which is the date + arr.each(function(c, i) { + i = matches[i]; + + switch(c) { + // year cases + case 'y': + i = '19' + i; // 2 digit year assumes 19th century (same as JS) + case 'Y': + dates[0] = i.toInt(); + break; + + // month cases + case 'F': + i = i.substr(0, 3); + case 'M': + i = this.options.months.map(function(month) { + return month.substr(0, 3); + }).indexOf(i) + 1; + case 'm': + case 'n': + dates[1] = i.toInt() - 1; + break; + + // day cases + case 'd': + case 'j': + dates[2] = i.toInt(); + break; + } + }, this); + } + return dates; + }, + + /** + * value: returns day value of calendar if set + * + * @param cal (obj) + * @returns day (int) or null + */ + value: function(cal) { + var day = null; + if (cal.val) { + if (cal.year == cal.val.getFullYear() && cal.month == cal.val.getMonth()) { + day = cal.val.getDate(); + } + } + return day; + }, + + /** + * values: returns the years, months (for curr year) and days (for curr month and year) for the calendar + * + * @param cal (obj) + * @returns obj + */ + values: function(cal) { + // make it possible + this.fireEvent('onValuesStart', this.element); + var years, months, days; + + cal.els.each(function(el) { + if (el.get('tag') == 'select') { + if (el.retrieve('format').test('(y|Y)')) { // search for a year select + years = []; + + el.getChildren().each(function(option) { // get options + var values = this.unformat(option.get('value'), el.retrieve('format')); + + if (!years.contains(values[0])) { + years.push(values[0]); + } // add to years array + }, this); + + years.sort(this.sort); + } + + if (el.retrieve('format').test('(F|m|M|n|N)')) { // search for a month select + months = []; // 0 - 11 should be + + el.getChildren().each(function(option) { // get options + var values = this.unformat(option.get('value'), el.retrieve('format')); + + if ($type(values[0]) != 'number' || values[0] == cal.year) { // if it's a year / month combo for curr year, or simply a month select + if (!months.contains(values[1])) { + months.push(values[1]); + } // add to months array + } + }, this); + + months.sort(this.sort); + } + + if (el.retrieve('format').test('(d|j)') && !el.retrieve('format').test('^(d|j)$')) { // search for a day select, but NOT a days only select + days = []; // 1 - 31 + + el.getChildren().each(function(option) { // get options + var values = this.unformat(option.get('value'), el.retrieve('format')); + + // in the special case of days we dont want the value if its a days only select + // otherwise that will screw up the options rebuilding + // we will take the values if they are exact dates though + if (values[0] == cal.year && values[1] == cal.month) { + if (!days.contains(values[2])) { + days.push(values[2]); + } // add to days array + } + }, this); + } + } + }, this); + + // we start with what would be the first and last days were there no restrictions + var first = 1; + var last = new Date(cal.year, cal.month + 1, 0).getDate(); // last day of the month + + // if we're in an out of bounds year + if (cal.year == cal.start.getFullYear()) { + // in the special case of improved navigation but no months array, we'll need to construct one + if (months == null && this.options.navigation == 2) { + months = []; + + for (var i = 0; i < 12; i ++) { + if (i >= cal.start.getMonth()) { + months.push(i); + } + } + } + + // if we're in an out of bounds month + if (cal.month == cal.start.getMonth()) { + first = cal.start.getDate(); // first day equals day of bound + } + } + if (cal.year == cal.end.getFullYear()) { + // in the special case of improved navigation but no months array, we'll need to construct one + if (months == null && this.options.navigation == 2) { + months = []; + + for (var i = 0; i < 12; i ++) { + if (i <= cal.end.getMonth()) { + months.push(i); + } + } + } + + if (cal.month == cal.end.getMonth()) { + last = cal.end.getDate(); // last day equals day of bound + } + } + + // let's get our invalid days + var blocked = this.blocked(cal); + + // finally we can prepare all the valid days in a neat little array + if ($type(days) == 'array') { // somewhere there was a days select + days = days.filter(function(day) { + if (day >= first && day <= last && !blocked.contains(day)) { + return day; + } + }); + } + else { // no days select we'll need to construct a valid days array + days = []; + + for (var i = first; i <= last; i++) { + if (!blocked.contains(i)) { + days.push(i); + } + } + } + + days.sort(this.sort); // sorting our days will give us first and last of month + + return { + 'days': days, + 'months': months, + 'years': years + }; + }, + + /** + * write: sets calendars value to form elements + * + * @param cal (obj) + */ + write: function(cal) { + this.rebuild(cal); // in the case of options, we'll need to make sure we have the correct number of days available + + cal.els.each(function(el) { // then we can set the value to the field + el.set('value',this.format(cal.val, el.retrieve('format'))); + }, this); + } +}); \ No newline at end of file diff --git a/static/stylesheets/calendar.css b/static/stylesheets/calendar.css new file mode 100644 index 000000000..60b6b567e --- /dev/null +++ b/static/stylesheets/calendar.css @@ -0,0 +1,99 @@ +/* + Document : calendar.css + Created on : 29.07.2009 + Author : jens klose + Description: + example styles for calendar +*/ +.calendarForm{ + float:right; +} +.calendar{ + font-family:verdana,sans-serif; + font-size:10px; + float:None; +} +button.calendar{ + position:relative; + top:-3px; + left:1px; + background-color:transparent; + background-image:url(../pics/calendar.gif); + border:none; + margin: 0px; + padding:0px; + margin-right:3px; + cursor:pointer; + width:16px; + height:16px; +} +input.calendar{ + cursor:pointer; + border: 1px solid #87847F; + height:12px; + width:100px; + color: #87847F; +} +div.calendar{ + background:white; + border: 1px solid #87847F; + width:180px +} +a.footer{ + color:#474747; + padding:2px; +} +.calendar table{ + width:100%; +} +.calendar th, +.calendar td{ + text-align:center; + font-size:10px; + padding: 0.1em 0.25em 0.1em 0.3em; +} +.calendar tbody td, +.calendar .invalid{ + color:silver; +} +.calendar .valid{ + color:blue; + font-weight:bold; +} +.calendar .valid.hover{ + background:#FFFF77; + cursor:pointer; + font-weight:bold; +} +.calendar caption a, +.calendar .next{ + color:blue; + font-weight:bold; + padding:2px; +} +.calendar .next.hover{ + background:#FFFF77; + cursor:pointer; + font-weight:bold; +} +.calendar .prev{ + color:blue; + font-weight:bold; + padding:2px; +} +.calendar .prev.hover{ + background:#FFFF77; + cursor:pointer; + font-weight:bold; +} +.calendar .active, +.calendar .hilite, +.calendar .inactive{ + color:white; + background-color:#BE3916; + font-weight:bold; +} +.calendar .active, +.calendar .inactive{ + background-color:red; +} \ No newline at end of file