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($("")
+ .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 = $(" ");
+ $.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($(" ").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 += ""+monday.getWeek()+" ";
+ }
+ 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 += ""+monday.getWeek()+" ";
+ }
+ 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" +
+ "" +
+ "" + (opt('weekNumbers') ? formatDate(t.visStart, 'w') : ' ') + " ";
+ for (i=0; i" + formatDate(d, colFormat) + "";
+ addDays(d, dis);
+ if (nwe) {
+ skipWeekend(d, dis);
+ }
+ }
+ s += " ";
+ if (opt('allDaySlot')) {
+ s += "" +
+ "" + opt('allDayText') + " " +
+ "" +
+ " " +
+ " " +
+ "
";
+ }
+ s+= "";
+ 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 += "" +
+ ((!slotNormal || !minutes) ? formatDate(d, opt('axisFormat')) : ' ') +
+ "
";
+ addMinutes(d, opt('slotMinutes'));
+ slotCnt++;
+ }
+ s += "
";
+ 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
+
+
+
+
+
+
+
+loading...
+
+
+
\ 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\"; "
+