/**
* jQuery calendar plug-in 1.1.1
* Copyright 2017, Custom D
* Licensed under the MIT license.
* https://customd.github.io/jquery-calendar/LICENSE.txt
*
* @author Sam Sehnert | sam@customd.com
* @docs https://customd.github.io/jquery-calendar
*
* Implement an extremely flexible calendar interface with minimal up front development.
*/
(function($){
"use strict";
// The name of your plugin. This is used to namespace your
// plugin methods, object data, and registerd events.
var plugin_name = 'cal';
var plugin_version = '1.1.1';
var const_month = 'month';
var const_week = 'week';
// Set up the plugin defaults.
// These will be stored in $this.data(plugin_name).settings,
// and can be overwritten by having 'options' passed through
// as the parameter to the init method.
var defaults = {
// Start date and days to display.
startdate : null, // Defaults to new Date() if none of start month/year/monthstodisplay are defined.
daystodisplay : null, // Defaults to 7 if none of start month/year/monthstodisplay are defined.
// Start month,year and months to display.
startweek : null, // CUSTOM
startmonth : null, // Defaults to (new Date()).getMonth()+1 if monthstodisplay or startyear are defined (we use 1-12, not 0-11).
startyear : null, // Defaults to (new Date()).getFullYear() if monthstodisplay or startmonth are defined.
monthstodisplay : null, // Defaults to 1 if either of startmonth or startyear are defined. TODO: Support more than one month???
// Default colors
defaultcolor : '#255BA1',
invalidcolor : '#888888',
// Date Masks
maskmonthlabel : 'l',
maskeventlabel : 'g:i A',
maskeventlabeldelimiter : '', // –
maskeventlabelend : '', // g:i A
maskdatelabel : 'D, jS',
masktimelabel : {
'00' : 'g:i <\\sp\\a\\n>A<\/\\sp\\a\\n>',
'noon' : '\\N\\O\\O\\N'
},
// Either false, or an array of resources.
resources : false,
// Default height and widths.
minwidth : 130,
minheight : null,
overlapoffset : 15,
// Start and end times for the days
daytimestart : '00:00:00',
daytimeend : '24:00:00',
// Which day the week starts on 1-7, 1 being Sunday, 7 being Saturday.
weekstart : 1,
// Other options...
dragincrement : '15 mins',
gridincrement : '15 mins',
creationsize : '15 mins',
// Global appointment permissions
allowcreation : 'both', // Options, 'both', 'click', 'drag', 'none', false.
allowmove : false, // Enable or disable appointment dragging/moving.
allowresize : false, // Enable or disable appointment resizing.
allowselect : false, // Enable or disable appointment selection.
allowremove : false, // Enable or disable appointment deletion.
allowoverlap : false, // Enable or disable appointment overlaps.
allownotesedit : false, // Enable or disable inline notes editing.
allowhtml : false, // Whether or not to allow users to embed html to appointment data
// Easing effects
easing : {
eventupdate : 'linear',
eventremove : 'linear',
eventeditin : 'linear',
eventeditout : 'linear',
datechange : 'linear'
},
// appointment events.
eventcreate : $.noop,
eventnotesedit : $.noop,
eventremove : $.noop,
eventselect : $.noop,
eventmove : $.noop,
eventresize : $.noop,
eventdraw : $.noop,
// day events
dayclick : $.noop,
daydblclick : $.noop,
// Other events.
onload : $.noop
};
var _private = {
/**
* Attached to several elements to prevent default actions.
*
* @param object e : An object representing the event that triggered this method.
*
* @return void
*/
prevent : function(e){ e.preventDefault(); },
/**
* Get the scrollbar width so we know how much space to allocate.
*
* @return int : Returns the width of the scrollbars in pixels.
*/
scrollbarSize : function() {
// Use the cached version if exists.
if (!_private._scrollbarSize) {
var $doc = $(document.body),
// Set the overflow to hidden, scroll and measure difference.
w =$doc.css({overflow:'hidden'}).width();
w-=$doc.css({overflow:'scroll'}).width();
// Add support for IE in Standards mode.
if(!w) w=$doc.width()-$doc[0].clientWidth;
// Restore the overflow setting.
$doc.css({overflow:''});
// Cache the scrollbar width.
_private._scrollbarSize = w;
}
// Return the width.
return _private._scrollbarSize;
},
// Cache the scrollbar size here.
_scrollbarSize : false,
/**
* Called with a jquery collection of textareas and/or input boxes as 'this'.
* Allows setting a selecting text easily cross browser.
*
* @param int start : The start index in the string that we should select from.
* @param int end : The end index in the string that we should select to.
*
* @return object : Returns the jquery collection that was passed as 'this'.
*/
selectrange : function(start, end) {
return this.each(function() {
if(this.setSelectionRange) {
this.focus();
this.setSelectionRange(start, end);
} else if(this.createTextRange) {
var range = this.createTextRange();
range.collapse(true);
range.moveEnd('character', end);
range.moveStart('character', start);
range.select();
}
});
},
/**
* Called when the scroll container elment is scrolled. Attached to the onscroll event.
*
* @param object e : An object representing the event that triggered this method.
*
* @return void
*/
onscroll : function(e){
// Called on scroll on the container element.
// Init the variables we'll need.
var $this = $(this),
$parent = $this.parent('.ui-'+plugin_name+'-container'),
scrollX = $this.scrollLeft(),
scrollY = $this.scrollTop(),
data = $parent.data(plugin_name);
if( data ){
// Position the date and timeline at the correct scroll position.
$parent.find('.ui-'+plugin_name+'-timeline').scrollTop(scrollY);
$parent.find('.ui-'+plugin_name+'-dateline').scrollLeft(scrollX);
$parent.find('.ui-'+plugin_name+'-resourceline').scrollLeft(scrollX);
}
},
/**
* Check to see if a number or date is between start and end.
*
* @param mixed item : The item we want to compare with start and end.
* @param mixed start : The beginning of the range we want to check item against.
* @param mixed end : The end of the range we want to check item against.
*
* @return bool : Returns true if item falls between the range start - end.
*/
between : function( item, start, end ){
// If we're dealing with a date.
if( item instanceof Date ){
var timestamp = item.getTime();
return ( timestamp >= start.getTime() && timestamp <= end.getTime() );
// If we're dealing with a number.
} else if( !isNaN( Number( item ) ) ){
return ( item >= start && item <= end );
}
return false;
},
/**
* Check to see if a date range overlaps any part of a second range.
*
* @param date partStart :
* @param date partEnd :
* @param date inStart :
* @param date inEnd :
*
* @return bool : in range, or not.
*/
inrange : function( partStart, partEnd, inStart, inEnd )
{
return !( partEnd.getTime() < inStart.getTime() || partStart.getTime() > inEnd.getTime() );
},
/**
* Called to get a resource at a given index from the settings object.
*
* @param int index : A number representing the index position of the resouce data we want.
* @param object data : Am object containing the plugin data (which we'll use to get the resources).
*
* @return object : The resource object at the given index.
*/
resource : function( index, data ){
var iterator = 0;
if( data.settings.resources !== false ){
for( var i in data.settings.resources ){
if( data.settings.resources.hasOwnProperty( i ) ){
if( iterator == index ){
return { 'id' : i, 'name' : data.settings.resources[i] };
}
iterator++;
}
}
}
return { 'id' : null, 'name' : null };
},
/**
* Called to get the index of a given resource item in the settings object.
*
* @param mixed id : A string or number type which represents the resource key of the resource index we want returned.
* @param object data : An object containing the plugin data (which we'll use to get the resources).
*
* @return int : Returns the index position of the passed resource in the settings object.
*/
resourceIndex : function( id, data ){
var index = 0;
if( data.settings.resources !== false ){
// If we've been given a straight array of keys, then
// we only check against the value.
if( $.isArray( data.settings.resources ) ){
for( var i in data.settings.resources ){
if( data.settings.resources[i] == id ){
return index;
}
index++;
}
} else {
for( var i in data.settings.resources ){
if( data.settings.resources.hasOwnProperty( i ) ){
if( i == id ){
return index;
}
index++;
}
}
}
}
return false;
},
/**
* Get dates for given repetition rules, limited by begin and end dates.
*
* TODO: This is incomplete, and will only handle basic 'daily', 'weekly', 'monthly', 'yearly'
* repetitions, 'until' dates, and repetition intervals, e.g., every 2 weeks, every 3 months, etc.
* More advanced repetitions, like every week on tuesdays and thursdays.
*
* @param object data : The calendar data object.
* @param object event : The event we're generating repetitions for.
*
* @return array : An array of dates which the repetitions fall on. Only the dates, not the times.
*/
repetitions : function( data, event )
{
// Set up variables used in rule_loop below.
var repeat = [], rule, from, until, interval, _increment_by, _date, _possible_increments;
//
// TODO: Extend this method to parse out 'dates' object, and add those to the repeat rules.
// This also includes adding exception dates, and we'll need to do a similar thing to
// support exception rules.
//
// Loop over each repetition rule.
rule_loop : for( var i in event.repeat.rules.include )
{
// Shortcut variables.
rule = event.repeat.rules.include[i];
from = event.begins > data.settings.startdate ? event.begins : data.settings.startdate ; // TODO: set this to the 'next' repetition date for this range.
until = 'until' in rule ? $[plugin_name].date(rule.until) : data.cache.enddate ,
interval = 'interval' in rule ? +rule.interval : 1 ;
//
// Skip rule parsing where we don't need to do it...
//
if(
// If the event begins after the end date.
( event.begins > data.cache.enddate ) ||
// If we've got an 'until' date, and it's before the start date.
( until <= data.settings.startdate )
) continue;
// Get the (required) frequency rule.
if( 'freq' in rule )
{
// Check which rule we've got.
switch( rule.freq )
{
case 'daily' : _increment_by = interval+' Day'; break;
case 'weekly' : _increment_by = interval+' Week'; break;
case 'monthly': _increment_by = interval+' Month'; break;
case 'yearly' : _increment_by = interval+' Year'; break;
// We can't handle any other frequency types at present, so if this is
// the case, break out and continue to the next iteration of rule_loop.
default: continue rule_loop;
}
// Copy the begins date.
_date = event.begins.copy();
// We need to make sure the begins date is the first in the iteration
// of repetitions to generate. This means bringing it forward by the difference
// between the event.begins date, and the data.settings.startdate
if( event.begins < data.settings.startdate )
{
// Increment the date so we've got the first repetition position.
_date = _date.incrementBy( _increment_by, Math.ceil( event.begins.getIncrementBetween( data.settings.startdate, _increment_by ) ) );
}
else
{
// Move to the first repetition position (i.e., NOT the actual event itself).
_date = _date.incrementBy( _increment_by );
}
// Get the number of increments we can fit in current display range.
_possible_increments = Math.ceil( _date.getIncrementBetween( data.cache.enddate, _increment_by ) );
// Loop over and create the basic array of repetitions.
// This won't work for irregular repetitions, like every Tuesday & Thursday.
for( var i=0; i<_possible_increments; i++ )
{
repeat[_date.getTime()] = _date;
_date = _date.incrementBy( _increment_by );
}
}
}
return repeat;
},
/**
* Parse event overlaps for the given event.
*
* @param date begin : The beginning of the range that we want to check for overlaps.
* @param date end : The end of the range that we want to check for overlaps.
* @param object resource : A resource id / label object if we want to check for overlaps on resources too.
*
* @return void;
*/
overlaps : function( begin, end, resource ){
// Get variables that we'll use later.
var $this = $(this),
data = $this.data(plugin_name),
check = [];
// If the calendar has been implemented on this object.
if( data ){
// Store shortcut to events array.
var events = data.cache.events;
// Loop through the cached event data.
for( var uid in events ){
if( // Part of the event falls into the date range that we're checking.
events.hasOwnProperty(uid) &&
(
events[uid].begins < end &&
events[uid].ends > begin &&
events[uid].resource === resource
)
){
// Initialise the overlap object.
events[uid].overlap = {
partial : true,
inset : 0,
zindex : 0,
count : 0,
index : 0,
items : {},
uid : uid
};
check.push(events[uid]);
}
}
// We only need to check if there is more than one appointment in this time span.
if( check.length > 1 ){
var index = 0;
// Sort by start date.
check.sort(function(a,b){ return a.begins.getTime()-b.begins.getTime(); });
// Loop through each of the events that in the date range,
// and build up the overlap settings.
for( var uid1 in check ){
// Make sure this property exists on the object (not a prototyped property).
if( check.hasOwnProperty(uid1) ){
// Increment the index.
check[uid1].overlap.index = index++;
// Loop through each of the events and compare.
for( var uid2 in check ){
// Skip this... we don't need to compare the same object.
if( uid1 === uid2 ) continue;
if( // These object overlap AND they haven't already been flagged as overlapping.
check.hasOwnProperty(uid2) &&
!( check[uid1].overlap.uid in check[uid2].overlap.items ) &&
!( check[uid2].overlap.uid in check[uid1].overlap.items ) &&
(
check[uid1].begins < check[uid2].ends &&
check[uid1].ends > check[uid2].begins &&
check[uid1].resource === check[uid2].resource
)
){
// Store a reference to the overlapped object.
check[uid1].overlap.items[check[uid2].overlap.uid] = check[uid2];
check[uid2].overlap.items[check[uid1].overlap.uid] = check[uid1];
check[uid1].overlap.count++;
check[uid2].overlap.count++;
if( // The begin times are exactly the same...
check[uid1].begins.getTime() == check[uid2].begins.getTime()
){
// Set these up as non-partial overlaps.
check[uid1].overlap.partial = false;
check[uid2].overlap.partial = false;
// Set the new inset for non-partial overlaps.
check[uid2].overlap.inset = check[uid1].overlap.inset+1;
check[uid2].overlap.zindex = check[uid1].overlap.zindex+1;
} else if( // The begins time is less than the ends time.
check[uid1].begins.getTime() < check[uid2].begins.getTime()
){
// Increment the inset if this is a partial overlap.
if( check[uid1].overlap.partial ) check[uid2].overlap.inset++;
check[uid2].overlap.zindex = check[uid1].overlap.zindex+1;
} else {
// Increment the first appointments inset if this is a partial overlap.
if( check[uid2].overlap.partial ) check[uid1].overlap.inset++;
check[uid1].overlap.zindex = check[uid2].overlap.zindex+1;
}
// Update the cache.
data.cache.events[check[uid1].overlap.uid] = check[uid1];
data.cache.events[check[uid2].overlap.uid] = check[uid2];
}
}
}
}
// Update each of the overlap items data.
for( var uid in check ) check[uid].elems.data(plugin_name,check[uid]);
}
// Update the plugin data.
$this.data(plugin_name,data);
}
},
// Error objects used by the calendar
errors : {
eventParse : function( message, event ){
// Create a new error object
var error = new Error( message );
error.type = 'EventParse';
error.event = event;
// Return the error object (usually to throw).
return error;
},
icsParse : function( message, line, value ){
// Create a new error object
var error = new Error( message );
error.type = 'ICSParse';
error.line = line;
error.value = value;
// Return the error object (usually to throw).
return error;
}
},
// Holds the event parser methods.
parse : {
// Patterns for parsing ICS files.
_icalendar : {
// Folded lines: start with a whitespace character */
folds : /^\s(.*)$/,
// Individual entry: name:value */
entry : /^([A-Za-z0-9-]+)((?:;[A-Za-z0-9-]+=(?:"[^"]+"|[^";:,]+)(?:,(?:"[^"]+"|[^";:,]+))*)*):(.*)$/,
// Individual parameter: name=value[,value] */
param : /;([A-Za-z0-9-]+)=((?:"[^"]+"|[^";:,]+)(?:,(?:"[^"]+"|[^";:,]+))*)/g,
// Individual parameter value: value | "value" */
value : /,?("[^"]+"|[^";:,]+)/g,
// Date only field: yyyymmdd */
date : /^(\d{4})(\d\d)(\d\d)$/,
// Date/time field: yyyymmddThhmmss[Z] */
time : /^(\d{4})(\d\d)(\d\d)T(\d\d)(\d\d)(\d\d)(Z?)$/,
// Date/time range field: yyyymmddThhmmss[Z]/yyyymmddThhmmss[Z] */
range : /^(\d{4})(\d\d)(\d\d)T(\d\d)(\d\d)(\d\d)(Z?)\/(\d{4})(\d\d)(\d\d)T(\d\d)(\d\d)(\d\d)(Z?)$/,
// Timezone offset field: +hhmm */
offset : /^([+-])(\d\d)(\d\d)$/,
// Duration: [+-]PnnW or [+-]PnnDTnnHnnMnnS */
duration : /^([+-])?P(\d+W)?(\d+D)?(T)?(\d+H)?(\d+M)?(\d+S)?$/,
/**
* Parses iCalendar (RFC 2445) RRULE property.
*
* @param string rule : The rule to parse.
*
* @return object : An object describing the repetition rules.
*/
_rrule : function( rule )
{
var parts = rule.split(';'),
props,
rules = {};
// Loop over each rule set.
for( var i in parts )
{
props = parts[i].split('=');
rules[props[0].toLowerCase()] = props[1].toLowerCase();
}
// Return this rule set.
return rules;
}
},
/**
* Parses iCalendar (RFC 2445) into a javascript object.
* @docs http://www.ietf.org/rfc/rfc2445.txt
*
* @param string ics : An iCalendar formatted string.
*
* @return object : Returns a javascript object representing the passed iCalendar format.
*/
icalendar : function( ics )
{
var err = _private.errors.icsParse,
parse = _private.parse._icalendar,
parsing = 'cal_begin',
calendars = [],
lines = ics.replace(/\r\n/g,'\n').split('\n'),
event = null,
calendar = null,
// Event prototype.
_event = {
repeat : {
rules : {
include : [],
exclude : []
},
dates : {
include : [],
exclude : []
}
}
};
for( var i=lines.length-1; i>0; i-- ){
var matches = parse.folds.exec(lines[i]);
if( matches ){
lines[i-1] += matches[1];
lines[i] = '';
}
}
$.each(lines,function(i,line){
// Skip blank lines.
if( !line ) return;
switch( parsing ){
//case 'done' : If we decide to support more than one calendar in the same ics.
case 'cal_begin' :
// Check for calendar begin.
if( line.indexOf('BEGIN:VCALENDAR')==-1 ) throw new err( 'Expecting BEGIN:VCALENDAR but found \''+line+'\' instead.', i, line );
// Initialise the calendar object.
calendar = {events:[]};
parsing = 'cal_info';
break;
case 'cal_info' :
// Check for a change in parsing mode.
if( line.indexOf('BEGIN:VEVENT')==0 ){ event = $.extend(true,{},_event); parsing = 'cal_event' };
if( line.indexOf('END:VCALENDAR')==0 ){ calendars.push(calendar); parsing = 'done' };
// If parsing mode has changed, continue with next line.
if( parsing !== 'cal_info' ) return;
break;
case 'cal_event' :
// Check for a change in parsing mode.
if( line.indexOf('END:VEVENT')==0 ){ calendar.events.push(event); parsing = 'cal_info' };
// If parsing mode has changed, continue with next line.
if( parsing !== 'cal_event' ) return;
// Match an entry line.
var matches = parse.entry.exec(line);
if (!matches) {
throw new err( 'Missing entry name.', i, line );
}
// Parse the different date values.
switch( matches[1].toLowerCase() ){
//
// Standard / Basic values.
//
case 'uid' : event.uid = matches[3]; break;
case 'dtstart' : event.begins = $[plugin_name].date(matches[3]); break;
case 'dtend' : event.ends = $[plugin_name].date(matches[3]); break;
case 'summary' : event.title = matches[3].replace(/\\([;,])/g,'$1').replace(/\\n/g,'\n'); break;
case 'description' : event.notes = matches[3].replace(/\\([;,])/g,'$1').replace(/\\n/g,'\n'); break;
//
// Repetition rules.
//
case 'rrule' : event.repeat.rules.include.push(parse._rrule(matches[3])); break;
case 'exrule' : event.repeat.rules.exclude.push(parse._rrule(matches[3])); break;
//
// Repetition dates.
//
case 'rdate' : event.repeat.dates.include.push($[plugin_name].date(matches[3])); break;
case 'exdate' : event.repeat.dates.exclude.push($[plugin_name].date(matches[3])); break;
}
break;
}
});
// Throw an error if we didn't find group end.
if( parsing !== 'done' ) throw new err( 'Unexpected end of file. Expecting END:VCALENDAR.', lines.length, '' );
// Return the parsed calendars.
return calendars.length > 0 ? calendars.pop() : false;
}
},
// Pseudo events used by the calendar.
event : {
/**
* Returns the number of elements required to draw the event.
*
* @param obj values : Should be a fully validated values object, with minimum of begins, ends timestamps.
*
* @return int : The number of elements that should be drawn (the number of days that this element spans, visually).
*/
calculateElementCount : function( values ){
// Return the number of elements that should be drawn for this object.
return Math.ceil( values.cache.begins.getDaysBetween( values.cache.ends, true ) );
},
/**
* Positions an event object on the screen according to its data object.
*
* @param mixed speed : (opt) If int is number of milleseconds for animation, or string 'fast', 'slow'. If undefined, no animation.
* @param string ease : (opt) The easing method to use. See jQuery easing documentation for details.
*
* @return void
*/
update : function( bData, speed, ease ){
// Clone the event element, and set up the values.
var $event = $(this),
values = $event.data(plugin_name),
data = values && values.calendar ? values.calendar.data(plugin_name) : false ;
// Make sure we've got values.
if( data && values ){
// Get each of the event elements.
var $events = values.elems;
// Set the new values.
if( 'begins' in bData ) values.begins = $[plugin_name].date( bData.begins );
if( 'ends' in bData ) values.ends = $[plugin_name].date( bData.ends );
if( 'color' in bData ) values.colors = bData.color ? $[plugin_name].colors.generate( bData.color ) : data.settings.defaultcolor;
if( 'title' in bData ) values.title = bData.title || null;
if( 'notes' in bData ) values.notes = bData.notes || '';
// Exit if there's no need to draw anything.
if( values.ends < data.settings.startdate || values.begins > data.cache.enddate ) return false;
// Work out the cached end date.
values.cache.ends = ( data.cache.enddate < values.ends ? data.cache.enddate.addSeconds(-1) : values.ends );
values.cache.begins = ( data.settings.startdate > values.begins ? data.settings.startdate.copy() : values.begins );
var content_setter = data.settings.allowhtml ? 'html' : 'text' ;
// Set the new value into the event data.
$events.find('pre.details')[content_setter]( values.notes );
$events.find('p.title')[content_setter](
values.title || ( values.begins.format(data.settings.maskeventlabel) +
(
data.settings.maskeventlabelend !== '' ? data.settings.maskeventlabeldelimiter + values.ends.format( data.settings.maskeventlabelend ) : ''
)
)
);
// Save the new values to the element.
$events.data(plugin_name,values);
data.cache.events[values.uid] = values;
values.calendar.data(plugin_name,data);
// Call the positioning code.
_private.draw[data.type].position.apply($events,[speed,ease]);
return true;
}
return false;
},
/**
* Creates an inline edit area for an appointment's description.
*
* @param object e : An object representing the event that triggered this method.
*
* @return void
*/
edit : function( e ){
var $event = e && !$(this).is('div.ui-'+plugin_name+'-event') ? $(this).parents('div.ui-'+plugin_name+'-event') : $(this),
values = $event.data(plugin_name),
data = values && values.calendar ? values.calendar.data(plugin_name) : false ;
if( data && values ){
// Exit now if we don't allow selection.
if( !data.settings.allownotesedit ) return;
// Set the notes base height.
var $notes = $event.find('pre.details'),
noteHeight = $notes.height();
var content_setter = data.settings.allowhtml ? 'html' : 'text' ;
// Now append the textarea.
var $textarea = $('')[content_setter](values.notes||'').css({
boxShadow : 'inset 0px 0px 6px '+values.colors.mainShadow
}).on('mousedown.'+plugin_name,function(e){e.stopPropagation()}).appendTo($event);
if( $event.height() <= 30 ){
// Work out the margin to use.
var marginSide = $event.height() <= 15 ? 4 : 1 ,
marginTop = $event.height() <= 15 ? -1 : 0 ;
// Unbind the double click handler while we're showing the dropdown.
// Also, stop (and complete) the animations on the parent, otherwise
// it can cause some weird animation issues.
$event.unbind('dblclick.'+plugin_name).stop(true,true);
$textarea.css({
marginTop : marginTop,
left : marginSide,
right : marginSide,
height : noteHeight,
border : '1px solid '+values.colors.mainSelected,
borderTop : 'none',
borderTopLeftRadius : 0,
borderTopRightRadius : 0,
opacity : 0,
overflow : 'hidden',
zIndex : 1
}).animate({ height: 45, opacity: 1 },'fast',data.settings.easing.eventeditin,function(){
// Make sure the body is scrollable.
$(this).css('overflow', 'scroll');
// Detatch the PRE element from the DOM.
$notes.detach();
// Trigger the selection now that we've done our animation.
_private.selectrange.apply($textarea,[values.notes.length||0,values.notes.length||0]);
});
} else {
// Detatch the PRE element from the DOM.
$notes.detach();
// Trigger the text selection
_private.selectrange.apply($textarea,[values.notes.length||0,values.notes.length||0]);
}
// Add the blur handler which will set the value when done.
$textarea.blur(function(){
// Store if we've changed the notes or not.
var hasChanged = values.notes != $(this).val(),
$events = values.elems;
// Get the new value, and re-apply it to the event.
values.notes = $(this).val();
// Add the tittle.
$events.attr('title',values.notes||'')
if( $event.height() <= 30 ){
$(this).css('overflow', 'hidden');
$(this).animate({ height: noteHeight, opacity: 0 },125,data.settings.easing.eventeditout,function(){
$(this).remove();
});
} else {
$(this).remove();
}
// Add the original notes back in.
$events.append($notes[content_setter](values.notes||''));
$event.bind('dblclick.'+plugin_name,_private.event.edit);
// Only bother with the callback if the notes have actually changed.
if( hasChanged ){
// Store the new value against the object.
$events.data(plugin_name,values);
data.cache.events[values.uid] = values;
values.calendar.data(plugin_name,data);
// Run the user function with the new notes.
data.settings.eventnotesedit.apply(values.calendar,[values.uid,values,$events]);
};
});
}
// Prevent default if we were given an event.
if( e ){
e.preventDefault();
e.stopPropagation();
}
},
/**
* Triggered when an appointment is clicked for selection, or manually selected via the.
*
* @param mixed speed : (opt) If int is number of milleseconds for animation, or string 'fast', 'slow'. If undefined, defaults to fast.
* @param string ease : (opt) The easing method to use. See jQuery easing documentation for details.
*
* @return void
*/
select : function( speed, ease ){
var $event = $(this),
values = $event.data(plugin_name),
data = values && values.calendar ? values.calendar.data(plugin_name) : false ;
if( data && values ){
// Exit now if we don't allow selection.
if( !data.settings.allowselect ) return;
// Set the default speed if its not already defined.
if( speed === undefined ) speed = 'fast';
// Get the previously seleted element.
var $old = values.calendar.find('div.ui-'+plugin_name+'-event.selected'),
oldvalues = $old.data(plugin_name);
// Were trying to select the same element.
if( oldvalues && ( oldvalues.uid === values.uid ) ) return;
// Run the on select handler, if the user has defined one for this instance.
var veto = data.settings.eventselect.apply(values.calendar,[values.uid,values,$event]);
// Check whether the eventselect handler overrode the selection.
if( veto === undefined || veto ){
// Get each of the elements for this event.
var $events = values.elems;
// Remove the currently selected appointment.
// Add the selected class to the appointment.
$old.removeClass('selected');
$events.addClass('selected');
// The position method will also apply any color changes.
_private.draw[data.type].position.apply($events,[speed, ease]);
_private.draw[data.type].position.apply($old,[speed, ease]);
}
}
},
/**
* Removes an event from the internal data array, and clears it off the calendar layout.
*
* @param object e : An object representing the event that triggered this method.
* @param mixed speed : (opt) If int is number of milleseconds for animation, or string 'fast', 'slow'. If undefined, defaults to fast.
* @param string ease : (opt) The easing method to use. See jQuery easing documentation for details.
*
* @return void
*/
remove : function( e, speed, ease ){
// This method can be called from an event handler OR direct invocation. Get the correct
// element depending on the calling method.
var $event = e ? $(this).closest('div.ui-'+plugin_name+'-event') : $(this),
values = $event.data(plugin_name),
data = values && values.calendar ? values.calendar.data(plugin_name) : false ;
// Make sure we've got valid data and values for the calendar.
if( data && values ){
// Set the default speed if its not already defined.
if( speed === undefined ) speed = 'fast';
if( !ease ) ease = data.settings.easing.eventremove;
// Get the events, check for a user veto, and define the animation end handler.
var $events = values.elems,
veto = data.settings.eventremove.apply(values.calendar,[values.uid,values,$events,e]),
removeEvent = function(){
// Remove the event from the DOM.
// This method also cleans up all data assigned to this event.
$(this).remove();
};
// Check whether the eventremove handler overrode the deletion.
if( veto === undefined || veto ){
// Animate the elements removal.
// Set the base animation properties.
var animation = {
width : 0,
height : 0,
fontSize : '0em',
opacity : .1
};
// Assume that the events will be removed, and remove
// them from the calendars event cache.
delete data.cache.events[values.uid];
values.calendar.data(plugin_name,data);
// Animate slightly differently depending on the
// number of elements that we're displaying.
switch( $events.length ){
// Animating just the one element.
case 1 :
// Set the animation properties.
animation.left = '+='+( $event.width()/2 );
animation.top = '+='+( $event.height()/2 );
// Run the animation.
$event.animate( animation, speed, ease, removeEvent );
break;
// Animating two elements
case 2 :
// Get animation details first.
var $e1 = $events.eq(0),
$e2 = $events.eq(1),
anim1 = $.extend({},animation),
anim2 = $.extend({},animation);
// Set the animation properties.
anim1.left = '+='+( $events.eq(0).width() );
anim1.top = data.cache.dayHeight;
anim2.top = 0;
// Run the animations. Delay the first animation to give time
// for the second one to start (so they animate at the same time).
// TODO: This might be different for other browsers, in which case
// we might just need to ignore the difference in animation time??
$e1.delay(375).animate( anim1, speed, ease, removeEvent );
$e2.animate( anim2, speed, ease, removeEvent );
break;
// Animating three or more elements.
default :
var eventLeft = 0;
// Find the left position to animate to.
$events.each(function(){ eventLeft += $(this).width(); });
// Set the absolute left position (no relative positions for the multiple days.
animation.left = $event.position().left + data.elements.container.scrollLeft() + ( eventLeft/2 );
animation.top = data.cache.dayHeight / 2;
// Animate each of the events.
// Again, the first animation seems to happen before the rest...
// this delay hack ensures they all run together...
// TODO: This might be different for other browsers, in which case
// we might just need to ignore the difference in animation time??
$events.first().delay(375).animate( animation, speed, ease, removeEvent );
$events.filter(':not(:first)').animate( animation, speed, ease, removeEvent );
break;
}
}
}
// Prevent default if we were given an event.
if( e ){
e.preventDefault();
e.stopPropagation();
}
}
},
// Drag event handlers.
drag : {
/**
* Called on mouse down for a drag object. Adds mouse move and mouse up events for drag operation.
*
* @param object e : An object representing the event that triggered this method.
*
* @return void
*/
start : function( e ){
// Drag start...
var $target = $(this),
$event = e && !$target.is('div.ui-'+plugin_name+'-event') ? $target.parents('div.ui-'+plugin_name+'-event') : $target,
operation = null,
values = $event.data(plugin_name),
data = values && values.calendar ? values.calendar.data(plugin_name) : false ;
if( data && values ){
// Store the $event and the data objects. These could change while
// we're dragging, but to avoid causing issues with the user, we ignore
// any changes until the beginning of of the next drag operation.
// Because the values data is expected to change as a result of dragging,
// we need to load it each time in the mousemove handler.
var eData = { 'event' : $event, 'data' : data, 'values' : values, i : 'cal_'+(new Date()).getTime(), 'type' : 'resize' };
// Store the current drag details against this object. We clean them up after.
_private.drag[eData.i] = {
started : false, // Has dragging officially started??
startX : e.pageX, // Start position of the element.
startY : e.pageY, //
lastX : e.pageX, // Last recorded position of the element.
lastY : e.pageY, //
deltaX : 0, // Mouse movement in px since last recorded position.
deltaY : 0, //
incX : 0, // Increment movement (based on dragincrement setting) since last position.
incY : 0, //
obTime : values.begins.copy(), // Original begins time (stored in case we need to roll back).
oeTime : values.ends.copy(), // Original end time (stored in case we need to roll back).
bTime : values.begins.roundToIncrement( data.settings.dragincrement ), // When dragging, we round the begins and
eTime : values.ends.roundToIncrement( data.settings.dragincrement ), // ends time to the drag increment.
}
// Stop animating, and snap to the end of the animation.
$event.stop(true,true);
// Choose the interaction based on criteria in each case.
switch( true ){
// Dragging.
case $target.is('.ui-'+plugin_name+'-event') :
// Set up comparisons for begins and ends dates.
eData.lockBegins = Number(data.settings.startdate.format("Ymd"));
eData.lockEnds = Number(data.cache.enddate.format("Ymd"));
eData.lockDayBegins = Number(data.settings.daytimestart.replace(/[^0-9]/g,''));
eData.lockDayEnds = Number(data.settings.daytimeend.replace(/[^0-9]/g,''));
eData.type = 'move';
// Bind mousemove event to the document.
$(document).bind('mousemove.'+plugin_name, eData,_private.drag.move)
break;
// Resizing up
case $target.is('.resize-top') :
// Lock the date to the current date for this drag.
eData.lockDate = values.begins.format('Y-m-d');
// Bind mousemove event to the document.
$(document).bind('mousemove.'+plugin_name, eData,_private.drag.resizeB);
break;
// Resizing down…
case $target.is('.resize-bottom') :
// Lock the date to the current date for this drag.
eData.lockDate = values.ends.format('Y-m-d');
// Bind mousemove event to the document.
$(document).bind('mousemove.'+plugin_name, eData,_private.drag.resizeE);
break;
// If none of the other cases work, remove the current drag details.
default: delete _private.drag[eData.i]; return true;
}
// Also, bind the drag end event for each case.
$(document).bind('mouseup.'+plugin_name, eData, _private.drag.end);
// Stop the mousedown event from propagating to elements under it.
e.stopPropagation();
}
},
/**
* Called on mousemove for a resize-top drag object.
*
* @param object e : An object representing the event that triggered this method.
*
* @return void
*/
resizeB : function( e ){
// Resize the 'begins' time.
var $event = e.data.event,
data = e.data.data,
values = e.data.values,
drag = _private.drag[e.data.i];
// Calculate the new delta.
drag.deltaY=drag.lastY-e.pageY;drag.lastY=e.pageY;drag.incY+=drag.deltaY;
// Exit now if there is no Y delta change to save processing time.
if( drag.deltaY == 0 ) return true;
// Don't officially start the drag operation until 5 pixels in.
if( !drag.started && ( drag.incY > 5 || drag.incY < -5 ) ){
// Start your engines!!
drag.started = true;
// Add the dragging class to the event.
$event.addClass('ui-dragging ui-resizing');
} else if( drag.started ){
// Calculate the drag movement in drag increment.
var incrementMovement = Math.round( drag.incY / data.cache.dragHeight );
// Check the increment. Don't go any further if there isn't one.
if( incrementMovement === 0 ) return true;
// get the increment.
var testTime = drag.bTime.incrementBy( data.settings.dragincrement, 0-incrementMovement );
var td = testTime.format('Y-m-d');
var tt = testTime.format('H:i:s');
// If we've still got a valid date.
if( td === e.data.lockDate || tt !== data.settings.daytimestart ){
if( td !== e.data.lockDate ){
// Work out what the new time is.
testTime = $[plugin_name].date( e.data.lockDate+' '+data.settings.daytimestart );
// work out how what the increment is between this and the day start time.
incrementMovement = Math.round( testTime.getIncrementBetween( values.begins, data.settings.dragincrement ) );
}
if( testTime >= values.ends ){
// If the new time is less than or equal to the current time, calculate the difference between the end and beginning time.
incrementMovement = Math.round( values.ends.getIncrementBetween( values.begins, data.settings.dragincrement ) );
// Then set the new beginning time (1 increment's difference than the other time in the direction that we're dragging).
testTime = values.ends.incrementBy( data.settings.dragincrement, -1 ).copy();
// Now, we unbind this handler, and bind the bottom resize handler.
$(document).unbind('mousemove.'+plugin_name).bind('mousemove.'+plugin_name,e.data,_private.drag.resizeE);
}
// We've incremented our date, so remove this from the increment counter.
drag.incY -= (incrementMovement*data.cache.dragHeight);
// Set the new values to the testTime.
values.begins = drag.bTime = testTime;
values.cache.begins = ( data.settings.startdate > values.begins ? data.settings.startdate.copy() : values.begins );
// Store the new event time to the element.
$event.data(plugin_name,values);
data.cache.events[values.uid] = values;
values.calendar.data(plugin_name,data);
// Now, just position the event. DONT animate while dragging.
_private.draw[data.type].position.apply(values.elems);
}
}
},
/**
* Called on mousemove for a resize-bottom drag object.
*
* @param object e : An object representing the event that triggered this method.
*
* @return void
*/
resizeE : function( e ){
// Resize the ends time.
var $event = e.data.event,
data = e.data.data,
values = e.data.values,
drag = _private.drag[e.data.i];
// Calculate the new delta.
drag.deltaY=drag.lastY-e.pageY;drag.lastY=e.pageY;drag.incY+=drag.deltaY;
// Exit now if there is no Y delta change to save processing time.
if( drag.deltaY == 0 ) return true;
// Don't officially start the drag operation until 5 pixels in.
if( !drag.started && ( drag.incY > 5 || drag.incY < -5 ) ){
// Start your engines!!
drag.started = true;
// Add the dragging class to the event.
$event.addClass('ui-dragging ui-resizing');
} else if( drag.started ){
// Calculate the drag movement in drag increment.
var incrementMovement = Math.round( drag.incY / data.cache.dragHeight );
// Check the increment. Don't go any further if there isn't one.
if( incrementMovement === 0 ) return true;
// get the increment.
var testTime = drag.eTime.incrementBy( data.settings.dragincrement, 0-incrementMovement );
var td = testTime.format('Y-m-d');
var tt = testTime.format('H:i:s');
// If we've still got a valid date.
if( td === e.data.lockDate || tt !== data.settings.daytimeend ){
if( td !== e.data.lockDate ){
// Work out what the new time is.
testTime = $[plugin_name].date( e.data.lockDate+' '+data.settings.daytimeend );
// work out how what the increment is between this and the day start time.
incrementMovement = Math.round( testTime.getIncrementBetween( values.ends, data.settings.dragincrement ) );
}
if( testTime <= values.begins ){
// If the new time is less than or equal to the current time, calculate the difference between the end and beginning time.
incrementMovement = Math.round( values.begins.getIncrementBetween( values.ends, data.settings.dragincrement ) );
// Then set the new beginning time (1 increment's difference than the other time in the direction that we're dragging).
testTime = values.begins.incrementBy( data.settings.dragincrement, 1 ).copy();
// Now, we unbind this handler, and bind the bottom resize handler.
$(document).unbind('mousemove.'+plugin_name).bind('mousemove.'+plugin_name,e.data,_private.drag.resizeB);
}
// We've incremented our date, so remove this from the increment counter.
drag.incY -= (incrementMovement*data.cache.dragHeight);
// Set the new values to the testTime.
values.ends = drag.eTime = testTime;
values.cache.ends = ( data.cache.enddate < testTime ? data.cache.enddate.addSeconds(-1) : testTime );
// Store the new event time to the element.
$event.data(plugin_name,values);
data.cache.events[values.uid] = values;
values.calendar.data(plugin_name,data);
// Now, just position the event. DONT animate while dragging.
_private.draw[data.type].position.apply(values.elems);
}
}
},
/**
* Called on mousemove for an event body drag object.
*
* @param object e : An object representing the event that triggered this method.
*
* @return void
*/
move : function( e ){
// Drag to move the appointment (start and end timestamp).
var $event = e.data.event,
data = e.data.data,
values = e.data.values,
drag = _private.drag[e.data.i];
// Calculate the new delta.
drag.deltaX=drag.lastX-e.pageX;drag.lastX=e.pageX;drag.incX+=drag.deltaX;
drag.deltaY=drag.lastY-e.pageY;drag.lastY=e.pageY;drag.incY+=drag.deltaY;
// Don't officially start the drag operation until 5 pixels in.
if( !drag.started && ( drag.incY > 5 || drag.incY < -5 || drag.incX > 5 || drag.incY < -5 ) ){
// Start your engines!!
drag.started = true;
// Add the dragging class to the event.
$event.addClass('ui-dragging');
} else if( drag.started ){
switch( data.type ){
case const_week :
// Calculate the drag movement in drag increment.
var incrementMovement = Math.round( drag.incY / data.cache.dragHeight );
var dayMovement = Math.round( drag.incX / data.cache.dayWidth );
// Check the movement. Don't go any further if there isn't any.
if( incrementMovement === 0 && dayMovement === 0 ) return true;
// Increment by the day.
var testTimeB = drag.bTime.addDays( 0-dayMovement ).incrementBy( data.settings.dragincrement, 0-incrementMovement );
var testTimeE = drag.eTime.addDays( 0-dayMovement ).incrementBy( data.settings.dragincrement, 0-incrementMovement );
// We've incremented our date, so remove this from the increment counter.
drag.incY -= (incrementMovement*data.cache.dragHeight);
drag.incX -= (dayMovement*data.cache.dayWidth);
values.begins = drag.bTime = testTimeB;
values.ends = drag.eTime = testTimeE;
values.cache.ends = ( data.cache.enddate < testTimeE ? data.cache.enddate.addSeconds(-1) : testTimeE );
values.cache.begins = ( data.settings.startdate > values.begins ? data.settings.startdate.copy() : values.begins );
// Store the new event time to the element.
$event.data(plugin_name,values);
data.cache.events[values.uid] = values;
values.calendar.data(plugin_name,data);
// Now, just position the event. DONT animate while dragging.
_private.draw[data.type].position.apply(values.elems);
break;
case const_month :
$event.css({ left : '-='+drag.deltaX, top : '-='+drag.deltaY });
break;
}
}
},
/**
* Called on mousemove for a resize-bottom drag object.
*
* @param object e : An object representing the event that triggered this method.
*
* @return void
*/
end : function( e ){
// Drag end...
var $event = e.data.event,
data = e.data.data,
values = e.data.values;
switch( e.data.type ){
case 'resize' : data.settings.eventresize.apply(values.calendar,[values.uid,values,$event]); break;
case 'move' : data.settings.eventmove.apply(values.calendar,[values.uid,values,$event]); break;
}
// Clean up the events we attached to the document.
$(document)
.unbind('mousemove.'+plugin_name)
.unbind('mouseup.'+plugin_name);
// Remove the dragging class.
$event.removeClass('ui-dragging ui-resizing');
// Clean up the temporary drag storage object.
delete _private.drag[e.data.i];
// Redraw one last time.
_private.draw[data.type].position.apply(values.elems);
}
},
// Methods to draw the different styles of appointment.
draw : {
// Draw week style calendar / event
week : {
/**
* Draw the calendar to the screen as a week view.
*
* @param object data : The calendar data object which describes how to draw the calendar.
*
* @return void;
*/
cal : function( data ){
// Initialise variables for the loops below.
var clonedTime,
clonedDate,
clonedTimeObject,
clonedDateObject,
clonedResourceLabel,
clonedTimeLabel,
clonedDateLabel,
clonedDateFormat,
todayDate = $[plugin_name].date().format('Y-m-d');
// Apply the CSS to the master element.
// This will automatically get cloned with the element.
data.elements.timeblock.css({
width : '100%',
height : data.cache.incrementHeight
});
data.elements.timelabel.css({
width : '100%',
height : data.cache.incrementHeight
});
// Loop through each increment to create a standard day fragment we can
// clone for each of the days we want to display.
for( var i=0, mPast, mTime; i values.begins ? data.settings.startdate.copy() : values.begins );
// Calculate the number of elements required to render this event.
// This would usually be one event per day * the number of resources this event is applied to.
var daysInEvent = _private.event.calculateElementCount.apply(this, [values]),
$event, $events;
// Check if we've create event fragments.
if( !( 'elems' in values ) || values.elems.length < 1 ){
// Store the event values against the element.
// If everything is in order, create the event data.
$event = fragments.event.clone(true);
// Hide certain elements if some interactions aren't allowed.
if( !data.settings.allowremove ) $event.find('span.button-remove').hide();
if( !data.settings.allowresize ) $event.find('p.resize-top, p.resize-bottom').hide();
if( data.settings.allowmove ){
$event.find('pre.details').bind('selectstart.'+plugin_name,_private.prevent);
} else {
$event.unbind('mousedown.'+plugin_name,_private.drag.start);
}
var content_setter = data.settings.allowhtml ? 'html' : 'text' ;
// Add the text straight to the event details.
$event.attr('data-id',values.uid);
$event.find('pre.details')[content_setter]( values.notes );
$event.find('p.title')[content_setter]( values.title || ( values.begins.format(data.settings.maskeventlabel) +
(
data.settings.maskeventlabelend !== '' ? data.settings.maskeventlabeldelimiter + values.ends.format( data.settings.maskeventlabelend ) : ''
)
)
);
// Start the events collection small with this element.
$events = $event;
} else {
// Get the first element, and restore default classes.
// Start the events collection with all the elements we've got.
$events = values.elems.detach();
// Whittle the events collection down to the elements we need.
if( $events.length > daysInEvent ) $events.slice( 0, daysInEvent );
// Reset the classes on the existing events, and get the first one to continue cloning if needed.
$event = $events.removeClass('begin end').addClass('mid').eq(0);
}
// Loop to create the number of elements required to render this event.
while( daysInEvent > $events.length ) $events = $events.add($event.clone(true));
// Set the classes for begin and end.
if( data.settings.startdate <= values.begins ) $events.first().removeClass('mid').addClass('begin');
if( data.cache.enddate >= values.ends ) $events.last().removeClass('mid').addClass('end');
// Only add the event to the data array, and to the DOM,
// if it falls within the date range.
data.elements.container.append($events);
// Store the $events elements in the event data object.
values.elems = $events;
$events.data(plugin_name,values);
// Run the positioning code.
_private.draw[data.type].position.apply($events);
},
/**
* Positions an event object on the screen according to its data object.
*
* @param int speed : (opt) The speed of animation in milliseconds. If ommited, we won't animate the changes.
* @param string ease : (opt) The easing method to use. See jQuery easing documentation for details.
*
* @return void
*/
position : function( speed, ease, detect ){
/* Position an event element on the screen */
var $events = $(this),
values = $events.data(plugin_name),
data = values && values.calendar ? values.calendar.data(plugin_name) : false,
detect = detect === undefined ? true : detect ;
// Make sure we've got values.
if( data && values ){
// Loop over each of the event elements and draw them.
$events.each(function( i, event ){
var $event = $(event),
selected = $event.hasClass('selected'),
dayBegins = $[plugin_name].date( values.begins.addDays(i), data.settings.daytimestart ),
dayEnds = $[plugin_name].date( values.begins.addDays(i), data.settings.daytimeend );
// Prevent detection of overlaps if we've passed through
// the detect flag.
if( detect ){
// Check if we were overlapping items previously.
var wasOverlapping = values.overlap.items;
// Get the event overlaps for this day.
_private.overlaps.apply(values.calendar,[dayBegins,dayEnds,values.resource]);
// Make sure we've got any update event values. In particular, the overlap data.
values = $event.data(plugin_name);
// Redraw any items that this event is overlapping.
for( var uid in values.overlap.items ){
_private.draw[data.type].position.apply( values.overlap.items[uid].elems, [false,false,false] );
}
// Redraw any items that we were previously overlapping.
// Double check that we haven't already re-drawn this item.
for( var uid in wasOverlapping ){
if( !( uid in values.overlap.items ) ){
_private.draw[data.type].position.apply( wasOverlapping[uid].elems, [false,false,true] );
}
}
}
// Calculate the new CSS.
var newStylesMain = {
top : i>0 ? 0 : data.cache.incrementHeight * $[plugin_name].date( values.cache.begins, data.settings.daytimestart ).getIncrementBetween( values.cache.begins, data.settings.gridincrement ),
left : data.cache.dayWidth * ( data.settings.startdate.getDaysBetween( values.cache.begins, true ) + i ) + ( data.cache.resourceWidth * values.resource ),
width : ( values.resource !== null ? data.cache.resourceWidth : data.cache.dayWidth ) - 1,
height : Math.min( data.cache.dayHeight, data.cache.incrementHeight * ( i<1 ? values.begins : dayBegins ).getIncrementBetween( ( i==$events.length-1 ? values.cache.ends : dayEnds ), data.settings.gridincrement ) ),
backgroundColor : selected ? values.colors.mainSelected : values.colors.mainBackground,
textShadow : values.colors.mainTextShadow+' 1px 1px 1px',
color : values.colors.mainText,
'z-index' : selected ? '9999' : values.overlap.zindex
}
newStylesMain.width -= data.settings.overlapoffset*values.overlap.count;
newStylesMain.left += data.settings.overlapoffset*values.overlap.inset;
var newStylesDetails = {
backgroundColor : values.colors.detailsBackground,
textShadow : values.colors.detailsTextShadow+' 1px 1px 1px',
color : values.colors.detailsText,
'z-index' : selected ? '9999' : values.overlap.zindex
}
// If the event display is too small to show any meaningful details area
// Use the title attribute instead.
if( newStylesMain.height <= 15 ){
newStylesDetails.display = 'none';
$event.attr('title',values.notes||'').unbind('dblclick.'+plugin_name).bind('dblclick.'+plugin_name,_private.event.edit);
} else {
newStylesDetails.display = 'block';
$event.removeAttr('title').unbind('dblclick.'+plugin_name);
}
var content_setter = data.settings.allowhtml ? 'html' : 'text' ;
// Set the appointment time while dragging.
if( !values.title )
$event.find('p.title')[content_setter]( values.begins.format(data.settings.maskeventlabel) + ( data.settings.maskeventlabelend !== '' ? data.settings.maskeventlabeldelimiter + values.ends.format( data.settings.maskeventlabelend ) : '' ) );
// Choose whether to animate or not.
if( !speed ){
$event.css(newStylesMain);
$event.find('pre.details').css(newStylesDetails);
} else {
// Animate the event.
$event
.stop(true, false)
.animate(newStylesMain, speed, ease || data.settings.easing.eventupdate)
.find('pre.details')
.stop(true, false)
.animate(newStylesDetails, speed, ease || data.settings.easing.eventupdate)
.css('display',newStylesDetails.display);
// If jQuery UI isn't loaded, we need to
// manually set the colours, as they won't animate.
if( jQuery.ui === undefined ){
$event.css({
backgroundColor : newStylesMain.backgroundColor,
textShadow : newStylesMain.textShadow,
color : newStylesMain.color
});
$event.find('pre.details').css({
backgroundColor : newStylesDetails.backgroundColor,
textShadow : newStylesDetails.textShadow,
color : newStylesDetails.color
});
}
}
});
}
}
},
// Draw month style calendar / event
month : {
/**
* Draw the calendar to the screen as a month view.
*
* @param object data : The calendar data object which describes how to draw the calendar.
*
* @return void;
*/
cal : function( data ){
var clonedMonth,
clonedDate,
clonedDateLabel,
clonedDateObject,
clonedDateFormat,
todayDate = $[plugin_name].date().format('Y-m-d');
data.elements.datelabel.css({
width : data.cache.dayWidth,
height : '100%'
});
data.elements.dayblock.css({
width : data.cache.dayWidth,
height : data.cache.dayHeight
});
for( var i=0; i values.begins ? data.settings.startdate.copy() : values.begins );
// Calculate the number of elements required to render this event.
// This would usually be one event per day * the number of resources this event is applied to.
var daysInEvent = _private.event.calculateElementCount.apply(this, [values]),
$event, $events;
// Check if we've create event fragments.
if( !( 'elems' in values ) || values.elems.length < 1 ){
// Store the event values against the element.
// If everything is in order, create the event data.
$event = fragments.event.clone(true);
// Hide certain elements if some interactions aren't allowed.
if( !data.settings.allowremove ) $event.find('span.button-remove').hide();
if( !data.settings.allowmove ) $event.unbind('mousedown.'+plugin_name,_private.drag.start);
// Add the text straight to the event details.
$event.find('p.resize-top, p.resize-bottom').hide();
$event.attr('data-id',values.uid);
var content_setter = data.settings.allowhtml ? 'html' : 'text' ;
$event.find('pre.details').text( values.notes );
$event.find('p.title')[content_setter]( '● ' + ( values.title || ( values.begins.format(data.settings.maskeventlabel) +
(
data.settings.maskeventlabelend !== '' ? data.settings.maskeventlabeldelimiter + values.ends.format( data.settings.maskeventlabelend ) : ''
)
) ) );
$event.attr('title',values.notes||'').unbind('dblclick.'+plugin_name).bind('dblclick.'+plugin_name,_private.event.edit);
// Start the events collection small with this element.
$events = $event;
} else {
// Get the first element, and restore default classes.
// Start the events collection with all the elements we've got.
$events = values.elems.detach();
// Whittle the events collection down to the elements we need.
if( $events.length > daysInEvent ) $events.slice( 0, daysInEvent );
// Reset the classes on the existing events, and get the first one to continue cloning if needed.
$event = $events.removeClass('begin end').addClass('mid').eq(0);
}
// Loop to create the number of elements required to render this event.
while( daysInEvent > $events.length ) $events = $events.add($event.clone(true));
// Set the classes for begin and end.
if( data.settings.startdate <= values.begins ) $events.first().removeClass('mid').addClass('begin');
if( data.cache.enddate >= values.ends ) $events.last().removeClass('mid').addClass('end');
// Only add the event to the data array, and to the DOM,
// if it falls within the date range.
data.elements.container.append($events);
// Store the $events elements in the event data object.
values.elems = $events;
$events.data(plugin_name,values);
// Event was drawn.
data.settings.eventdraw.apply(values.calendar,[values.uid,values,$event]);
// Run the positioning code.
_private.draw[data.type].position.apply($events);
},
/**
* Positions an event object on the screen according to its data object.
*
* @param int speed : (opt) The speed of animation in milliseconds. If ommited, we won't animate the changes.
* @param string ease : (opt) The easing method to use. See jQuery easing documentation for details.
*
* @return void
*/
position : function( speed, ease, detect ){
/* Position an event element on the screen */
var $events = $(this),
values = $events.data(plugin_name),
data = values && values.calendar ? values.calendar.data(plugin_name) : false,
detect = detect === undefined ? true : detect ;
// Make sure we've got values.
if( data && values ){
// Loop over each of the event elements and draw them.
$events.each(function( i, event ){
var oneDay = 24*60*60*1000; //hour*minutes*seconds*milliseconds
var diffDays = Math.floor( Math.abs(data.settings.startdate.getTime() - values.begins.getTime()) / (oneDay) ) + 1;
var $event = $(event),
selected = $event.hasClass('selected'),
spanning = $events.length > 1,
dayBegins = $[plugin_name].date( values.begins.addDays(i), data.settings.daytimestart ),
dayEnds = $[plugin_name].date( values.begins.addDays(i), data.settings.daytimeend );
// Prevent detection of overlaps if we've passed through
// the detect flag.
if( detect ){
// Check if we were overlapping items previously.
var wasOverlapping = values.overlap.items;
// Get the event overlaps for this day.
_private.overlaps.apply(values.calendar,[dayBegins,dayEnds,values.resource]);
// Make sure we've got any update event values. In particular, the overlap data.
values = $event.data(plugin_name);
// Redraw any items that this event is overlapping.
for( var uid in values.overlap.items ){
_private.draw[data.type].position.apply( values.overlap.items[uid].elems, [false,false,false] );
}
// Redraw any items that we were previously overlapping.
// Double check that we haven't already re-drawn this item.
for( var uid in wasOverlapping ){
if( !( uid in values.overlap.items ) ){
_private.draw[data.type].position.apply( wasOverlapping[uid].elems, [false,false,true] );
}
}
}
// Calculate the new CSS.
var newStylesMain = {
top : data.cache.dayHeight * Math.floor((diffDays-1)/7) + 2 + 18,
left : ( data.cache.dayWidth * ((diffDays-1)%7) ) + ( i > 0 ? 0 : 2 ),
width : data.cache.dayWidth - ( spanning && i==0 ? 2 : 4 ),
height : data.cache.incrementHeight,
backgroundColor : selected ? values.colors.mainSelected : 'transparent' ,
textShadow : selected ? values.colors.mainTextShadow+' 1px 1px 1px' : 'none' ,
color : selected ? values.colors.mainText : values.colors.mainBackground
}
// Calculate top position...
newStylesMain.top += ( 14 * values.overlap.index || 0 );
// Re-set the notes attribute on this event.
$event.attr('title',values.notes||'');
if( selected ){
if( event.style.removeAttribute ){
event.style.removeAttribute('background-image');
} else {
event.style.removeProperty('background-image');
}
$event.find('span.button-remove').show();
if( spanning && i==0 ){
$event.find('span.button-remove').hide();
}
if( i > 0 ){
$event.find('p.title').hide();
}
} else {
event.style.backgroundImage = 'none';
$event.find('span.button-remove').hide()
if( i > 0 ){
$event.find('p.title').show();
}
}
// Choose whether to animate or not.
if( !speed ){
$event.css(newStylesMain).find('pre.details').hide();
} else {
// Animate the event.
$event
.stop(true, false)
.animate(newStylesMain, speed, ease || data.settings.easing.eventupdate)
.find('pre.details').hide();
// If jQuery UI isn't loaded, we need to
// manually set the colours, as they won't animate.
if( jQuery.ui === undefined ){
$event.css({
backgroundColor : newStylesMain.backgroundColor,
textShadow : newStylesMain.textShadow,
color : newStylesMain.color
});
}
}
});
}
}
}
}
};
// Some of these fragments will be initialized once in the code
// if they happen to be used with the current setup.
// Others will be used in all setups, so are initialized now.
var fragments = {
// Create the event element and bind event handlers…
// this will be cloned for each event we create.
event : $([
'',
'
',
'
',
'
',
'
',
'',
'
',
'
'
].join(''))
.bind('selectstart.'+plugin_name,_private.prevent)
.bind('mousedown.'+plugin_name,_private.event.select)
.bind('mousedown.'+plugin_name,_private.drag.start)
.find('p.resize-top, p.resize-bottom').bind('mousedown.'+plugin_name,_private.drag.start).end()
.find('pre.details').bind('dblclick.'+plugin_name,_private.event.edit).end()
.find('p').bind('selectstart.'+plugin_name,_private.prevent).end()
.find('span.button-remove').bind('click.'+plugin_name,_private.event.remove).end(),
// Create layout elements.
day : $('').bind('selectstart.'+plugin_name,_private.prevent),
time : $(''),
labels : {
time : $(''),
date : $(''),
resource : $(''),
},
timeline : $('').bind('selectstart.'+plugin_name,_private.prevent),
dateline : $('').bind('selectstart.'+plugin_name,_private.prevent),
datelinefill : $(''),
resourceline : $(''),
resourcelinefill : $(''),
container : $('').bind('scroll.'+plugin_name,_private.onscroll)
};
// Publicly accessable methods.
var methods = {
/**
* Initialises the calendar. Responsible for drawing, and setting up events etc.
*
* @param object options : An object of key -> value pairs that maps with the defaults object above.
*
* @return object : Returns the jQuery collection that this plugin was called on.
* @scope public.
*/
init : function( options ){
// Loop through each passed element.
return $(this).each(function(){
// Settings to the defaults.
var settings = $.extend({},defaults);
// If options exist, lets merge them
// with our default settings.
if( options ) $.extend( settings, options );
// Create shortcuts, and get any existing data.
var $this = $(this),data = $this.data(plugin_name);
// If the plugin hasn't been initialized yet
if ( ! data ) {
// Create the data object.
data = {
type : const_week, // Defaults to week view.
target : $this, // This element.
settings : settings, // The settings for this plugin.
// An object which contains all HTML elments related to the
// presentation of the calendar itself (anything except events).
elements : {},
// The cache is an object where we store internally calculated
// values that are reused in other places, but aren't necessarily
// that useful for implementors to access.
cache : {
scrollbarSize : _private.scrollbarSize(),
enddate : undefined,
incrementsInDay : undefined,
incrementHeight : undefined,
dragHeight : undefined,
dayWidth : undefined,
dayHeight : undefined,
events : {},
calendars : {}
}
}
if( // Check if this is a day or a month calendar.
data.settings.startmonth === null &&
data.settings.startyear === null &&
data.settings.monthstodisplay === null
){
// This is a day calendar.
if( data.settings.startdate === null ) data.settings.startdate = new Date();
if( data.settings.daystodisplay === null ) data.settings.daystodisplay = 7;
} else {
// This is a month calendar.
if( data.settings.startmonth === null ) data.settings.startmonth = (new Date()).getMonth()+1;
if( data.settings.startyear === null ) data.settings.startyear = (new Date()).getFullYear();
if( data.settings.monthstodisplay === null ) data.settings.monthstodisplay = 1;
// Work out the start and end dates.
data.settings.startdate = $[plugin_name].date( data.settings.startyear+'-'+( Number( data.settings.startmonth ) < 10 ? '0' : '' )+Number( data.settings.startmonth )+'-01');
data.cache.enddate = data.settings.startdate.addMonths( data.settings.monthstodisplay );
// Now work out the number of days to display.
data.settings.daystodisplay = data.settings.startdate.getDaysBetween( data.cache.enddate );
// We're showing a month calendar.
data.type = const_month;
}
// Convert the start date to an enhanced date object.
data.settings.startdate = $[plugin_name].date( data.settings.startdate, data.settings.daytimestart );
data.settings.defaultcolor = $[plugin_name].colors.generate( data.settings.defaultcolor );
data.settings.invalidcolor = $[plugin_name].colors.generate( data.settings.invalidcolor );
data.settings.minheight = Math.round( data.settings.minheight === null ? $[plugin_name].incrementsIn( data.settings.gridincrement, '1 min' ) : data.settings.minheight );
// Calculate the number of increments in a day. We use this in loops to create day blocks.
data.cache.enddate = data.settings.startdate.addDays(data.settings.daystodisplay);
data.cache.incrementsInDay = data.settings.startdate.getIncrementBetween( $[plugin_name].date( data.settings.startdate, data.settings.daytimeend ), data.settings.gridincrement );
data.cache.incrementHeight = data.settings.minheight;
data.cache.dragHeight = ( data.cache.incrementHeight / $[plugin_name].incrementsIn( data.settings.gridincrement, data.settings.dragincrement ) );
data.cache.resourcecount = 1;
// Add the container class, and the base HTML structure
$this.addClass( 'ui-'+plugin_name+' ui-'+plugin_name+'-container ui-'+plugin_name+'-'+data.type );
// Store fragments that are shared between month and week view for this instance in the data object.
data.elements.dateline = fragments.dateline.clone(true);
data.elements.container = fragments.container.clone(true);
data.elements.dayblock = fragments.day.clone(true);
data.elements.datelabel = fragments.labels.date.clone(true);
data.elements.resourcelabel = fragments.labels.resource.clone(true);
// Draw the correct type of calendar...
switch( data.type ){
case const_month : // Pass the data to save a call to the data API.
// Work out the day of week for the start date, and when the week begins.
var startDayOfWeek = data.settings.startdate.getDay();
var endDayOfWeek = data.cache.enddate.addDays(1).getDay();
var beginDayOfWeek = data.settings.weekstart-1;
var finishDayOfWeek = beginDayOfWeek == 0 ? 6 : beginDayOfWeek-1;
data.settings.startdate = data.settings.startdate.addDays( ( beginDayOfWeek > startDayOfWeek ? beginDayOfWeek-7 : beginDayOfWeek ) - startDayOfWeek );
data.cache.enddate = data.cache.enddate.addDays( finishDayOfWeek - endDayOfWeek + 2 );
data.settings.daystodraw = data.settings.startdate.getDaysBetween( data.cache.enddate );
data.cache.weekstodraw = data.settings.startdate.getWeeksBetween( data.cache.enddate );
// Add paragraph to dayblock element.
data.elements.dayblock.append('');
// Work out widths and heights
data.cache.monthWidth = $this.outerWidth() / data.settings.monthstodisplay
data.cache.dayWidth = data.cache.monthWidth / 7;
data.cache.dayHeight = ( $this.outerHeight() - 22 ) / data.cache.weekstodraw;
// Finally, call the draw method.
_private.draw.month.cal.apply(this,[data]);
break;
case const_week : // Pass the data to save a call to the data API.
// Work out the number of resources we're displaying.
if( $.isArray( data.settings.resources ) || typeof data.settings.resources == 'object' ){
data.cache.resourcecount=0;
for( var i in data.settings.resources ){
if( data.settings.resources.hasOwnProperty( i ) ){
data.cache.resourcecount++;
}
}
$this.addClass('ui-'+plugin_name+'-resources');
// Clone the elements from our fragment store.
data.elements.resourceline = fragments.resourceline.clone(true);
data.elements.resourcelinefill = fragments.resourcelinefill.clone(true);
}
// Add fragments that aren't shared between month and week view.
data.elements.datelinefill = fragments.datelinefill.clone(true);
data.elements.timeline = fragments.timeline.clone(true);
data.elements.timeblock = fragments.time.clone(true);
data.elements.timelabel = fragments.labels.time.clone(true);
// Work out the optimum day width.
var optimumDayWidth = ($this.outerWidth()-60) / (data.settings.daystodisplay*data.cache.resourcecount);
// Work out widths and heights.
data.cache.resourceWidth = Math.max( data.settings.minwidth, optimumDayWidth )
data.cache.dayWidth = data.cache.resourceWidth * data.cache.resourcecount;
data.cache.dayHeight = data.cache.incrementHeight * data.cache.incrementsInDay;
// Finally, call the draw method.
_private.draw.week.cal.apply(this,[data]);
break;
}
// Store the data.
$this.data(plugin_name,data);
// If we've specified an events array / object then
if( !$.isArray( data.settings.calendars ) ){
data.settings.calendars = [data.settings.events];
}
// Allow addition of multiple calendars.
$(data.settings.calendars).each(function(){
var events, color;
if( $.isArray( this ) ){ events = this; } else if( typeof this == 'string' ){ events = this } else if( $.isPlainObject( this ) ){ events = this.events; color = this.color; }
if( $.isArray( events ) ){
// Loop over the events and convert to date objects if required.
for( var i=0; i 0 ){
// Loop over the events and convert to date objects if required.
for( var i=0, event; i value pairs representing an appointment.
* @param obj rData : The given appointment is a repetition of THIS appointment object.
*
* @return obj : The jQuery context (calendar collection) that this method was called from.
* @scope public.
*/
add : function( bData, rData ){
/* Adds a new event object to the calendar */
// Get shortcuts to calendar container and data.
var $this = $(this), data = $this.data(plugin_name);
// If the calendar has been set up already...
if( data ){
// Make sure everything we need exists in bdata.
if( !'uid' in bData ) throw _private.errors.eventParse('Missing unique id (uid)',bData);
if( !'begins' in bData ) throw _private.errors.eventParse('Missing start date/time (begins)',bData);
if( !'ends' in bData ) throw _private.errors.eventParse('Missing end date/time (ends)',bData);
// Make sure this UID doesn't already exist.
if( bData.uid in data.cache.events ) throw _private.errors.eventParse('UID must be unique', bData);
// Parse the dates.
var dataBegins = $[plugin_name].date( bData.begins ),
dataEnds = $[plugin_name].date( bData.ends );
// Clone the event element, and set up the values.
var values = {
elems : $([]),
calendar : $this,
uid : bData.uid,
begins : dataBegins,
ends : dataEnds,
resource : data.settings.resources && bData.resource ? _private.resourceIndex.apply( this, [bData.resource,data] ) : null ,
colors : bData.color ? $[plugin_name].colors.generate( bData.color ) : data.settings.defaultcolor,
title : bData.title || null,
notes : bData.notes || '',
//
// Repetition rules
//
repeat : {
rules : {
include : bData.repeat && bData.repeat.rules && bData.repeat.rules.include ? bData.repeat.rules.include : [],
exclude : bData.repeat && bData.repeat.rules && bData.repeat.rules.exclude ? bData.repeat.rules.exclude : []
},
dates : {
include : bData.repeat && bData.repeat.dates && bData.repeat.dates.include ? bData.repeat.dates.include : [],
exclude : bData.repeat && bData.repeat.dates && bData.repeat.dates.exclude ? bData.repeat.dates.exclude : []
}
},
//
// The cache object
//
cache : {},
//
// Structure for holding overlap data.
//
overlap : {
inset : [],
depth : []
}
};
// Throw an error if we've been passed an invalid resource.
// Allow blank end dates... in the future we'll use event data
// with no end date/time to show 'all day' events.
// For now, we just show them as minimum length.
if( values.resource === false ) throw _private.errors.eventParse('Invalid resource id (resource)',bData);
if( !(values.begins instanceof Date) ) throw _private.errors.eventParse('Invalid start date/time (begins)',bData);
// STORE the event!
data.cache.events[values.uid] = values;
$this.data(plugin_name,data);
// A repetition can't have repetitions... it IS a repetition!!
if( $.isPlainObject( rData ) )
{
values.cache.repeat = false;
values.cache.repeats = rData.uid; // Only need to store the UID for now...
}
else
{
// Store the parsed repetitions for this event.
values.cache.repetitions = _private.repetitions.apply(this, [data,values]);
// Create the repetition elements.
for( var i in values.cache.repetitions )
{
rData = $.extend(true,{},bData);
rData.repeat = false;
rData.uid = '--repeat--'+i+'|'+values.uid;
rData.begins = values.cache.repetitions[i];
rData.ends = values.cache.repetitions[i].addDays( values.begins.getDaysBetween( values.ends ) );
// Add the repeating event...
methods.add.apply(this,[rData,bData]);
}
}
// Only create the events if they're in range.
if( _private.inrange.apply( this, [dataBegins, dataEnds, data.settings.startdate, data.cache.enddate] ) ){
// Call the positioning code.
_private.draw[data.type].event.apply($this,[data,values]);
}
}
return $this;
},
/**
* Remove an event from the data array.
*
* @param mixed uid : The UID of the event to remove.
* @param mixed speed : The speed with which we want to remove the event.
* @param string ease : The easing to use when animating the removal.
*
* @return obj : The jQuery context (calendar collection) that this method was called from.
*/
remove : function( uid, speed, ease )
{
var $this = $(this), $event,
data = $this.data(plugin_name);
if( data && uid in data.cache.events ){
// Find the event element.
$event = data.cache.events[uid].elems;
// Call the remove method.
_private.event.remove.apply($event,[null,speed,ease]);
}
return $this;
},
/**
* Clear all event objects from the calendar
*
* @param mixed speed : The speed with which we want to remove the event.
* @param string ease : The easing to use when animating the removal.
*
* @scope public.
*/
clear : function(speed, ease) {
// Get shortcuts to calendar container and data.
var $this = $(this),
$event = null,
data = $this.data(plugin_name);
// Set the default speed if its not already defined.
if( speed === undefined ) speed = 'fast';
if( !ease ) ease = data.settings.easing.eventremove;
// If the calendar has been set up already...
if( data ) {
for( i in data.cache.events ) {
$event = data.cache.events[i].elems;
_private.event.remove.apply($event,[null,speed,ease]);
}
// Clear the events cache
data.cache.events = new Object();
}
},
/**
* Update a calendar appointment
*
* @param obj values : An object of key => value pairs representing the data to update.
* @param int speed : (Opt) Time in milliseconds for the animation. Could also be 'slow' or 'fast'.
* @param string ease : (Opt) Name of the easing method to use for the animation.
*
* @return obj : The jQuery context (calendar collection) that this method was called from.
* @scope public.
*/
update : function( values, speed, ease ){
var $this = $(this), $event,
data = $this.data(plugin_name);
if( data ){
// Update animation speed.
if( speed === undefined ) speed = 'fast';
// If we've got an array, loop through and add differences to each element.
if( $.isArray( values ) ){
// Loop through the array and apply each of the updates in turn.
for( var i=0; i 0 ){
_private.event.update.apply( $event, [values[i], speed, ease] );
}
}
} else {
// Find the event element.
$event = data.elements.container.find('div.ui-'+plugin_name+'-event[data-id="'+values.uid+'"]');
if( $event.length > 0 ){
// IF we find one, then we need to update it with the new data.
_private.event.update.apply( $event, [values, speed, ease] );
}
}
}
return $this;
},
/**
* Refresh calendar events
* For each event in 'values' arg:
*
* If event exists in calendar then function update it
* If event does not exist in calendar then function add it
* ========
* Each event which exists in calendar and does not exist in arg values is removed
*
* @param obj values : (Array) An object of key => value pairs representing the data to refresh.
* @param int speed : (Opt) Time in milliseconds for the animation. Could also be 'slow' or 'fast'.
* @param string ease : (Opt) Name of the easing method to use for the animation.
*
* @return obj : The jQuery context (calendar collection) that this method was called from.
* @scope public.
*/
refresh : function( values, speed, ease )
{
var $this = $(this), $event,
data = $this.data(plugin_name);
if( data )
{
var elements = data.elements.container.find('div.ui-'+plugin_name+'-event');
// Set default animation speed.
if( speed === undefined ) speed = 'fast';
// If we've got an array, loop through and add differences to each element.
if( $.isArray( values ) )
{
// Loop through the array and apply each of the updates in turn.
for( var i=0; i 0 ){
_private.event.update.apply( $event, [values[i], speed, ease] );
//TODO slow performance
//delete event from 'elements' array
for( var j=0; j 0 ){
_private.event.select.apply( $event, [speed, ease] );
}
}
return $this;
},
/**
* Update or Set the settings value. This may require further data parsing, or UI updates.
*
* @param mixed key : The key of the settings value to set or get, or an object containing a map of values to set.
* @param mixed value : (opt) The value to set into @key if @key is a string.
*
* @return mixed : Returns the settings value if @value is omitted and @key is a string, otherwise, the jQuery collection.
* @scope public.
*/
option : function( key, value )
{
var $this = $(this), data = $this.data(plugin_name);
// Only bother if we've set this up before.
if( data ){
// Return settings array if no key is provided.
if( typeof key == 'undefined' ) return data.settings;
if( typeof key == 'object' ){
for( var k in key ){
if( key.hasOwnProperty(k) ){
methods.option.apply($this,[k,key[k]]);
}
}
return $this;
}
// The key has to exist, otherwise its invalid.
if( !key in data.settings ) return false;
// Check if we're adding or updating.
if( typeof value == 'undefined' ){
return data.settings[key];
} else {
switch( key ){
case 'allowremove' :
data.elements.container.find('div.ui-'+plugin_name+'-event span.button-remove').toggle(Boolean(value));
break;
case 'allowresize' :
data.elements.container.find('p.resize-top, p.resize-bottom').toggle(Boolean(value));
break;
case 'startdate' :
var newdate = $[plugin_name].date( value, data.settings.daytimestart ),
olddate = data.settings.startdate;
// Set the new date details.
data.settings.startdate = newdate;
data.cache.enddate = newdate.addDays(data.settings.daystodisplay);
// Save the data against the plugin.
$this.data(plugin_name,data);
var event, repeat;
// Detatch all of the events, as these elements have
// data stored against them... we may want to add these later.
for( var i in data.cache.events ){
// Cache the event.
event = data.cache.events[i];
// A repetition can't have repetitions... it IS a repetition!!
if( !event.cache.repeats )
{
for( var j in event.cache.repetitions )
{
var repeat = event.cache.repetitions[j];
// Remove the temporary repetition events
// from the calendars event cache.
delete data.cache.events[repeat.uid];
$this.data(plugin_name,data);
// Detatch the elements.
if( 'elems' in repeat && repeat.elems.length > 0 ){
// Remove this event from its container.
repeat.elems.remove();
}
}
// Store the newly parsed repetitions for this event.
event.cache.repetitions = _private.repetitions.apply(this, [data,event]);
// Create the repetition elements.
// These are by definition a part of the current range.
for( var j in event.cache.repetitions )
{
repeat = $.extend(true,{},event);
repeat.repeat = false;
repeat.uid = '--repeat--'+j+'|'+event.uid;
repeat.begins = event.cache.repetitions[j];
repeat.ends = event.cache.repetitions[j].addDays( event.begins.getDaysBetween( event.ends ) );
// Remove the temporary repetition events
// from the calendars event cache.
// Detatch the elements.
if( !( repeat.uid in data.cache.events ) )
{
// Add the repeating event...
methods.add.apply(this,[repeat,event]);
}
else
{
// Redraw this element. The length and position may have changed.
_private.draw[data.type].event.apply($this,[data,repeat]);
}
}
// Check if this event is in the current range.
if( !_private.inrange.apply(this,[event.begins,event.ends,data.settings.startdate,data.cache.enddate]) )
{
if( 'elems' in event && event.elems.length > 0 )
{
// Remove this event from its container.
event.elems.detach();
}
}
else
{
// Redraw this element. The length and position may have changed.
_private.draw[data.type].event.apply($this,[data,event]);
}
}
else
{
// Remove the temporary repetition events
// from the calendars event cache.
delete data.cache.events[event.uid];
$this.data(plugin_name,data);
// Detatch the elements.
if( 'elems' in event && event.elems.length > 0 )
{
// Remove this event from its container.
event.elems.remove();
}
else
{
// Redraw this element. The length and position may have changed.
_private.draw[data.type].event.apply($this,[data,event]);
}
}
}
// Modify the date attributes stored on each of the dayblocks and labels.
//
var $dates = $('div.ui-'+plugin_name+'-date, div.ui-'+plugin_name+'-label-date',data.elements.container).removeClass('ui-'+plugin_name+'-today'),
todayDate = $[plugin_name].date().format('Y-m-d');
switch( data.type ) {
/**
* Different method for days.
*/
case const_month :
$('div.ui-'+plugin_name+'-label-date',data.elements.dateline).removeClass('ui-'+plugin_name+'-today').each(function(i,label){
// Get a shortcut to the label, and create the new date objects.
var $label = $(label),
clonedDateObject = newdate.addDays(i),
clonedDateFormat = clonedDateObject.format('Y-m-d');
// Set the dayblock's date attribute
$label.attr({
'date' : clonedDateFormat
});
// Make sure we add the 'today' class to the calendar.
if( clonedDateFormat === todayDate ) $label.addClass('ui-'+plugin_name+'-today');
});
// Loop over each of the dateline elements.
$('div.ui-'+plugin_name+'-date',data.elements.container).removeClass('ui-'+plugin_name+'-today').each(function(i,date){
// Get a shortcut to the label, and create the new date objects.
var $date = $(date),
clonedDateObject = newdate.addDays(i),
clonedDateFormat = clonedDateObject.format('Y-m-d');
// Set the dayblock's date attribute
$date.attr({
'date' : clonedDateFormat,
'day' : clonedDateObject.getDay()
})
.find('p')
.html( clonedDateObject.format( 'j' ) )
.end();
// Make sure we add the 'today' class to the calendar.
if( clonedDateFormat === todayDate ) $date.addClass('ui-'+plugin_name+'-today');
});
break;
/**
* Different method for weeks.
*/
case const_week :
// Loop over each of the dateline elements.
$('div.ui-'+plugin_name+'-label-date',data.elements.dateline).removeClass('ui-'+plugin_name+'-today').each(function(i,label){
// Get a shortcut to the label, and create the new date objects.
var $label = $(label),
clonedDateObject = newdate.addDays(i),
clonedDateFormat = clonedDateObject.format('Y-m-d');
// Set the dayblock's date attribute
$dates.eq(i).attr({
'date' : clonedDateFormat,
'day' : clonedDateObject.getDay()
});
// Set the labels date attribute.
$label.attr({
'date' : clonedDateFormat,
'day' : clonedDateObject.getDay()
})
.find('p')
.html( clonedDateObject.format( data.settings.maskdatelabel ) )
.end();
// Make sure we add the 'today' class to the calendar.
if( clonedDateFormat === todayDate ) $label.add($dates.eq(i)).addClass('ui-'+plugin_name+'-today');
});
break;
}
/**
* TODO:
* This is almost the code we need... we've just got to figure out
* a mechanism of storing the old layouts safely where we can re-use them.
* We don't want to have to clone the layout again... in fact, it would be better
* if we didn't clone the entire layout at all. Would be much more efficient,
* and far less error prone to create the new layouts on the fly as we need them.
* Or maybe even create them one in advance (after the animation and init had run).
*/
// var old_container = data.elements.container,
// old_dateline = data.elements.dateline;
//
// data.elements.container = old_container.clone(true).appendTo($this);
// data.elements.dateline = old_dateline.clone(true).appendTo($this);
//
// data.elements.dateline
// .add(data.elements.container)
// .css({
// left : (olddate>newdate?'-':'+')+'='+old_container.width(),
// width : old_container.width()
// })
//
// data.elements.dateline
// .add(data.elements.container)
// .add(old_container)
// .add(old_dateline)
// .animate({
// left : (olddate>newdate?'+':'-')+'='+data.elements.container.width()
// }, 'fast', data.settings.easing.datechange, function(){
// old_container.remove();
// old_dateline.remove();
// });
return $this;
break;
default: data.settings[key] = value; break;
}
return $this;
}
}
},
version : function(){
// Returns the version string for this plugin.
return plugin_name+' v'+plugin_version;
},
destroy: function(){
/* Remove the ui plugin from these elements that have it */
return this.each(function(){
var $this = $(this),
data = $this.data(plugin_name);
// Only bother if we've set this up before.
if( data ){
// Remove all of the events, as these elements have
// data stored against them... we need to make sure
// its cleaned up properly.
for( var i=0; i 3 ? h : h.substring(0,1)+h.substring(0,1)+h.substring(1,2)+h.substring(1,2)+h.substring(2,3)+h.substring(2,3) ; },
HueToRGB : function(p, q, t){
if(t < 0) t += 1;
if(t > 1) t -= 1;
if(t < 1/6) return p + (q - p) * 6 * t;
if(t < 1/2) return q;
if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
},
HSLToRGB : function(h, s, l, a, f){
var r, g, b;
// Convert HSL back to fractions.
h/=360;s/=100;l/=100;
if(s == 0){
r = g = b = l; // achromatic
}else{
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = $[plugin_name].colors.HueToRGB(p, q, h + 1/3);
g = $[plugin_name].colors.HueToRGB(p, q, h);
b = $[plugin_name].colors.HueToRGB(p, q, h - 1/3);
}
var rgb = [Math.round(r*255),Math.round(g*255),Math.round(b*255)];
// If we've specified an alpha level, then we want to include that in the result.
if( f ){ if(a!==undefined){ rgb[3]=a }; return rgb; };
return (a!==undefined?'rgba':'rgb')+'('+rgb.join(', ')+(a!==undefined?', '+a+')':')');
},
// Method to convert RGB to HSL for manipulation.
RGBToHSL : function(r, g, b){
r /= 255, g /= 255, b /= 255;
var max = Math.max(r, g, b), min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
if(max == min){
h = s = 0; // achromatic
}else{
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch(max){
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
// Convert HSL from fractions.
return [Math.round(h*360), Math.round(s*100), Math.round(l*100)];
},
brightness : function (r, g, b){
// Returns the brightness of a color.
return ((r*299)+(g*587)+(b*114))/1000;
},
// Generate a bunch of complimentary colors from the given color for use in the calendar.
generate : function( color ){
var step = 10;
// Check to see if we've been passed a named color.
if( color in $[plugin_name].colors.named ){
// Look up the hex representation for this named color.
color = $[plugin_name].colors.named[color];
}
// First check if we've got a hex color...
if( color.match( /^#?([0-9A-Fa-f]{3,6})$/ ) ){
// Get the red, green and blue values from the hex color.
var rgb = [
$[plugin_name].colors.HexToR(color),
$[plugin_name].colors.HexToG(color),
$[plugin_name].colors.HexToB(color)
];
// Now convert into HSL.
var hsl = $[plugin_name].colors.RGBToHSL( rgb[0], rgb[1], rgb[2] );
// Checks (pretty exhaustively) if the colour is an RGB colour.
} else if ( color.replace( / /g, '' ).match( /([rR][gG][bB]\s*\(\s*\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\b\s*,\s*\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\b\s*,s*\b([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\b\s*\))|([rR][gG][bB]\(\s*(\d?\d%|100%)+\s*,\s*(\d?\d%|100%)+\s*,\s*(\d?\d%|100%)+\s*\))/ ) ){
// if it is, we remove everythign but the numbers and commas, then split it out into an array.
var rgb = color.replace( /[a-zA-Z\(\) ]/g, '' ).split( ',' );
var hsl = $[plugin_name].colors.RGBToHSL( rgb[0], rgb[1], rgb[2] );
// Otherwise, throw an error…
} else if( color.replace( / /g, '' ).match( /([hH][sS][lL]\s*\(\s*([0-9]|[1-9][0-9]|[1-2][0-9][0-9]|3[0-5][0-9]|360)\s*,\s*(\d?\d%|100%)\s*,\s*(\d?\d%|100%)\s*\))/ ) ){
// Split into HSL array.
var hsl = color.replace( /[a-zA-Z\(\) %]/g, '' ).split( ',' );
} else {
throw new Error('Invalid Colour: could not parse '+color+' as a color.');
}
// If there isn't enough room for manipulation
// then just make room on the base color.
if( hsl[1] < step ) hsl[1]+=step;
if( hsl[1] > 100-step ) hsl[1]-=step;
if( hsl[2] > 100-step ) hsl[2]-=step;
if( hsl[2] < step ) hsl[2]+=step;
// Function to shortcut the stepping method.
function stepLighter(c,s,a){
var h=$.extend([],c);
h[1]=Math.max(0,h[1]+(s/2));
h[2]=Math.min(100,h[2]+s);
if(a!==undefined)h[3]=a;
return h;
}
function stepDarker(c,s,a){
var h=$.extend([],c);
h[2]=Math.max(0,h[2]-s);
if(a!==undefined)h[3]=a;
return h;
}
// Reused values…
var txtShadow = $[plugin_name].colors.HSLToRGB.apply( this, stepLighter(hsl,step*5,.3) );
var boxShadow = $[plugin_name].colors.HSLToRGB.apply( this, stepLighter(hsl,step*3,.75) );
var textColor = ( $[plugin_name].colors.brightness.apply( this, $[plugin_name].colors.HSLToRGB( hsl[0], hsl[1], hsl[2], undefined, true ) ) > 125 ? '#333333' : '#FFFFFF' );
// Convert our HSL colors back into RGB… there are
// some browsers which don't support HSL correctly.
return {
original : color,
mainBackground : $[plugin_name].colors.HSLToRGB.apply( this, hsl ),
mainTextShadow : txtShadow,
mainShadow : boxShadow,
mainText : textColor,
mainSelected : $[plugin_name].colors.HSLToRGB.apply( this, stepDarker(hsl,step) ),
detailsBackground : $[plugin_name].colors.HSLToRGB.apply( this, stepLighter(hsl,step) ),
detailsTextShadow : txtShadow,
detailsShadow : boxShadow,
detailsText : textColor
};
}
};
$[plugin_name].lang = {
// Use ISO 639-1 language codes, for now.
'en' : {
// Text date/time representations.
short_month : Array('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'),
long_month : Array('January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'),
short_day : Array('Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'),
long_day : Array('Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'),
// Text labels.
label_remove : 'Remove',
label_today : 'Today'
}
};
// You must call these methods in the context of a date object.
// Formats the string from the localized time.
var _replace = {
// Day
d: function() { return (this.getDate() < 10 ? '0' : '') + this.getDate(); },
D: function() { return $[plugin_name].lang[this.lang].short_day[this.getDay()]; },
j: function() { return this.getDate(); },
l: function() { return $[plugin_name].lang[this.lang].long_day[this.getDay()]; },
N: function() { return this.getDay() + 1; },
S: function() { return (this.getDate() % 10 == 1 && this.getDate() != 11 ? 'st' : (this.getDate() % 10 == 2 && this.getDate() != 12 ? 'nd' : (this.getDate() % 10 == 3 && this.getDate() != 13 ? 'rd' : 'th'))); },
w: function() { return this.getDay(); },
z: function() { if( window.console ){ console.error('Mask \'z\' Not Yet Supported'); }; return "z"; },
// Week
W: function() { var onejan = new Date(this.getFullYear(),0,1); return Math.ceil((((this - onejan) / 86400000 ) + onejan.getDay()+1)/7); },
// Month
F: function() { return $[plugin_name].lang[this.lang].long_month[this.getMonth()]; },
m: function() { return (this.getMonth()+1 < 10 ? '0' : '') + (this.getMonth() + 1); },
M: function() { return $[plugin_name].lang[this.lang].short_month[this.getMonth()]; },
n: function() { return this.getMonth() + 1; },
t: function() { return (new Date((new Date(this.getFullYear(), this.getMonth()+1,1))-1)).getDate(); },
// Year
L: function() { if( window.console ){ console.error('Mask \'L\' Not Yet Supported'); }; return "L"; },
o: function() { if( window.console ){ console.error('Mask \'o\' Not Yet Supported'); }; return "o"; },
Y: function() { return this.getFullYear(); },
y: function() { return ('' + this.getFullYear()).substr(2); },
// Time
a: function() { return this.getHours() < 12 ? 'am' : 'pm'; },
A: function() { return this.getHours() < 12 ? 'AM' : 'PM'; },
B: function() { if( window.console ){ console.error('Mask \'B\' Not Yet Supported'); }; return "B"; },
g: function() { return this.getHours() == 0 ? 12 : (this.getHours() > 12 ? this.getHours() - 12 : this.getHours()); },
G: function() { return this.getHours(); },
h: function() { return (this.getHours() < 10 || (12 < this.getHours() < 22) ? '0' : '') + (this.getHours() < 10 ? this.getHours() + 1 : this.getHours() - 12); },
H: function() { return (this.getHours() < 10 ? '0' : '') + this.getHours(); },
i: function() { return (this.getMinutes() < 10 ? '0' : '') + this.getMinutes(); },
s: function() { return (this.getSeconds() < 10 ? '0' : '') + this.getSeconds(); },
// Timezone
e: function() { if( window.console ){ console.error('Mask \'e\' Not Supported'); }; return "e"; },
I: function() { if( window.console ){ console.error('Mask \'I\' Not Supported'); }; return "I"; },
O: function() { return this.toString().match(/GMT([+-][0-9]+) \(([a-zA-Z]+)\)/)[1]; },
P: function() { var str = this.toString().match(/GMT([+-][0-9]+) \(([a-zA-Z]+)\)/)[1].split(''); str.splice(-2,0,':'); return str.join(''); },
T: function() { return this.toString().match(/GMT([+-][0-9]+) \(([a-zA-Z]+)\)/)[2]; },
Z: function() { return this.getTimezoneOffset() * 60; },
// Full Date/Time
c: function() { return _replace.Y.apply(this)+'-'+_replace.m.apply(this)+'-'+_replace.d.apply(this)+'T'+_replace.H.apply(this)+':'+_replace.i.apply(this)+':'+_replace.s.apply(this)+_replace.P.apply(this); },
C: function() { return _replace.c.apply(this).replace(_replace.P.apply(this),'')+'Z'; },
r: function() { return this.toString(); },
U: function() { return this.getTime() / 1000; }
};
// You must call these methods in the context of a date object.
// Formats the string from the UTC time.
var _replaceUTC = {
// Day
d: function() { return (this.getUTCDate() < 10 ? '0' : '') + this.getUTCDate(); },
D: function() { return $[plugin_name].lang[this.lang].short_day[this.getUTCDay()]; },
j: function() { return this.getUTCDate(); },
l: function() { return $[plugin_name].lang[this.lang].long_day[this.getUTCDay()]; },
N: function() { return this.getUTCDay() + 1; },
S: function() { return (this.getUTCDate() % 10 == 1 && this.getUTCDate() != 11 ? 'st' : (this.getUTCDate() % 10 == 2 && this.getUTCDate() != 12 ? 'nd' : (this.getUTCDate() % 10 == 3 && this.getUTCDate() != 13 ? 'rd' : 'th'))); },
w: function() { return this.getUTCDay(); },
z: _replace.z,
// Week
W: function() { var onejan = new Date(this.getUTCFullYear(),0,1); return Math.ceil((((this - onejan) / 86400000 ) + onejan.getUTCDay()+1)/7); },
// Month
F: function() { return $[plugin_name].lang[this.lang].long_month[this.getUTCMonth()]; },
m: function() { return (this.getUTCMonth()+1 < 10 ? '0' : '') + (this.getUTCMonth() + 1); },
M: function() { return $[plugin_name].lang[this.lang].short_month[this.getUTCMonth()]; },
n: function() { return this.getUTCMonth() + 1; },
t: function() { return (new Date((new Date(this.getUTCFullYear(), this.getUTCMonth()+1,1))-1)).getUTCDate(); },
// Year
L: _replace.L,
o: _replace.o,
Y: function() { return this.getUTCFullYear(); },
y: function() { return ('' + this.getUTCFullYear()).substr(2); },
// Time
a: function() { return this.getUTCHours() < 12 ? 'am' : 'pm'; },
A: function() { return this.getUTCHours() < 12 ? 'AM' : 'PM'; },
B: _replace.B,
g: function() { return this.getUTCHours() == 0 ? 12 : (this.getUTCHours() > 12 ? this.getUTCHours() - 12 : this.getUTCHours()); },
G: function() { return this.getUTCHours(); },
h: function() { return (this.getUTCHours() < 10 || (12 < this.getUTCHours() < 22) ? '0' : '') + (this.getUTCHours() < 10 ? this.getUTCHours() + 1 : this.getUTCHours() - 12); },
H: function() { return (this.getUTCHours() < 10 ? '0' : '') + this.getUTCHours(); },
i: function() { return (this.getUTCMinutes() < 10 ? '0' : '') + this.getUTCMinutes(); },
s: function() { return (this.getUTCSeconds() < 10 ? '0' : '') + this.getUTCSeconds(); },
// Timezone (mostly irrelevant for UTC replaces…)
e: _replace.e,
I: _replace.I,
O: _replace.O,
P: _replace.P,
T: _replace.T,
Z: _replace.Z,
// Full Date/Time
c: function() { return _replaceUTC.Y.apply(this)+'-'+_replaceUTC.m.apply(this)+'-'+_replaceUTC.d.apply(this)+'T'+_replaceUTC.H.apply(this)+':'+_replaceUTC.i.apply(this)+':'+_replaceUTC.s.apply(this)+_replaceUTC.P.apply(this); },
C: function() { return _replaceUTC.c.apply(this).replace(_replaceUTC.P.apply(this),'')+'Z'; },
r: _replace.r,
U: function() { return this.getUTCTime() / 1000; }
};
$[plugin_name].format = function(date,format,utc){
/* Format a string based on a date mask. */
// Init variables.
var formatted = '', charat = '';
var replace = utc ? _replaceUTC : _replace;
// Loop through the array, and format the string.
for( var i=0; i 3 ){
newdate = this.addDays(7-dayinweek);
} else {
newdate = this.addDays( this.getHours() >= 12 ? 7-dayinweek : 0-dayinweek );
}
// round to the beginning of the new date, and return.
newdate.setHours(0,0,0,0);
return newdate;
break;
case 'month' :
// Get the first day in the month, and the halfway point.
var firstofmonth = $[plugin_name].date(this.format('Y-m-01 00:00:00'));
var halfway = firstofmonth.getDaysBetween(firstofmonth.addMonths(1)) / 2;
// Check if the date we have is half way or not, and return accordingly.
if( this.getDate() >= halfway ){
return firstofmonth.addMonths(1);
} else {
return firstofmonth;
}
break;
case 'year' :
// Get the first day of the year, the half way point, and the current day number.
var firstofyear = $[plugin_name].date(this.format('Y-01-01 00:00:00'));
var halfway = firstofyear.getDaysBetween(firstofyear.addYears(1)) / 2;
var dayofyear = firstofyear.getDaysBetween(this);
// Check if the date we have is half way or not, and return accordingly.
if( dayofyear >= halfway ){
return firstofyear.addYears(1);
} else {
return firstofyear;
}
break;
}
return $[plugin_name].date(new Date((Math.round(timestamp/rounder)*rounder)));
}
return this;
}
return NewDate;
};
$[plugin_name].incrementsIn = function( bigger, smaller ){
/* Returns the number of newincrements that will fit inside increment */
var aDate = $[plugin_name].date('1970-01-01 00:00:00');
return aDate.getIncrementBetween( aDate.incrementBy( bigger ), smaller );
};
})(jQuery);