diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..73332e6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ + +/sgical.php \ No newline at end of file diff --git a/README b/README new file mode 100644 index 0000000..264e3d5 --- /dev/null +++ b/README @@ -0,0 +1,54 @@ +A simple and fast iCal parser. +------------------------------------------------------------------------------- + +http://github.com/fangel/SG-iCalendar +With massive help from http://github.com/tpruvot/PHP-iCal +and http://github.com/xonev/SG-iCalendar +------------------------------------------------------------------------------- + +A simple example : + $ical = new SG_iCalReader( "./basic.ics" ); + //or + $ical = new SG_iCalReader( "http://example.com/calendar.ics" ); + foreach( $ical->getEvents() As $event ) { + // Do stuff with the event $event + } + +To check unit tests with phpunit, goto tests/ directory and : + phpunit AllTests + phpunit helpers/FreqTest + +------------------------------------------------------------------------------- +CHANGELOG : +------------------------------------------------------------------------------- + +current (31 oct 2010) + + ical RDATE support (added dates in a range) + + RDATE and EXDATE arrays support + +0.7.0 (30 oct 2010) + + ical EXDATE support (excluded dates in a range) + + $event->isWholeDay() + + getAllOccurrences() for repeated events + + implemented a cache for repeated events + +0.6.0 (29 oct 2010) + + Added demo based on fullcalendar + + Added duration unit tests + + Support of Recurrent events in query Between() + * various fixes on actual (5) issues + +------------------------------------------------------------------------------- +TODO : +------------------------------------------------------------------------------- + +These iCal keywords are not supported for the moment : + - RECURRENCE-ID : to move one event from a recurrence + - EXRULE : to exclude multiple days by a complex rule + +Also, multiple RRULE could be specified for an event, +but that is not the case for most calendar applications + +------------------------------------------------------------------------------- +To get more information about ical format and rules : +see http://www.ietf.org/rfc/rfc2445.txt diff --git a/SG_iCal.php b/SG_iCal.php index e8ea2b9..77a4145 100755 --- a/SG_iCal.php +++ b/SG_iCal.php @@ -1,17 +1,18 @@ getEvents() As $event ) { * // Do stuff with the event $event * } @@ -19,26 +20,32 @@ define('SG_ICALREADER_VERSION', '0.5'); * * @package SG_iCalReader * @author Morten Fangel (C) 2008 + * @author xonev (C) 2010 + * @author Tanguy Pruvot (C) 2010 * @license http://creativecommons.org/licenses/by-sa/2.5/dk/deed.en_GB CC-BY-SA-DK */ class SG_iCal { - private $information; - private $events; - private $timezones; + + //objects + public $information; //SG_iCal_VCalendar + public $timezones; //SG_iCal_VTimeZone + + protected $events; //SG_iCal_VEvent[] /** * Constructs a new iCalReader. You can supply the url now, or later using setUrl * @param $url string */ public function __construct($url = false) { - require_once dirname(__FILE__) . '/helpers/SG_iCal_Factory.php'; // BUILD: Remove line require_once dirname(__FILE__) . '/helpers/SG_iCal_Line.php'; // BUILD: Remove line - require_once dirname(__FILE__) . '/helpers/SG_iCal_Query.php'; // BUILD: Remove line + require_once dirname(__FILE__) . '/helpers/SG_iCal_Duration.php'; // BUILD: Remove line + require_once dirname(__FILE__) . '/helpers/SG_iCal_Freq.php'; // BUILD: Remove line + require_once dirname(__FILE__) . '/helpers/SG_iCal_Recurrence.php'; // BUILD: Remove line require_once dirname(__FILE__) . '/helpers/SG_iCal_Parser.php'; // BUILD: Remove line + require_once dirname(__FILE__) . '/helpers/SG_iCal_Query.php'; // BUILD: Remove line + require_once dirname(__FILE__) . '/helpers/SG_iCal_Factory.php'; // BUILD: Remove line - if( $url !== false ) { - SG_iCal_Parser::Parse($url, $this); - } + $this->setUrl($url); } /** @@ -50,16 +57,16 @@ class SG_iCal { SG_iCal_Parser::Parse($url, $this); } } - + /** * Returns the main calendar info. You can then query the returned - * object with ie getTitle(). + * object with ie getTitle(). * @return SG_iCal_VCalendar */ public function getCalendarInfo() { return $this->information; } - + /** * Sets the calendar info for this calendar * @param SG_iCal_VCalendar $info @@ -67,8 +74,8 @@ class SG_iCal { public function setCalendarInfo( SG_iCal_VCalendar $info ) { $this->information = $info; } - - + + /** * Returns a given timezone for the calendar. This is mainly used * by VEvents to adjust their date-times if they have specified a @@ -95,7 +102,7 @@ class SG_iCal { return null; } } - + /** * Adds a new timezone to this calendar * @param SG_iCal_VTimeZone $tz @@ -103,7 +110,7 @@ class SG_iCal { public function addTimeZone( SG_iCal_VTimeZone $tz ) { $this->timezones[] = $tz; } - + /** * Returns the events found * @return array diff --git a/blocks/SG_iCal_VCalendar.php b/blocks/SG_iCal_VCalendar.php index 138432a..1cdafd6 100755 --- a/blocks/SG_iCal_VCalendar.php +++ b/blocks/SG_iCal_VCalendar.php @@ -10,17 +10,17 @@ * @license http://creativecommons.org/licenses/by-sa/2.5/dk/deed.en_GB CC-BY-SA-DK */ class SG_iCal_VCalendar implements IteratorAggregate { - private $data; - + protected $data; + /** * Creates a new SG_iCal_VCalendar. */ public function __construct($data) { $this->data = $data; } - + /** - * Returns the title of the calendar. If no title is known, NULL + * Returns the title of the calendar. If no title is known, NULL * will be returned * @return string */ @@ -31,7 +31,7 @@ class SG_iCal_VCalendar implements IteratorAggregate { return null; } } - + /** * Returns the description of the calendar. If no description is * known, NULL will be returned. @@ -44,7 +44,7 @@ class SG_iCal_VCalendar implements IteratorAggregate { return null; } } - + /** * @see IteratorAggregate.getIterator() */ diff --git a/blocks/SG_iCal_VEvent.php b/blocks/SG_iCal_VEvent.php index d7f68b3..0e6c084 100755 --- a/blocks/SG_iCal_VEvent.php +++ b/blocks/SG_iCal_VEvent.php @@ -1,12 +1,12 @@ uid = $data['uid']->getData(); unset($data['uid']); @@ -38,33 +52,66 @@ class SG_iCal_VEvent { $this->recurrence = new SG_iCal_Recurrence($data['rrule']); unset($data['rrule']); } - + + if ( isset($data['exrule']) ) { + $this->recurex = new SG_iCal_Recurrence($data['exrule']); + unset($data['exrule']); + } + if( isset($data['dtstart']) ) { - $this->start = $this->getTimestamp( $data['dtstart'], $ical ); + $this->start = $this->getTimestamp($data['dtstart'], $ical); unset($data['dtstart']); } - + if( isset($data['dtend']) ) { $this->end = $this->getTimestamp($data['dtend'], $ical); unset($data['dtend']); } elseif( isset($data['duration']) ) { - require_once dirname(__FILE__).'/../helpers/SG_iCal_Duration.php'; // BUILD: Remove line $dur = new SG_iCal_Duration( $data['duration']->getData() ); $this->end = $this->start + $dur->getDuration(); unset($data['duration']); - } elseif ( isset($this->recurrence) ) { + } + + //google cal set dtend as end of initial event (duration) + if ( isset($this->recurrence) ) { //if there is a recurrence rule + + //exclusions + if ( isset($data['exdate']) ) { + foreach ($data['exdate'] as $exdate) { + foreach ($exdate->getDataAsArray() as $ts) { + $this->excluded[] = strtotime($ts); + } + } + unset($data['exdate']); + } + //additions + if ( isset($data['rdate']) ) { + foreach ($data['rdate'] as $rdate) { + foreach ($rdate->getDataAsArray() as $ts) { + $this->added[] = strtotime($ts); + } + } + unset($data['rdate']); + } + $until = $this->recurrence->getUntil(); $count = $this->recurrence->getCount(); //check if there is either 'until' or 'count' set - if ( $this->recurrence->getUntil() or $this->recurrence->getCount() ) { - //if until is set, set that as the end date (using getTimeStamp) - if ( $until ) { - $this->end = strtotime( $until ); - } + if ( $until ) { + //ok.. + } elseif ($count) { //if count is set, then figure out the last occurrence and set that as the end date + $this->getFrequency(); + $until = $this->freq->lastOccurrence($this->start); + } else { + //forever... limit to 3 years + $this->recurrence->setUntil('+3 years'); + $until = $this->recurrence->getUntil(); } - + //date_default_timezone_set( xx ) needed ?; + $this->laststart = strtotime($until); + $this->lastend = $this->laststart + $this->getDuration(); } $imports = array('summary','description','location'); @@ -74,10 +121,28 @@ class SG_iCal_VEvent { unset($data[$import]); } } - + + if( isset($this->previous_tz) ) { + date_default_timezone_set($this->previous_tz); + } + $this->data = SG_iCal_Line::Remove_Line($data); } - + + + /** + * Returns the Event Occurrences Iterator (if recurrence set) + * @return SG_iCal_Freq + */ + public function getFrequency() { + if (! isset($this->freq)) { + if ( isset($this->recurrence) ) { + $this->freq = new SG_iCal_Freq($this->recurrence->rrule, $this->start, $this->excluded, $this->added); + } + } + return $this->freq; + } + /** * Returns the UID of the event * @return string @@ -85,7 +150,7 @@ class SG_iCal_VEvent { public function getUID() { return $this->uid; } - + /** * Returns the summary (or null if none is given) of the event * @return string @@ -93,7 +158,7 @@ class SG_iCal_VEvent { public function getSummary() { return $this->summary; } - + /** * Returns the description (or null if none is given) of the event * @return string @@ -101,7 +166,7 @@ class SG_iCal_VEvent { public function getDescription() { return $this->description; } - + /** * Returns the location (or null if none is given) of the event * @return string @@ -109,7 +174,7 @@ class SG_iCal_VEvent { public function getLocation() { return $this->location; } - + /** * Returns true if the event is blocking (ie not transparent) * @return bool @@ -117,7 +182,7 @@ class SG_iCal_VEvent { public function isBlocking() { return !(isset($this->data['transp']) && $this->data['transp'] == 'TRANSPARENT'); } - + /** * Returns true if the event is confirmed * @return bool @@ -129,7 +194,19 @@ class SG_iCal_VEvent { return $this->data['status'] == 'CONFIRMED'; } } - + + /** + * Returns true if duration is multiple of 86400 + * @return bool + */ + public function isWholeDay() { + $dur = $this->getDuration(); + if ($dur > 0 && ($dur % 86400) == 0) { + return true; + } + return false; + } + /** * Returns the timestamp for the beginning of the event * @return int @@ -137,7 +214,7 @@ class SG_iCal_VEvent { public function getStart() { return $this->start; } - + /** * Returns the timestamp for the end of the event * @return int @@ -145,7 +222,15 @@ class SG_iCal_VEvent { public function getEnd() { return $this->end; } - + + /** + * Returns the timestamp for the end of the last event + * @return int + */ + public function getRangeEnd() { + return max($this->end,$this->lastend); + } + /** * Returns the duration of this event in seconds * @return int @@ -153,7 +238,7 @@ class SG_iCal_VEvent { public function getDuration() { return $this->end - $this->start; } - + /** * Returns the given property of the event. * @param string $prop @@ -168,19 +253,40 @@ class SG_iCal_VEvent { return null; } } - + + + + /** + * Set default timezone (temporary) to get timestamps + * @return string + */ + protected function setLineTimeZone(SG_iCal_Line $line) { + if( isset($line['tzid']) ) { + if (!isset($this->previous_tz)) { + $this->previous_tz = @ date_default_timezone_get(); + } + $this->tzid = $line['tzid']; + date_default_timezone_set($this->tzid); + return true; + } + return false; + } + /** * Calculates the timestamp from a DT line. * @param $line SG_iCal_Line * @return int */ - private function getTimestamp( SG_iCal_Line $line, SG_iCal $ical ) { - $ts = strtotime($line->getData()); + protected function getTimestamp( SG_iCal_Line $line, SG_iCal $ical ) { + if( isset($line['tzid']) ) { - $tz = $ical->getTimeZoneInfo($line['tzid']); - $offset = $tz->getOffset($ts); - $ts = strtotime(date('D, d M Y H:i:s', $ts) . ' ' . $offset); + $this->setLineTimeZone($line); + //$tz = $ical->getTimeZoneInfo($line['tzid']); + //$offset = $tz->getOffset($ts); + //$ts = strtotime(date('D, d M Y H:i:s', $ts) . ' ' . $offset); } + $ts = strtotime($line->getData()); + return $ts; } } diff --git a/blocks/SG_iCal_VTimeZone.php b/blocks/SG_iCal_VTimeZone.php index 506f90a..6b587ba 100755 --- a/blocks/SG_iCal_VTimeZone.php +++ b/blocks/SG_iCal_VTimeZone.php @@ -9,31 +9,31 @@ * @license http://creativecommons.org/licenses/by-sa/2.5/dk/deed.en_GB CC-BY-SA-DK */ class SG_iCal_VTimeZone { - private $tzid; - private $daylight; - private $standard; - private $cache = array(); - + protected $tzid; + protected $daylight; + protected $standard; + protected $cache = array(); + /** * Constructs a new SG_iCal_VTimeZone */ public function __construct( $data ) { require_once dirname(__FILE__).'/../helpers/SG_iCal_Freq.php'; // BUILD: Remove line - + $this->tzid = $data['tzid']; $this->daylight = $data['daylight']; $this->standard = $data['standard']; } - + /** - * Returns the timezone-id for this timezone. (Used to + * Returns the timezone-id for this timezone. (Used to * differentiate between different tzs in a calendar) * @return string */ public function getTimeZoneId() { return $this->tzid; } - + /** * Returns the given offset in this timezone for the given * timestamp. (eg +0200) @@ -44,7 +44,7 @@ class SG_iCal_VTimeZone { $act = $this->getActive($ts); return $this->{$act}['tzoffsetto']; } - + /** * Returns the timezone name for the given timestamp (eg CEST) * @param int $ts @@ -54,7 +54,7 @@ class SG_iCal_VTimeZone { $act = $this->getActive($ts); return $this->{$act}['tzname']; } - + /** * Determines which of the daylight or standard is the active * setting. @@ -65,20 +65,31 @@ class SG_iCal_VTimeZone { * @return string standard|daylight */ private function getActive( $ts ) { - if( isset($this->cache[$ts]) ) { + + if (class_exists('DateTimeZone')) { + + //PHP >= 5.2 + $tz = new DateTimeZone( $this->tzid ); + $date = new DateTime("@$ts", $tz); + return ($date->format('I') == 1) ? 'daylight' : 'standard'; + + } else { + + if( isset($this->cache[$ts]) ) { + return $this->cache[$ts]; + } + + $daylight_freq = new SG_iCal_Freq($this->daylight['rrule'], strtotime($this->daylight['dtstart'])); + $standard_freq = new SG_iCal_Freq($this->standard['rrule'], strtotime($this->standard['dtstart'])); + $last_standard = $standard_freq->previousOccurrence($ts); + $last_dst = $daylight_freq->previousOccurrence($ts); + if( $last_dst > $last_standard ) { + $this->cache[$ts] = 'daylight'; + } else { + $this->cache[$ts] = 'standard'; + } + return $this->cache[$ts]; } - - $daylight_freq = new SG_iCal_Freq($this->daylight['rrule'], strtotime($this->daylight['dtstart'])); - $standard_freq = new SG_iCal_Freq($this->standard['rrule'], strtotime($this->standard['dtstart'])); - $last_standard = $standard_freq->previousOccurrence($ts); - $last_dst = $daylight_freq->previousOccurrence($ts); - if( $last_dst > $last_standard ) { - $this->cache[$ts] = 'daylight'; - } else { - $this->cache[$ts] = 'standard'; - } - - return $this->cache[$ts]; } } diff --git a/build.cmd b/build.cmd new file mode 100644 index 0000000..dfce940 --- /dev/null +++ b/build.cmd @@ -0,0 +1,5 @@ +@SET OUTPUT=.\sgical.php + +cat SG_iCal.php | grep -v "BUILD: Remove line" > %OUTPUT% +DIR /B /S helpers | grep SG_iCal | grep -v svn | sed -e "s/\\/\//g" | xargs cat | grep -v "BUILD: Remove line" >> %OUTPUT% +DIR /B /S blocks | grep SG_iCal | grep -v svn | sed -e "s/\\/\//g" | xargs cat | grep -v "BUILD: Remove line" >> %OUTPUT% diff --git a/demo/basic.ics b/demo/basic.ics new file mode 100644 index 0000000..b63f591 --- /dev/null +++ b/demo/basic.ics @@ -0,0 +1,107 @@ +BEGIN:VCALENDAR +PRODID:-//Google Inc//Google Calendar 70.9054//EN +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:PUBLISH +X-WR-CALNAME:Particuliers - Grand Tour du Bassin +X-WR-TIMEZONE:Europe/Paris +X-WR-CALDESC: +BEGIN:VTIMEZONE +TZID:Europe/Paris +X-LIC-LOCATION:Europe/Paris +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +TZNAME:CEST +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +TZNAME:CET +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +DTSTART;TZID=Europe/Paris:20100526T143000 +DTEND;TZID=Europe/Paris:20100526T171500 +DTSTAMP:20101028T215738Z +UID:ptb5jqomu2vu0sr2nm24qungag@google.com +ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;CN=Partic + uliers - Grand Tour du Bassin;X-NUM-GUESTS=0:mailto:l2n7ajiud0oaiua4qcdarg1 + krg@group.calendar.google.com +RECURRENCE-ID;TZID=Europe/Paris:20100526T143000 +CREATED:20100428T225030Z +DESCRIPTION: +LAST-MODIFIED:20100527T102538Z +LOCATION:Jetée Thiers\, Arcachon +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:Horaire +TRANSP:TRANSPARENT +END:VEVENT +BEGIN:VEVENT +DTSTART;TZID=Europe/Paris:20100403T143000 +DTEND;TZID=Europe/Paris:20100403T171500 +RRULE:FREQ=WEEKLY;WKST=MO;UNTIL=20100630T123000Z;BYDAY=SU,WE,SA +DTSTAMP:20101028T215738Z +UID:ptb5jqomu2vu0sr2nm24qungag@google.com +ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=ACCEPTED;CN=Partic + uliers - Grand Tour du Bassin;X-NUM-GUESTS=0:mailto:l2n7ajiud0oaiua4qcdarg1 + krg@group.calendar.google.com +CREATED:20100428T225030Z +DESCRIPTION: +LAST-MODIFIED:20100518T052328Z +LOCATION:Jetée Thiers\, Arcachon +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:Horaire +TRANSP:TRANSPARENT +END:VEVENT +BEGIN:VEVENT +DTSTART;TZID=Europe/Paris:20101002T143000 +DTEND;TZID=Europe/Paris:20101002T171500 +RRULE:FREQ=WEEKLY;BYDAY=SU,SA;WKST=MO +DTSTAMP:20101028T215738Z +UID:41v060qrjvpvsv90n6ulljjeg4@google.com +CREATED:20100428T225427Z +DESCRIPTION: +LAST-MODIFIED:20100518T052328Z +LOCATION:Jetée Thiers\, Arcachon +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:Horaire +TRANSP:TRANSPARENT +END:VEVENT +BEGIN:VEVENT +DTSTART;TZID=Europe/Paris:20100703T143000 +DTEND;TZID=Europe/Paris:20100703T171500 +RRULE:FREQ=DAILY;UNTIL=20100930T123000Z;WKST=MO +DTSTAMP:20101028T215738Z +UID:92tmcm95ktr40ofn3okf0hhr2c@google.com +CREATED:20100428T225334Z +DESCRIPTION: +LAST-MODIFIED:20100518T052327Z +LOCATION:Jetée Thiers\, Arcachon +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:Horaire +TRANSP:TRANSPARENT +END:VEVENT +BEGIN:VEVENT +DTSTART:20100513T123000Z +DTEND:20100513T151500Z +DTSTAMP:20101028T215738Z +UID:254jd6v58v92k7bj69sgu3ulb0@google.com +CREATED:20100428T225221Z +DESCRIPTION: +LAST-MODIFIED:20100518T052327Z +LOCATION:Jetée Thiers\, Arcachon +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:Horaire +TRANSP:TRANSPARENT +END:VEVENT +END:VCALENDAR diff --git a/demo/exdate.ics b/demo/exdate.ics new file mode 100644 index 0000000..5869f00 --- /dev/null +++ b/demo/exdate.ics @@ -0,0 +1,43 @@ +BEGIN:VCALENDAR +PRODID:-//Google Inc//Google Calendar 70.9054//EN +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:PUBLISH +X-WR-CALNAME:uba.gestion@gmail.com +X-WR-TIMEZONE:Europe/Paris +BEGIN:VTIMEZONE +TZID:Europe/Paris +X-LIC-LOCATION:Europe/Paris +BEGIN:DAYLIGHT +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +TZNAME:CEST +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +TZNAME:CET +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +DTSTART;VALUE=DATE:20100907 +DTEND;VALUE=DATE:20100908 +RRULE:FREQ=DAILY;UNTIL=20100912 +EXDATE;VALUE=DATE:20100911 +EXDATE;VALUE=DATE:20100909,20100910 +DTSTAMP:20101031T155459Z +UID:5oo2ridecth26kcavj8elhtd4s@google.com +CREATED:00001231T000000Z +DESCRIPTION: +LAST-MODIFIED:20101030T193954Z +LOCATION: +SEQUENCE:1 +STATUS:CONFIRMED +SUMMARY:occur +TRANSP:TRANSPARENT +END:VEVENT +END:VCALENDAR \ No newline at end of file diff --git a/demo/fullcalendar.css b/demo/fullcalendar.css new file mode 100644 index 0000000..8047439 --- /dev/null +++ b/demo/fullcalendar.css @@ -0,0 +1,615 @@ +/* + * FullCalendar v1.4.8-IE9 Stylesheet + * + * Feel free to edit this file to customize the look of FullCalendar. + * When upgrading to newer versions, please upgrade this file as well, + * porting over any customizations afterwards. + * + * Date: Mon Oct 25 02:35:06 2010 +0200 + * + */ + + +/* TODO: make font sizes look the same in all doctypes */ + + +.fc, +.fc .fc-header, +.fc .fc-content { + font-size: 1em; + } + +.fc { + direction: ltr; + text-align: left; + } + +.fc table { + border-collapse: collapse; + border-spacing: 0; + } + +.fc td, .fc th { + padding: 0; + vertical-align: top; + } + + + +/* Header +------------------------------------------------------------------------*/ + +table.fc-header { + width: 100%; + } + +.fc-header-left { + width: 25%; + } + +.fc-header-left table { + float: left; + } + +.fc-header-center { + width: 50%; + text-align: center; + } + +.fc-header-center table { + margin: 0 auto; + } + +.fc-header-right { + width: 25%; + } + +.fc-header-right table { + float: right; + } + +.fc-header-title { + margin-top: 0; + white-space: nowrap; + } + +.fc-header-space { + padding-left: 10px; + } + +/* right-to-left */ + +.fc-rtl .fc-header-title { + direction: rtl; + } + + + +/* Buttons +------------------------------------------------------------------------*/ + +.fc-header .fc-state-default, +.fc-header .ui-state-default { + margin-bottom: 1em; + cursor: pointer; + } + +.fc-header .fc-state-default { + border-width: 1px 0; + padding: 0 1px; + } + +.fc-header .fc-state-default, +.fc-header .fc-state-default a { + border-style: solid; + } + +.fc-header .fc-state-default a { + display: block; + border-width: 0 1px; + margin: 0 -1px; + width: 100%; + text-decoration: none; + } + +.fc-header .fc-state-default span { + display: block; + border-style: solid; + border-width: 1px 0 1px 1px; + padding: 3px 5px; + } + +.fc-header .ui-state-default { + padding: 4px 6px; + } + +.fc-header .fc-state-default span, +.fc-header .ui-state-default span { + white-space: nowrap; + } + +/* for adjacent buttons */ + +.fc-header .fc-no-right { + padding-right: 0; + } + +.fc-header .fc-no-right a { + margin-right: 0; + border-right: 0; + } + +.fc-header .ui-no-right { + border-right: 0; + } + +/* for fake rounded corners */ + +.fc-header .fc-corner-left { + margin-left: 1px; + padding-left: 0; + } + +.fc-header .fc-corner-right { + margin-right: 1px; + padding-right: 0; + } + +/* DEFAULT button COLORS */ + +.fc-header .fc-state-default, +.fc-header .fc-state-default a { + border-color: #777; /* outer border */ + color: #333; + } + +.fc-header .fc-state-default span { + border-color: #fff #fff #d1d1d1; /* inner border */ + background: #e8e8e8; + } + +/* PRESSED button COLORS (down and active) */ + +.fc-header .fc-state-active a { + color: #fff; + } + +.fc-header .fc-state-down span, +.fc-header .fc-state-active span { + background: #888; + border-color: #808080 #808080 #909090; /* inner border */ + } + +/* DISABLED button COLORS */ + +.fc-header .fc-state-disabled a { + color: #999; + } + +.fc-header .fc-state-disabled, +.fc-header .fc-state-disabled a { + border-color: #ccc; /* outer border */ + } + +.fc-header .fc-state-disabled span { + border-color: #fff #fff #f0f0f0; /* inner border */ + background: #f0f0f0; + } + + + +/* Content Area & Global Cell Styles +------------------------------------------------------------------------*/ + +.fc-widget-content { + border: 1px solid #ccc; /* outer border color */ + } + +.fc-content { + clear: both; + } + +.fc-content .fc-state-default { + border-style: solid; + border-color: #ccc; /* inner border color */ + } + +.fc-content .ui-state-highlight, +.fc-content .fc-state-highlight { /* today */ + background: #ffd; + } + +.fc-content .fc-not-today { /* override jq-ui highlight (TODO: ui-widget-content) */ + background: none; + } + +.fc-content .fc-before-today { + background: #fafafa; + } + +.fc-cell-overlay { /* semi-transparent rectangle while dragging */ + background: #9cf; + opacity: .2; + filter: alpha(opacity=20); /* for IE */ + } + +.fc-view { /* prevents dragging outside of widget */ + width: 100%; + overflow: hidden; + } + + + + + +/* Global Event Styles +------------------------------------------------------------------------*/ + +.fc-event, +.fc-agenda .fc-event-time, +.fc-event a { + border-style: solid; + border-color: #36c; /* default BORDER color (probably the same as background-color) */ + background-color: #36c; /* default BACKGROUND color */ + color: #fff; /* default TEXT color */ + } + + /* Use the 'className' CalEvent property and the following + * example CSS to change event color on a per-event basis: + * + * .myclass, + * .fc-agenda .myclass .fc-event-time, + * .myclass a { + * background-color: black; + * border-color: black; + * color: red; + * } + */ + +.fc-event { + text-align: left; + } + +.fc-event a { + overflow: hidden; + font-size: .85em; + text-decoration: none; + cursor: pointer; + } + +.fc-event-editable { + cursor: pointer; + } + +.fc-event-time, +.fc-event-title { + padding: 0 1px; + } + +/* for fake rounded corners */ + +.fc-event a { + display: block; + position: relative; + width: 100%; + height: 100%; + } + +/* right-to-left */ + +.fc-rtl .fc-event a { + text-align: right; + } + +/* resizable */ + +.fc .ui-resizable-handle { + display: block; + position: absolute; + z-index: 99999; + border: 0 !important; /* important overrides pre jquery ui 1.7 styles */ + background: url(data:image/gif;base64,AAAA) !important; /* hover fix for IE */ + } + + + +/* Horizontal Events +------------------------------------------------------------------------*/ + +.fc-event-hori { + border-width: 1px 0; + margin-bottom: 1px; + } + +.fc-event-hori a { + border-width: 0; + } + +/* for fake rounded corners */ + +.fc-content .fc-corner-left { + margin-left: 1px; + } + +.fc-content .fc-corner-left a { + margin-left: -1px; + border-left-width: 1px; + } + +.fc-content .fc-corner-right { + margin-right: 1px; + } + +.fc-content .fc-corner-right a { + margin-right: -1px; + border-right-width: 1px; + } + +/* resizable */ + +.fc-event-hori .ui-resizable-e { + top: 0 !important; /* importants override pre jquery ui 1.7 styles */ + right: -3px !important; + width: 7px !important; + height: 100% !important; + cursor: e-resize; + } + +.fc-event-hori .ui-resizable-w { + top: 0 !important; + left: -3px !important; + width: 7px !important; + height: 100% !important; + cursor: w-resize; + } + +.fc-event-hori .ui-resizable-handle { + _padding-bottom: 14px; /* IE6 had 0 height */ + } + + + + +/* Month View, Basic Week View, Basic Day View +------------------------------------------------------------------------*/ + +.fc-grid table { + width: 100%; + } + +.fc .fc-grid th { + border-width: 0 0 0 1px; + text-align: center; + } + +.fc .fc-grid td { + border-width: 1px 0 0 1px; + } + +.fc-grid th.fc-leftmost, +.fc-grid td.fc-leftmost { + border-left: 0; + } + +.fc-grid .fc-day-number { + float: right; + padding: 0 2px; + } + +.fc-grid .fc-other-month .fc-day-number { + opacity: 0.3; + filter: alpha(opacity=30); /* for IE */ + /* opacity with small font can sometimes look too faded + might want to set the 'color' property instead + making day-numbers bold also fixes the problem */ + } + +.fc-grid .fc-day-content { + clear: both; + padding: 2px 2px 0; /* distance between events and day edges */ + } + +/* event styles */ + +.fc-grid .fc-event-time { + font-weight: bold; + } + +/* right-to-left */ + +.fc-rtl .fc-grid { + direction: rtl; + } + +.fc-rtl .fc-grid .fc-day-number { + float: left; + } + +.fc-rtl .fc-grid .fc-event-time { + float: right; + } + +/* week numbers */ + +.fc .fc-grid th.fc-weeknumber { + border-top-width: 1px; +} +/* Agenda Week View, Agenda Day View +------------------------------------------------------------------------*/ + +.fc .fc-agenda th, +.fc .fc-agenda td { + border-width: 1px 0 0 1px; + } + +.fc .fc-agenda .fc-leftmost { + border-left: 0; + } + +.fc-agenda tr.fc-first th, +.fc-agenda tr.fc-first td { + border-top: 0; + } + +.fc-agenda-head tr.fc-last th { + border-bottom-width: 1px; + } + +.fc .fc-agenda-head td, +.fc .fc-agenda-body td { + background: none; + } + +.fc-agenda-head th { + text-align: center; + } + +/* the time axis running down the left side */ + +.fc-agenda .fc-axis { + width: 50px; + padding: 0 4px; + vertical-align: middle; + white-space: nowrap; + text-align: right; + font-weight: normal; + } + +/* all-day event cells at top */ + +.fc-agenda-head tr.fc-all-day th { + height: 35px; + } + +.fc-agenda-head td { + padding-bottom: 10px; + } + +.fc .fc-divider div { + font-size: 1px; /* for IE6/7 */ + height: 2px; + } + +.fc .fc-divider .fc-state-default { + background: #eee; /* color for divider between all-day and time-slot events */ + } + +/* body styles */ + +.fc .fc-agenda-body td div { + height: 20px; /* slot height */ + } + +.fc .fc-agenda-body tr.fc-minor th, +.fc .fc-agenda-body tr.fc-minor td { + border-top-style: dotted; + } + +.fc-agenda .fc-day-content { + padding: 2px 2px 0; /* distance between events and day edges */ + } + +/* vertical background columns */ + +.fc .fc-agenda-bg .ui-state-highlight { + background-image: none; /* tall column, don't want repeating background image */ + } + + + +/* Vertical Events +------------------------------------------------------------------------*/ + +.fc-event-vert { + border-width: 0 1px; + } + +.fc-event-vert a { + border-width: 0; + } + +/* for fake rounded corners */ + +.fc-content .fc-corner-top { + margin-top: 1px; + } + +.fc-content .fc-corner-top a { + margin-top: -1px; + border-top-width: 1px; + } + +.fc-content .fc-corner-bottom { + margin-bottom: 1px; + } + +.fc-content .fc-corner-bottom a { + margin-bottom: -1px; + border-bottom-width: 1px; + } + +/* event content */ + +.fc-event-vert span { + display: block; + position: relative; + z-index: 2; + } + +.fc-event-vert span.fc-event-time { + white-space: nowrap; + _white-space: normal; + overflow: hidden; + border: 0; + font-size: 10px; + } + +.fc-event-vert span.fc-event-title { + line-height: 13px; + } + +.fc-event-vert span.fc-event-bg { /* makes the event lighter w/ a semi-transparent overlay */ + position: absolute; + z-index: 1; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: #fff; + opacity: .3; + filter: alpha(opacity=30); /* for IE */ + } + +/* resizable */ + +.fc-event-vert .ui-resizable-s { + bottom: 0 !important; /* importants override pre jquery ui 1.7 styles */ + width: 100% !important; + height: 8px !important; + line-height: 8px !important; + font-size: 11px !important; + font-family: monospace; + text-align: center; + cursor: s-resize; + } + + + +/* JOMRES */ +.gcal-blackbooking-0, +.fc-agenda .gcal-blackbooking-0 .fc-event-time, +.gcal-blackbooking-0 a { + border-style: solid; + border-color: #400; /* default BORDER color (probably the same as background-color) */ + background-color: #D60; /* default BACKGROUND color */ + color: #fff; /* default TEXT color */ +} + +.gcal-blackbooking-1, +.fc-agenda .gcal-blackbooking-1 .fc-event-time, +.gcal-blackbooking-1 a { + border-style: solid; + border-color: #400; /* default BORDER color (probably the same as background-color) */ + background-color: #800; /* default BACKGROUND color */ + color: #fff; /* default TEXT color */ +} diff --git a/demo/fullcalendar.js b/demo/fullcalendar.js new file mode 100644 index 0000000..5f19482 --- /dev/null +++ b/demo/fullcalendar.js @@ -0,0 +1,4765 @@ +/** + * @preserve + * FullCalendar v1.4.9-gcalendar + * http://arshaw.com/fullcalendar/ + * + * with some adaptations for joomla gcalendar component + * http://github.com/tpruvot/fullcalendar + * + * Use fullcalendar.css for basic styling. + * For event drag & drop, requires jQuery UI draggable. + * For event resizing, requires jQuery UI resizable. + * + * Copyright (c) 2010 Adam Shaw + * Dual licensed under the MIT and GPL licenses, located in + * MIT-LICENSE.txt and GPL-LICENSE.txt respectively. + * + * Date: Wed Oct 27 01:12:13 2010 +0200 + * + */ + +(function($, undefined) { + + +var defaults = { + + // display + defaultView: 'month', + aspectRatio: 1.35, + header: { + left: 'title', + center: '', + right: 'today prev,next' + }, + weekends: true, + + // editing + //editable: false, + //disableDragging: false, + //disableResizing: false, + + allDayDefault: true, + ignoreTimezone: true, + + // event ajax + lazyFetching: true, + startParam: 'start', + endParam: 'end', + + // time formats + titleFormat: { + month: 'MMMM yyyy', + week: "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", + day: 'dddd, MMM d, yyyy' + }, + columnFormat: { + month: 'ddd', + week: 'ddd M/d', + day: 'dddd M/d' + }, + timeFormat: { // for event elements + '': 'h(:mm)t' // default + }, + + // locale + isRTL: false, + firstDay: 0, + monthNames: ['January','February','March','April','May','June','July','August','September','October','November','December'], + monthNamesShort: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'], + dayNames: ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'], + dayNamesShort: ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'], + buttonText: { + prev: ' ◄ ', + next: ' ► ', + prevYear: ' << ', + nextYear: ' >> ', + today: 'today', + month: 'month', + week: 'week', + day: 'day' + }, + + // jquery-ui theming + theme: false, + buttonIcons: { + prev: 'circle-triangle-w', + next: 'circle-triangle-e' + }, + + //selectable: false, + unselectAuto: true, + + dropAccept: '*' + +}; + +// right-to-left defaults +var rtlDefaults = { + header: { + left: 'next,prev today', + center: '', + right: 'title' + }, + buttonText: { + prev: ' ► ', + next: ' ◄ ', + prevYear: ' >> ', + nextYear: ' << ' + }, + buttonIcons: { + prev: 'circle-triangle-e', + next: 'circle-triangle-w' + } +}; + + + +var fc = $.fullCalendar = { version: "1.4.9-gcalendar" }; +var fcViews = fc.views = {}; + + +//IE9 dnd fix (jQuery UI Mouse (<= 1.8.5) doesnt support IE9) +var msie9 = false; +if ($.ui && $.browser.msie && parseInt($.browser.version,10) >= 9) { + msie9 = true; + var mm=$.ui.mouse.prototype._mouseMove; + $.ui.mouse.prototype._mouseMove=function(b){b.button=1;mm.apply(this,[b]);} +} + + +$.fn.fullCalendar = function(options) { + + + // method calling + if (typeof options == 'string') { + var args = Array.prototype.slice.call(arguments, 1); + var res; + this.each(function() { + var calendar = $.data(this, 'fullCalendar'); + if (calendar && $.isFunction(calendar[options])) { + var r = calendar[options].apply(calendar, args); + if (res === undefined) { + res = r; + } + if (options == 'destroy') { + $.removeData(this, 'fullCalendar'); + } + } + }); + if (res !== undefined) { + return res; + } + return this; + } + + + // would like to have this logic in EventManager, but needs to happen before options are extended + var eventSources = options.eventSources || []; + delete options.eventSources; + if (options.events) { + eventSources.push(options.events); + delete options.events; + } + + + options = $.extend(true, {}, + defaults, + (options.isRTL || options.isRTL===undefined && defaults.isRTL) ? rtlDefaults : {}, + options + ); + + + this.each(function(i, _element) { + var element = $(_element); + var calendar = new Calendar(element, options, eventSources); + element.data('fullCalendar', calendar); // TODO: look into memory leak implications + calendar.render(); + }); + + + return this; + +}; + + +// function for adding/overriding defaults +function setDefaults(d) { + $.extend(true, defaults, d); +} + + + + +function Calendar(element, options, eventSources) { + var t = this; + + + // exports + t.options = options; + t.render = render; + t.destroy = destroy; + t.refetchEvents = refetchEvents; + t.reportEvents = reportEvents; + t.reportEventChange = reportEventChange; + t.changeView = changeView; + t.select = select; + t.unselect = unselect; + t.prev = prev; + t.next = next; + t.prevYear = prevYear; + t.nextYear = nextYear; + t.today = today; + t.gotoDate = gotoDate; + t.incrementDate = incrementDate; + t.formatDate = function(format, date) { return formatDate(format, date, options) }; + t.formatDates = function(format, date1, date2) { return formatDates(format, date1, date2, options) }; + t.getDate = getDate; + t.getView = getView; + t.option = option; + t.trigger = trigger; + + + // imports + EventManager.call(t, options, eventSources); + var isFetchNeeded = t.isFetchNeeded; + var fetchEvents = t.fetchEvents; + + + // locals + var _element = element[0]; + var header; + var headerElement; + var content; + var tm; // for making theme classes + var currentView; + var viewInstances = {}; + var elementOuterWidth; + var suggestedViewHeight; + var absoluteViewElement; + var resizeUID = 0; + var ignoreWindowResize = 0; + var date = new Date(); + var events = []; + + + + /* Main Rendering + -----------------------------------------------------------------------------*/ + + + setYMD(date, options.year, options.month, options.date); + + + function render(inc) { + if (!content) { + initialRender(); + }else{ + calcSize(); + markSizesDirty(); + markEventsDirty(); + renderView(inc); + } + } + + + function initialRender() { + tm = options.theme ? 'ui' : 'fc'; + element.addClass('fc'); + if (options.isRTL) { + element.addClass('fc-rtl'); + } + if (options.theme) { + element.addClass('ui-widget'); + } + content = $("
") + .prependTo(element); + header = new Header(t, options); + headerElement = header.render(); + if (headerElement) { + element.prepend(headerElement); + } + changeView(options.defaultView); + $(window).resize(windowResize); + // needed for IE in a 0x0 iframe, b/c when it is resized, never triggers a windowResize + if (!bodyVisible()) { + lateRender(); + } + } + + + // called when we know the calendar couldn't be rendered when it was initialized, + // but we think it's ready now + function lateRender() { + setTimeout(function() { // IE7 needs this so dimensions are calculated correctly + if (!currentView.start && bodyVisible()) { // !currentView.start makes sure this never happens more than once + renderView(); + } + },0); + } + + + function destroy() { + $(window).unbind('resize', windowResize); + header.destroy(); + content.remove(); + element.removeClass('fc fc-rtl fc-ui-widget'); + } + + + + function elementVisible() { + return _element.offsetWidth !== 0; + } + + + function bodyVisible() { + return $('body')[0].offsetWidth !== 0; + } + + + + /* View Rendering + -----------------------------------------------------------------------------*/ + + + function changeView(newViewName) { + if (!currentView || newViewName != currentView.name) { + ignoreWindowResize++; // because setMinHeight might change the height before render (and subsequently setSize) is reached + + unselect(); + + var oldView = currentView; + var newViewElement; + + if (oldView) { + (oldView.beforeHide || noop)(); // called before changing min-height. if called after, scroll state is reset (in Opera) + setMinHeight(content, getHeight(content)); + oldView.element.hide(); + }else{ + setMinHeight(content, 1); // needs to be 1 (not 0) for IE7, or else view dimensions miscalculated + } + content.css('overflow', 'hidden'); + + currentView = viewInstances[newViewName]; + if (currentView) { + currentView.element.show(); + }else{ + currentView = viewInstances[newViewName] = new fcViews[newViewName]( + newViewElement = absoluteViewElement = + $("
") + .appendTo(content), + t // the calendar object + ); + } + + if (oldView) { + header.deactivateButton(oldView.name); + } + header.activateButton(newViewName); + + renderView(); // after height has been set, will make absoluteViewElement's position=relative, then set to null + + content.css('overflow', ''); + if (oldView) { + setMinHeight(content, 1); + } + + if (!newViewElement) { + (currentView.afterShow || noop)(); // called after setting min-height/overflow, so in final scroll state (for Opera) + } + + ignoreWindowResize--; + } + } + + + + function renderView(inc) { + if (elementVisible()) { + ignoreWindowResize++; // because renderEvents might temporarily change the height before setSize is reached + + unselect(); + + if (suggestedViewHeight === undefined) { + calcSize(); + } + + var forceEventRender = false; + if (!currentView.start || inc || date < currentView.start || date >= currentView.end) { + // view must render an entire new date range (and refetch/render events) + currentView.render(date, inc || 0); // responsible for clearing events + setSize(true); + forceEventRender = true; + } + else if (currentView.sizeDirty) { + // view must resize (and rerender events) + currentView.clearEvents(); + setSize(); + forceEventRender = true; + } + else if (currentView.eventsDirty) { + currentView.clearEvents(); + forceEventRender = true; + } + currentView.sizeDirty = false; + currentView.eventsDirty = false; + updateEvents(forceEventRender); + + elementOuterWidth = element.outerWidth(); + + header.updateTitle(currentView.title); + var today = new Date(); + if (today >= currentView.start && today < currentView.end) { + header.disableButton('today'); + }else{ + header.enableButton('today'); + } + + ignoreWindowResize--; + currentView.trigger('viewDisplay', _element); + } + } + + + + /* Resizing + -----------------------------------------------------------------------------*/ + + + function updateSize() { + markSizesDirty(); + if (elementVisible()) { + calcSize(); + setSize(); + unselect(); + currentView.renderEvents(events); + currentView.sizeDirty = false; + } + } + + + function markSizesDirty() { + $.each(viewInstances, function(i, inst) { + inst.sizeDirty = true; + }); + } + + + function calcSize() { + if (options.contentHeight) { + suggestedViewHeight = options.contentHeight; + } + else if (options.height) { + suggestedViewHeight = options.height - vsides(content[0]) - (headerElement ? getHeight(headerElement) : 0); + } + else { + suggestedViewHeight = Math.round(getWidth(content) / Math.max(options.aspectRatio, .5)); + } + } + + + function setSize(dateChanged) { // todo: dateChanged? + ignoreWindowResize++; + currentView.setHeight(suggestedViewHeight, dateChanged); + if (absoluteViewElement) { + absoluteViewElement.css('position', 'relative'); + absoluteViewElement = null; + } + currentView.setWidth(getWidth(content), dateChanged); + ignoreWindowResize--; + } + + + function windowResize() { + if (!ignoreWindowResize) { + if (currentView.start) { // view has already been rendered + var uid = ++resizeUID; + setTimeout(function() { // add a delay + if (uid == resizeUID && !ignoreWindowResize && elementVisible()) { + if (elementOuterWidth != (elementOuterWidth = element.outerWidth())) { + ignoreWindowResize++; // in case the windowResize callback changes the height + updateSize(); + currentView.trigger('windowResize', _element); + ignoreWindowResize--; + } + } + }, 200); + }else{ + // calendar must have been initialized in a 0x0 iframe that has just been resized + lateRender(); + } + } + } + + + + /* Event Fetching/Rendering + -----------------------------------------------------------------------------*/ + + + // fetches events if necessary, rerenders events if necessary (or if forced) + function updateEvents(forceRender) { + if (!options.lazyFetching || isFetchNeeded(currentView.visStart, currentView.visEnd)) { + refetchEvents(); + } + else if (forceRender) { + rerenderEvents(); + } + } + + + function refetchEvents() { + fetchEvents(currentView.visStart, currentView.visEnd); // will call reportEvents + } + + + // called when event data arrives + function reportEvents(_events) { + events = _events; + rerenderEvents(); + } + + + // called when a single event's data has been changed + function reportEventChange(eventID) { + rerenderEvents(eventID); + } + + + // attempts to rerenderEvents + function rerenderEvents(modifiedEventID) { + markEventsDirty(); + if (elementVisible()) { + currentView.clearEvents(); + currentView.renderEvents(events, modifiedEventID); + currentView.eventsDirty = false; + } + } + + + function markEventsDirty() { + $.each(viewInstances, function(i, inst) { + inst.eventsDirty = true; + }); + } + + + + /* Selection + -----------------------------------------------------------------------------*/ + + + function select(start, end, allDay) { + currentView.select(start, end, allDay===undefined ? true : allDay); + } + + + function unselect() { // safe to be called before renderView + if (currentView) { + currentView.unselect(); + } + } + + + + /* Date + -----------------------------------------------------------------------------*/ + + + function prev() { + renderView(-1); + } + + + function next() { + renderView(1); + } + + + function prevYear() { + addYears(date, -1); + renderView(); + } + + + function nextYear() { + addYears(date, 1); + renderView(); + } + + + function today() { + date = new Date(); + renderView(); + } + + + function gotoDate(year, month, dateOfMonth) { + if (year instanceof Date) { + date = cloneDate(year); // provided 1 argument, a Date + }else{ + setYMD(date, year, month, dateOfMonth); + } + renderView(); + } + + + function incrementDate(years, months, days) { + if (years !== undefined) { + addYears(date, years); + } + if (months !== undefined) { + addMonths(date, months); + } + if (days !== undefined) { + addDays(date, days); + } + renderView(); + } + + + function getDate() { + return cloneDate(date); + } + + + + /* Misc + -----------------------------------------------------------------------------*/ + + + function getView() { + return currentView; + } + + + function option(name, value) { + if (value === undefined) { + return options[name]; + } + if (name == 'height' || name == 'contentHeight' || name == 'aspectRatio') { + options[name] = value; + updateSize(); + } + } + + + function trigger(name, thisObj) { + if (options[name]) { + return options[name].apply( + thisObj || _element, + Array.prototype.slice.call(arguments, 2) + ); + } + } + + + + /* External Dragging + ------------------------------------------------------------------------*/ + + var _dragElement; + + if (options.droppable) { + $(document) + .bind('dragstart', function(ev, ui) { + var _e = ev.target; + var e = $(_e); + if (!e.parents('.fc').length) { // not already inside a calendar + var accept = options.dropAccept; + if ($.isFunction(accept) ? accept.call(_e, e) : e.is(accept)) { + _dragElement = _e; + currentView.dragStart(_dragElement, ev, ui); + } + } + }) + .bind('dragstop', function(ev, ui) { + if (_dragElement) { + currentView.dragStop(_dragElement, ev, ui); + _dragElement = null; + } + }); + } + + +} + +function Header(calendar, options) { + var t = this; + + + // exports + t.render = render; + t.destroy = destroy; + t.updateTitle = updateTitle; + t.activateButton = activateButton; + t.deactivateButton = deactivateButton; + t.disableButton = disableButton; + t.enableButton = enableButton; + + + // locals + var element = $([]); + var tm; + + + + function render() { + tm = options.theme ? 'ui' : 'fc'; + var sections = options.header; + if (sections) { + element = $("") + .append($("") + .append($(""); + $.each(buttonStr.split(' '), function(i) { + if (i > 0) { + tr.append(""); + } + var prevButton; + $.each(this.split(','), function(j, buttonName) { + if (buttonName == 'title') { + tr.append(""); + if (prevButton) { + prevButton.addClass(tm + '-corner-right'); + } + prevButton = null; + }else{ + var buttonClick; + if (calendar[buttonName]) { + buttonClick = calendar[buttonName]; // calendar method + } + else if (fcViews[buttonName]) { + buttonClick = function() { + button.removeClass(tm + '-state-hover'); // forget why + calendar.changeView(buttonName); + }; + } + if (buttonClick) { + if (prevButton) { + prevButton.addClass(tm + '-no-right'); + } + var button; + var icon = options.theme ? smartProperty(options.buttonIcons, buttonName) : null; + var text = smartProperty(options.buttonText, buttonName); + if (icon) { + button = $("
" + + "
"); + } + else if (text) { + button = $(""); + } + if (button) { + button + .click(function() { + if (!button.hasClass(tm + '-state-disabled')) { + buttonClick(); + } + }) + .mousedown(function() { + button + .not('.' + tm + '-state-active') + .not('.' + tm + '-state-disabled') + .addClass(tm + '-state-down'); + }) + .mouseup(function() { + button.removeClass(tm + '-state-down'); + }) + .hover( + function() { + button + .not('.' + tm + '-state-active') + .not('.' + tm + '-state-disabled') + .addClass(tm + '-state-hover'); + }, + function() { + button + .removeClass(tm + '-state-hover') + .removeClass(tm + '-state-down'); + } + ) + .appendTo($("
") + .append(renderSection(sections.left))) + .append($("") + .append(renderSection(sections.center))) + .append($("") + .append(renderSection(sections.right)))); + return element; + } + } + + + function destroy() { + element.remove(); + } + + + function renderSection(buttonStr) { + if (buttonStr) { + var tr = $("

 

").appendTo(tr)); + if (prevButton) { + prevButton.addClass(tm + '-no-right'); + }else{ + button.addClass(tm + '-corner-left'); + } + disableTextSelection(button.closest('td')); + prevButton = button; + } + } + } + }); + if (prevButton) { + prevButton.addClass(tm + '-corner-right'); + } + }); + return $("").append(tr); + } + } + + + function updateTitle(html) { + element.find('h2.fc-header-title') + .html(html); + } + + + function activateButton(buttonName) { + element.find('div.fc-button-' + buttonName) + .addClass(tm + '-state-active'); + } + + + function deactivateButton(buttonName) { + element.find('div.fc-button-' + buttonName) + .removeClass(tm + '-state-active'); + } + + + function disableButton(buttonName) { + element.find('div.fc-button-' + buttonName) + .addClass(tm + '-state-disabled'); + } + + + function enableButton(buttonName) { + element.find('div.fc-button-' + buttonName) + .removeClass(tm + '-state-disabled'); + } + + +} + +var eventGUID = 1; + +function EventManager(options, sources) { + var t = this; + + + // exports + t.isFetchNeeded = isFetchNeeded; + t.fetchEvents = fetchEvents; + t.addEventSource = addEventSource; + t.addEventSourceFast = addEventSourceFast; + t.clearEventSources = clearEventSources; + t.removeEventSource = removeEventSource; + t.updateEvent = updateEvent; + t.renderEvent = renderEvent; + t.removeEvents = removeEvents; + t.clientEvents = clientEvents; + t.normalizeEvent = normalizeEvent; + + + // imports + var trigger = t.trigger; + var getView = t.getView; + var reportEvents = t.reportEvents; + + + // locals + var rangeStart, rangeEnd; + var currentFetchID = 0; + var pendingSourceCnt = 0; + var loadingLevel = 0; + var dynamicEventSource = []; + var cache = []; + + + + /* Fetching + -----------------------------------------------------------------------------*/ + function isFetchNeeded(start, end) { + return !rangeStart || start < rangeStart || end > rangeEnd; + } + + function fetchEvents(start, end) { + rangeStart = start; + rangeEnd = end; + currentFetchID++; + cache = []; + pendingSourceCnt = sources.length; + for (var i=0; i").appendTo(element); + + s = ""; + if (wkn) { + s += ""; + } + for (i=0; i" + formatDate(d, colFormat) + ""; + addDays(d, 1); + if (nwe) { + skipWeekend(d); + } + } + thead = $(s + "").appendTo(table); + + s = ""; + d = cloneDate(t.visStart); + for (i=0; i"; + if (wkn) { + var monday = cloneDate(d); //skip first day (could be sunday) + addDays(monday, 1); + s += ""; + } + for (j=0; j1 && d.getMonth() != month ? ' fc-other-month' : '') + + (+d == +today ? + ' fc-today '+tm+'-state-highlight' : + (+d < +today ? ' fc-before-today fc-not-today' : + ' fc-not-today')) + "'>" + + (showNumbers ? "
" + d.getDate() + "
" : '') + + "
 
"; + addDays(d, 1); + if (nwe) { + skipWeekend(d); + } + } + s += "
"; + } + tbody = $(s + "").appendTo(table); + dayBind(tbody.find('td.fc-new').removeClass('fc-new')); + + daySegmentContainer = $("
").appendTo(element); + + }else{ // NOT first time, reuse as many cells as possible + + clearEvents(); + + var prevRowCnt = tbody.find('tr').length; + if (rowCnt < prevRowCnt) { + tbody.find('tr:gt(' + (rowCnt-1) + ')').remove(); // remove extra rows + } + else if (rowCnt > prevRowCnt) { // needs to create new rows... + s = ''; + for (i=prevRowCnt; i"; + if (wkn) { + var monday = cloneDate(d); //skip first day (could be sunday) + addDays(monday, 1); + s += "
"; + } + for (j=0; j" + + (showNumbers ? "
" : '') + + "
 
" + + ""; + addDays(d, 1); + if (nwe) { + skipWeekend(d); + } + } + s += "
"; + } + tbody.append(s); + } + dayBind(tbody.find('td.fc-new').removeClass('fc-new')); + + // re-label and re-class existing cells + d = cloneDate(t.visStart); + tbody.find('td').each(function() { + var td = $(this); + if (wkn && d.getDay() == 1) + td.closest('tr').find('th').text(d.getWeek()); + if (rowCnt > 1) { + if (d.getMonth() == month) { + td.removeClass('fc-other-month'); + }else{ + td.addClass('fc-other-month'); + } + } + if (+d == +today) { + td.removeClass('fc-not-today fc-before-today') + .addClass(tm + '-state-highlight fc-today'); + }else if (+d < +today) { + td.addClass('fc-not-today fc-before-today') + .removeClass(tm + '-state-highlight fc-today'); + }else{ + td.addClass('fc-not-today') + .removeClass(tm + '-state-highlight fc-today fc-before-today'); + } + td.find('div.fc-day-number').text(d.getDate()); + addDays(d, 1); + if (nwe) { + skipWeekend(d); + } + }); + + if (rowCnt == 1) { // more changes likely (week or day view) + + // redo column header text and class + d = cloneDate(t.visStart); + thead.find('th').not('.fc-axis').each(function(i, th) { + $(th).text(formatDate(d, colFormat)); + th.className = th.className.replace(/^fc-\w+(?= )/, 'fc-' + dayIDs[d.getDay()]); + addDays(d, 1); + if (nwe) { + skipWeekend(d); + } + }); + + // redo cell day-of-weeks + d = cloneDate(t.visStart); + tbody.find('td').each(function(i, td) { + td.className = td.className.replace(/^fc-\w+(?= )/, 'fc-' + dayIDs[d.getDay()]); + addDays(d, 1); + if (nwe) { + skipWeekend(d); + } + }); + + } + + } + + } + + + function setHeight(height) { + viewHeight = height; + var leftTDs = tbody.find('tr td:first-child'), + tbodyHeight = viewHeight - thead.outerHeight(), + rowHeight1, rowHeight2; + if (wkn) + leftTDs = tbody.find('tr th:first-child'); + if (opt('weekMode') == 'variable') { + rowHeight1 = rowHeight2 = Math.floor(tbodyHeight / (rowCnt==1 ? 2 : 6)); + }else{ + rowHeight1 = Math.floor(tbodyHeight / rowCnt); + rowHeight2 = tbodyHeight - rowHeight1*(rowCnt-1); + } + if (tdHeightBug === undefined) { + // bug in firefox where cell height includes padding + var tr = tbody.find('tr:first'), + td = tr.find('td:first'); + setOuterHeight(td,rowHeight1); + tdHeightBug = rowHeight1 != td.outerHeight(); + } + if (tdHeightBug) { + leftTDs.slice(0, -1).height(rowHeight1); + leftTDs.slice(-1).height(rowHeight2); + }else{ + setOuterHeight(leftTDs.slice(0, -1), rowHeight1); + setOuterHeight(leftTDs.slice(-1), rowHeight2); + } + } + + + function setWidth(width) { + viewWidth = width; + colContentPositions.clear(); + if (wkn) { + colWidth = Math.floor((viewWidth-25) / colCnt); + setOuterWidth(thead.find('th').slice(1, -1), colWidth); + } else { + colWidth = Math.floor(viewWidth / colCnt); + setOuterWidth(thead.find('th').slice(0, -1), colWidth); + } + } + + + /* Day clicking and binding + -----------------------------------------------------------*/ + + + function dayBind(days) { + days.click(dayClick) + .mousedown(daySelectionMousedown); + } + + + function dayClick(ev) { + if (!opt('selectable')) { // SelectionManager will worry about dayClick + var n = parseInt(this.className.match(/fc\-day(\d+)/)[1],10), + date = addDays( + cloneDate(t.visStart), + Math.floor(n/colCnt) * 7 + n % colCnt + ); + // TODO: what about weekends in middle of week? + trigger('dayClick', this, date, true, ev); + } + } + + + + /* Semi-transparent Overlay Helpers + ------------------------------------------------------*/ + + + function renderDayOverlay(overlayStart, overlayEnd, refreshCoordinateGrid) { // overlayEnd is exclusive + if (refreshCoordinateGrid) { + coordinateGrid.build(); + } + var rowStart = cloneDate(t.visStart); + var rowEnd = addDays(cloneDate(rowStart), colCnt); + for (var i=0; i" + + "" + + ""; + for (i=0; i" + formatDate(d, colFormat) + ""; + addDays(d, dis); + if (nwe) { + skipWeekend(d, dis); + } + } + s += ""; + if (opt('allDaySlot')) { + s += "" + + "" + + "" + + "" + + ""; + } + s+= "
 
"+monday.getWeek()+"
"+monday.getWeek()+"
" + (opt('weekNumbers') ? formatDate(t.visStart, 'w') : ' ') + " 
" + opt('allDayText') + "" + + "
 
 
"; + head = $(s).appendTo(element); + dayBind(head.find('td')); + + // all-day event container + daySegmentContainer = $("
").appendTo(head); + + // body + d = zeroDate(); + var maxd = addMinutes(cloneDate(d), maxMinute); + addMinutes(d, minMinute); + s = ""; + for (i=0; d < maxd; i++) { + minutes = d.getMinutes(); + s += ""; + addMinutes(d, opt('slotMinutes')); + slotCnt++; + } + s += "
" + + ((!slotNormal || !minutes) ? formatDate(d, opt('axisFormat')) : ' ') + + "
 
"; + body = $("
") + .append(bodyContent = $("
") + .append(bodyTable = $(s))) + .appendTo(element); + slotBind(body.find('td')); + + // slot event container + slotSegmentContainer = $("
").appendTo(bodyContent); + + // background stripes + d = cloneDate(d0); + s = "
" + + ""; + for (i=0; i
 
"; + addDays(d, dis); + if (nwe) { + skipWeekend(d, dis); + } + } + s += "
"; + bg = $(s).appendTo(element); + + }else{ // skeleton already built, just modify it + + clearEvents(); + + if (opt('weekNumbers')) { + head.find('tr:first th:first').text( formatDate(t.visStart, 'w') ); + } + + // redo column header text and class + head.find('tr:first th').slice(1, -1).each(function(i, th) { + $(th).text(formatDate(d, colFormat)); + th.className = th.className.replace(/^fc-\w+(?= )/, 'fc-' + dayIDs[d.getDay()]); + addDays(d, dis); + if (nwe) { + skipWeekend(d, dis); + } + }); + + // change classes of background stripes + d = cloneDate(d0); + bg.find('td').each(function(i, td) { + td.className = td.className.replace(/^fc-\w+(?= )/, 'fc-' + dayIDs[d.getDay()]); + if (+d == +today) { + $(td).removeClass('fc-not-today fc-before-today') + .addClass(tm + '-state-highlight fc-today'); + }else if (+d < +today) { + $(td).addClass('fc-not-today fc-before-today') + .removeClass(tm + '-state-highlight fc-today'); + }else{ + $(td).addClass('fc-not-today') + .removeClass(tm + '-state-highlight fc-today fc-before-today'); + } + addDays(d, dis); + if (nwe) { + skipWeekend(d, dis); + } + }); + + } + + } + + + + function setHeight(height, dateChanged) { + + if (height === undefined) { + height = viewHeight; + } + + viewHeight = height; + slotTopCache = {}; + + var bodyHeight = height - getHeight(head); + bodyHeight = Math.min(bodyHeight, getHeight(bodyTable)); // shrink to fit table + setOuterHeight(body, bodyHeight); + if (msie9) setOuterHeight(body,height - getHeight(head) - 1); + + slotHeight = getHeight(body.find('tr:first div')) + 1; + + if (dateChanged) { + resetScroll(); + } + } + + + + function setWidth(width) { + viewWidth = width; + colContentPositions.clear(); + + setOuterWidth(body,width); + body.css('overflow', 'auto').width(width); + + var topTDs = head.find('tr:first th'), + allDayLastTH = head.find('tr.fc-all-day th:last'), + stripeTDs = bg.find('td'), + clientWidth = body[0].clientWidth; + + setOuterWidth(bodyTable,clientWidth); + + clientWidth = body[0].clientWidth; // in ie6, sometimes previous clientWidth was wrongly reported + bodyTable.width(clientWidth); + + // time-axis width + axisWidth = 0; + var axisTHs = head.find('tr:lt(2) th:first').add(body.find('tr:first th')); + axisTHs.width(1); + axisTHs.each(function() { + axisWidth = Math.max(axisWidth, $(this).outerWidth()); + }); + + //if (!msie9) + setOuterWidth(axisTHs,axisWidth); + + // column width, except for last column + colWidth = Math.floor((clientWidth - axisWidth) / colCnt); + setOuterWidth(topTDs.slice(1, -2), colWidth); + setOuterWidth(stripeTDs.slice(0, -1), colWidth); + + //if (msie9) //no border on first day (to recheck with different themes) + // stripeTDs.first().outerWidth(colWidth - 1); + + // column width for last column + if (width != clientWidth) { // has scrollbar + setOuterWidth(topTDs.slice(-2, -1), clientWidth - axisWidth - colWidth*(colCnt-1)); + topTDs.slice(-1).show(); + allDayLastTH.show(); + }else{ + body.css('overflow', 'hidden'); + topTDs.slice(-2, -1).width(''); + topTDs.slice(-1).hide(); + allDayLastTH.hide(); + } + + bg.css({ + top: getHeight(head.find('tr')), + left: axisWidth, + width: clientWidth - axisWidth, + height: viewHeight + }); + } + + + function resetScroll() { + var d0 = zeroDate(), + scrollDate = cloneDate(d0); + scrollDate.setHours(opt('firstHour')); + var top = timePosition(d0, scrollDate) + 1, // +1 for the border + scroll = function() { + body.scrollTop(top); + }; + scroll(); + setTimeout(scroll, 0); // overrides any previous scroll state made by the browser + } + + + function beforeHide() { + savedScrollTop = body.scrollTop(); + } + + + function afterShow() { + body.scrollTop(savedScrollTop); + } + + + + /* Slot/Day clicking and binding + -----------------------------------------------------------------------*/ + + + function dayBind(tds) { + tds.click(slotClick) + .mousedown(daySelectionMousedown); + } + + + function slotBind(tds) { + tds.click(slotClick) + .mousedown(slotSelectionMousedown); + } + + + function slotClick(ev) { + if (!opt('selectable')) { // SelectionManager will worry about dayClick + var col = Math.min(colCnt-1, Math.floor((ev.pageX - bg.offset().left) / colWidth)), + date = addDays(cloneDate(t.visStart), col*dis+dit), + rowMatch = this.className.match(/fc-slot(\d+)/); + if (rowMatch) { + var mins = parseInt(rowMatch[1],10) * opt('slotMinutes'), + hours = Math.floor(mins/60); + date.setHours(hours); + date.setMinutes(mins%60 + minMinute); + trigger('dayClick', this, date, false, ev); + }else{ + trigger('dayClick', this, date, true, ev); + } + } + } + + + + /* Semi-transparent Overlay Helpers + -----------------------------------------------------*/ + + + function renderDayOverlay(startDate, endDate, refreshCoordinateGrid) { // endDate is exclusive + if (refreshCoordinateGrid) { + coordinateGrid.build(); + } + var visStart = cloneDate(t.visStart); + var startCol, endCol; + if (rtl) { + startCol = dayDiff(endDate, visStart)*dis+dit+1; + endCol = dayDiff(startDate, visStart)*dis+dit+1; + }else{ + startCol = dayDiff(startDate, visStart); + endCol = dayDiff(endDate, visStart); + } + startCol = Math.max(0, startCol); + endCol = Math.min(colCnt, endCol); + if (startCol < endCol) { + dayBind( + renderCellOverlay(0, startCol, 0, endCol-1) + ); + } + } + + + function renderCellOverlay(col0, row0, col1, row1) { + var rect = coordinateGrid.rect(col0, row0, col1, row1, head); + return renderOverlay(rect, head); + } + + + function renderSlotOverlay(overlayStart, overlayEnd) { + var dayStart = cloneDate(t.visStart); + var dayEnd = addDays(cloneDate(dayStart), 1); + for (var i=0; i= addMinutes(cloneDate(day), maxMinute)) { + return getHeight(bodyContent); + } + var slotMinutes = opt('slotMinutes'), + minutes = time.getHours()*60 + time.getMinutes() - minMinute, + slotI = Math.floor(minutes / slotMinutes), + slotTop = slotTopCache[slotI]; + if (slotTop === undefined) { + slotTop = slotTopCache[slotI] = body.find('tr:eq(' + slotI + ') td div')[0].offsetTop; + } + return Math.max(0, Math.round( + slotTop - 1 + slotHeight * ((minutes % slotMinutes) / slotMinutes) + )); + } + + + function cellDate(cell) { + var d = addDays(cloneDate(t.visStart), cell.col*dis+dit); + var slotIndex = cell.row; + if (opt('allDaySlot')) { + slotIndex--; + } + if (slotIndex >= 0) { + addMinutes(d, minMinute + slotIndex*opt('slotMinutes')); + } + return d; + } + + + function cellIsAllDay(cell) { + return opt('allDaySlot') && !cell.row; + } + + + function allDayBounds() { + return { + left: axisWidth, + right: viewWidth + } + } + + + function allDayTR(index) { + return head.find('tr.fc-all-day'); + } + + + function defaultEventEnd(event) { + var start = cloneDate(event.start); + if (event.allDay) { + return start; + } + return addMinutes(start, opt('defaultEventMinutes')); + } + + + + /* Selection + ---------------------------------------------------------------------------------*/ + + + function defaultSelectionEnd(startDate, allDay) { + if (allDay) { + return cloneDate(startDate); + } + return addMinutes(cloneDate(startDate), opt('slotMinutes')); + } + + + function renderSelection(startDate, endDate, allDay) { + if (allDay) { + if (opt('allDaySlot')) { + renderDayOverlay(startDate, addDays(cloneDate(endDate), 1), true); + } + }else{ + renderSlotSelection(startDate, endDate); + } + } + + + function renderSlotSelection(startDate, endDate) { + var helperOption = opt('selectHelper'); + coordinateGrid.build(); + if (helperOption) { + var col = dayDiff(startDate, t.visStart) * dis + dit; + if (col >= 0 && col < colCnt) { // only works when times are on same day + var rect = coordinateGrid.rect(0, col, 0, col, bodyContent); // only for horizontal coords + var top = timePosition(startDate, startDate); + var bottom = timePosition(startDate, endDate); + if (bottom > top) { // protect against selections that are entirely before or after visible range + rect.top = top; + rect.height = bottom - top; + rect.left += 2; + rect.width -= 5; + if ($.isFunction(helperOption)) { + var helperRes = helperOption(startDate, endDate); + if (helperRes) { + rect.position = 'absolute'; + rect.zIndex = 8; + selectionHelper = $(helperRes) + .css(rect) + .appendTo(bodyContent); + } + }else{ + selectionHelper = $(slotSegHtml( + { + title: '', + start: startDate, + end: endDate, + className: [], + editable: false + }, + rect, + 'fc-event fc-event-vert fc-corner-top fc-corner-bottom ' + )); + if ($.browser.msie) { + selectionHelper.find('span.fc-event-bg').hide(); // nested opacities mess up in IE, just hide + } + selectionHelper.css('opacity', opt('dragOpacity')); + } + if (selectionHelper) { + slotBind(selectionHelper); + bodyContent.append(selectionHelper); + setOuterWidth(selectionHelper, rect.width, true); // needs to be after appended + setOuterHeight(selectionHelper, rect.height, true); + } + } + } + }else{ + renderSlotOverlay(startDate, endDate); + } + } + + + function clearSelection() { + clearOverlays(); + if (selectionHelper) { + selectionHelper.remove(); + selectionHelper = null; + } + } + + + function slotSelectionMousedown(ev) { + if (ev.which == 1 && opt('selectable')) { // ev.which==1 means left mouse button + unselect(ev); + var _mousedownElement = this; + var dates; + hoverListener.start(function(cell, origCell) { + clearSelection(); + if (cell && cell.col == origCell.col && !cellIsAllDay(cell)) { + var d1 = cellDate(origCell); + var d2 = cellDate(cell); + dates = [ + d1, + addMinutes(cloneDate(d1), opt('slotMinutes')), + d2, + addMinutes(cloneDate(d2), opt('slotMinutes')) + ].sort(cmp); + renderSlotSelection(dates[0], dates[3]); + }else{ + dates = null; + } + }, ev); + $(document).one('mouseup', function(ev) { + hoverListener.stop(); + if (dates) { + if (+dates[0] == +dates[1]) { + trigger('dayClick', _mousedownElement, dates[0], false, ev); + // BUG: _mousedownElement will sometimes be the overlay + } + reportSelection(dates[0], dates[3], false, ev); + } + }); + } + } + + + + /* External Dragging + --------------------------------------------------------------------------------*/ + + + function dragStart(_dragElement, ev, ui) { + hoverListener.start(function(cell) { + clearOverlays(); + if (cell) { + if (cellIsAllDay(cell)) { + renderCellOverlay(cell.row, cell.col, cell.row, cell.col); + }else{ + var d1 = cellDate(cell); + var d2 = addMinutes(cloneDate(d1), opt('defaultEventMinutes')); + renderSlotOverlay(d1, d2); + } + } + }, ev); + } + + + function dragStop(_dragElement, ev, ui) { + var cell = hoverListener.stop(); + clearOverlays(); + if (cell) { + trigger('drop', _dragElement, cellDate(cell), cellIsAllDay(cell), ev, ui); + } + } + + +} + +function AgendaEventRenderer() { + var t = this; + + + // exports + t.renderEvents = renderEvents; + t.clearEvents = clearEvents; + t.slotSegHtml = slotSegHtml; + t.bindDaySeg = bindDaySeg; + + + // imports + DayEventRenderer.call(t); + var opt = t.opt; + var trigger = t.trigger; + var eventEnd = t.eventEnd; + var reportEvents = t.reportEvents; + var reportEventClear = t.reportEventClear; + var eventElementHandlers = t.eventElementHandlers; + var setHeight = t.setHeight; + var getDaySegmentContainer = t.getDaySegmentContainer; + var getSlotSegmentContainer = t.getSlotSegmentContainer; + var getHoverListener = t.getHoverListener; + var getMaxMinute = t.getMaxMinute; + var getMinMinute = t.getMinMinute; + var timePosition = t.timePosition; + var colContentLeft = t.colContentLeft; + var colContentRight = t.colContentRight; + var renderDaySegs = t.renderDaySegs; + var resizableDayEvent = t.resizableDayEvent; // TODO: streamline binding architecture + var getColCnt = t.getColCnt; + var getColWidth = t.getColWidth; + var getSlotHeight = t.getSlotHeight; + var getBodyContent = t.getBodyContent; + var reportEventElement = t.reportEventElement; + var showEvents = t.showEvents; + var hideEvents = t.hideEvents; + var eventDrop = t.eventDrop; + var eventResize = t.eventResize; + var renderDayOverlay = t.renderDayOverlay; + var clearOverlays = t.clearOverlays; + var calendar = t.calendar; + var formatDate = calendar.formatDate; + var formatDates = calendar.formatDates; + + + + /* Rendering + ----------------------------------------------------------------------------*/ + + + function renderEvents(events, modifiedEventId) { + reportEvents(events); + var i, len=events.length, + dayEvents=[], + slotEvents=[]; + for (i=0; i" + + "" + + "" + + "" + htmlEscape(formatDates(event.start, event.end, opt('timeFormat'))) + "" + + "" + htmlEscape(event.title) + "" + + "" + + ((event.resizable || event.resizable === undefined) && (event.editable || event.editable === undefined && opt('editable')) && !opt('disableResizing') && $.fn.resizable ? + "
=
" + : '') + + "
"; + } + + + function bindDaySeg(event, eventElement, seg) { + eventElementHandlers(event, eventElement); + if (event.editable || event.editable === undefined && opt('editable')) { + draggableDayEvent(event, eventElement, seg.isStart); + if (seg.isEnd) { + resizableDayEvent(event, eventElement, getColWidth()); + } + } + } + + + function bindSlotSeg(event, eventElement, seg) { + eventElementHandlers(event, eventElement); + if (event.editable || event.editable === undefined && opt('editable')) { + var timeElement = eventElement.find('span.fc-event-time'); + draggableSlotEvent(event, eventElement, timeElement); + if (seg.isEnd) { + resizableSlotEvent(event, eventElement, timeElement); + } + } + } + + + + /* Dragging + -----------------------------------------------------------------------------------*/ + + + // when event starts out FULL-DAY + + function draggableDayEvent(event, eventElement, isStart) { + if (!opt('disableDragging') && eventElement.draggable) { + var origWidth; + var allDay=true; + var dayDelta; + var dis = opt('isRTL') ? -1 : 1; + var hoverListener = getHoverListener(); + var colWidth = getColWidth(); + var slotHeight = getSlotHeight(); + var minMinute = getMinMinute(); + eventElement.draggable({ + zIndex: 9, + opacity: opt('dragOpacity', 'month'), // use whatever the month view was using + revertDuration: opt('dragRevertDuration'), + start: function(ev, ui) { + trigger('eventDragStart', eventElement, event, ev, ui); + hideEvents(event, eventElement); + origWidth = getWidth(eventElement); + hoverListener.start(function(cell, origCell, rowDelta, colDelta) { + eventElement.draggable('option', 'revert', !cell || !rowDelta && !colDelta); + clearOverlays(); + if (cell) { + dayDelta = colDelta * dis; + if (!cell.row) { + // on full-days + renderDayOverlay( + addDays(cloneDate(event.start), dayDelta), + addDays(exclEndDay(event), dayDelta) + ); + resetElement(); + }else{ + // mouse is over bottom slots + if (isStart && allDay) { + // convert event to temporary slot-event + setOuterWidth(eventElement,colWidth - 10); + setOuterHeight( + eventElement, // don't use entire width + slotHeight * Math.round( + (event.end ? ((event.end - event.start) / MINUTE_MS) : opt('defaultEventMinutes')) + / opt('slotMinutes') + ) + ); + eventElement.draggable('option', 'grid', [colWidth, 1]); + allDay = false; + } + } + } + }, ev, 'drag'); + }, + stop: function(ev, ui) { + var cell = hoverListener.stop(); + clearOverlays(); + trigger('eventDragStop', eventElement, event, ev, ui); + if (cell && (!allDay || dayDelta)) { + // changed! + eventElement.find('a').removeAttr('href'); // prevents safari from visiting the link + var minuteDelta = 0; + if (!allDay) { + minuteDelta = Math.round((eventElement.offset().top - getBodyContent().offset().top) / slotHeight) + * opt('slotMinutes') + + minMinute + - (event.start.getHours() * 60 + event.start.getMinutes()); + } + eventDrop(this, event, dayDelta, minuteDelta, allDay, ev, ui); + }else{ + // hasn't moved or is out of bounds (draggable has already reverted) + resetElement(); + if ($.browser.msie) { + eventElement.css('filter', ''); // clear IE opacity side-effects + } + showEvents(event, eventElement); + } + } + }); + function resetElement() { + if (!allDay) { + setOuterWidth(eventElement,origWidth); + eventElement.height(''); + eventElement.draggable('option', 'grid', null); + allDay = true; + } + } + } + } + + + // when event starts out IN TIMESLOTS + + function draggableSlotEvent(event, eventElement, timeElement) { + if (!opt('disableDragging') && eventElement.draggable) { + var origPosition; + var allDay=false; + var dayDelta; + var minuteDelta; + var prevMinuteDelta; + var dis = opt('isRTL') ? -1 : 1; + var hoverListener = getHoverListener(); + var colCnt = getColCnt(); + var colWidth = getColWidth(); + var slotHeight = getSlotHeight(); + eventElement.draggable({ + zIndex: 9, + scroll: false, + grid: [colWidth, slotHeight], + axis: colCnt==1 ? 'y' : false, + opacity: opt('dragOpacity'), + revertDuration: opt('dragRevertDuration'), + start: function(ev, ui) { + trigger('eventDragStart', eventElement, event, ev, ui); + hideEvents(event, eventElement); + if ($.browser.msie) { + eventElement.find('span.fc-event-bg').hide(); // nested opacities mess up in IE, just hide + } + origPosition = eventElement.position(); + minuteDelta = prevMinuteDelta = 0; + hoverListener.start(function(cell, origCell, rowDelta, colDelta) { + eventElement.draggable('option', 'revert', !cell); + clearOverlays(); + if (cell) { + dayDelta = colDelta * dis; + if (opt('allDaySlot') && !cell.row) { + // over full days + if (!allDay) { + // convert to temporary all-day event + allDay = true; + timeElement.hide(); + eventElement.draggable('option', 'grid', null); + } + renderDayOverlay( + addDays(cloneDate(event.start), dayDelta), + addDays(exclEndDay(event), dayDelta) + ); + }else{ + // on slots + resetElement(); + } + } + }, ev, 'drag'); + }, + drag: function(ev, ui) { + minuteDelta = Math.round((ui.position.top - origPosition.top) / slotHeight) * opt('slotMinutes'); + if (minuteDelta != prevMinuteDelta) { + if (!allDay) { + updateTimeText(minuteDelta); + } + prevMinuteDelta = minuteDelta; + } + }, + stop: function(ev, ui) { + var cell = hoverListener.stop(); + clearOverlays(); + trigger('eventDragStop', eventElement, event, ev, ui); + if (cell && (dayDelta || minuteDelta || allDay)) { + // changed! + eventDrop(this, event, dayDelta, allDay ? 0 : minuteDelta, allDay, ev, ui); + }else{ + // either no change or out-of-bounds (draggable has already reverted) + resetElement(); + eventElement.css(origPosition); // sometimes fast drags make event revert to wrong position + updateTimeText(0); + if ($.browser.msie) { + eventElement + .css('filter', '') // clear IE opacity side-effects + .find('span.fc-event-bg') + .css('display', ''); // .show() made display=inline + } + showEvents(event, eventElement); + } + } + }); + function updateTimeText(minuteDelta) { + var newStart = addMinutes(cloneDate(event.start), minuteDelta); + var newEnd; + if (event.end) { + newEnd = addMinutes(cloneDate(event.end), minuteDelta); + } + timeElement.text(formatDates(newStart, newEnd, opt('timeFormat'))); + } + function resetElement() { + // convert back to original slot-event + if (allDay) { + timeElement.css('display', ''); // show() was causing display=inline + eventElement.draggable('option', 'grid', [colWidth, slotHeight]); + allDay = false; + } + } + } + } + + + + /* Resizing + --------------------------------------------------------------------------------------*/ + + + function resizableSlotEvent(event, eventElement, timeElement) { + if (!opt('disableResizing') && eventElement.resizable) { + var slotDelta, prevSlotDelta; + var slotHeight = getSlotHeight(); + eventElement.resizable({ + handles: { + s: 'div.ui-resizable-s' + }, + grid: slotHeight, + start: function(ev, ui) { + slotDelta = prevSlotDelta = 0; + hideEvents(event, eventElement); + if ($.browser.msie && $.browser.version == '6.0') { + eventElement.css('overflow', 'hidden'); + } + eventElement.css('z-index', 9); + trigger('eventResizeStart', this, event, ev, ui); + }, + resize: function(ev, ui) { + // don't rely on ui.size.height, doesn't take grid into account + slotDelta = Math.round((Math.max(slotHeight, getHeight(eventElement)) - ui.originalSize.height) / slotHeight); + if (slotDelta != prevSlotDelta) { + timeElement.text( + formatDates( + event.start, + (!slotDelta && !event.end) ? null : // no change, so don't display time range + addMinutes(eventEnd(event), opt('slotMinutes')*slotDelta), + opt('timeFormat') + ) + ); + prevSlotDelta = slotDelta; + } + }, + stop: function(ev, ui) { + trigger('eventResizeStop', this, event, ev, ui); + if (slotDelta) { + eventResize(this, event, 0, opt('slotMinutes')*slotDelta, ev, ui); + }else{ + eventElement.css('z-index', 8); + showEvents(event, eventElement); + // BUG: if event was really short, need to put title back in span + } + } + }); + } + } + + +} + + +function countForwardSegs(levels) { + var i, j, k, level, segForward, segBack; + for (i=levels.length-1; i>0; i--) { + level = levels[i]; + for (j=0; j" + + "" + + (!event.allDay && seg.isStart ? + "" + + htmlEscape(formatDates(event.start, event.end, opt('timeFormat'))) + + "" + :'') + + "" + htmlEscape(event.title) + "" + + "" + + ((event.resizable || event.resizable === undefined) && (event.editable || event.editable === undefined && opt('editable')) && !opt('disableResizing') && $.fn.resizable ? + "
" + : '') + + "
"; + seg.left = left; + seg.outerWidth = right - left; + } + segmentContainer[0].innerHTML = html; // faster than html() + eventElements = segmentContainer.children(); + + // retrieve elements, run through eventRender callback, bind handlers + for (i=0; i div'); // optimal selector? + setOuterHeight(rowDivs[rowI], maxHeight); + } + + /* + // set row heights, calculate event tops (in relation to row top) + for (i=0, rowI=0; rowI div'); // optimal selector? + if (msie9) setOuterHeight(rowDivs[rowI], top + levelHeight); + rowDivs[rowI].height(top + levelHeight); + } + */ + + // calculate row tops + for (rowI=0; rowI"); + } + if (e[0].parentNode != parent[0]) { + e.appendTo(parent); + } + usedOverlays.push(e.css(rect).show()); + return e; + } + + + function clearOverlays() { + var e; + while (e = usedOverlays.shift()) { + unusedOverlays.push(e.hide().unbind()); + } + } + + +} + +function CoordinateGrid(buildFunc) { + + var t = this; + var rows; + var cols; + + t.build = function() { + rows = []; + cols = []; + buildFunc(rows, cols); + }; + + t.cell = function(x, y) { + var rowCnt = rows.length; + var colCnt = cols.length; + var i, r=-1, c=-1; + for (i=0; i= rows[i][0] && y < rows[i][1]) { + r = i; + break; + } + } + for (i=0; i= cols[i][0] && x < cols[i][1]) { + c = i; + break; + } + } + return (r>=0 && c>=0) ? { row:r, col:c } : null; + }; + + t.rect = function(row0, col0, row1, col1, originElement) { // row1,col1 is inclusive + var origin = originElement.offset(); + return { + top: rows[row0][0] - origin.top, + left: cols[col0][0] - origin.left, + width: cols[col1][1] - cols[col0][0], + height: rows[row1][1] - rows[row0][0] + }; + }; + +} + +function HoverListener(coordinateGrid) { + + + var t = this; + var bindType; + var change; + var firstCell; + var cell; + + + t.start = function(_change, ev, _bindType) { + change = _change; + firstCell = cell = null; + coordinateGrid.build(); + mouse(ev); + bindType = _bindType || 'mousemove'; + $(document).bind(bindType, mouse); + }; + + + function mouse(ev) { + var newCell = coordinateGrid.cell(ev.pageX, ev.pageY); + if (!newCell != !cell || newCell && (newCell.row != cell.row || newCell.col != cell.col)) { + if (newCell) { + if (!firstCell) { + firstCell = newCell; + } + change(newCell, firstCell, newCell.row-firstCell.row, newCell.col-firstCell.col); + }else{ + change(newCell, firstCell); + } + cell = newCell; + } + } + + + t.stop = function() { + $(document).unbind(bindType, mouse); + return cell; + }; + + +} + +function HorizontalPositionCache(getElement) { + + var t = this, + elements = {}, + lefts = {}, + rights = {}; + + function e(i) { + return elements[i] = elements[i] || getElement(i); + } + + t.left = function(i) { + return lefts[i] = lefts[i] === undefined ? e(i).position().left : lefts[i]; + }; + + t.right = function(i) { + return rights[i] = rights[i] === undefined ? t.left(i) + getWidth(e(i)) : rights[i]; + }; + + t.clear = function() { + elements = {}; + lefts = {}; + rights = {}; + }; + +} + + +fc.addDays = addDays; +fc.cloneDate = cloneDate; +fc.parseDate = parseDate; +fc.parseISO8601 = parseISO8601; +fc.parseTime = parseTime; +fc.formatDate = formatDate; +fc.formatDates = formatDates; + + + +/* Date Math +-----------------------------------------------------------------------------*/ + +var dayIDs = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'], + DAY_MS = 86400000, + HOUR_MS = 3600000, + MINUTE_MS = 60000; + + +function addYears(d, n, keepTime) { + d.setFullYear(d.getFullYear() + n); + if (!keepTime) { + clearTime(d); + } + return d; +} + + +function addMonths(d, n, keepTime) { // prevents day overflow/underflow + if (+d) { // prevent infinite looping on invalid dates + var m = d.getMonth() + n, + check = cloneDate(d); + check.setDate(1); + check.setMonth(m); + d.setMonth(m); + if (!keepTime) { + clearTime(d); + } + while (d.getMonth() != check.getMonth()) { + d.setDate(d.getDate() + (d < check ? 1 : -1)); + } + } + return d; +} + + +function addDays(d, n, keepTime) { // deals with daylight savings + if (+d) { + var dd = d.getDate() + n, + check = cloneDate(d); + check.setHours(9); // set to middle of day + check.setDate(dd); + d.setDate(dd); + if (!keepTime) { + clearTime(d); + } + fixDate(d, check); + } + return d; +} + + +function fixDate(d, check) { // force d to be on check's YMD, for daylight savings purposes + if (+d) { // prevent infinite looping on invalid dates + while (d.getDate() != check.getDate()) { + d.setTime(+d + (d < check ? 1 : -1) * HOUR_MS); + } + } +} + + +function addMinutes(d, n) { + d.setMinutes(d.getMinutes() + n); + return d; +} + + +function clearTime(d) { + d.setHours(0); + d.setMinutes(0); + d.setSeconds(0); + d.setMilliseconds(0); + return d; +} + + +function cloneDate(d, dontKeepTime) { + if (dontKeepTime) { + return clearTime(new Date(+d)); + } + return new Date(+d); +} + + +function zeroDate() { // returns a Date with time 00:00:00 and dateOfMonth=1 + var i=0, d; + do { + d = new Date(1970, i++, 1); + } while (d.getHours()); // != 0 + return d; +} + + +function skipWeekend(date, inc, excl) { + inc = inc || 1; + while (!date.getDay() || (excl && date.getDay()==1 || !excl && date.getDay()==6)) { + addDays(date, inc); + } + return date; +} + + +function dayDiff(d1, d2) { // d1 - d2 + return Math.round((cloneDate(d1, true) - cloneDate(d2, true)) / DAY_MS); +} + + +function setYMD(date, y, m, d) { + if (y !== undefined && y != date.getFullYear()) { + date.setDate(1); + date.setMonth(0); + date.setFullYear(y); + } + if (m !== undefined && m != date.getMonth()) { + date.setDate(1); + date.setMonth(m); + } + if (d !== undefined) { + date.setDate(d); + } +} + + + +/* Date Parsing +-----------------------------------------------------------------------------*/ + + +function parseDate(s, ignoreTimezone) { // ignoreTimezone defaults to true + if (typeof s == 'object') { // already a Date object + return s; + } + if (typeof s == 'number') { // a UNIX timestamp + return new Date(s * 1000); + } + if (typeof s == 'string') { + if (s.match(/^\d+$/)) { // a UNIX timestamp + return new Date(parseInt(s,10) * 1000); + } + if (ignoreTimezone === undefined) { + ignoreTimezone = true; + } + return parseISO8601(s, ignoreTimezone) || (s ? new Date(s) : null); + } + // TODO: never return invalid dates (like from new Date()), return null instead + return null; +} + + +function parseISO8601(s, ignoreTimezone) { // ignoreTimezone defaults to false + // derived from http://delete.me.uk/2005/03/iso8601.html + // TODO: for a know glitch/feature, read tests/issue_206_parseDate_dst.html + var m = s.match(/^([0-9]{4})(-([0-9]{2})(-([0-9]{2})([T ]([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?(Z|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?$/); + if (!m) { + return null; + } + var date = new Date(m[1], 0, 1); + if (ignoreTimezone || !m[14]) { + var check = new Date(m[1], 0, 1, 9, 0); + if (m[3]) { + date.setMonth(m[3] - 1); + check.setMonth(m[3] - 1); + } + if (m[5]) { + date.setDate(m[5]); + check.setDate(m[5]); + } + fixDate(date, check); + if (m[7]) { + date.setHours(m[7]); + } + if (m[8]) { + date.setMinutes(m[8]); + } + if (m[10]) { + date.setSeconds(m[10]); + } + if (m[12]) { + date.setMilliseconds(Number("0." + m[12]) * 1000); + } + fixDate(date, check); + }else{ + date.setUTCFullYear( + m[1], + m[3] ? m[3] - 1 : 0, + m[5] || 1 + ); + date.setUTCHours( + m[7] || 0, + m[8] || 0, + m[10] || 0, + m[12] ? Number("0." + m[12]) * 1000 : 0 + ); + var offset = Number(m[16]) * 60 + Number(m[17]); + offset *= m[15] == '-' ? 1 : -1; + date = new Date(+date + (offset * 60 * 1000)); + } + return date; +} + + +function parseTime(s) { // returns minutes since start of day + if (typeof s == 'number') { // an hour + return s * 60; + } + if (typeof s == 'object') { // a Date object + return s.getHours() * 60 + s.getMinutes(); + } + var m = s.match(/(\d+)(?::(\d+))?\s*(\w+)?/); + if (m) { + var h = parseInt(m[1],10); + if (m[3]) { + h %= 12; + if (m[3].toLowerCase().charAt(0) == 'p') { + h += 12; + } + } + return h * 60 + (m[2] ? parseInt(m[2],10) : 0); + } +} + + + +/* Date Formatting +-----------------------------------------------------------------------------*/ +// TODO: use same function formatDate(date, [date2], format, [options]) + + +function formatDate(date, format, options) { + return formatDates(date, null, format, options); +} + + +function formatDates(date1, date2, format, options) { + options = options || defaults; + var date = date1, + otherDate = date2, + i, len = format.length, c, + i2, formatter, + res = ''; + for (i=0; ii; i2--) { + if (formatter = dateFormatters[format.substring(i, i2)]) { + if (date) { + res += formatter(date, options); + } + i = i2 - 1; + break; + } + } + if (i2 == i) { + if (date) { + res += c; + } + } + } + } + return res; +}; + + +var dateFormatters = { + s : function(d) { return d.getSeconds() }, + ss : function(d) { return zeroPad(d.getSeconds()) }, + m : function(d) { return d.getMinutes() }, + mm : function(d) { return zeroPad(d.getMinutes()) }, + h : function(d) { return d.getHours() % 12 || 12 }, + hh : function(d) { return zeroPad(d.getHours() % 12 || 12) }, + H : function(d) { return d.getHours() }, + HH : function(d) { return zeroPad(d.getHours()) }, + d : function(d) { return d.getDate() }, + dd : function(d) { return zeroPad(d.getDate()) }, + ddd : function(d,o) { return o.dayNamesShort[d.getDay()] }, + dddd: function(d,o) { return o.dayNames[d.getDay()] }, + M : function(d) { return d.getMonth() + 1 }, + MM : function(d) { return zeroPad(d.getMonth() + 1) }, + MMM : function(d,o) { return o.monthNamesShort[d.getMonth()] }, + MMMM: function(d,o) { return o.monthNames[d.getMonth()] }, + yy : function(d) { return (d.getFullYear()+'').substring(2) }, + yyyy: function(d) { return d.getFullYear() }, + t : function(d) { return d.getHours() < 12 ? 'a' : 'p' }, + tt : function(d) { return d.getHours() < 12 ? 'am' : 'pm' }, + T : function(d) { return d.getHours() < 12 ? 'A' : 'P' }, + TT : function(d) { return d.getHours() < 12 ? 'AM' : 'PM' }, + u : function(d) { return formatDate(d, "yyyy-MM-dd'T'HH:mm:ss'Z'") }, + S : function(d) { + var date = d.getDate(); + if (date > 10 && date < 20) { + return 'th'; + } + return ['st', 'nd', 'rd'][date%10-1] || 'th'; + }, + w : function(d) { return d.getWeek(); } +}; + +if (Date.prototype.getWeek === undefined) { + + Date.prototype.getWeek = function() { + //By tanguy.pruvot at gmail.com (2010) + var week, days, jan4, jan4Day, thisDay, msDay = 86400000; + + //first week of year always contains 4th Jan (ISO) + jan4 = new Date(this.getFullYear(),0,4 ,this.getHours()); + days = Math.round( (this - jan4) / msDay); + + //ISO weeks begins on monday, so rotate monday:sunday to 0:6 + jan4Day = (jan4.getDay() + 6) % 7; + + week = Math.floor( (days + jan4Day) / 7) + 1; + + //special cases + thisDay = (this.getDay() + 6) % 7; + if (this.getMonth() == 11 && this.getDate() >= 28) { + + jan4.setFullYear( this.getFullYear() + 1 ); + jan4Day = (jan4.getDay() + 6) % 7; + + if (thisDay < jan4Day) + return 1; + + var prevWeek = new Date( this-(msDay*7) ); + week = prevWeek.getWeek() + 1; + } + + if (week === 0 && thisDay > 3 && this.getMonth() == 0) { + var prevWeek = new Date( this-(msDay*7) ); + week = prevWeek.getWeek() + 1; + } + + return week; + } + +} + + +/* Event Date Math +-----------------------------------------------------------------------------*/ + + +function exclEndDay(event) { + if (event.end) { + return _exclEndDay(event.end, event.allDay); + }else{ + return addDays(cloneDate(event.start), 1); + } +} + + +function _exclEndDay(end, allDay) { + end = cloneDate(end); + return allDay || end.getHours() || end.getMinutes() ? addDays(end, 1) : clearTime(end); +} + + +function segCmp(a, b) { + if (a.event.allDay && !b.event.allDay) return -1; + if (!a.event.allDay && b.event.allDay) return 1; + return (b.msLength - a.msLength) * 100 + (a.event.start - b.event.start); +} + + +function segsCollide(seg1, seg2) { + return seg1.end > seg2.start && seg1.start < seg2.end; +} + + + +/* Event Sorting +-----------------------------------------------------------------------------*/ + + +// event rendering utilities +function sliceSegs(events, visEventEnds, start, end) { + var segs = [], + i, len=events.length, event, + eventStart, eventEnd, + segStart, segEnd, + isStart, isEnd; + for (i=0; i start && eventStart < end) { + if (eventStart < start) { + segStart = cloneDate(start); + isStart = false; + }else{ + segStart = eventStart; + isStart = true; + } + if (eventEnd > end) { + segEnd = cloneDate(end); + isEnd = false; + }else{ + segEnd = eventEnd; + isEnd = true; + } + segs.push({ + event: event, + start: segStart, + end: segEnd, + isStart: isStart, + isEnd: isEnd, + msLength: segEnd - segStart + }); + } + } + return segs.sort(segCmp); +} + + +// event rendering calculation utilities +function stackSegs(segs) { + var levels = [], + i, len = segs.length, seg, + j, collide, k; + for (i=0; i=0; i--) { + res = obj[parts[i].toLowerCase()]; + if (res !== undefined) { + return res; + } + } + return obj['']; +} + + +function htmlEscape(s) { + return s.replace(/&/g, '&') + .replace(//g, '>') + .replace(/'/g, ''') + .replace(/"/g, '"') + .replace(/\n/g, '
'); +} + + +function cssKey(_element) { + return _element.id + '/' + _element.className + '/' + _element.style.cssText.replace(/(^|;)\s*(top|left|width|height)\s*:[^;]*/ig, ''); +} + + +function disableTextSelection(element) { + element + .attr('unselectable', 'on') + .css('MozUserSelect', 'none') + .bind('selectstart.ui', function() { return false; }); +} + + +/* +function enableTextSelection(element) { + element + .attr('unselectable', 'off') + .css('MozUserSelect', '') + .unbind('selectstart.ui'); +} +*/ + + + +})(jQuery); \ No newline at end of file diff --git a/demo/index.php b/demo/index.php new file mode 100644 index 0000000..de17ce5 --- /dev/null +++ b/demo/index.php @@ -0,0 +1,122 @@ +".print_r($x,true).""; +} +$ICS = "exdate.ics"; +//echo dump_t(file_get_contents($ICS)); + +$ical = new SG_iCalReader($ICS); +$query = new SG_iCal_Query(); + +$evts = $ical->getEvents(); +//$evts = $query->Between($ical,strtotime('20100901'),strtotime('20101131')); + + +$data = array(); +foreach($evts as $id => $ev) { + $jsEvt = array( + "id" => ($id+1), + "title" => $ev->getProperty('summary'), + "start" => $ev->getStart(), + "end" => $ev->getEnd()-1, + "allDay" => $ev->isWholeDay() + ); + + if (isset($ev->recurrence)) { + $count = 0; + $start = $ev->getStart(); + $freq = $ev->getFrequency(); + if ($freq->firstOccurrence() == $start) + $data[] = $jsEvt; + while (($next = $freq->nextOccurrence($start)) > 0 ) { + if (!$next or $count >= 1000) break; + $count++; + $start = $next; + $jsEvt["start"] = $start; + $jsEvt["end"] = $start + $ev->getDuration()-1; + + $data[] = $jsEvt; + } + } else + $data[] = $jsEvt; + +} +//echo(date('Ymd\n',$data[0][start])); +//echo(date('Ymd\n',$data[1][start])); +//dump_t($data); + +$events = "events:".json_encode($data).','; + +?> + + + +Fullcalendar iCal Loader + + + + + + + + +
+ + \ No newline at end of file diff --git a/helpers/SG_iCal_Duration.php b/helpers/SG_iCal_Duration.php index 4379218..b0622d8 100755 --- a/helpers/SG_iCal_Duration.php +++ b/helpers/SG_iCal_Duration.php @@ -9,8 +9,8 @@ */ class SG_iCal_Duration { - private $dur; - + protected $dur; + /** * Constructs a new SG_iCal_Duration from a duration-rule. * The basic build-up of DURATIONs are: @@ -20,30 +20,32 @@ class SG_iCal_Duration { * @param $duration string */ public function __construct( $duration ) { - if( $duration{0} == 'P' || (($duration{0} == '+' || $duration{0} == '-') && $duration{1} == 'P') ) { - preg_match('/P((\d+)W)?((\d+)D)?(T)?((\d+)H)?((\d+)M)?((\d+)S)?/', $duration, $matches); - $results = array('weeks'=>(int)$matches[2], - 'days'=>(int)$matches[4], - 'hours'=>(int)$matches[7], - 'minutes'=>(int)$matches[9], - 'seconds'=>(int)$matches[11]); - - $ts = 0; + + $ts = 0; + + if (preg_match('/[\\+\\-]{0,1}P((\d+)W)?((\d+)D)?(T)?((\d+)H)?((\d+)M)?((\d+)S)?/', $duration, $matches) === 1) { + $results = array( + 'weeks'=> (int)@ $matches[2], + 'days'=> (int)@ $matches[4], + 'hours'=> (int)@ $matches[7], + 'minutes'=>(int)@ $matches[9], + 'seconds'=>(int)@ $matches[11] + ); + $ts += $results['seconds']; $ts += 60 * $results['minutes']; $ts += 60 * 60 * $results['hours']; $ts += 24 * 60 * 60 * $results['days']; - $ts += 7 * 24 * 60 * 60 * $results['weeks']; - - $dir = ($duration{0} == '-') ? -1 : 1; - - $this->dur = $dir * $ts; + $ts += 7 * 24 * 60 * 60 * $results['weeks']; } else { // Invalid duration! - $this->dur = 0; } + + $dir = ($duration{0} == '-') ? -1 : 1; + + $this->dur = $dir * $ts; } - + /** * Returns the duration in seconds * @return int diff --git a/helpers/SG_iCal_Factory.php b/helpers/SG_iCal_Factory.php index 05bb7d6..089d12e 100755 --- a/helpers/SG_iCal_Factory.php +++ b/helpers/SG_iCal_Factory.php @@ -13,7 +13,7 @@ class SG_iCal_Factory { /** * Returns a new block-object for the section/data-pair. The list * of returned objects is: - * + * * vcalendar => SG_iCal_VCalendar * vtimezone => SG_iCal_VTimeZone * vevent => SG_iCal_VEvent @@ -34,7 +34,7 @@ class SG_iCal_Factory { case "vevent": require_once dirname(__FILE__).'/../blocks/SG_iCal_VEvent.php'; // BUILD: Remove line return new SG_iCal_VEvent($data, $ical ); - + default: return new ArrayObject(SG_iCal_Line::Remove_Line((array) $data) ); } diff --git a/helpers/SG_iCal_Freq.php b/helpers/SG_iCal_Freq.php index 8558ed3..3773374 100755 --- a/helpers/SG_iCal_Freq.php +++ b/helpers/SG_iCal_Freq.php @@ -1,10 +1,10 @@ 'monday', 'TU'=>'tuesday', 'WE'=>'wednesday', 'TH'=>'thursday', 'FR'=>'friday', 'SA'=>'saturday', 'SU'=>'sunday'); - private $knownRules = array('month', 'weekno', 'day', 'monthday', 'yearday', 'hour', 'minute'); - private $simpleMode = true; - - private $rules = array('freq'=>'yearly', 'interval'=>1); - private $start = 0; - private $freq = ''; - + protected $weekdays = array('MO'=>'monday', 'TU'=>'tuesday', 'WE'=>'wednesday', 'TH'=>'thursday', 'FR'=>'friday', 'SA'=>'saturday', 'SU'=>'sunday'); + protected $knownRules = array('month', 'weekno', 'day', 'monthday', 'yearday', 'hour', 'minute'); //others : 'setpos', 'second' + protected $ruleModifiers = array('wkst'); + protected $simpleMode = true; + + protected $rules = array('freq'=>'yearly', 'interval'=>1); + protected $start = 0; + protected $freq = ''; + + protected $excluded; //EXDATE + protected $added; //RDATE + + protected $cache; // getAllOccurrences() + /** * Constructs a new Freqency-rule - * @param $rule string - * @param $start int Unix-timestamp (important!) + * @param $rule string + * @param $start int Unix-timestamp (important : Need to be the start of Event) + * @param $excluded array of int (timestamps), see EXDATE documentation + * @param $added array of int (timestamps), see RDATE documentation */ - public function __construct( $rule, $start ) { + public function __construct( $rule, $start, $excluded=array(), $added=array()) { $this->start = $start; - + $this->excluded = array(); + $rules = array(); foreach( explode(';', $rule) AS $v) { list($k, $v) = explode('=', $v); @@ -49,7 +58,6 @@ class SG_iCal_Freq { $this->rules['until'] = strtotime($this->rules['until']); } $this->freq = strtolower($this->rules['freq']); - foreach( $this->knownRules AS $rule ) { if( isset($this->rules['by' . $rule]) ) { @@ -58,22 +66,64 @@ class SG_iCal_Freq { } } } - + if(!$this->simpleMode) { if(! (isset($this->rules['byday']) || isset($this->rules['bymonthday']) || isset($this->rules['byyearday']))) { $this->rules['bymonthday'] = date('d', $this->start); } } + //set until, and cache if( isset($this->rules['count']) ) { - $n = $start; - for($i=0;$i<$this->rules['count'];$i++) { - $n = $this->findNext($n); + + $cache[$ts] = $ts = $this->start; + for($n=1; $n < $this->rules['count']; $n++) { + $ts = $this->findNext($ts); + $cache[$ts] = $ts; } - $this->rules['until'] = $n; + $this->rules['until'] = $ts; + + //EXDATE + if (!empty($excluded)) { + foreach($excluded as $ts) { + unset($cache[$ts]); + } + } + //RDATE + if (!empty($added)) { + $cache = $cache + $added; + asort($cache); + } + + $this->cache = array_values($cache); } + + $this->excluded = $excluded; + $this->added = $added; } - + + + /** + * Returns all timestamps array(), build the cache if not made before + * @return array + */ + public function getAllOccurrences() { + if (empty($this->cache)) { + //build cache + $next = $this->firstOccurrence(); + while ($next) { + $cache[] = $next; + $next = $this->findNext($next); + } + if (!empty($this->added)) { + $cache = $cache + $this->added; + asort($cache); + } + $this->cache = $cache; + } + return $this->cache; + } + /** * Returns the previous (most recent) occurrence of the rule from the * given offset @@ -81,68 +131,95 @@ class SG_iCal_Freq { * @return int */ public function previousOccurrence( $offset ) { - $t1 = $this->start; - while( ($t2 = $this->findNext($t1)) < $offset) { - if( $t2 == false ){ - break; + if (!empty($this->cache)) { + $t2=$this->start; + foreach($this->cache as $ts) { + if ($ts >= $offset) + return $t2; + $t2 = $ts; + } + } else { + $ts = $this->start; + while( ($t2 = $this->findNext($ts)) < $offset) { + if( $t2 == false ){ + break; + } + $ts = $t2; } - $t1 = $t2; } - return $t1; + return $ts; } - + /** * Returns the next occurrence of this rule after the given offset * @param int $offset * @return int */ public function nextOccurrence( $offset ) { - return $this->findNext( $this->previousOccurrence( $offset) ); + if ($offset < $this->start) + return $this->firstOccurrence(); + return $this->findNext($offset); + } + + /** + * Finds the first occurrence of the rule. + * @return int timestamp + */ + public function firstOccurrence() { + $t = $this->start; + if (in_array($t, $this->excluded)) + $t = $this->findNext($t); + return $t; } /** * Finds the absolute last occurrence of the rule from the given offset. + * Builds also the cache, if not set before... * @return int timestamp */ public function lastOccurrence() { - $temp_timestamp = $this->findNext($this->start); - $timestamp = 0; - while ($temp_timestamp) { - $timestamp = $temp_timestamp; - $temp_timestamp = $this->findNext($temp_timestamp); - } - return $timestamp; + //build cache if not done + $this->getAllOccurrences(); + //return last timestamp in cache + return end($this->cache); } - + /** - * Calculates the next time after the given offset that the rule + * Calculates the next time after the given offset that the rule * will apply. * * The approach to finding the next is as follows: * First we establish a timeframe to find timestamps in. This is * between $offset and the end of the period that $offset is in. - * - * We then loop though all the rules (that is a Prerule in the + * + * We then loop though all the rules (that is a Prerule in the * current freq.), and finds the smallest timestamp inside the * timeframe. * * If we find something, we check if the date is a valid recurrence - * (with validDate). If it is, we return it. Otherwise we try to + * (with validDate). If it is, we return it. Otherwise we try to * find a new date inside the same timeframe (but using the new- * found date as offset) * - * If no new timestamps were found in the period, we try in the + * If no new timestamps were found in the period, we try in the * next period * * @param int $offset * @return int */ - public function findNext($offset) { - $echo = false; + public function findNext($offset) { + if (!empty($this->cache)) { + foreach($this->cache as $ts) { + if ($ts > $offset) + return $ts; + } + } + + $debug = false; //make sure the offset is valid - if( $offset === false || (isset($this->rules['until']) && $this->rules['until'] <= $offset) ) { - if($echo) echo 'STOP: ' . date('r', $offset) . "\n"; + if( $offset === false || (isset($this->rules['until']) && $offset > $this->rules['until']) ) { + if($debug) echo 'STOP: ' . date('r', $offset) . "\n"; return false; } @@ -150,26 +227,29 @@ class SG_iCal_Freq { //set the timestamp of the offset (ignoring hours and minutes unless we want them to be //part of the calculations. - if($echo) echo 'O: ' . date('r', $offset) . "\n"; + if($debug) echo 'O: ' . date('r', $offset) . "\n"; $hour = (in_array($this->freq, array('hourly','minutely')) && $offset > $this->start) ? date('H', $offset) : date('H', $this->start); $minute = (($this->freq == 'minutely' || isset($this->rules['byminute'])) && $offset > $this->start) ? date('i', $offset) : date('i', $this->start); $t = mktime($hour, $minute, date('s', $this->start), date('m', $offset), date('d', $offset), date('Y',$offset)); - if($echo) echo 'START: ' . date('r', $t) . "\n"; - + if($debug) echo 'START: ' . date('r', $t) . "\n"; + if( $this->simpleMode ) { if( $offset < $t ) { - return $t; + $ts = $t; + if ($ts && in_array($ts, $this->excluded)) + $ts = $this->findNext($ts); + } else { + $ts = $this->findStartingPoint( $t, $this->rules['interval'], false ); + if( !$this->validDate( $ts ) ) { + $ts = $this->findNext($ts); + } } - $next = $this->findStartingPoint( $t, $this->rules['interval'], false ); - if( !$this->validDate( $next ) ) { - return $this->findNext($next); - } - return $next; + return $ts; } - + $eop = $this->findEndOfPeriod($offset); - if($echo) echo 'EOP: ' . date('r', $eop) . "\n"; - + if($debug) echo 'EOP: ' . date('r', $eop) . "\n"; + foreach( $this->knownRules AS $rule ) { if( $found && isset($this->rules['by' . $rule]) ) { if( $this->isPrerule($rule, $this->freq) ) { @@ -180,7 +260,7 @@ class SG_iCal_Freq { if( $imm === false ) { break; } - if($echo) echo strtoupper($rule) . ': ' . date('r', $imm) . ' A: ' . ((int) ($imm > $offset && $imm < $eop)) . "\n"; + if($debug) echo strtoupper($rule) . ': ' . date('r', $imm) . ' A: ' . ((int) ($imm > $offset && $imm < $eop)) . "\n"; if( $imm > $offset && $imm < $eop && ($_t == null || $imm < $_t) ) { $_t = $imm; } @@ -194,22 +274,26 @@ class SG_iCal_Freq { } } - if( $this->start > $offset && $this->start < $t ) { - return $this->start; + if( $offset < $this->start && $this->start < $t ) { + $ts = $this->start; } else if( $found && ($t != $offset)) { if( $this->validDate( $t ) ) { - if($echo) echo 'OK' . "\n"; - return $t; + if($debug) echo 'OK' . "\n"; + $ts = $t; } else { - if($echo) echo 'Invalid' . "\n"; - return $this->findNext($t); + if($debug) echo 'Invalid' . "\n"; + $ts = $this->findNext($t); } } else { - if($echo) echo 'Not found' . "\n"; - return $this->findNext( $this->findStartingPoint( $offset, $this->rules['interval'] ) ); - } + if($debug) echo 'Not found' . "\n"; + $ts = $this->findNext( $this->findStartingPoint( $offset, $this->rules['interval'] ) ); + } + if ($ts && in_array($ts, $this->excluded)) + return $this->findNext($ts); + + return $ts; } - + /** * Finds the starting point for the next rule. It goes $interval * 'freq' forward in time since the given offset @@ -229,14 +313,14 @@ class SG_iCal_Freq { } $sp = strtotime($t, $offset); - + if( $truncate ) { $sp = $this->truncateToPeriod($sp, $this->freq); } - + return $sp; } - + /** * Finds the earliest timestamp posible outside this perioid * @param int $offset @@ -245,11 +329,11 @@ class SG_iCal_Freq { public function findEndOfPeriod($offset) { return $this->findStartingPoint($offset, 1); } - + /** * Resets the timestamp to the beginning of the * period specified by freq - * + * * Yes - the fall-through is on purpose! * * @param int $time @@ -283,7 +367,7 @@ class SG_iCal_Freq { $d = mktime($date['hours'], $date['minutes'], $date['seconds'], $date['mon'], $date['mday'], $date['year']); return $d; } - + /** * Applies the BYDAY rule to the given timestamp * @param string $rule @@ -293,24 +377,24 @@ class SG_iCal_Freq { private function ruleByday($rule, $t) { $dir = ($rule{0} == '-') ? -1 : 1; $dir_t = ($dir == 1) ? 'next' : 'last'; - - + + $d = $this->weekdays[substr($rule,-2)]; $s = $dir_t . ' ' . $d . ' ' . date('H:i:s',$t); - + if( $rule == substr($rule, -2) ) { if( date('l', $t) == ucfirst($d) ) { $s = 'today ' . date('H:i:s',$t); } - + $_t = strtotime($s, $t); - + if( $_t == $t && in_array($this->freq, array('monthly', 'yearly')) ) { // Yes. This is not a great idea.. but hey, it works.. for now $s = 'next ' . $d . ' ' . date('H:i:s',$t); $_t = strtotime($s, $_t); } - + return $_t; } else { $_f = $this->freq; @@ -323,10 +407,10 @@ class SG_iCal_Freq { $_t = $this->truncateToPeriod($t, $this->freq); } $this->freq = $_f; - + $c = preg_replace('/[^0-9]/','',$rule); $c = ($c == '') ? 1 : $c; - + $n = $_t; while($c > 0 ) { if( $dir == 1 && $c == 1 && date('l', $t) == ucfirst($d) ) { @@ -335,11 +419,11 @@ class SG_iCal_Freq { $n = strtotime($s, $n); $c--; } - + return $n; } } - + private function ruleBymonth($rule, $t) { $_t = mktime(date('H',$t), date('i',$t), date('s',$t), $rule, date('d', $t), date('Y', $t)); if( $t == $_t && isset($this->rules['byday']) ) { @@ -349,14 +433,14 @@ class SG_iCal_Freq { return $_t; } } - + private function ruleBymonthday($rule, $t) { if( $rule < 0 ) { $rule = date('t', $t) + $rule + 1; } return mktime(date('H',$t), date('i',$t), date('s',$t), date('m', $t), $rule, date('Y', $t)); } - + private function ruleByyearday($rule, $t) { if( $rule < 0 ) { $_t = $this->findEndOfPeriod(); @@ -376,12 +460,12 @@ class SG_iCal_Freq { } else { $_t = $this->truncateToPeriod($t, $this->freq); $d = '+'; - } + } $sub = (date('W', $_t) == 1) ? 2 : 1; $s = $d . abs($rule - $sub) . ' weeks ' . date('H:i:s',$t); $_t = strtotime($s, $_t); - + return $_t; } @@ -389,17 +473,21 @@ class SG_iCal_Freq { $_t = mktime($rule, date('i',$t), date('s',$t), date('m',$t), date('d', $t), date('Y', $t)); return $_t; } - + private function ruleByminute($rule, $t) { $_t = mktime(date('h',$t), $rule, date('s',$t), date('m',$t), date('d', $t), date('Y', $t)); return $_t; } - + private function validDate( $t ) { - if( isset($this->rules['until']) && $this->rules['until'] <= $t ) { + if( isset($this->rules['until']) && $t > $this->rules['until'] ) { return false; } - + + if (in_array($t, $this->excluded)) { + return false; + } + if( isset($this->rules['bymonth']) ) { $months = explode(',', $this->rules['bymonth']); if( !in_array(date('m', $t), $months)) { @@ -438,10 +526,10 @@ class SG_iCal_Freq { return false; } } - + return true; } - + private function isPrerule($rule, $freq) { if( $rule == 'year') return false; @@ -456,11 +544,11 @@ class SG_iCal_Freq { return true; if( $rule == 'day' && in_array($freq, array('yearly', 'monthly', 'weekly'))) return true; - if( $rule == 'hour' && in_array($freq, array('yearly', 'monthly', 'weekly', 'daily'))) + if( $rule == 'hour' && in_array($freq, array('yearly', 'monthly', 'weekly', 'daily'))) return true; - if( $rule == 'minute' ) + if( $rule == 'minute' ) return true; - + return false; } } diff --git a/helpers/SG_iCal_Line.php b/helpers/SG_iCal_Line.php index 2a734dc..f386cd7 100755 --- a/helpers/SG_iCal_Line.php +++ b/helpers/SG_iCal_Line.php @@ -4,7 +4,7 @@ * A class for storing a single (complete) line of the iCal file. * Will find the line-type, the arguments and the data of the file and * store them. - * + * * The line-type can be found by querying getIdent(), data via either * getData() or typecasting to a string. * Params can be access via the ArrayAccess. A iterator is also avilable @@ -15,12 +15,12 @@ * @license http://creativecommons.org/licenses/by-sa/2.5/dk/deed.en_GB CC-BY-SA-DK */ class SG_iCal_Line implements ArrayAccess, Countable, IteratorAggregate { - private $ident; - private $data; - private $params = array(); - - private $replacements = array('from'=>array('\\,', '\\n', '\\;', '\\:', '\\"'), 'to'=>array(',', "\n", ';', ':', '"')); - + protected $ident; + protected $data; + protected $params = array(); + + protected $replacements = array('from'=>array('\\,', '\\n', '\\;', '\\:', '\\"'), 'to'=>array(',', "\n", ';', ':', '"')); + /** * Constructs a new line. */ @@ -28,21 +28,21 @@ class SG_iCal_Line implements ArrayAccess, Countable, IteratorAggregate { $split = strpos($line, ':'); $idents = explode(';', substr($line, 0, $split)); $ident = strtolower(array_shift($idents)); - + $data = trim(substr($line, $split+1)); $data = str_replace($this->replacements['from'], $this->replacements['to'], $data); - + $params = array(); foreach( $idents AS $v) { list($k, $v) = explode('=', $v); $params[ strtolower($k) ] = $v; } - + $this->ident = $ident; - $this->params = $params; + $this->params = $params; $this->data = $data; } - + /** * Is this line the begining of a new block? * @return bool @@ -50,7 +50,7 @@ class SG_iCal_Line implements ArrayAccess, Countable, IteratorAggregate { public function isBegin() { return $this->ident == 'begin'; } - + /** * Is this line the end of a block? * @return bool @@ -58,7 +58,7 @@ class SG_iCal_Line implements ArrayAccess, Countable, IteratorAggregate { public function isEnd() { return $this->ident == 'end'; } - + /** * Returns the line-type (ident) of the line * @return string @@ -66,7 +66,7 @@ class SG_iCal_Line implements ArrayAccess, Countable, IteratorAggregate { public function getIdent() { return $this->ident; } - + /** * Returns the content of the line * @return string @@ -74,7 +74,19 @@ class SG_iCal_Line implements ArrayAccess, Countable, IteratorAggregate { public function getData() { return $this->data; } - + + /** + * Returns the content of the line + * @return string + */ + public function getDataAsArray() { + if (strpos($this->data,",") !== false) { + return explode(",",$this->data); + } + else + return array($this->data); + } + /** * A static helper to get a array of SG_iCal_Line's, and calls * getData() on each of them to lay the data "bare".. @@ -95,14 +107,14 @@ class SG_iCal_Line implements ArrayAccess, Countable, IteratorAggregate { } return $rtn; } - + /** * @see ArrayAccess.offsetExists */ public function offsetExists( $param ) { return isset($this->params[ strtolower($param) ]); } - + /** * @see ArrayAccess.offsetGet */ @@ -112,7 +124,7 @@ class SG_iCal_Line implements ArrayAccess, Countable, IteratorAggregate { return $this->params[ $index ]; } } - + /** * Disabled ArrayAccess requirement * @see ArrayAccess.offsetSet @@ -120,7 +132,7 @@ class SG_iCal_Line implements ArrayAccess, Countable, IteratorAggregate { public function offsetSet( $param, $val ) { return false; } - + /** * Disabled ArrayAccess requirement * @see ArrayAccess.offsetUnset @@ -128,7 +140,7 @@ class SG_iCal_Line implements ArrayAccess, Countable, IteratorAggregate { public function offsetUnset( $param ) { return false; } - + /** * toString method. * @see getData() @@ -136,14 +148,14 @@ class SG_iCal_Line implements ArrayAccess, Countable, IteratorAggregate { public function __toString() { return $this->getData(); } - + /** * @see Countable.count */ public function count() { return count($this->params); } - + /** * @see IteratorAggregate.getIterator */ diff --git a/helpers/SG_iCal_Parser.php b/helpers/SG_iCal_Parser.php index bf295d9..5a68576 100755 --- a/helpers/SG_iCal_Parser.php +++ b/helpers/SG_iCal_Parser.php @@ -1,4 +1,4 @@ -getIdent()] = $line; + // It _is_ the main section else + if (in_array($line->getIdent(), $array_idents)) + //exdate could appears more that once + $current_data[$s][$line->getIdent()][] = $line; + else { + $current_data[$s][$line->getIdent()] = $line; + } } else { // Sub section - $current_data[$s][$section][$line->getIdent()] = $line; + $current_data[$s][$section][$line->getIdent()] = $line; } break; } @@ -155,10 +161,10 @@ class SG_iCal_Parser { } /** - * This functions does some regexp checking to see if the value is + * This functions does some regexp checking to see if the value is * valid UTF-8. * - * The function is from the book "Building Scalable Web Sites" by + * The function is from the book "Building Scalable Web Sites" by * Cal Henderson. * * @param string $data @@ -181,7 +187,7 @@ class SG_iCal_Parser { $rx .= '|^[\x80-\xBF]'; return ( ! (bool) preg_match('!'.$rx.'!', $data) ); - } + } } diff --git a/helpers/SG_iCal_Query.php b/helpers/SG_iCal_Query.php index 8889150..4d3db1e 100755 --- a/helpers/SG_iCal_Query.php +++ b/helpers/SG_iCal_Query.php @@ -22,23 +22,23 @@ class SG_iCal_Query { if( $ical instanceof SG_iCalReader ) { $ical = $ical->getEvents(); } - if( !is_array($evs) ) { + if( !is_array($ical) ) { throw new Exception('SG_iCal_Query::Between called with invalid input!'); } - + $rtn = array(); - foreach( $evs AS $e ) { + foreach( $ical AS $e ) { if( ($start <= $e->getStart() && $e->getStart() < $end) - || ($start < $e->getEnd() && $e->getEnd() <= $end) ) { + || ($start < $e->getRangeEnd() && $e->getRangeEnd() <= $end) ) { $rtn[] = $e; } } return $rtn; } - + /** * Returns all events from the calendar after a given timestamp - * + * * @param SG_iCalReader|array $ical The calendar to query * @param int $start * @return SG_iCal_VEvent[] @@ -50,19 +50,19 @@ class SG_iCal_Query { if( !is_array($ical) ) { throw new Exception('SG_iCal_Query::After called with invalid input!'); } - + $rtn = array(); foreach( $ical AS $e ) { - if( $start <= $e->getStart() ) { + if($e->getStart() >= $start || $e->getRangeEnd() >= $start) { $rtn[] = $e; } } return $rtn; } - + /** * Sorts the events from the calendar after the specified column. - * Column can be all valid entires that getProperty can return. + * Column can be all valid entires that getProperty can return. * So stuff like uid, start, end, summary etc. * @param SG_iCalReader|array $ical The calendar to query * @param string $column @@ -75,7 +75,7 @@ class SG_iCal_Query { if( !is_array($ical) ) { throw new Exception('SG_iCal_Query::Sort called with invalid input!'); } - + $cmp = create_function('$a, $b', 'return strcmp($a->getProperty("' . $column . '"), $b->getProperty("' . $column . '"));'); usort($ical, $cmp); return $ical; diff --git a/helpers/SG_iCal_Recurrence.php b/helpers/SG_iCal_Recurrence.php index 3294c84..dd0ebb5 100755 --- a/helpers/SG_iCal_Recurrence.php +++ b/helpers/SG_iCal_Recurrence.php @@ -14,11 +14,13 @@ */ class SG_iCal_Recurrence { + public $rrule; + protected $freq; protected $until; protected $count; - + protected $interval; protected $bysecond; protected $byminute; @@ -29,6 +31,7 @@ class SG_iCal_Recurrence { protected $byyearno; protected $bymonth; protected $bysetpos; + protected $wkst; /** @@ -40,7 +43,6 @@ class SG_iCal_Recurrence { 'byyearday', 'byyearno', 'bymonth', 'bysetpos' ); - /** * Creates an recurrence object with a passed in line. Parses the line. * @param object $line an SG_iCal_Line object which will be parsed to get the @@ -56,6 +58,8 @@ class SG_iCal_Recurrence { * @param string $line the line to be parsed */ protected function parseLine($line) { + $this->rrule = $line; + //split up the properties $recurProperties = explode(';', $line); $recur = array(); @@ -77,6 +81,18 @@ class SG_iCal_Recurrence { } } + /** + * Set the $until member + * @param mixed timestamp (int) / Valid DateTime format (string) + */ + public function setUntil($ts) { + if ( is_int($ts) ) + $dt = new DateTime('@'.$ts); + else + $dt = new DateTime($ts); + $this->until = $dt->format('Ymd\THisO'); + } + /** * Retrieves the desired member variable and returns it (if it's set) * @param string $member name of the member variable @@ -103,7 +119,6 @@ class SG_iCal_Recurrence { * @return mixed string if the member has been set, false otherwise */ public function getUntil() { - return $this->getMember('until'); } diff --git a/tests/blocks/VEventTest.php b/tests/blocks/VEventTest.php index 64cee32..1a0f1b1 100755 --- a/tests/blocks/VEventTest.php +++ b/tests/blocks/VEventTest.php @@ -116,6 +116,6 @@ class VEventTest extends PHPUnit_Framework_TestCase { date_default_timezone_set('America/New_York'); $event = new SG_iCal_VEvent($data, $ical); - $this->assertEquals(strtotime('20091030T090000'), $event->getEnd()); + $this->assertEquals(strtotime('20091030T090000'), $event->getProperty('laststart')); } } diff --git a/tests/helpers/AllTests.php b/tests/helpers/AllTests.php index c33ed7b..b70eb30 100755 --- a/tests/helpers/AllTests.php +++ b/tests/helpers/AllTests.php @@ -2,6 +2,7 @@ require_once 'PHPUnit/Framework.php'; require_once dirname(__FILE__).'/FreqTest.php'; require_once dirname(__FILE__).'/RecurrenceTest.php'; +require_once dirname(__FILE__).'/DurationTest.php'; class Helpers_AllTests { @@ -9,6 +10,7 @@ class Helpers_AllTests { $suite = new PHPUnit_Framework_TestSuite('Helpers'); $suite->addTestSuite('FreqTest'); $suite->addTestSuite('RecurrenceTest'); + $suite->addTestSuite('DurationTest'); return $suite; } diff --git a/tests/helpers/DurationTest.php b/tests/helpers/DurationTest.php new file mode 100755 index 0000000..3f6cead --- /dev/null +++ b/tests/helpers/DurationTest.php @@ -0,0 +1,35 @@ +secsMin = 60; + $this->secsHour = 60 * 60; + $this->secsDay = 24 * 60 * 60; + $this->secsWeek = 7 * 24 * 60 * 60; + } + + public function testDurationDateTime() { + + //A duration of 10 days, 6 hours and 20 seconds + $dur = new SG_iCal_Duration('P10DT6H0M20S'); + $this->assertEquals($this->secsDay*10 + $this->secsHour*6 + 20, $dur->getDuration() ); + } + + public function testDurationWeek() { + + //A duration of 2 weeks + $dur = new SG_iCal_Duration('P2W'); + $this->assertEquals($this->secsWeek * 2, $dur->getDuration() ); + } + + public function testDurationNegative() { + + //A duration of -1 day + $dur = new SG_iCal_Duration('-P1D'); + $this->assertEquals(-1 * $this->secsDay, $dur->getDuration() ); + } + +} diff --git a/tests/helpers/FreqTest.php b/tests/helpers/FreqTest.php index d390d95..3b0877c 100755 --- a/tests/helpers/FreqTest.php +++ b/tests/helpers/FreqTest.php @@ -23,12 +23,12 @@ class FreqTest extends PHPUnit_Framework_TestCase { 873961200, -1 ); - + $rule = 'FREQ=DAILY;COUNT=10'; $start = strtotime('19970902T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testDailyUntil() { $dateset = array( 873183600, @@ -43,15 +43,15 @@ class FreqTest extends PHPUnit_Framework_TestCase { 873961200, 874047600 ); - + $rule = 'FREQ=DAILY;UNTIL=19971224T000000Z'; $start = strtotime('19970902T090000'); $this->assertRule( $rule, $start, $dateset); - + $freq = new SG_iCal_Freq($rule, $start); $this->assertEquals(882864000, $freq->previousOccurrence(time())); } - + public function testDailyInterval() { $dateset = array( 873183600, @@ -66,7 +66,7 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19970902T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testDailyIntervalCount() { $dateset = array( 873183600, @@ -80,7 +80,7 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19970902T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testDailyBydayBymonthUntil() { $rules = array( 'FREQ=YEARLY;UNTIL=20000131T090000Z;BYMONTH=1;BYDAY=SU,MO,TU,WE,TH,FR,SA', @@ -106,22 +106,22 @@ class FreqTest extends PHPUnit_Framework_TestCase { 946972800 ) ); - + foreach( $rules As $rule ) { $start = strtotime('19980101T090000'); $this->assertRule( $rule, $start, $datesets[0]); - + $start = strtotime('+1 year', $start); $this->assertRule( $rule, $start, $datesets[1]); - + $start = strtotime('+1 year', $start); $this->assertRule( $rule, $start, $datesets[2]); - + $freq = new SG_iCal_Freq($rule, $start); $this->assertEquals(949305600, $freq->previousOccurrence(time())); } } - + public function testWeeklyCount() { $dateset = array( 873183600, @@ -140,7 +140,7 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19970902T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testWeeklyUntil() { $dateset = array( 873183600, @@ -158,11 +158,11 @@ class FreqTest extends PHPUnit_Framework_TestCase { $rule = 'FREQ=WEEKLY;UNTIL=19971224T000000Z'; $start = strtotime('19970902T090000'); $this->assertRule( $rule, $start, $dateset); - + $freq = new SG_iCal_Freq($rule, $start); $this->assertEquals(882864000, $freq->previousOccurrence(time()), 'Failed getting correct end date'); } - + public function testWeeklyBydayLimit() { $rules = array( 'FREQ=WEEKLY;UNTIL=19971007T000000Z;WKST=SU;BYDAY=TU,TH', @@ -186,7 +186,7 @@ class FreqTest extends PHPUnit_Framework_TestCase { $this->assertRule( $rule, $start, $dateset); } } - + public function testWeeklyIntervalUntilByday() { $dateset = array( 873183600, @@ -204,11 +204,11 @@ class FreqTest extends PHPUnit_Framework_TestCase { $rule = 'FREQ=WEEKLY;INTERVAL=2;UNTIL=19971224T000000Z;WKST=SU;BYDAY=MO,WE,FR'; $start = strtotime('19970902T090000'); $this->assertRule( $rule, $start, $dateset); - + $freq = new SG_iCal_Freq($rule, $start); $this->assertEquals(882777600, $freq->previousOccurrence(time()), 'Failed getting correct end date'); } - + public function testWeeklyIntervalBydayCount() { $dateset = array( 873183600, @@ -225,7 +225,7 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19970902T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testMonthlyBydayCount() { $dateset = array( 873442800, @@ -244,7 +244,7 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19970905T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testMonthlyBydayUntil() { $dateset = array( 873442800, @@ -257,7 +257,7 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19970905T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testMonthlyIntervalBydayCount2() { $dateset = array( 873615600, @@ -291,7 +291,7 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19970922T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testMonthlyBymonthday() { $dateset = array( 875430000, @@ -305,7 +305,7 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19970928T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testMonthlyBymonthdayCount() { $dateset = array( 873183600, @@ -324,7 +324,7 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19970902T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testMonthlyBymonthdayCount2() { $dateset = array( 875602800, @@ -343,7 +343,7 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19970930T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testMonthlyIntervalBymonthdayCount() { $dateset = array( 873874800, @@ -362,7 +362,7 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19970910T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testMonthlyIntervalByday() { $dateset = array( 873183600, @@ -380,7 +380,7 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19970902T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testYearlyCountBymonth() { $dateset = array( 865926000, @@ -399,7 +399,7 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19970610T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testYearlyIntervalCountBymonth() { $dateset = array( 857980800, @@ -418,7 +418,7 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19970310T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testYearlyIntervalCountByyearday() { $dateset = array( 852105600, @@ -437,7 +437,7 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19970101T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testYearlyByday() { $dateset = array( 864025200, @@ -448,7 +448,7 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19970519T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testYearlyByweeknoByday() { $dateset = array( 863420400, @@ -509,7 +509,7 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19970902T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testYearlyBydayBymonthday2() { $dateset = array( 874134000, @@ -527,7 +527,7 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19970913T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testYearlyIntervalBymonthBydayBymonthday() { $dateset = array( 847180800, @@ -538,9 +538,9 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19961105T090000'); $this->assertRule( $rule, $start, $dateset); } - + // TODO: SETPOS rules - + public function testHourlyIntervalUntil() { $dateset = array( 873183600, @@ -552,7 +552,7 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19970902T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testMinutelyIntervalCount() { $dateset = array( 873183600, @@ -567,7 +567,7 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19970902T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testMinutelyIntervalCount2() { $dateset = array( 873183600, @@ -580,11 +580,11 @@ class FreqTest extends PHPUnit_Framework_TestCase { $start = strtotime('19970902T090000'); $this->assertRule( $rule, $start, $dateset); } - + public function testMinutelyIntervalByhour() { $rules = array( 'FREQ=MINUTELY;INTERVAL=20;BYHOUR=9,10,11,12,13,14,15,16'/*, - 'FREQ=DAILY;BYHOUR=9,10,11,12,13,14,15,16;BYMINUTE=0,20,40'*/ + 'FREQ=DAILY;BYHOUR=9,10,11,12,13,14,15,16;BYMINUTE=0,20,40'*/ ); // TODO: Fix it so multi byhour and byminute will work $dateset = array( @@ -605,25 +605,68 @@ class FreqTest extends PHPUnit_Framework_TestCase { } } - public function testLastOccurrence() { + /* + weird : in this test $start is not a matched occurrence but... + + to do something like that, we need EXDATE : + DTSTART;TZID=US-Eastern:19970902T090000 + EXDATE;TZID=US-Eastern:19970902T090000 + RRULE:FREQ=MONTHLY;BYDAY=FR;BYMONTHDAY=13 + */ + + public function testFirstOccurrencesByYearDay() { $rule = 'FREQ=YEARLY;INTERVAL=2;BYYEARDAY=1;COUNT=5'; $start = strtotime('2009-10-27T090000'); $freq = new SG_iCal_Freq($rule, $start); - $this->assertEquals(strtotime('2018-01-01T09:00:00'), $freq->lastOccurrence()); + $this->assertEquals(strtotime('2009-10-27T09:00:00'), $freq->firstOccurrence()); + $this->assertEquals(strtotime('2011-01-01T09:00:00'), $freq->nextOccurrence($start)); } - - // TODO: WKST rule - + + public function testFirstOccurrencesByYearDayWithoutFirstDate() { + $rule = 'FREQ=YEARLY;INTERVAL=2;BYYEARDAY=1;COUNT=5'; + $start = strtotime('2009-10-27T090000'); + $freq = new SG_iCal_Freq($rule, $start, array($start)); + $this->assertEquals(strtotime('2011-01-01T09:00:00'), $freq->firstOccurrence()); + } + + public function testLastOccurrenceByYearDay() { + $rule = 'FREQ=YEARLY;INTERVAL=2;BYYEARDAY=1;COUNT=5'; + $start = strtotime('2011-01-01T090000'); + $freq = new SG_iCal_Freq($rule, $start); + $this->assertEquals(strtotime('2019-01-01T09:00:00'), $freq->lastOccurrence()); + } + + public function testCacheCount() { + $rule = 'FREQ=YEARLY;INTERVAL=2;BYYEARDAY=1;COUNT=5'; + $start = strtotime('2011-01-01T090000'); + $freq = new SG_iCal_Freq($rule, $start); + $this->assertEquals(5, count($freq->getAllOccurrences())); + $this->assertEquals(strtotime('2019-01-01T09:00:00'), $freq->lastOccurrence()); + } + + /* TODO: BYSETPOS rule : + The 3rd instance into the month of one of Tuesday, Wednesday or + Thursday, for the next 3 months: + + DTSTART;TZID=US-Eastern:19970904T090000 + RRULE:FREQ=MONTHLY;COUNT=3;BYDAY=TU,WE,TH;BYSETPOS=3 + */ + + /* TODO: WKST rule + */ + + //check a serie of dates private function assertRule( $rule, $start, $dateset ) { $freq = new SG_iCal_Freq($rule, $start); reset($dateset); $n = $start - 1; do { $n = $freq->findNext($n); + //echo date('Y-m-d H:i:sO ',$n); $e = (current($dateset) != -1) ? current($dateset) : false; $this->assertEquals($e, $n); } while( next($dateset) !== false ); } } -?> \ No newline at end of file +?> diff --git a/tests/test.sh b/tests/test.sh new file mode 100755 index 0000000..5b0538a --- /dev/null +++ b/tests/test.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +# aptitude install phpunit + +phpunit AllTests.php diff --git a/tests/timestamp.sh b/tests/timestamp.sh new file mode 100755 index 0000000..a2f431e --- /dev/null +++ b/tests/timestamp.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +TIMESTAMP=$1 + +php -r "echo date('Y-m-d H:i:s O',$TIMESTAMP); echo \"\n\"; " +