web/data/cubicweb.calendar.js
author Aurelien Campeas <aurelien.campeas@logilab.fr>
Tue, 30 Nov 2010 15:05:07 +0100
changeset 6712 3e6cd6048be8
parent 5774 0d792bceb25d
child 6880 4be32427b2b9
permissions -rw-r--r--
[merge] backport stable

/**
 *  This file contains Calendar utilities
 *  :organization: Logilab
 *  :copyright: 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
 *  :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 */

// IMPORTANT NOTE: the variables DAYNAMES AND MONTHNAMES will be added
//                 by cubicweb automatically
// dynamically computed (and cached)
var _CAL_HEADER = null;

TODAY = new Date();

/**
 * .. class:: Calendar
 *
 * Calendar (graphical) widget
 * public methods are :
 *   __init__ :
 *    @param containerId: the DOM node's ID where the calendar will be displayed
 *    @param inputId: which input needs to be updated when a date is selected
 *    @param year, @param month: year and month to be displayed
 *    @param cssclass: CSS class of the calendar widget (default is commandCal)
 *
 *   show() / hide():
 *    show or hide the calendar widget
 *
 *   toggle():
 *    show (resp. hide) the calendar if it's hidden (resp. displayed)
 *
 *   displayNextMonth(): (resp. displayPreviousMonth())
 *    update the calendar to display next (resp. previous) month
 */
Calendar = function(containerId, inputId, year, month, cssclass) {
    this.containerId = containerId;
    this.inputId = inputId;
    this.year = year;
    this.month = month - 1; // Javascript's counter starts at 0 for january
    this.cssclass = cssclass || "popupCalendar";
    this.visible = false;
    this.domtable = null;

    this.cellprops = {
        'onclick': function() {
            dateSelected(this, containerId);
        },
        'onmouseover': function() {
            this.style.fontWeight = 'bold';
        },
        'onmouseout': function() {
            this.style.fontWeight = 'normal';
        }
    };

    this.todayprops = jQuery.extend({},
    this.cellprops, {
        'class': 'today'
    });

    this._rowdisplay = function(row) {
        var _td = function(elt) {
            return TD(this.cellprops, elt);
        };
        return TR(null, $.map(row, _td));
    };

    this._makecell = function(cellinfo) {
        return TD(cellinfo[0], cellinfo[1]);
    };

    /**
     * .. function:: Calendar._uppercaseFirst(s)
     *
     * utility function (the only use for now is inside the calendar)
     */
    this._uppercaseFirst = function(s) {
        return s.charAt(0).toUpperCase();
    };

    /**
     * .. function:: Calendar._domForRows(rows)
     *
     * accepts the cells data and builds the corresponding TR nodes
     *
     * * `rows`, a list of list of couples (daynum, cssprops)
     */
    this._domForRows = function(rows) {
        var lines = [];
        for (i = 0; i < rows.length; i++) {
            lines.push(TR(null, $.map(rows[i], this._makecell)));
        }
        return lines;
    };

    /**
     * .. function:: Calendar._headdisplay(row)
     *
     * builds the calendar headers
     */
    this._headdisplay = function(row) {
        if (_CAL_HEADER) {
            return _CAL_HEADER;
        }
        var self = this;
        var _th = function(day) {
            return TH(null, self._uppercaseFirst(day));
        };
        return TR(null, $.map(DAYNAMES, _th));
    };

    this._getrows = function() {
        var rows = [];
        var firstday = new Date(this.year, this.month, 1);
        var stopdate = firstday.nextMonth();
        var curdate = firstday.sub(firstday.getRealDay());
        while (curdate.getTime() < stopdate) {
            var row = [];
            for (var i = 0; i < 7; i++) {
                if (curdate.getMonth() == this.month) {
                    props = curdate.equals(TODAY) ? this.todayprops: this.cellprops;
                    row.push([props, curdate.getDate()]);
                } else {
                    row.push([this.cellprops, ""]);
                }
                curdate.iadd(1);
            }
            rows.push(row);
        }
        return rows;
    };

    this._makecal = function() {
        var rows = this._getrows();
        var monthname = MONTHNAMES[this.month] + " " + this.year;
        var prevlink = "javascript: togglePreviousMonth('" + this.containerId + "');";
        var nextlink = "javascript: toggleNextMonth('" + this.containerId + "');";
        this.domtable = TABLE({
            'class': this.cssclass
        },
        THEAD(null, TR(null, TH(null, A({
            'href': prevlink
        },
        "<<")),
        // IE 6/7 requires colSpan instead of colspan
        TH({
            'colSpan': 5,
            'colspan': 5,
            'style': "text-align: center;"
        },
        monthname), TH(null, A({
            'href': nextlink
        },
        ">>")))), TBODY(null, this._headdisplay(), this._domForRows(rows)));
        return this.domtable;
    };

    this._updateDiv = function() {
        if (!this.domtable) {
            this._makecal();
        }
        cw.jqNode(this.containerId).empty().append(this.domtable);
        // replaceChildNodes($(this.containerId), this.domtable);
    };

    this.displayNextMonth = function() {
        this.domtable = null;
        if (this.month == 11) {
            this.year++;
        }
        this.month = (this.month + 1) % 12;
        this._updateDiv();
    };

    this.displayPreviousMonth = function() {
        this.domtable = null;
        if (this.month == 0) {
            this.year--;
        }
        this.month = (this.month + 11) % 12;
        this._updateDiv();
    };

    this.show = function() {
        if (!this.visible) {
            var container = cw.jqNode(this.containerId);
            if (!this.domtable) {
                this._makecal();
            }
            container.empty().append(this.domtable);
            toggleVisibility(container);
            this.visible = true;
        }
    };

    this.hide = function(event) {
        var self;
        if (event) {
            self = event.data.self;
        } else {
            self = this;
        }
        if (self.visible) {
            toggleVisibility(self.containerId);
            self.visible = false;
        }
    };

    this.toggle = function() {
        if (this.visible) {
            this.hide();
        }
        else {
            this.show();
        }
    };

    // call hide() when the user explicitly sets the focus on the matching input
    cw.jqNode(inputId).bind('focus', {
        'self': this
    },
    this.hide); // connect(inputId, 'onfocus', this, 'hide');
};

// keep track of each calendar created
Calendar.REGISTRY = {};

/**
 * .. function:: toggleCalendar(containerId, inputId, year, month)
 *
 * popup / hide calendar associated to `containerId`
 */
function toggleCalendar(containerId, inputId, year, month) {
    var cal = Calendar.REGISTRY[containerId];
    if (!cal) {
        cal = new Calendar(containerId, inputId, year, month);
        Calendar.REGISTRY[containerId] = cal;
    }
    /* hide other calendars */
    for (containerId in Calendar.REGISTRY) {
        var othercal = Calendar.REGISTRY[containerId];
        if (othercal !== cal) {
            othercal.hide();
        }
    }
    cal.toggle();
}

/**
 * .. function:: toggleNextMonth(containerId)
 *
 * ask for next month to calendar displayed in `containerId`
 */
function toggleNextMonth(containerId) {
    var cal = Calendar.REGISTRY[containerId];
    cal.displayNextMonth();
}

/**
 * .. function:: togglePreviousMonth(containerId)
 *
 * ask for previous month to calendar displayed in `containerId`
 */
function togglePreviousMonth(containerId) {
    var cal = Calendar.REGISTRY[containerId];
    cal.displayPreviousMonth();
}

/**
 * .. function:: dateSelected(cell, containerId)
 *
 * Callback called when the user clicked on a cell in the popup calendar
 */
function dateSelected(cell, containerId) {
    var cal = Calendar.REGISTRY[containerId];
    var input = cw.getNode(cal.inputId);
    // XXX: the use of innerHTML might cause problems, but it seems to be
    //      the only way understood by both IE and Mozilla. Otherwise,
    //      IE accepts innerText and mozilla accepts textContent
    var selectedDate = new Date(cal.year, cal.month, cell.innerHTML, 12);
    input.value = remoteExec("format_date", cw.utils.toISOTimestamp(selectedDate));
    cal.hide();
}

function whichElement(e) {
    var targ;
    if (!e) {
        e = window.event;
    }
    if (e.target) {
        targ = e.target;
    }
    else if (e.srcElement) {
        targ = e.srcElement;
    }
    if (targ.nodeType == 3) // defeat Safari bug
    {
        targ = targ.parentNode;
    }
    return targ;
}

function getPosition(element) {
    var left;
    var top;
    var offset;
    // TODO: deal scrollbar positions also!
    left = element.offsetLeft;
    top = element.offsetTop;

    if (element.offsetParent != null) {
        offset = getPosition(element.offsetParent);
        left = left + offset[0];
        top = top + offset[1];

    }
    return [left, top];
}

function getMouseInBlock(event) {
    var elt = event.target;
    var x = event.clientX;
    var y = event.clientY;
    var w = elt.clientWidth;
    var h = elt.clientHeight;
    var offset = getPosition(elt);

    x = 1.0 * (x - offset[0]) / w;
    y = 1.0 * (y - offset[1]) / h;
    return [x, y];
}
function getHourFromMouse(event, hmin, hmax) {
    var pos = getMouseInBlock(event);
    var y = pos[1];
    return Math.floor((hmax - hmin) * y + hmin);
}

function addCalendarItem(event, hmin, hmax, year, month, day, duration, baseurl) {
    var hour = getHourFromMouse(event, hmin, hmax);

    if (0 <= hour && hour < 24) {
        baseurl += "&start=" + year + "%2F" + month + "%2F" + day + "%20" + hour + ":00";
        baseurl += "&stop=" + year + "%2F" + month + "%2F" + day + "%20" + (hour + duration) + ":00";

        stopPropagation(event);
        window.location.assign(baseurl);
        return false;
    }
    return true;
}

function stopPropagation(event) {
    event.cancelBubble = true;
    if (event.stopPropagation) event.stopPropagation();
}