/***************************************************************************** * FILE: anytime.js - The Any+Time(TM) JavaScript Library (source) * * VERSION: 4.1112D * * Copyright 2008-2010 Andrew M. Andrews III (www.AMA3.com). Some Rights * Reserved. This work licensed under the Creative Commons Attribution- * Noncommercial-Share Alike 3.0 Unported License except in jurisdicitons * for which the license has been ported by Creative Commons International, * where the work is licensed under the applicable ported license instead. * For a copy of the unported license, visit * http://creativecommons.org/licenses/by-nc-sa/3.0/ * or send a letter to Creative Commons, 171 Second Street, Suite 300, * San Francisco, California, 94105, USA. For ported versions of the * license, visit http://creativecommons.org/international/ * * Alternative licensing arrangements may be made by contacting the * author at http://www.AMA3.com/contact/ * * The Any+Time(TM) JavaScript Library provides the following ECMAScript * functionality: * * AnyTime.Converter * Converts Dates to/from Strings, allowing a wide range of formats * closely matching those provided by the MySQL DATE_FORMAT() function, * with some noteworthy enhancements. * * AnyTime.pad() * Pads a value with a specific number of leading zeroes. * * AnyTime.noPicker() * Destroys a calendar widget previously added by AnyTime.picker(). * Can also be invoked via jQuery using $(selector).AnyTime_noPicker() * * AnyTime.picker() * Attaches a calendar widget to a text field for selecting date/time * values with fewer mouse movements than most similar pickers. Any * format supported by AnyTime.Converter can be used for the text field. * If JavaScript is disabled, the text field remains editable without * any of the picker features. * Can also be invoked via jQuery using $(selector).AnyTime_picker() * * IMPORTANT NOTICE: This code depends upon the jQuery JavaScript Library * (www.jquery.com), currently version 1.4. * * The Any+Time(TM) code and styles in anytime.css have been tested (but not * extensively) on Windows Vista in Internet Explorer 8.0, Firefox 3.0, Opera * 10.10 and Safari 4.0. Minor variations in IE6+7 are to be expected, due * to their broken box model. Please report any other problems to the author * (URL above). * * Any+Time is a trademark of Andrew M. Andrews III. * Thanks to Chu for help with a setMonth() issue! ****************************************************************************/ var AnyTime = { //============================================================================= // AnyTime.pad() pads a value with a specified number of zeroes and returns // a string containing the padded value. //============================================================================= pad: function( val, len ) { var str = String(Math.abs(val)); while ( str.length < len ) str = '0'+str; if ( val < 0 ) str = '-'+str; return str; } }; (function($) { // private members var __oneDay = (24*60*60*1000); var __daysIn = [ 31,28,31,30,31,30,31,31,30,31,30,31 ]; var __iframe = null; var __initialized = false; var __msie6 = ( navigator.userAgent.indexOf('MSIE 6') > 0 ); var __msie7 = ( navigator.userAgent.indexOf('MSIE 7') > 0 ); var __pickers = []; // Add methods to jQuery to create and destroy pickers using // the typical jQuery approach. jQuery.prototype.AnyTime_picker = function( options ) { return this.each( function(i) { AnyTime.picker( this.id, options ); } ); } jQuery.prototype.AnyTime_noPicker = function() { return this.each( function(i) { AnyTime.noPicker( this.id ); } ); } // Add special methods to jQuery to compute the height and width // of picker components differently for Internet Explorer 6.x // This prevents the pickers from being too tall and wide. jQuery.prototype.AnyTime_height = function(inclusive) { return ( __msie6 ? Number(this.css('height').replace(/[^0-9]/g,'')) : this.outerHeight(inclusive) ); }; jQuery.prototype.AnyTime_width = function(inclusive) { return ( __msie6 ? (1+Number(this.css('width').replace(/[^0-9]/g,''))) : this.outerWidth(inclusive) ); }; // Add a method to jQuery to change the classes of an element to // indicate whether it's value is current (used by AnyTime.picker), // and another to trigger the click handler for the currently- // selected button under an element. jQuery.prototype.AnyTime_current = function(isCurrent,isLegal) { if ( isCurrent ) { this.removeClass('AnyTime-out-btn ui-state-default ui-state-disabled ui-state-highlight'); this.addClass('AnyTime-cur-btn ui-state-default ui-state-highlight'); } else { this.removeClass('AnyTime-cur-btn ui-state-highlight'); if ( ! isLegal ) this.addClass('AnyTime-out-btn ui-state-disabled'); else this.removeClass('AnyTime-out-btn ui-state-disabled'); } }; jQuery.prototype.AnyTime_clickCurrent = function() { this.find('.AnyTime-cur-btn').triggerHandler('click'); } $(document).ready( function() { // Ping the server for statistical purposes (remove if offended). if ( window.location.hostname.length && ( window.location.hostname != 'www.ama3.com' ) ) $(document.body).append(''); // IE6 doesn't float popups over // // // // The appearance of the picker can be extensively modified using CSS styles. // A default appearance can be achieved by the "anytime.css" stylesheet that // accompanies this script. The default style looks better in browsers other // than Internet Explorer (before IE8) because older versions of IE do not // properly implement the CSS box model standard; however, it is passable in // Internet Explorer as well. // // Method parameters: // // id - the "id" attribute of the textfield to associate with the // AnyTime.picker object. The AnyTime.picker will attach itself // to the textfield and manage its value. // // options - an object (associative array) of optional parameters that // override default behaviors. The supported options are: // // ajaxOptions - options passed to jQuery's $.ajax() method whenever // the user dismisses a popup picker or selects a value in an inline // picker. The input's name (or ID) and value are passed to the // server (appended to ajaxOptions.data, if present), and the // "success" handler sets the input's value to the responseText. // Therefore, the text returned by the server must be valid for the // input'sdate/time format, and the server can approve or veto the // value chosen by the user. For more information, see: // http://docs.jquery.com/Ajax. // If ajaxOptions.success is specified, it is used instead of the // default "success" behavior. // // askEra - if true, buttons to select the era are shown on the year // selector popup, even if format specifier does not include the // era. If false, buttons to select the era are NOT shown, even // if the format specifier includes ther era. Normally, era buttons // are only shown if the format string specifies the era. // // askSecond - if false, buttons for number-of-seconds are not shown // even if the format includes seconds. Normally, the buttons // are shown if the format string includes seconds. // // earliest - String or Date object representing the earliest date/time // that a user can select. For best results if the field is only // used to specify a date, be sure to set the time to 00:00:00. // If a String is used, it will be parsed according to the picker's // format (see AnyTime.Converter.format()). // // firstDOW - a value from 0 (Sunday) to 6 (Saturday) stating which // day should appear at the beginning of the week. The default is 0 // (Sunday). The most common substitution is 1 (Monday). Note that // if custom arrays are specified for AnyTime.Converter's dayAbbreviations // and/or dayNames options, they should nonetheless begin with the // value for Sunday. // // hideInput - if true, the is "hidden" (the picker appears in // its place). This actually sets the border, height, margin, padding // and width of the field as small as possivle, so it can still get focus. // If you try to hide the field using traditional techniques (such as // setting "display:none"), the picker will not behave correctly. // // labelDayOfMonth - the label for the day-of-month "buttons". // Can be any HTML! If not specified, "Day of Month" is assumed. // // labelDismiss - the label for the dismiss "button" (if placement is // "popup"). Can be any HTML! If not specified, "X" is assumed. // // labelHour - the label for the hour "buttons". // Can be any HTML! If not specified, "Hour" is assumed. // // labelMinute - the label for the minute "buttons". // Can be any HTML! If not specified, "Minute" is assumed. // // labelMonth - the label for the month "buttons". // Can be any HTML! If not specified, "Month" is assumed. // // labelTimeZone - the label for the UTC offset (timezone) "buttons". // Can be any HTML! If not specified, "Time Zone" is assumed. // // labelSecond - the label for the second "buttons". // Can be any HTML! If not specified, "Second" is assumed. // This option is ignored if askSecond is false! // // labelTitle - the label for the "title bar". Can be any HTML! // If not specified, then whichever of the following is most // appropriate is used: "Select a Date and Time", "Select a Date" // or "Select a Time", or no label if only one field is present. // // labelYear - the label for the year "buttons". // Can be any HTML! If not specified, "Year" is assumed. // // latest - String or Date object representing the latest date/time // that a user can select. For best results if the field is only // used to specify a date, be sure to set the time to 23:59:59. // If a String is used, it will be parsed according to the picker's // format (see AnyTime.Converter.format()). // // placement - One of the following strings: // // "popup" = the picker appears above its when the input // receives focus, and disappears when it is dismissed. This is // the default behavior. // // "inline" = the picker is placed immediately after the // and remains visible at all times. When choosing this placement, // it is best to make the invisible and use only the // picker to select dates. The value can still be used // during form submission as it will always reflect the current // picker state. // // WARNING: when using "inline" and XHTML and including a day-of- // the-month format field, the input may only appear where a // element is permitted (for example, NOT within a

element). // This is because the picker uses a

element to arrange // the day-of-the-month (calendar) buttons. Failure to follow this // advice may result in an "unknown error" in Internet Explorer. // // The following additional options may be specified; see documentation // for AnyTime.Converter (above) for information about these options: // // baseYear // dayAbbreviations // dayNames // eraAbbreviations // format // monthAbbreviations // monthNames // // Other behavior, such as how to format the values on the display // and which "buttons" to include, is inferred from the format string. //============================================================================= AnyTime.picker = function( id, options ) { // Create a new private object instance to manage the picker, // if one does not already exist. if ( __pickers[id] ) throw 'Cannot create another AnyTime picker for "'+id+'"'; var _this = null; __pickers[id] = { // private members twelveHr: false, ajaxOpts: null, // options for AJAX requests denyTab: true, // set to true to stop Opera from tabbing away askEra: false, // prompt the user for the era in yDiv? cloak: null, // cloak div conv: null, // AnyTime.Converter bMinW: 0, // min width of body div bMinH: 0, // min height of body div dMinW: 0, // min width of date div dMinH: 0, // min height of date div div: null, // picker div dB: null, // body div dD: null, // date div dY: null, // years div dMo: null, // months div dDoM: null, // date-of-month table hDoM: null, // date-of-month heading hMo: null, // month heading hTitle: null, // title heading hY: null, // year heading dT: null, // time div dH: null, // hours div dM: null, // minutes div dS: null, // seconds div dO: null, // offset (time zone) div earliest: null, // earliest selectable date/time fBtn: null, // button with current focus fDOW: 0, // index to use as first day-of-week hBlur: null, // input handler hClick: null, // input handler hFocus: null, // input handler hKeydown: null, // input handler hKeypress: null, // input handler id: null, // picker ID inp: null, // input text field latest: null, // latest selectable date/time lastAjax: null, // last value submitted using AJAX lostFocus: false, // when focus is lost, must redraw lX: 'X', // label for dismiss button lY: 'Year', // label for year lO: 'Time Zone', // label for UTC offset (time zone) oBody: null, // UTC offset selector popup oConv: null, // AnyTime.Converter for offset display oCur: null, // current-UTC-offset button oDiv: null, // UTC offset selector popup oLab: null, // UTC offset label oListMinW: 0, // min width of offset list element oMinW: 0, // min width of UTC offset element oSel: null, // select (plus/minus) UTC-offset button offMin: Number.MIN_VALUE, // current UTC offset in minutes offSI: -1, // current UTC label sub-index (if any) offStr: "", // current UTC offset (time zone) string pop: true, // picker is a popup? time: null, // current date/time tMinW: 0, // min width of time div tMinH: 0, // min height of time div url: null, // URL to submit value using AJAX wMinW: 0, // min width of picker wMinH: 0, // min height of picker yAhead: null, // years-ahead button y0XXX: null, // millenium-digit-zero button (for focus) yCur: null, // current-year button yDiv: null, // year selector popup yLab: null, // year label yNext: null, // next-year button yPast: null, // years-past button yPrior: null, // prior-year button //--------------------------------------------------------------------- // .initialize() initializes the picker instance. //--------------------------------------------------------------------- initialize: function( id ) { _this = this; this.id = 'AnyTime--'+id; options = jQuery.extend(true,{},options||{}); options.utcParseOffsetCapture = true; this.conv = new AnyTime.Converter(options); if ( options.placement ) { if ( options.placement == 'inline' ) this.pop = false; else if ( options.placement != 'popup' ) throw 'unknown placement: ' + options.placement; } if ( options.ajaxOptions ) { this.ajaxOpts = jQuery.extend( {}, options.ajaxOptions ); if ( ! this.ajaxOpts.success ) this.ajaxOpts.success = function(data,status) { _this.inp.val(data); }; } if ( options.earliest ) { if ( typeof options.earliest.getTime == 'function' ) this.earliest = options.earliest.getTime(); else this.earliest = this.conv.parse( options.earliest.toString() ); } if ( options.firstDOW ) { if ( ( options.firstDOW < 0 ) || ( options.firstDOW > 6 ) ) throw new Exception('illegal firstDOW: ' + options.firstDOW); this.fDOW = options.firstDOW; } if ( options.latest ) { if ( typeof options.latest.getTime == 'function' ) this.latest = options.latest.getTime(); else this.latest = this.conv.parse( options.latest.toString() ); } this.lX = options.labelDismiss || 'X'; this.lY = options.labelYear || 'Year'; this.lO = options.labelTimeZone || 'Time Zone'; // Infer what we can about what to display from the format. var i; var t; var lab; var shownFields = 0; var format = this.conv.fmt; if ( typeof options.askEra != 'undefined' ) this.askEra = options.askEra; else this.askEra = (format.indexOf('%B')>=0) || (format.indexOf('%C')>=0) || (format.indexOf('%E')>=0); var askYear = (format.indexOf('%Y')>=0) || (format.indexOf('%y')>=0) || (format.indexOf('%Z')>=0) || (format.indexOf('%z')>=0); var askMonth = (format.indexOf('%b')>=0) || (format.indexOf('%c')>=0) || (format.indexOf('%M')>=0) || (format.indexOf('%m')>=0); var askDoM = (format.indexOf('%D')>=0) || (format.indexOf('%d')>=0) || (format.indexOf('%e')>=0); var askDate = askYear || askMonth || askDoM; this.twelveHr = (format.indexOf('%h')>=0) || (format.indexOf('%I')>=0) || (format.indexOf('%l')>=0) || (format.indexOf('%r')>=0); var askHour = this.twelveHr || (format.indexOf('%H')>=0) || (format.indexOf('%k')>=0) || (format.indexOf('%T')>=0); var askMinute = (format.indexOf('%i')>=0) || (format.indexOf('%r')>=0) || (format.indexOf('%T')>=0); var askSec = ( (format.indexOf('%r')>=0) || (format.indexOf('%S')>=0) || (format.indexOf('%s')>=0) || (format.indexOf('%T')>=0) ); if ( askSec && ( typeof options.askSecond != 'undefined' ) ) askSec = options.askSecond; var askOff = ( (format.indexOf('%#')>=0) || (format.indexOf('%+')>=0) || (format.indexOf('%-')>=0) || (format.indexOf('%:')>=0) || (format.indexOf('%;')>=0) || (format.indexOf('%<')>=0) || (format.indexOf('%>')>=0) || (format.indexOf('%@')>=0) ); var askTime = askHour || askMinute || askSec || askOff; if ( askOff ) this.oConv = new AnyTime.Converter( { format: options.formatUtcOffset || format.match(/\S*%[-+:;<>#@]\S*/g).join(' ') } ); // Create the picker HTML and add it to the page. // Popup pickers will be moved to the end of the body // once the entire page has loaded. this.inp = $('#'+id); this.div = $( '
' ); this.inp.after(this.div); this.wMinW = this.div.outerWidth(!$.browser.safari); this.wMinH = this.div.AnyTime_height(true); this.hTitle = $( '
' ); this.div.append( this.hTitle ); this.dB = $( '
' ); this.div.append( this.dB ); this.bMinW = this.dB.outerWidth(true); this.bMinH = this.dB.AnyTime_height(true); if ( options.hideInput ) this.inp.css({border:0,height:'1px',margin:0,padding:0,width:'1px'}); // Add dismiss box to title (if popup) var t = null; var xDiv = null; if ( this.pop ) { xDiv = $( '
'+this.lX+'
' ); this.hTitle.append( xDiv ); xDiv.click(function(e){_this.dismiss(e);}); } // date (calendar) portion var lab = ''; if ( askDate ) { this.dD = $( '
' ); this.dB.append( this.dD ); this.dMinW = this.dD.outerWidth(true); this.dMinH = this.dD.AnyTime_height(true); if ( askYear ) { this.yLab = $('
' + this.lY + '
'); this.dD.append( this.yLab ); this.dY = $( '
    ' ); this.dD.append( this.dY ); this.yPast = this.btn(this.dY,'<',this.newYear,['yrs-past'],'- '+this.lY); this.yPrior = this.btn(this.dY,'1',this.newYear,['yr-prior'],'-1 '+this.lY); this.yCur = this.btn(this.dY,'2',this.newYear,['yr-cur'],this.lY); this.yCur.removeClass('ui-state-default'); this.yCur.addClass('AnyTime-cur-btn ui-state-default ui-state-highlight'); this.yNext = this.btn(this.dY,'3',this.newYear,['yr-next'],'+1 '+this.lY); this.yAhead = this.btn(this.dY,'>',this.newYear,['yrs-ahead'],'+ '+this.lY); shownFields++; } // if ( askYear ) if ( askMonth ) { lab = options.labelMonth || 'Month'; this.hMo = $( '
    ' + lab + '
    ' ); this.dD.append( this.hMo ); this.dMo = $('
      '); this.dD.append(this.dMo); for ( i = 0 ; i < 12 ; i++ ) { var mBtn = this.btn( this.dMo, this.conv.mAbbr[i], function( event ) { var elem = $(event.target); if ( elem.hasClass("AnyTime-out-btn") ) return; var mo = event.target.AnyTime_month; var t = new Date(this.time.getTime()); if ( t.getDate() > __daysIn[mo] ) t.setDate(__daysIn[mo]) t.setMonth(mo); this.set(t); this.upd(elem); }, ['mon','mon'+String(i+1)], lab+' '+this.conv.mNames[i] ); mBtn[0].AnyTime_month = i; } shownFields++; } if ( askDoM ) { lab = options.labelDayOfMonth || 'Day of Month'; this.hDoM = $('
      ' + lab + '
      ' ); this.dD.append( this.hDoM ); this.dDoM = $( '
' ); this.dD.append( this.dDoM ); t = $( '' ); this.dDoM.append(t); var tr = $( '' ); t.append(tr); for ( i = 0 ; i < 7 ; i++ ) tr.append( '' ); var tbody = $( '' ); this.dDoM.append(tbody); for ( var r = 0 ; r < 6 ; r++ ) { tr = $( '' ); tbody.append(tr); for ( i = 0 ; i < 7 ; i++ ) this.btn( tr, 'x', function( event ) { var elem = $(event.target); if ( elem.hasClass("AnyTime-out-btn") ) return; var dom = Number(elem.html()); if ( dom ) { var t = new Date(this.time.getTime()); t.setDate(dom); this.set(t); this.upd(elem); } }, ['dom'], lab ); } shownFields++; } // if ( askDoM ) } // if ( askDate ) // time portion if ( askTime ) { var tensDiv, onesDiv; this.dT = $('
'); this.dB.append(this.dT); this.tMinW = this.dT.outerWidth(true); this.tMinH = this.dT.AnyTime_height(true); if ( askHour ) { this.dH = $('
'); this.dT.append(this.dH); lab = options.labelHour || 'Hour'; this.dH.append( $('
'+lab+'
') ); var amDiv = $('
") .append($(""); $.each(buttonStr.split(' '), function(i) { if (i > 0) { tr.append(""); } var prevButton; $.each(this.split(','), function(j, buttonName) { if (buttonName == 'title') { tr.append(""); if (prevButton) { prevButton.addClass(tm + '-corner-right'); } prevButton = null; }else{ var buttonClick; if (publicMethods[buttonName]) { buttonClick = publicMethods[buttonName]; } else if (views[buttonName]) { buttonClick = function() { button.removeClass(tm + '-state-hover'); changeView(buttonName); }; } if (buttonClick) { if (prevButton) { prevButton.addClass(tm + '-no-right'); } var button, icon = options.theme ? smartProperty(options.buttonIcons, buttonName) : null, text = smartProperty(options.buttonText, buttonName); if (icon) { button = $("
" + "
"); } else if (text) { button = $("
" + "" + text + "
"); } if (button) { button .click(function() { if (!button.hasClass(tm + '-state-disabled')) { buttonClick(); } }) .mousedown(function() { button .not('.' + tm + '-state-active') .not('.' + tm + '-state-disabled') .addClass(tm + '-state-down'); }) .mouseup(function() { button.removeClass(tm + '-state-down'); }) .hover( function() { button .not('.' + tm + '-state-active') .not('.' + tm + '-state-disabled') .addClass(tm + '-state-hover'); }, function() { button .removeClass(tm + '-state-hover') .removeClass(tm + '-state-down'); } ) .appendTo($("
'+this.conv.dAbbr[(this.fDOW+i)%7]+'
").append(buildSection(sections.left))) .append($("").append(buildSection(sections.center))) .append($("").append(buildSection(sections.right)))) .prependTo(element); } function buildSection(buttonStr) { if (buttonStr) { var tr = $("

 

").appendTo(tr)); if (prevButton) { prevButton.addClass(tm + '-no-right'); }else{ button.addClass(tm + '-corner-left'); } prevButton = button; } } } }); if (prevButton) { prevButton.addClass(tm + '-corner-right'); } }); return $("").append(tr); } } /* Resizing -----------------------------------------------------------------------------*/ function calcSize() { if (options.contentHeight) { suggestedViewHeight = options.contentHeight; } else if (options.height) { suggestedViewHeight = options.height - (header ? header.height() : 0) - vsides(content[0]); } else { suggestedViewHeight = Math.round(content.width() / Math.max(options.aspectRatio, .5)); } } function setSize(dateChanged) { ignoreWindowResize++; view.setHeight(suggestedViewHeight, dateChanged); if (absoluteViewElement) { absoluteViewElement.css('position', 'relative'); absoluteViewElement = null; } view.setWidth(content.width(), dateChanged); ignoreWindowResize--; } function windowResize() { if (!ignoreWindowResize) { if (view.start) { // view has already been rendered var uid = ++resizeUID; setTimeout(function() { // add a delay if (uid == resizeUID && !ignoreWindowResize && elementVisible()) { if (elementOuterWidth != (elementOuterWidth = element.outerWidth())) { ignoreWindowResize++; // in case the windowResize callback changes the height sizeChanged(); view.trigger('windowResize', _element); ignoreWindowResize--; } } }, 200); }else{ // calendar must have been initialized in a 0x0 iframe that has just been resized lateRender(); } } } $(window).resize(windowResize); /* External event dropping --------------------------------------------------------*/ if (options.droppable) { var _dragElement; $(document) .bind('dragstart', function(ev, ui) { var _e = ev.target; var e = $(_e); if (!e.parents('.fc').length) { // not already inside a calendar var accept = options.dropAccept; if ($.isFunction(accept) ? accept.call(_e, e) : e.is(accept)) { _dragElement = _e; view.dragStart(_dragElement, ev, ui); } } }) .bind('dragstop', function(ev, ui) { if (_dragElement) { view.dragStop(_dragElement, ev, ui); _dragElement = null; } }); } // let's begin... changeView(options.defaultView); // needed for IE in a 0x0 iframe, b/c when it is resized, never triggers a windowResize if (!bodyVisible()) { lateRender(); } // called when we know the calendar couldn't be rendered when it was initialized, // but we think it's ready now function lateRender() { setTimeout(function() { // IE7 needs this so dimensions are calculated correctly if (!view.start && bodyVisible()) { // !view.start makes sure this never happens more than once render(); } },0); } }); return this; }; /* Important Event Utilities -----------------------------------------------------------------------------*/ var fakeID = 0; function normalizeEvent(event, options) { event._id = event._id || (event.id === undefined ? '_fc' + fakeID++ : event.id + ''); if (event.date) { if (!event.start) { event.start = event.date; } delete event.date; } event._start = cloneDate(event.start = parseDate(event.start)); event.end = parseDate(event.end); if (event.end && event.end <= event.start) { event.end = null; } event._end = event.end ? cloneDate(event.end) : null; if (event.allDay === undefined) { event.allDay = options.allDayDefault; } if (event.className) { if (typeof event.className == 'string') { event.className = event.className.split(/\s+/); } }else{ event.className = []; } } // TODO: if there is no start date, return false to indicate an invalid event /* Grid-based Views: month, basicWeek, basicDay -----------------------------------------------------------------------------*/ setDefaults({ weekMode: 'fixed' }); views.month = function(element, options, viewName) { return new Grid(element, options, { render: function(date, delta) { if (delta) { addMonths(date, delta); date.setDate(1); } // start/end var start = this.start = cloneDate(date, true); start.setDate(1); this.end = addMonths(cloneDate(start), 1); // visStart/visEnd var visStart = this.visStart = cloneDate(start), visEnd = this.visEnd = cloneDate(this.end), nwe = options.weekends ? 0 : 1; if (nwe) { skipWeekend(visStart); skipWeekend(visEnd, -1, true); } addDays(visStart, -((visStart.getDay() - Math.max(options.firstDay, nwe) + 7) % 7)); addDays(visEnd, (7 - visEnd.getDay() + Math.max(options.firstDay, nwe)) % 7); // row count var rowCnt = Math.round((visEnd - visStart) / (DAY_MS * 7)); if (options.weekMode == 'fixed') { addDays(visEnd, (6 - rowCnt) * 7); rowCnt = 6; } // title this.title = formatDate( start, this.option('titleFormat'), options ); // render this.renderGrid( rowCnt, options.weekends ? 7 : 5, this.option('columnFormat'), true ); } }, viewName); }; views.basicWeek = function(element, options, viewName) { return new Grid(element, options, { render: function(date, delta) { if (delta) { addDays(date, delta * 7); } var visStart = this.visStart = cloneDate( this.start = addDays(cloneDate(date), -((date.getDay() - options.firstDay + 7) % 7)) ), visEnd = this.visEnd = cloneDate( this.end = addDays(cloneDate(visStart), 7) ); if (!options.weekends) { skipWeekend(visStart); skipWeekend(visEnd, -1, true); } this.title = formatDates( visStart, addDays(cloneDate(visEnd), -1), this.option('titleFormat'), options ); this.renderGrid( 1, options.weekends ? 7 : 5, this.option('columnFormat'), false ); } }, viewName); }; views.basicDay = function(element, options, viewName) { return new Grid(element, options, { render: function(date, delta) { if (delta) { addDays(date, delta); if (!options.weekends) { skipWeekend(date, delta < 0 ? -1 : 1); } } this.title = formatDate(date, this.option('titleFormat'), options); this.start = this.visStart = cloneDate(date, true); this.end = this.visEnd = addDays(cloneDate(this.start), 1); this.renderGrid( 1, 1, this.option('columnFormat'), false ); } }, viewName); }; // rendering bugs var tdHeightBug; function Grid(element, options, methods, viewName) { var tm, firstDay, nwe, // no weekends (int) rtl, dis, dit, // day index sign / translate viewWidth, viewHeight, rowCnt, colCnt, colWidth, thead, tbody, cachedEvents=[], segmentContainer, dayContentPositions = new HorizontalPositionCache(function(dayOfWeek) { return tbody.find('td:eq(' + ((dayOfWeek - Math.max(firstDay,nwe)+colCnt) % colCnt) + ') div div'); }), // ... // initialize superclass view = $.extend(this, viewMethods, methods, { renderGrid: renderGrid, renderEvents: renderEvents, rerenderEvents: rerenderEvents, clearEvents: clearEvents, setHeight: setHeight, setWidth: setWidth, defaultEventEnd: function(event) { // calculates an end if event doesnt have one, mostly for resizing return cloneDate(event.start); } }); view.name = viewName; view.init(element, options); /* Grid Rendering -----------------------------------------------------------------------------*/ disableTextSelection(element.addClass('fc-grid')); function renderGrid(r, c, colFormat, showNumbers) { rowCnt = r; colCnt = c; // update option-derived variables tm = options.theme ? 'ui' : 'fc'; nwe = options.weekends ? 0 : 1; firstDay = options.firstDay; if (rtl = options.isRTL) { dis = -1; dit = colCnt - 1; }else{ dis = 1; dit = 0; } var month = view.start.getMonth(), today = clearTime(new Date()), s, i, j, d = cloneDate(view.visStart); if (!tbody) { // first time, build all cells from scratch var table = $("
").appendTo(element); s = ""; for (i=0; i" + formatDate(d, colFormat, options) + ""; addDays(d, 1); if (nwe) { skipWeekend(d); } } thead = $(s + "").appendTo(table); s = ""; d = cloneDate(view.visStart); for (i=0; i"; for (j=0; j1 && d.getMonth() != month ? ' fc-other-month' : '') + (+d == +today ? ' fc-today '+tm+'-state-highlight' : ' fc-not-today') + "'>" + (showNumbers ? "
" + d.getDate() + "
" : '') + "
 
"; addDays(d, 1); if (nwe) { skipWeekend(d); } } s += ""; } tbody = $(s + "
").appendTo(table); dayBind(tbody.find('td')); segmentContainer = $("
").appendTo(element); }else{ // NOT first time, reuse as many cells as possible clearEvents(); var prevRowCnt = tbody.find('tr').length; if (rowCnt < prevRowCnt) { tbody.find('tr:gt(' + (rowCnt-1) + ')').remove(); // remove extra rows } else if (rowCnt > prevRowCnt) { // needs to create new rows... s = ''; for (i=prevRowCnt; i"; for (j=0; j" + (showNumbers ? "
" : '') + "
 
" + ""; addDays(d, 1); if (nwe) { skipWeekend(d); } } s += ""; } tbody.append(s); } dayBind(tbody.find('td.fc-new').removeClass('fc-new')); // re-label and re-class existing cells d = cloneDate(view.visStart); tbody.find('td').each(function() { var td = $(this); if (rowCnt > 1) { if (d.getMonth() == month) { td.removeClass('fc-other-month'); }else{ td.addClass('fc-other-month'); } } if (+d == +today) { td.removeClass('fc-not-today') .addClass('fc-today') .addClass(tm + '-state-highlight'); }else{ td.addClass('fc-not-today') .removeClass('fc-today') .removeClass(tm + '-state-highlight'); } td.find('div.fc-day-number').text(d.getDate()); addDays(d, 1); if (nwe) { skipWeekend(d); } }); if (rowCnt == 1) { // more changes likely (week or day view) // redo column header text and class d = cloneDate(view.visStart); thead.find('th').each(function() { $(this).text(formatDate(d, colFormat, options)); this.className = this.className.replace(/^fc-\w+(?= )/, 'fc-' + dayIDs[d.getDay()]); addDays(d, 1); if (nwe) { skipWeekend(d); } }); // redo cell day-of-weeks d = cloneDate(view.visStart); tbody.find('td').each(function() { this.className = this.className.replace(/^fc-\w+(?= )/, 'fc-' + dayIDs[d.getDay()]); addDays(d, 1); if (nwe) { skipWeekend(d); } }); } } } function setHeight(height) { viewHeight = height; var leftTDs = tbody.find('tr td:first-child'), tbodyHeight = viewHeight - thead.height(), rowHeight1, rowHeight2; if (options.weekMode == 'variable') { rowHeight1 = rowHeight2 = Math.floor(tbodyHeight / (rowCnt==1 ? 2 : 6)); }else{ rowHeight1 = Math.floor(tbodyHeight / rowCnt); rowHeight2 = tbodyHeight - rowHeight1*(rowCnt-1); } if (tdHeightBug === undefined) { // bug in firefox where cell height includes padding var tr = tbody.find('tr:first'), td = tr.find('td:first'); td.height(rowHeight1); tdHeightBug = rowHeight1 != td.height(); } if (tdHeightBug) { leftTDs.slice(0, -1).height(rowHeight1); leftTDs.slice(-1).height(rowHeight2); }else{ setOuterHeight(leftTDs.slice(0, -1), rowHeight1); setOuterHeight(leftTDs.slice(-1), rowHeight2); } } function setWidth(width) { viewWidth = width; dayContentPositions.clear(); setOuterWidth( thead.find('th').slice(0, -1), colWidth = Math.floor(viewWidth / colCnt) ); } /* Event Rendering -----------------------------------------------------------------------------*/ function renderEvents(events) { view.reportEvents(cachedEvents = events); renderSegs(compileSegs(events)); } function rerenderEvents(modifiedEventId) { clearEvents(); renderSegs(compileSegs(cachedEvents), modifiedEventId); } function clearEvents() { view._clearEvents(); // only clears the hashes segmentContainer.empty(); } function compileSegs(events) { var d1 = cloneDate(view.visStart), d2 = addDays(cloneDate(d1), colCnt), visEventsEnds = $.map(events, exclEndDay), i, row, j, level, k, seg, segs=[]; for (i=0; i" + "" + (!event.allDay && seg.isStart ? "" + htmlEscape(formatDates(event.start, event.end, view.option('timeFormat'), options)) + "" :'') + "" + htmlEscape(event.title) + "" + "" + ((event.editable || event.editable === undefined && options.editable) && !options.disableResizing && $.fn.resizable ? "
" : '') + "
"; seg.left = left; seg.outerWidth = right - left; } segmentContainer[0].innerHTML = html; // faster than html() eventElements = segmentContainer.children(); // retrieve elements, run through eventRender callback, bind handlers for (i=0; i div') // optimal selector? .height(top + levelHeight); } // calculate row tops for (rowI=0; rowI" + "" + ""; for (i=0; i" + formatDate(d, colFormat, options) + ""; addDays(d, dis); if (nwe) { skipWeekend(d, dis); } } s += ""; if (options.allDaySlot) { s += "" + "" + "" + "" + ""; } s+= "
  
" + options.allDayText + "" + "
 
 
"; head = $(s).appendTo(element); dayBind(head.find('td')); // all-day event container daySegmentContainer = $("
").appendTo(head); // body d = zeroDate(); var maxd = addMinutes(cloneDate(d), maxMinute); addMinutes(d, minMinute); s = ""; for (i=0; d < maxd; i++) { minutes = d.getMinutes(); s += ""; addMinutes(d, options.slotMinutes); slotCnt++; } s += "
" + ((!slotNormal || !minutes) ? formatDate(d, options.axisFormat) : ' ') + "
 
"; body = $("
") .append(bodyContent = $("
") .append(bodyTable = $(s))) .appendTo(element); slotBind(body.find('td')); // slot event container slotSegmentContainer = $("
").appendTo(bodyContent); // background stripes d = cloneDate(d0); s = "
" + ""; for (i=0; i
 
"; addDays(d, dis); if (nwe) { skipWeekend(d, dis); } } s += "
"; bg = $(s).appendTo(element); }else{ // skeleton already built, just modify it clearEvents(); // redo column header text and class head.find('tr:first th').slice(1, -1).each(function() { $(this).text(formatDate(d, colFormat, options)); this.className = this.className.replace(/^fc-\w+(?= )/, 'fc-' + dayIDs[d.getDay()]); addDays(d, dis); if (nwe) { skipWeekend(d, dis); } }); // change classes of background stripes d = cloneDate(d0); bg.find('td').each(function() { this.className = this.className.replace(/^fc-\w+(?= )/, 'fc-' + dayIDs[d.getDay()]); if (+d == +today) { $(this) .removeClass('fc-not-today') .addClass('fc-today') .addClass(tm + '-state-highlight'); }else{ $(this) .addClass('fc-not-today') .removeClass('fc-today') .removeClass(tm + '-state-highlight'); } addDays(d, dis); if (nwe) { skipWeekend(d, dis); } }); } } function resetScroll() { var d0 = zeroDate(), scrollDate = cloneDate(d0); scrollDate.setHours(options.firstHour); var top = timePosition(d0, scrollDate) + 1, // +1 for the border scroll = function() { body.scrollTop(top); }; scroll(); setTimeout(scroll, 0); // overrides any previous scroll state made by the browser } function setHeight(height, dateChanged) { viewHeight = height; slotTopCache = {}; body.height(height - head.height()); slotHeight = body.find('tr:first div').height() + 1; bg.css({ top: head.find('tr').height(), height: height }); if (dateChanged) { resetScroll(); } } function setWidth(width) { viewWidth = width; colContentPositions.clear(); body.width(width); bodyTable.width(''); var topTDs = head.find('tr:first th'), stripeTDs = bg.find('td'), clientWidth = body[0].clientWidth; bodyTable.width(clientWidth); // time-axis width axisWidth = 0; setOuterWidth( head.find('tr:lt(2) th:first').add(body.find('tr:first th')) .width('') .each(function() { axisWidth = Math.max(axisWidth, $(this).outerWidth()); }), axisWidth ); // column width colWidth = Math.floor((clientWidth - axisWidth) / colCnt); setOuterWidth(stripeTDs.slice(0, -1), colWidth); setOuterWidth(topTDs.slice(1, -2), colWidth); setOuterWidth(topTDs.slice(-2, -1), clientWidth - axisWidth - colWidth*(colCnt-1)); bg.css({ left: axisWidth, width: clientWidth - axisWidth }); } /* Slot/Day clicking and binding -----------------------------------------------------------------------*/ function dayBind(tds) { tds.click(slotClick) .mousedown(daySelectionMousedown); } function slotBind(tds) { tds.click(slotClick) .mousedown(slotSelectionMousedown); } function slotClick(ev) { if (!view.option('selectable')) { // SelectionManager will worry about dayClick var col = Math.min(colCnt-1, Math.floor((ev.pageX - bg.offset().left) / colWidth)), date = addDays(cloneDate(view.visStart), col*dis+dit), rowMatch = this.className.match(/fc-slot(\d+)/); if (rowMatch) { var mins = parseInt(rowMatch[1]) * options.slotMinutes, hours = Math.floor(mins/60); date.setHours(hours); date.setMinutes(mins%60 + minMinute); view.trigger('dayClick', this, date, false, ev); }else{ view.trigger('dayClick', this, date, true, ev); } } } /* Event Rendering -----------------------------------------------------------------------------*/ function renderEvents(events, modifiedEventId) { view.reportEvents(cachedEvents = events); var i, len=events.length, dayEvents=[], slotEvents=[]; for (i=0; i" + "" + "" + "" + htmlEscape(formatDates(event.start, event.end, view.option('timeFormat'))) + "" + "" + htmlEscape(event.title) + "" + "" + ((event.editable || event.editable === undefined && options.editable) && !options.disableResizing && $.fn.resizable ? "
=
" : '') + "
"; } function daySegBind(event, eventElement, seg) { view.eventElementHandlers(event, eventElement); if (event.editable || event.editable === undefined && options.editable) { draggableDayEvent(event, eventElement, seg.isStart); if (seg.isEnd) { view.resizableDayEvent(event, eventElement, colWidth); } } } function slotSegBind(event, eventElement, seg) { view.eventElementHandlers(event, eventElement); if (event.editable || event.editable === undefined && options.editable) { var timeElement = eventElement.find('span.fc-event-time'); draggableSlotEvent(event, eventElement, timeElement); if (seg.isEnd) { resizableSlotEvent(event, eventElement, timeElement); } } } /* Event Dragging -----------------------------------------------------------------------------*/ // when event starts out FULL-DAY function draggableDayEvent(event, eventElement, isStart) { if (!options.disableDragging && eventElement.draggable) { var origWidth; var allDay=true; var dayDelta; eventElement.draggable({ zIndex: 9, opacity: view.option('dragOpacity', 'month'), // use whatever the month view was using revertDuration: options.dragRevertDuration, start: function(ev, ui) { view.trigger('eventDragStart', eventElement, event, ev, ui); view.hideEvents(event, eventElement); origWidth = eventElement.width(); hoverListener.start(function(cell, origCell, rowDelta, colDelta) { eventElement.draggable('option', 'revert', !cell || !rowDelta && !colDelta); clearOverlay(); if (cell) { dayDelta = colDelta * dis; if (!cell.row) { // on full-days renderDayOverlay( addDays(cloneDate(event.start), dayDelta), addDays(exclEndDay(event), dayDelta) ); resetElement(); }else{ // mouse is over bottom slots if (isStart && allDay) { // convert event to temporary slot-event setOuterHeight( eventElement.width(colWidth - 10), // don't use entire width slotHeight * Math.round( (event.end ? ((event.end - event.start) / MINUTE_MS) : options.defaultEventMinutes) / options.slotMinutes ) ); eventElement.draggable('option', 'grid', [colWidth, 1]); allDay = false; } } } }, ev, 'drag'); }, stop: function(ev, ui) { var cell = hoverListener.stop(); clearOverlay(); view.trigger('eventDragStop', eventElement, event, ev, ui); if (cell && (!allDay || dayDelta)) { // changed! eventElement.find('a').removeAttr('href'); // prevents safari from visiting the link var minuteDelta = 0; if (!allDay) { minuteDelta = Math.round((eventElement.offset().top - bodyContent.offset().top) / slotHeight) * options.slotMinutes + minMinute - (event.start.getHours() * 60 + event.start.getMinutes()); } view.eventDrop(this, event, dayDelta, minuteDelta, allDay, ev, ui); }else{ // hasn't moved or is out of bounds (draggable has already reverted) resetElement(); if ($.browser.msie) { eventElement.css('filter', ''); // clear IE opacity side-effects } view.showEvents(event, eventElement); } } }); function resetElement() { if (!allDay) { eventElement .width(origWidth) .height('') .draggable('option', 'grid', null); allDay = true; } } } } // when event starts out IN TIMESLOTS function draggableSlotEvent(event, eventElement, timeElement) { if (!options.disableDragging && eventElement.draggable) { var origPosition; var allDay=false; var dayDelta; var minuteDelta; var prevMinuteDelta; eventElement.draggable({ zIndex: 9, scroll: false, grid: [colWidth, slotHeight], axis: colCnt==1 ? 'y' : false, opacity: view.option('dragOpacity'), revertDuration: options.dragRevertDuration, start: function(ev, ui) { view.trigger('eventDragStart', eventElement, event, ev, ui); view.hideEvents(event, eventElement); if ($.browser.msie) { eventElement.find('span.fc-event-bg').hide(); // nested opacities mess up in IE, just hide } origPosition = eventElement.position(); minuteDelta = prevMinuteDelta = 0; hoverListener.start(function(cell, origCell, rowDelta, colDelta) { eventElement.draggable('option', 'revert', !cell); clearOverlay(); if (cell) { dayDelta = colDelta * dis; if (options.allDaySlot && !cell.row) { // over full days if (!allDay) { // convert to temporary all-day event allDay = true; timeElement.hide(); eventElement.draggable('option', 'grid', null); } renderDayOverlay( addDays(cloneDate(event.start), dayDelta), addDays(exclEndDay(event), dayDelta) ); }else{ // on slots resetElement(); } } }, ev, 'drag'); }, drag: function(ev, ui) { minuteDelta = Math.round((ui.position.top - origPosition.top) / slotHeight) * options.slotMinutes; if (minuteDelta != prevMinuteDelta) { if (!allDay) { updateTimeText(minuteDelta); } prevMinuteDelta = minuteDelta; } }, stop: function(ev, ui) { var cell = hoverListener.stop(); clearOverlay(); view.trigger('eventDragStop', eventElement, event, ev, ui); if (cell && (dayDelta || minuteDelta || allDay)) { // changed! view.eventDrop(this, event, dayDelta, allDay ? 0 : minuteDelta, allDay, ev, ui); }else{ // either no change or out-of-bounds (draggable has already reverted) resetElement(); eventElement.css(origPosition); // sometimes fast drags make event revert to wrong position updateTimeText(0); if ($.browser.msie) { eventElement .css('filter', '') // clear IE opacity side-effects .find('span.fc-event-bg') .css('display', ''); // .show() made display=inline } view.showEvents(event, eventElement); } } }); function updateTimeText(minuteDelta) { var newStart = addMinutes(cloneDate(event.start), minuteDelta); var newEnd; if (event.end) { newEnd = addMinutes(cloneDate(event.end), minuteDelta); } timeElement.text(formatDates(newStart, newEnd, view.option('timeFormat'))); } function resetElement() { // convert back to original slot-event if (allDay) { timeElement.css('display', ''); // show() was causing display=inline eventElement.draggable('option', 'grid', [colWidth, slotHeight]); allDay = false; } } } } /* Event Resizing -----------------------------------------------------------------------------*/ // for TIMESLOT events function resizableSlotEvent(event, eventElement, timeElement) { if (!options.disableResizing && eventElement.resizable) { var slotDelta, prevSlotDelta; eventElement.resizable({ handles: { s: 'div.ui-resizable-s' }, grid: slotHeight, start: function(ev, ui) { slotDelta = prevSlotDelta = 0; view.hideEvents(event, eventElement); if ($.browser.msie && $.browser.version == '6.0') { eventElement.css('overflow', 'hidden'); } eventElement.css('z-index', 9); view.trigger('eventResizeStart', this, event, ev, ui); }, resize: function(ev, ui) { // don't rely on ui.size.height, doesn't take grid into account slotDelta = Math.round((Math.max(slotHeight, eventElement.height()) - ui.originalSize.height) / slotHeight); if (slotDelta != prevSlotDelta) { timeElement.text( formatDates( event.start, (!slotDelta && !event.end) ? null : // no change, so don't display time range addMinutes(view.eventEnd(event), options.slotMinutes*slotDelta), view.option('timeFormat') ) ); prevSlotDelta = slotDelta; } }, stop: function(ev, ui) { view.trigger('eventResizeStop', this, event, ev, ui); if (slotDelta) { view.eventResize(this, event, 0, options.slotMinutes*slotDelta, ev, ui); }else{ eventElement.css('z-index', 8); view.showEvents(event, eventElement); // BUG: if event was really short, need to put title back in span } } }); } } /* Coordinate Utilities -----------------------------------------------------------------------------*/ var coordinateGrid = new CoordinateGrid(function(rows, cols) { var e, n, p; bg.find('td').each(function(i, _e) { e = $(_e); n = e.offset().left; if (i) { p[1] = n; } p = [n]; cols[i] = p; }); p[1] = n + e.outerWidth(); if (options.allDaySlot) { e = head.find('td'); n = e.offset().top; rows[0] = [n, n+e.outerHeight()]; } var bodyContentTop = bodyContent.offset().top; var bodyTop = body.offset().top; var bodyBottom = bodyTop + body.outerHeight(); function constrain(n) { return Math.max(bodyTop, Math.min(bodyBottom, n)); } for (var i=0; i= addMinutes(cloneDate(day), maxMinute)) { return bodyContent.height(); } var slotMinutes = options.slotMinutes, minutes = time.getHours()*60 + time.getMinutes() - minMinute, slotI = Math.floor(minutes / slotMinutes), slotTop = slotTopCache[slotI]; if (slotTop === undefined) { slotTop = slotTopCache[slotI] = body.find('tr:eq(' + slotI + ') td div')[0].offsetTop; } return Math.max(0, Math.round( slotTop - 1 + slotHeight * ((minutes % slotMinutes) / slotMinutes) )); } /* Selecting -----------------------------------------------------------------------------*/ var selected = false; var daySelectionMousedown = selection_dayMousedown( view, hoverListener, cellDate, cellIsAllDay, renderDayOverlay, clearOverlay, reportSelection, unselect ); function slotSelectionMousedown(ev) { if (view.option('selectable')) { unselect(ev); var _mousedownElement = this; var dates; hoverListener.start(function(cell, origCell) { clearSelection(); if (cell && cell.col == origCell.col && !cellIsAllDay(cell)) { var d1 = cellDate(origCell); var d2 = cellDate(cell); dates = [ d1, addMinutes(cloneDate(d1), options.slotMinutes), d2, addMinutes(cloneDate(d2), options.slotMinutes) ].sort(cmp); renderSlotSelection(dates[0], dates[3]); }else{ dates = null; } }, ev); $(document).one('mouseup', function(ev) { hoverListener.stop(); if (dates) { if (+dates[0] == +dates[1]) { view.trigger('dayClick', _mousedownElement, dates[0], false, ev); // BUG: _mousedownElement will sometimes be the overlay } reportSelection(dates[0], dates[3], false, ev); } }); } } view.select = function(startDate, endDate, allDay) { coordinateGrid.build(); unselect(); if (allDay) { if (options.allDaySlot) { if (!endDate) { endDate = cloneDate(startDate); } renderDayOverlay(startDate, addDays(cloneDate(endDate), 1)); } }else{ if (!endDate) { endDate = addMinutes(cloneDate(startDate), options.slotMinutes); } renderSlotSelection(startDate, endDate); } reportSelection(startDate, endDate, allDay); }; function reportSelection(startDate, endDate, allDay, ev) { selected = true; view.trigger('select', view, startDate, endDate, allDay, ev); } function unselect(ev) { if (selected) { clearSelection(); selected = false; view.trigger('unselect', view, ev); } } view.unselect = unselect; selection_unselectAuto(view, unselect); /* Selecting drawing utils -----------------------------------------------------------------------------*/ var selectionHelper; function renderSlotSelection(startDate, endDate) { var helperOption = view.option('selectHelper'); if (helperOption) { var col = dayDiff(startDate, view.visStart) * dis + dit; if (col >= 0 && col < colCnt) { // only works when times are on same day var rect = coordinateGrid.rect(0, col, 0, col, bodyContent); // only for horizontal coords var top = timePosition(startDate, startDate); var bottom = timePosition(startDate, endDate); if (bottom > top) { // protect against selections that are entirely before or after visible range rect.top = top; rect.height = bottom - top; rect.left += 2; rect.width -= 5; if ($.isFunction(helperOption)) { var helperRes = helperOption(startDate, endDate); if (helperRes) { rect.position = 'absolute'; rect.zIndex = 8; selectionHelper = $(helperRes) .css(rect) .appendTo(bodyContent); } }else{ selectionHelper = $(slotSegHtml( { title: '', start: startDate, end: endDate, className: [], editable: false }, rect, 'fc-event fc-event-vert fc-corner-top fc-corner-bottom ' )); if ($.browser.msie) { selectionHelper.find('span.fc-event-bg').hide(); // nested opacities mess up in IE, just hide } selectionHelper.css('opacity', view.option('dragOpacity')); } if (selectionHelper) { slotBind(selectionHelper); bodyContent.append(selectionHelper); setOuterWidth(selectionHelper, rect.width, true); // needs to be after appended setOuterHeight(selectionHelper, rect.height, true); } } } }else{ renderSlotOverlay(startDate, endDate); } } function clearSelection() { clearOverlay(); if (selectionHelper) { selectionHelper.remove(); selectionHelper = null; } } /* Semi-transparent Overlay Helpers -----------------------------------------------------*/ function renderDayOverlay(startDate, endDate) { var startCol, endCol; if (rtl) { startCol = dayDiff(endDate, view.visStart)*dis+dit+1; endCol = dayDiff(startDate, view.visStart)*dis+dit+1; }else{ startCol = dayDiff(startDate, view.visStart); endCol = dayDiff(endDate, view.visStart); } startCol = Math.max(0, startCol); endCol = Math.min(colCnt, endCol); if (startCol < endCol) { dayBind( _renderDayOverlay(0, startCol, 0, endCol-1) ); } } function _renderDayOverlay(col0, row0, col1, row1) { var rect = coordinateGrid.rect(col0, row0, col1, row1, head); return view.renderOverlay(rect, head); } function renderSlotOverlay(overlayStart, overlayEnd) { var dayStart = cloneDate(view.visStart); var dayEnd = addDays(cloneDate(dayStart), 1); for (var i=0; i= 0) { addMinutes(d, minMinute + slotIndex*options.slotMinutes); } return d; } function cellIsAllDay(cell) { return options.allDaySlot && !cell.row; } } // count the number of colliding, higher-level segments (for event squishing) function countForwardSegs(levels) { var i, j, k, level, segForward, segBack; for (i=levels.length-1; i>0; i--) { level = levels[i]; for (j=0; j"); } if (e[0].parentNode != parent[0]) { e.appendTo(parent); } this.usedOverlays.push(e.css(rect).show()); return e; }, clearOverlays: function() { var e; while (e = this.usedOverlays.shift()) { this.unusedOverlays.push(e.hide().unbind()); } }, // common horizontal event resizing resizableDayEvent: function(event, eventElement, colWidth) { var view = this; if (!view.options.disableResizing && eventElement.resizable) { eventElement.resizable({ handles: view.options.isRTL ? {w:'div.ui-resizable-w'} : {e:'div.ui-resizable-e'}, grid: colWidth, minWidth: colWidth/2, // need this or else IE throws errors when too small containment: view.element.parent().parent(), // the main element... // ... a fix. wouldn't allow extending to last column in agenda views (jq ui bug?) start: function(ev, ui) { eventElement.css('z-index', 9); view.hideEvents(event, eventElement); view.trigger('eventResizeStart', this, event, ev, ui); }, stop: function(ev, ui) { view.trigger('eventResizeStop', this, event, ev, ui); // ui.size.width wasn't working with grid correctly, use .width() var dayDelta = Math.round((eventElement.width() - ui.originalSize.width) / colWidth); if (dayDelta) { view.eventResize(this, event, dayDelta, 0, ev, ui); }else{ eventElement.css('z-index', 8); view.showEvents(event, eventElement); } } }); } }, // attaches eventClick, eventMouseover, eventMouseout eventElementHandlers: function(event, eventElement) { var view = this; eventElement .click(function(ev) { if (!eventElement.hasClass('ui-draggable-dragging') && !eventElement.hasClass('ui-resizable-resizing')) { return view.trigger('eventClick', this, event, ev); } }) .hover( function(ev) { view.trigger('eventMouseover', this, event, ev); }, function(ev) { view.trigger('eventMouseout', this, event, ev); } ); }, // get a property from the 'options' object, using smart view naming option: function(name, viewName) { var v = this.options[name]; if (typeof v == 'object') { return smartProperty(v, viewName || this.name); } return v; }, // event rendering utilities sliceSegs: function(events, visEventEnds, start, end) { var segs = [], i, len=events.length, event, eventStart, eventEnd, segStart, segEnd, isStart, isEnd; for (i=0; i start && eventStart < end) { if (eventStart < start) { segStart = cloneDate(start); isStart = false; }else{ segStart = eventStart; isStart = true; } if (eventEnd > end) { segEnd = cloneDate(end); isEnd = false; }else{ segEnd = eventEnd; isEnd = true; } segs.push({ event: event, start: segStart, end: segEnd, isStart: isStart, isEnd: isEnd, msLength: segEnd - segStart }); } } return segs.sort(segCmp); } }; function lazySegBind(container, segs, bindHandlers) { container.unbind('mouseover').mouseover(function(ev) { var parent=ev.target, e, i, seg; while (parent != this) { e = parent; parent = parent.parentNode; } if ((i = e._fci) !== undefined) { e._fci = undefined; seg = segs[i]; bindHandlers(seg.event, seg.element, seg); $(ev.target).trigger(ev); } ev.stopPropagation(); }); } // event rendering calculation utilities function stackSegs(segs) { var levels = [], i, len = segs.length, seg, j, collide, k; for (i=0; i seg2.start && seg1.start < seg2.end; } function selection_dayMousedown(view, hoverListener, cellDate, cellIsAllDay, renderSelection, clearSelection, reportSelection, unselect) { return function(ev) { if (view.option('selectable')) { unselect(ev); var _mousedownElement = this; var dates; hoverListener.start(function(cell, origCell) { clearSelection(); if (cell && cellIsAllDay(cell)) { dates = [ cellDate(origCell), cellDate(cell) ].sort(cmp); renderSelection(dates[0], addDays(cloneDate(dates[1]), 1), true); }else{ dates = null; } }, ev); $(document).one('mouseup', function(ev) { hoverListener.stop(); if (dates) { if (+dates[0] == +dates[1]) { view.trigger('dayClick', _mousedownElement, dates[0], true, ev); // BUG: _mousedownElement will sometimes be the overlay } reportSelection(dates[0], dates[1], true, ev); } }); } } } function selection_unselectAuto(view, unselect) { if (view.option('selectable') && view.option('unselectAuto')) { $(document).mousedown(function(ev) { var ignore = view.option('unselectCancel'); if (ignore) { if ($(ev.target).parents(ignore).length) { // could be optimized to stop after first match return; } } unselect(ev); }); } } /* Date Math -----------------------------------------------------------------------------*/ var DAY_MS = 86400000, HOUR_MS = 3600000, MINUTE_MS = 60000; function addYears(d, n, keepTime) { d.setFullYear(d.getFullYear() + n); if (!keepTime) { clearTime(d); } return d; } function addMonths(d, n, keepTime) { // prevents day overflow/underflow if (+d) { // prevent infinite looping on invalid dates var m = d.getMonth() + n, check = cloneDate(d); check.setDate(1); check.setMonth(m); d.setMonth(m); if (!keepTime) { clearTime(d); } while (d.getMonth() != check.getMonth()) { d.setDate(d.getDate() + (d < check ? 1 : -1)); } } return d; } function addDays(d, n, keepTime) { // deals with daylight savings if (+d) { var dd = d.getDate() + n, check = cloneDate(d); check.setHours(9); // set to middle of day check.setDate(dd); d.setDate(dd); if (!keepTime) { clearTime(d); } fixDate(d, check); } return d; } fc.addDays = addDays; function fixDate(d, check) { // force d to be on check's YMD, for daylight savings purposes if (+d) { // prevent infinite looping on invalid dates while (d.getDate() != check.getDate()) { d.setTime(+d + (d < check ? 1 : -1) * HOUR_MS); } } } function addMinutes(d, n) { d.setMinutes(d.getMinutes() + n); return d; } function clearTime(d) { d.setHours(0); d.setMinutes(0); d.setSeconds(0); d.setMilliseconds(0); return d; } function cloneDate(d, dontKeepTime) { if (dontKeepTime) { return clearTime(new Date(+d)); } return new Date(+d); } fc.cloneDate = cloneDate; function zeroDate() { // returns a Date with time 00:00:00 and dateOfMonth=1 var i=0, d; do { d = new Date(1970, i++, 1); } while (d.getHours()); // != 0 return d; } function skipWeekend(date, inc, excl) { inc = inc || 1; while (!date.getDay() || (excl && date.getDay()==1 || !excl && date.getDay()==6)) { addDays(date, inc); } return date; } function dayDiff(d1, d2) { // d1 - d2 return Math.round((cloneDate(d1, true) - cloneDate(d2, true)) / DAY_MS); } function setYMD(date, y, m, d) { if (y !== undefined && y != date.getFullYear()) { date.setDate(1); date.setMonth(0); date.setFullYear(y); } if (m !== undefined && m != date.getMonth()) { date.setDate(1); date.setMonth(m); } if (d !== undefined) { date.setDate(d); } } /* Date Parsing -----------------------------------------------------------------------------*/ var parseDate = fc.parseDate = function(s) { if (typeof s == 'object') { // already a Date object return s; } if (typeof s == 'number') { // a UNIX timestamp return new Date(s * 1000); } if (typeof s == 'string') { if (s.match(/^\d+$/)) { // a UNIX timestamp return new Date(parseInt(s) * 1000); } return parseISO8601(s, true) || (s ? new Date(s) : null); } // TODO: never return invalid dates (like from new Date()), return null instead return null; }; var parseISO8601 = fc.parseISO8601 = function(s, ignoreTimezone) { // derived from http://delete.me.uk/2005/03/iso8601.html // TODO: for a know glitch/feature, read tests/issue_206_parseDate_dst.html var m = s.match(/^([0-9]{4})(-([0-9]{2})(-([0-9]{2})([T ]([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?(Z|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?$/); if (!m) { return null; } var date = new Date(m[1], 0, 1), check = new Date(m[1], 0, 1, 9, 0), offset = 0; if (m[3]) { date.setMonth(m[3] - 1); check.setMonth(m[3] - 1); } if (m[5]) { date.setDate(m[5]); check.setDate(m[5]); } fixDate(date, check); if (m[7]) { date.setHours(m[7]); } if (m[8]) { date.setMinutes(m[8]); } if (m[10]) { date.setSeconds(m[10]); } if (m[12]) { date.setMilliseconds(Number("0." + m[12]) * 1000); } fixDate(date, check); if (!ignoreTimezone) { if (m[14]) { offset = Number(m[16]) * 60 + Number(m[17]); offset *= m[15] == '-' ? 1 : -1; } offset -= date.getTimezoneOffset(); } return new Date(+date + (offset * 60 * 1000)); }; var parseTime = fc.parseTime = function(s) { // returns minutes since start of day if (typeof s == 'number') { // an hour return s * 60; } if (typeof s == 'object') { // a Date object return s.getHours() * 60 + s.getMinutes(); } var m = s.match(/(\d+)(?::(\d+))?\s*(\w+)?/); if (m) { var h = parseInt(m[1]); if (m[3]) { h %= 12; if (m[3].toLowerCase().charAt(0) == 'p') { h += 12; } } return h * 60 + (m[2] ? parseInt(m[2]) : 0); } }; /* Date Formatting -----------------------------------------------------------------------------*/ var formatDate = fc.formatDate = function(date, format, options) { return formatDates(date, null, format, options); }; var formatDates = fc.formatDates = function(date1, date2, format, options) { options = options || defaults; var date = date1, otherDate = date2, i, len = format.length, c, i2, formatter, res = ''; for (i=0; ii; i2--) { if (formatter = dateFormatters[format.substring(i, i2)]) { if (date) { res += formatter(date, options); } i = i2 - 1; break; } } if (i2 == i) { if (date) { res += c; } } } } return res; }; var dateFormatters = { s : function(d) { return d.getSeconds() }, ss : function(d) { return zeroPad(d.getSeconds()) }, m : function(d) { return d.getMinutes() }, mm : function(d) { return zeroPad(d.getMinutes()) }, h : function(d) { return d.getHours() % 12 || 12 }, hh : function(d) { return zeroPad(d.getHours() % 12 || 12) }, H : function(d) { return d.getHours() }, HH : function(d) { return zeroPad(d.getHours()) }, d : function(d) { return d.getDate() }, dd : function(d) { return zeroPad(d.getDate()) }, ddd : function(d,o) { return o.dayNamesShort[d.getDay()] }, dddd: function(d,o) { return o.dayNames[d.getDay()] }, M : function(d) { return d.getMonth() + 1 }, MM : function(d) { return zeroPad(d.getMonth() + 1) }, MMM : function(d,o) { return o.monthNamesShort[d.getMonth()] }, MMMM: function(d,o) { return o.monthNames[d.getMonth()] }, yy : function(d) { return (d.getFullYear()+'').substring(2) }, yyyy: function(d) { return d.getFullYear() }, t : function(d) { return d.getHours() < 12 ? 'a' : 'p' }, tt : function(d) { return d.getHours() < 12 ? 'am' : 'pm' }, T : function(d) { return d.getHours() < 12 ? 'A' : 'P' }, TT : function(d) { return d.getHours() < 12 ? 'AM' : 'PM' }, u : function(d) { return formatDate(d, "yyyy-MM-dd'T'HH:mm:ss'Z'") }, S : function(d) { var date = d.getDate(); if (date > 10 && date < 20) { return 'th'; } return ['st', 'nd', 'rd'][date%10-1] || 'th'; } }; /* Element Dimensions -----------------------------------------------------------------------------*/ function setOuterWidth(element, width, includeMargins) { element.each(function(i, _element) { _element.style.width = width - hsides(_element, includeMargins) + 'px'; }); } function setOuterHeight(element, height, includeMargins) { element.each(function(i, _element) { _element.style.height = height - vsides(_element, includeMargins) + 'px'; }); } function hsides(_element, includeMargins) { return (parseFloat(jQuery.curCSS(_element, 'paddingLeft', true)) || 0) + (parseFloat(jQuery.curCSS(_element, 'paddingRight', true)) || 0) + (parseFloat(jQuery.curCSS(_element, 'borderLeftWidth', true)) || 0) + (parseFloat(jQuery.curCSS(_element, 'borderRightWidth', true)) || 0) + (includeMargins ? hmargins(_element) : 0); } function hmargins(_element) { return (parseFloat(jQuery.curCSS(_element, 'marginLeft', true)) || 0) + (parseFloat(jQuery.curCSS(_element, 'marginRight', true)) || 0); } function vsides(_element, includeMargins) { return (parseFloat(jQuery.curCSS(_element, 'paddingTop', true)) || 0) + (parseFloat(jQuery.curCSS(_element, 'paddingBottom', true)) || 0) + (parseFloat(jQuery.curCSS(_element, 'borderTopWidth', true)) || 0) + (parseFloat(jQuery.curCSS(_element, 'borderBottomWidth', true)) || 0) + (includeMargins ? vmargins(_element) : 0); } function vmargins(_element) { return (parseFloat(jQuery.curCSS(_element, 'marginTop', true)) || 0) + (parseFloat(jQuery.curCSS(_element, 'marginBottom', true)) || 0); } function setMinHeight(element, h) { h = typeof h == 'number' ? h + 'px' : h; element[0].style.cssText += ';min-height:' + h + ';_height:' + h; } /* Position Calculation -----------------------------------------------------------------------------*/ // nasty bugs in opera 9.25 // position()'s top returning incorrectly with TR/TD or elements within TD var topBug; function topCorrect(tr) { // tr/th/td or anything else if (topBug !== false) { var cell; if (tr.is('th,td')) { tr = (cell = tr).parent(); } if (topBug === undefined && tr.is('tr')) { topBug = tr.position().top != tr.children().position().top; } if (topBug) { return tr.parent().position().top + (cell ? tr.position().top - cell.position().top : 0); } } return 0; } /* Coordinate Grid -----------------------------------------------------------------------------*/ function CoordinateGrid(buildFunc) { var t = this; var rows; var cols; t.build = function() { rows = []; cols = []; buildFunc(rows, cols); }; t.cell = function(x, y) { var rowCnt = rows.length; var colCnt = cols.length; var i, r=-1, c=-1; for (i=0; i= rows[i][0] && y < rows[i][1]) { r = i; break; } } for (i=0; i= cols[i][0] && x < cols[i][1]) { c = i; break; } } return (r>=0 && c>=0) ? { row:r, col:c } : null; }; t.rect = function(row0, col0, row1, col1, originElement) { // row1,col1 is inclusive var origin = originElement.offset(); return { top: rows[row0][0] - origin.top, left: cols[col0][0] - origin.left, width: cols[col1][1] - cols[col0][0], height: rows[row1][1] - rows[row0][0] }; }; } /* Hover Listener -----------------------------------------------------------------------------*/ function HoverListener(coordinateGrid) { var t = this; var bindType; var change; var firstCell; var cell; t.start = function(_change, ev, _bindType) { change = _change; firstCell = cell = null; coordinateGrid.build(); mouse(ev); bindType = _bindType || 'mousemove'; $(document).bind(bindType, mouse); }; function mouse(ev) { var newCell = coordinateGrid.cell(ev.pageX, ev.pageY); if (!newCell != !cell || newCell && (newCell.row != cell.row || newCell.col != cell.col)) { if (newCell) { if (!firstCell) { firstCell = newCell; } change(newCell, firstCell, newCell.row-firstCell.row, newCell.col-firstCell.col); }else{ change(newCell, firstCell); } cell = newCell; } } t.stop = function() { $(document).unbind(bindType, mouse); return cell; }; } /* Misc Utils -----------------------------------------------------------------------------*/ var dayIDs = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']; function zeroPad(n) { return (n < 10 ? '0' : '') + n; } function smartProperty(obj, name) { // get a camel-cased/namespaced property of an object if (obj[name] !== undefined) { return obj[name]; } var parts = name.split(/(?=[A-Z])/), i=parts.length-1, res; for (; i>=0; i--) { res = obj[parts[i].toLowerCase()]; if (res !== undefined) { return res; } } return obj['']; } function htmlEscape(s) { return s.replace(/&/g, '&') .replace(//g, '>') .replace(/'/g, ''') .replace(/"/g, '"') .replace(/\n/g, '
'); } function HorizontalPositionCache(getElement) { var t = this, elements = {}, lefts = {}, rights = {}; function e(i) { return elements[i] = elements[i] || getElement(i); } t.left = function(i) { return lefts[i] = lefts[i] === undefined ? e(i).position().left : lefts[i]; }; t.right = function(i) { return rights[i] = rights[i] === undefined ? t.left(i) + e(i).width() : rights[i]; }; t.clear = function() { elements = {}; lefts = {}; rights = {}; }; } function cssKey(_element) { return _element.id + '/' + _element.className + '/' + _element.style.cssText.replace(/(^|;)\s*(top|left|width|height)\s*:[^;]*/ig, ''); } function cmp(a, b) { return a - b; } function exclEndDay(event) { if (event.end) { return _exclEndDay(event.end, event.allDay); }else{ return addDays(cloneDate(event.start), 1); } } function _exclEndDay(end, allDay) { end = cloneDate(end); return allDay || end.getHours() || end.getMinutes() ? addDays(end, 1) : clearTime(end); } function disableTextSelection(element) { element .attr('unselectable', 'on') .css('MozUserSelect', 'none') .bind('selectstart.ui', function() { return false; }); } /* function enableTextSelection(element) { element .attr('unselectable', 'off') .css('MozUserSelect', '') .unbind('selectstart.ui'); } */ })(jQuery);