source: trunk/prototype/app/plugins/icalendar/jquery.icalendar.js @ 5341

Revision 5341, 30.1 KB checked in by wmerlotto, 12 years ago (diff)

Ticket #2434 - Commit inicial do novo módulo de agenda do Expresso - expressoCalendar

Line 
1/* http://keith-wood.name/icalendar.html
2   iCalendar processing for jQuery v1.1.1.
3   Written by Keith Wood (kbwood{at}iinet.com.au) October 2008.
4   Dual licensed under the GPL (http://dev.jquery.com/browser/trunk/jquery/GPL-LICENSE.txt) and
5   MIT (http://dev.jquery.com/browser/trunk/jquery/MIT-LICENSE.txt) licenses.
6   Please attribute the author if you use it. */
7
8(function($) { // Hide scope, no $ conflict
9
10var PROP_NAME = 'icalendar';
11var FLASH_ID = 'icalendar-flash-copy';
12
13/* iCalendar sharing manager. */
14function iCalendar() {
15        this._defaults = {
16                sites: [],  // List of site IDs to use, empty for all
17                icons: 'icalendar.png', // Horizontal amalgamation of all site icons
18                iconSize: 16,  // The size of the individual icons
19                target: '_blank',  // The name of the target window for the iCalendar links
20                compact: false,  // True if a compact presentation should be used, false for full
21                popup: false,  // True to have it popup on demand, false to show always
22                popupText: 'Send to my calendar...', // Text for the popup trigger
23                tipPrefix: '',  // Additional text to show in the tool tip for each icon
24                echoUrl: '',  // The URL to echo back iCalendar content, or blank for clipboard
25                echoField: '', // The ID of a field to copy the iCalendar definition into, or blank for clipboard
26                start: null,  // The start date/time of the event
27                end: null,  // The end date/time of the event
28                title: '',  // The title of the event
29                summary: '',  // The summary of the event
30                description: '',  // The description of the event
31                location: '',  // The location of the event
32                url: '',  // A URL with more information about the event
33                contact: '',  // An e-mail address for further contact about the event
34                recurrence: null, // Details about a recurring event, an object with attributes:
35                        // dates (Date or Date[]) or times (Date or Date[]) or
36                        // periods (Date[2] or Date[][2] or [][Date, string]) or
37                        // freq (string - secondly, minutely, hourly, daily, weekly, monthly, yearly),
38                        // interval (number), until (Date), count (number), weekStart (number),
39                        // by (object or object[] - type (string - second, minute, day, monthday, yearday,
40                        // weekno, month, setpos), values (number or number[] or string or string[]))
41                // Confirmation message for clipboard copy
42                copyConfirm: 'The event will be copied to your clipboard. Continue?',
43                // Success message during clipboard copy
44                copySucceeded: 'The event has been copied to your clipboard',
45                // Failure message during clipboard copy
46                copyFailed: 'Failed to copy the event to the clipboard\n',
47                copyFlash: 'clipboard.swf', // The URL for the Flash clipboard copy module
48                // Clipboard not supported message
49                copyUnavailable: 'The clipboard is unavailable, please copy the event details from below:\n'
50        };
51        this._sites = {  // The definitions of the available iCalendar sites
52                'google': {display: 'Google', icon: 0, override: null,
53                        url: 'http://www.google.com/calendar/event?action=TEMPLATE' +
54                        '&text={t}&dates={s}/{e}&details={d}&location={l}&sprop=website:{u}'},
55                'icalendar': {display: 'iCalendar', icon: 1, override: null, url: 'echo'},
56                'outlook': {display: 'Outlook', icon: 2, override: null, url: 'echo'},
57                'yahoo': {display: 'Yahoo', icon: 3, override: yahooOverride,
58                        url: 'http://calendar.yahoo.com/?v=60&view=d&type=20' +
59                        '&title={t}&st={s}&dur={p}&desc={d}&in_loc={l}&url={u}&rpat={r}'}
60        };
61}
62
63var FREQ_SETTINGS = [{method: 'Seconds', factor: 1},
64        {method: 'Minutes', factor: 60}, {method: 'Hours', factor: 3600},
65        {method: 'Date', factor: 86400}, {method: 'Month', factor: 1},
66        {method: 'FullYear', factor: 12}, {method: 'Date', factor: 604800}];
67var SE = 0;
68var MI = 1;
69var HR = 2;
70var DY = 3;
71var MO = 4;
72var YR = 5;
73var WK = 6;
74
75$.extend(iCalendar.prototype, {
76        /* Class name added to elements to indicate already configured with iCalendar. */
77        markerClassName: 'hasICalendar',
78
79        /* Override the default settings for all iCalendar instances.
80           @param  settings  (object) the new settings to use as defaults
81           @return void */
82        setDefaults: function(settings) {
83                extendRemove(this._defaults, settings || {});
84                return this;
85        },
86
87        /* Add a new iCalendar site to the list.
88           @param  id        (string) the ID of the new site
89           @param  display   (string) the display name for this site
90           @param  icon      (url) the location of an icon for this site (16x16), or
91                             (number) the index of the icon within the combined image
92           @param  url       (url) the submission URL for this site
93                             with {t} marking where the event title should be inserted,
94                             {s} indicating the event start date/time insertion point,
95                             {e} indicating the event end date/time insertion point,
96                             {p} indicating the event period (duration) insertion point,
97                             {d} indicating the event description insertion point,
98                             {l} indicating the event location insertion point,
99                             {u} indicating the event URL insertion point,
100                             {c} indicating the event contact insertion point,
101                             {r} indicating the event recurrence insertion point
102           @param  override  (function, optional) a function to override default settings
103           @return void */
104        addSite: function(id, display, icon, url, override) {
105                this._sites[id] = {display: display, icon: icon, override: override, url: url};
106                return this;
107        },
108
109        /* Return the list of defined sites.
110           @return  object[] - indexed by site id (string), each object contains
111                    display (string) the display name,
112                    icon    (string) the location of the icon, or
113                            (number) the icon's index in the combined image
114                    url     (string) the submission URL for the site */
115        getSites: function() {
116                return this._sites;
117        },
118
119        /* Attach the iCalendar widget to a div. */
120        _attachICalendar: function(target, settings) {
121                target = $(target);
122                if (target.hasClass(this.markerClassName)) {
123                        return;
124                }
125                target.addClass(this.markerClassName);
126                this._updateICalendar(target, settings);
127        },
128
129        /* Reconfigure the settings for an iCalendar div. */
130        _changeICalendar: function(target, settings) {
131                target = $(target);
132                if (!target.hasClass(this.markerClassName)) {
133                        return;
134                }
135                this._updateICalendar(target, settings);
136        },
137
138        /* Construct the requested iCalendar links. */
139        _updateICalendar: function(target, settings) {
140                settings = extendRemove($.extend({}, this._defaults,
141                        $.data(target[0], PROP_NAME) || {}), settings);
142                $.data(target[0], PROP_NAME, settings);
143                var sites = settings.sites || this._defaults.sites;
144                if (sites.length == 0) { // default to all sites
145                        $.each(this._sites, function(id) {
146                                sites[sites.length] = id;
147                        });
148                }
149                var addSite = function(site, calId) {
150                        var inserts = {t: encodeURIComponent(settings.title),
151                                d: encodeURIComponent(settings.description),
152                                s: $.icalendar.formatDateTime(settings.start),
153                                e: $.icalendar.formatDateTime(settings.end),
154                                p: $.icalendar.calculateDuration(settings.start, settings.end),
155                                l: encodeURIComponent(settings.location),
156                                u: encodeURIComponent(settings.url),
157                                c: encodeURIComponent(settings.contact),
158                                r: makeRecurrence(settings.recurrence)};
159                        if (site.override) {
160                                site.override.apply(target, [inserts, settings]);
161                        }
162                        var url = site.url;
163                        $.each(inserts, function(n, v) {
164                                var re = new RegExp('\\{' + n + '\\}', 'g');
165                                url = url.replace(re, v);
166                        });
167                        var url = (site.url == 'echo' ? '#' : url);
168                        var item = $('<li></li>');
169                        var anchor = $('<a href="' + url + '" title="' + settings.tipPrefix + site.display + '"' +
170                                (site.url == 'echo' ? '' : ' target="' + settings._target + '"') + '></a>');
171                        if (site.url == 'echo') {
172                                anchor.click(function() {
173                                        return $.icalendar._echo(target[0], calId);
174                                });
175                        }
176                        var html = '';
177                        if (site.icon != null) {
178                                if (typeof site.icon == 'number') {
179                                        html += '<span style="background: ' +
180                                                'transparent url(' + settings.icons + ') no-repeat -' +
181                                                (site.icon * settings.iconSize) + 'px 0px;' +
182                                                ($.browser.mozilla && $.browser.version < '1.9' ?
183                                                ' padding-left: ' + settings.iconSize + 'px; padding-bottom: ' +
184                                                Math.max(0, (settings.iconSize / 2) - 5) + 'px;' : '') + '"></span>';
185                                }
186                                else {
187                                        html += '<img src="' + site.icon + '"' +
188                                                (($.browser.mozilla && $.browser.version < '1.9') ||
189                                                ($.browser.msie && $.browser.version < '7.0') ?
190                                                ' style="vertical-align: bottom;"' :
191                                                ($.browser.msie ? ' style="vertical-align: middle;"' :
192                                                ($.browser.opera || $.browser.safari ?
193                                                ' style="vertical-align: baseline;"' : ''))) + '/>';
194                                }
195                                html += (settings.compact ? '' : '&#xa0;');
196                        }
197                        anchor.html(html + (settings.compact ? '' : site.display));
198                        item.append(anchor);
199                        return item;
200                };
201                var list = $('<ul class="icalendar_list' +
202                        (settings.compact ? ' icalendar_compact' : '') + '"></ul>');
203                var allSites = this._sites;
204                $.each(sites, function(index, id) {
205                        list.append(addSite(allSites[id], id));
206                });
207                target.empty().append(list);
208                if (settings.popup) {
209                        list.before('<span class="icalendar_popup_text">' +
210                                settings.popupText + '</span>').
211                                wrap('<div class="icalendar_popup"></div>');
212                        target.click(function() {
213                                var target = $(this);
214                                var offset = target.offset();
215                                $('.icalendar_popup', target).css('left', offset.left).
216                                        css('top', offset.top + target.outerHeight()).toggle();
217                        });
218                }
219        },
220
221        /* Remove the iCalendar widget from a div. */
222        _destroyICalendar: function(target) {
223                target = $(target);
224                if (!target.hasClass(this.markerClassName)) {
225                        return;
226                }
227                target.removeClass(this.markerClassName).empty();
228                $.removeData(target[0], PROP_NAME);
229        },
230
231        /* Echo the iCalendar text back to the user either as a
232           downloadable file or via the clipboard.
233           @param  target  (element) the owning division
234           @param  calId  (string) the ID of the site to send the calendar to */
235        _echo: function(target, calId) {
236                var settings = $.data(target, PROP_NAME);
237                var event = makeICalendar(settings);
238                if (settings.echoUrl) {
239                        window.location.href = settings.echoUrl + '?content=' + escape(event);
240                }
241                else if (settings.echoField) {
242                        $(settings.echoField).val(event);
243                }
244                else if (!settings.copyFlash) {
245                        alert(settings.copyUnavailable + event);
246                }
247                else if (confirm(settings.copyConfirm)) {
248                        var error = '';
249                        if (error = copyViaFlash(event, settings.copyFlash)) {
250                                alert(settings.copyFailed + error);
251                                }
252                                else {
253                                alert(settings.copySucceeded);
254                                }
255                        }
256                return false; // Don't follow link
257        },
258       
259        /* Ensure a string has at least two digits.
260           @param  value  (number) the number to convert
261           @return  (string) the string equivalent */
262        _ensureTwo: function(value) {
263                return (value < 10 ? '0' : '') + value;
264        },
265
266        /* Format a date for iCalendar: yyyymmdd.
267           @param  date   (Date) the date to format
268           @return  (string) the formatted date */
269        formatDate: function(date, local) {
270                return (!date ? '' : '' + date.getFullYear() +
271                        this._ensureTwo(date.getMonth() + 1) + this._ensureTwo(date.getDate()));
272        },
273
274        /* Format a date/time for iCalendar: yyyymmddThhmmss[Z].
275           @param  dateTime  (Date) the date/time to format
276           @param  local     (boolean) true if this should be a local date/time
277           @return  (string) the formatted date/time */
278        formatDateTime: function(dateTime, local) {
279                return (!dateTime ? '' : (local ?
280                        '' + dateTime.getFullYear() + this._ensureTwo(dateTime.getMonth() + 1) +
281                        this._ensureTwo(dateTime.getDate()) + 'T' + this._ensureTwo(dateTime.getHours()) +
282                        this._ensureTwo(dateTime.getMinutes()) + this._ensureTwo(dateTime.getSeconds()) :
283                        '' + dateTime.getUTCFullYear() + this._ensureTwo(dateTime.getUTCMonth() + 1) +
284                        this._ensureTwo(dateTime.getUTCDate()) + 'T' + this._ensureTwo(dateTime.getUTCHours()) +
285                        this._ensureTwo(dateTime.getUTCMinutes()) + this._ensureTwo(dateTime.getUTCSeconds()) + 'Z'));
286        },
287
288        /* Calculate the duration between two date/times.
289           @param  start  (Date) the starting date/time
290           @param  end    (Date) the ending date/time
291           @return  (string) the formatted duration or blank if invalid parameters */
292        calculateDuration: function(start, end) {
293                if (!start || !end) {
294                        return '';
295                }
296                var seconds = Math.abs(end.getTime() - start.getTime()) / 1000;
297                var days = Math.floor(seconds / 86400);
298                seconds -= days * 86400;
299                var hours = Math.floor(seconds / 3600);
300                seconds -= hours * 3600;
301                var minutes = Math.floor(seconds / 60);
302                seconds -= minutes * 60;
303                return (start.getTime() > end.getTime() ? '-' : '') +
304                        'P' + (days > 0 ? days + 'D' : '') +
305                        (hours || minutes || seconds ? 'T' + hours + 'H' : '') +
306                        (minutes || seconds ? minutes + 'M' : '') + (seconds ? seconds + 'S' : '');
307        },
308
309        /* Calculate the end date/time given a start and a duration.
310           @param  start     (Date) the starting date/time
311           @param  duration  (string) the description of the duration
312           @return  (Date) the ending date/time
313           @throws  error if an invalid duration is found */
314        addDuration: function(start, duration) {
315                if (!duration) {
316                        return start;
317                }
318                var end = new Date(start.getTime());
319                var matches = DURATION.exec(duration);
320                if (!matches) {
321                        throw 'Invalid duration';
322                }
323                if (matches[2] && (matches[3] || matches[5] || matches[6] || matches[7])) {
324                        throw 'Invalid duration - week must be on its own'; // Week must be on its own
325                }
326                if (!matches[4] && (matches[5] || matches[6] || matches[7])) {
327                        throw 'Invalid duration - missing time marker'; // Missing T with hours/minutes/seconds
328                }
329                var sign = (matches[1] == '-' ? -1 : +1);
330                var apply = function(value, factor, method) {
331                        value = parseInt(value);
332                        if (!isNaN(value)) {
333                                end['setUTC' + method](end['getUTC' + method]() + sign * value * factor);
334                        }
335                };
336                if (matches[2]) {
337                        apply(matches[2], 7, 'Date');
338                }
339                else {
340                        apply(matches[3], 1, 'Date');
341                        apply(matches[5], 1, 'Hours');
342                        apply(matches[6], 1, 'Minutes');
343                        apply(matches[7], 1, 'Seconds');
344                }
345                return end;
346        },
347
348        /* Parse the iCalendar data into a JavaScript object model.
349           @param  content  (string) the original iCalendar data
350           @return  (object) the iCalendar JavaScript model
351           @throws  errors if the iCalendar structure is incorrect */
352        parse: function(content) {
353                var cal = {};
354                var timezones = {};
355                var lines = unfoldLines(content);
356                parseGroup(lines, 0, cal, timezones);
357                if (!cal.vcalendar) {
358                        throw 'Invalid iCalendar data';
359                }
360                return cal.vcalendar;
361        },
362
363        /* Calculate the week of the year for a given date
364           according to the ISO 8601 definition.
365           @param  date       (Date) the date to calculate the week for
366           @param  weekStart  (number) the day on which a week starts:
367                              0 = Sun, 1 = Mon, ... (optional, defaults to 1)
368           @return  (number) the week for these parameters (1-53) */
369        getWeekOfYear: function(date, weekStart) {
370                return getWeekOfYear(date, weekStart);
371        },
372
373        _parseParams: function(owner, params) {
374                return parseParams(owner, params);
375        }
376});
377
378/* jQuery extend now ignores nulls! */
379function extendRemove(target, props) {
380        $.extend(target, props);
381        for (var name in props) {
382                if (props[name] == null) {
383                        target[name] = null;
384                }
385        }
386        return target;
387}
388
389/* Attach the iCalendar functionality to a jQuery selection.
390   @param  command  (string) the command to run (optional, default 'attach')
391   @param  options  (object) the new settings to use for these iCalendar instances
392   @return  (jQuery object) jQuery for chaining further calls */
393$.fn.icalendar = function(options) {
394        var otherArgs = Array.prototype.slice.call(arguments, 1);
395        return this.each(function() {
396                if (typeof options == 'string') {
397                        $.icalendar['_' + options + 'ICalendar'].
398                                apply($.icalendar, [this].concat(otherArgs));
399                }
400                else {
401                        $.icalendar._attachICalendar(this, options || {});
402                }
403        });
404};
405
406/* Initialise the iCalendar functionality. */
407$.icalendar = new iCalendar(); // singleton instance
408
409/* Override some substitution values for Yahoo.
410   @param  inserts   (object) current values (updated)
411   @param  settings  (object) current instance settings */
412function yahooOverride(inserts, settings) {
413        var twoDigits = function(value) {
414                return (value < 10 ? '0' : '') + value;
415        };
416        var dur = (settings.end ? (settings.end.getTime() - settings.start.getTime()) / 60000 : 0);
417        inserts.p = (dur ? twoDigits(Math.floor(dur / 60)) + '' + twoDigits(dur % 60) : ''); // hhmm
418        if (inserts.r) {
419                var byDay = (settings.recurrence.by && settings.recurrence.by[0].type == 'day' ?
420                        settings.recurrence.by[0].values.join('').toLowerCase() : '');
421                var freq = {daily: 'dy', weekly: 'wk', monthly: 'mh', yearly: 'yr'}[settings.recurrence.freq];
422                inserts.r = (byDay || freq ? twoDigits(settings.recurrence.interval || 1) + (byDay || freq) : '');
423        }
424}
425
426/* Construct an iCalendar with an event object.
427   @param  event  (object) the event details
428   @return  (string) the iCalendar definition */
429function makeICalendar(event) {
430        var limit75 = function(text) {
431                var out = '';
432                while (text.length > 75) {
433                        out += text.substr(0, 75) + '\n';
434                        text = ' ' + text.substr(75);
435                }
436                out += text;
437                return out;
438        };
439        return 'BEGIN:VCALENDAR\n' +
440                'VERSION:2.0\n' +
441                'PRODID:jquery.icalendar\n' +
442                'METHOD:PUBLISH\n' +
443                'BEGIN:VEVENT\n' +
444                'UID:' + new Date().getTime() + '@' +
445                (window.location.href.replace(/^[^\/]*\/\/([^\/]*)\/.*$/, '$1') || 'localhost') + '\n' +
446                'DTSTAMP:' + $.icalendar.formatDateTime(new Date()) + '\n' +
447                (event.url ? limit75('URL:' + event.url) + '\n' : '') +
448                (event.contact ? limit75('MAILTO:' + event.contact) + '\n' : '') +
449                limit75('TITLE:' + event.title) + '\n' +
450                'DTSTART:' + $.icalendar.formatDateTime(event.start) + '\n' +
451                'DTEND:' + $.icalendar.formatDateTime(event.end) + '\n' +
452                (event.summary ? limit75('SUMMARY:' + event.summary) + '\n' : '') +
453                (event.description ? limit75('DESCRIPTION:' + event.description) + '\n' : '') +
454                (event.location ? limit75('LOCATION:' + event.location) + '\n' : '') +
455                (event.recurrence ? makeRecurrence(event.recurrence) + '\n' : '') +
456                'END:VEVENT\n' +
457                'END:VCALENDAR';
458}
459
460/* Construct an iCalendar recurrence definition.
461   @param  recur  (object) the recurrence details
462   @return  (string) the iCalendar definition */
463function makeRecurrence(recur) {
464        if (!recur) {
465        return '';
466        }
467        var def = '';
468        if (recur.dates) {
469                def = 'RDATE;VALUE=DATE:';
470                if (!isArray(recur.dates)) {
471                        recur.dates = [recur.dates];
472                }
473                for (var i = 0; i < recur.dates.length; i++) {
474                        def += (i > 0 ? ',' : '') + $.icalendar.formatDate(recur.dates[i]);
475                }
476        }
477        else if (recur.times) {
478                def = 'RDATE;VALUE=DATE-TIME:';
479                if (!isArray(recur.times)) {
480                        recur.times = [recur.times];
481                }
482                for (var i = 0; i < recur.times.length; i++) {
483                        def += (i > 0 ? ',' : '') + $.icalendar.formatDateTime(recur.times[i]);
484                }
485        }
486        else if (recur.periods) {
487                def = 'RDATE;VALUE=PERIOD:';
488                if (!isArray(recur.periods[0])) {
489                        recur.periods = [recur.periods];
490                }
491                for (var i = 0; i < recur.periods.length; i++) {
492                        def += (i > 0 ? ',' : '') + $.icalendar.formatDateTime(recur.periods[i][0]) +
493                                '/' + (recur.periods[i][1].constructor != Date ? recur.periods[i][1] :
494                                $.icalendar.formatDateTime(recur.periods[i][1]));
495                }
496        }
497        else {
498                def = 'RRULE:FREQ=' + (recur.freq || 'daily').toUpperCase() +
499                        (recur.interval ? ';INTERVAL=' + recur.interval : '') +
500                        (recur.until ? ';UNTIL=' + $.icalendar.formatDateTime(recur.until) :
501                        (recur.count ? ';COUNT=' + recur.count : '')) +
502                        (recur.weekStart != null ? ';WKST=' +
503                        ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'][recur.weekStart] : '');
504                if (recur.by) {
505                        if (!isArray(recur.by)) {
506                                recur.by = [recur.by];
507                        }
508                        for (var i = 0; i < recur.by.length; i++) {
509                                if (!isArray(recur.by[i].values)) {
510                                        recur.by[i].values = [recur.by[i].values];
511                                }
512                                def += ';BY' + recur.by[i].type.toUpperCase() + '=' +
513                                        recur.by[i].values.join(',');
514                        }
515                }
516        }
517        return def;
518}
519
520/* Copy the given text to the system clipboard via Flash.
521   @param  text  (string) the text to copy
522   @param  url   (string) the URL for the Flash clipboard copy module
523   @return  (string) '' if successful, error message if not */
524function copyViaFlash(text, url) {
525        $('#' + FLASH_ID).remove();
526        try {
527                $('body').append('<div id="' + FLASH_ID + '"><embed src="' + url +
528                '" FlashVars="clipboard=' + encodeURIComponent(text) +
529                '" width="0" height="0" type="application/x-shockwave-flash"></embed></div>');
530                return '';
531        }
532        catch(e) {
533                return e;
534        }
535}
536
537/* Pattern for folded lines: start with a whitespace character */
538var FOLDED = /^\s(.*)$/;
539/* Pattern for an individual entry: name:value */
540var ENTRY = /^([A-Za-z0-9-]+)((?:;[A-Za-z0-9-]+=(?:"[^"]+"|[^";:,]+)(?:,(?:"[^"]+"|[^";:,]+))*)*):(.*)$/;
541/* Pattern for an individual parameter: name=value[,value] */
542var PARAM = /;([A-Za-z0-9-]+)=((?:"[^"]+"|[^";:,]+)(?:,(?:"[^"]+"|[^";:,]+))*)/g;
543/* Pattern for an individual parameter value: value | "value" */
544var PARAM_VALUE = /,?("[^"]+"|[^";:,]+)/g;
545/* Pattern for a date only field: yyyymmdd */
546var DATEONLY = /^(\d{4})(\d\d)(\d\d)$/;
547/* Pattern for a date/time field: yyyymmddThhmmss[Z] */
548var DATETIME = /^(\d{4})(\d\d)(\d\d)T(\d\d)(\d\d)(\d\d)(Z?)$/;
549/* Pattern for a date/time range field: yyyymmddThhmmss[Z]/yyyymmddThhmmss[Z] */
550var DATETIME_RANGE = /^(\d{4})(\d\d)(\d\d)T(\d\d)(\d\d)(\d\d)(Z?)\/(\d{4})(\d\d)(\d\d)T(\d\d)(\d\d)(\d\d)(Z?)$/;
551/* Pattern for a timezone offset field: +hhmm */
552var TZ_OFFSET = /^([+-])(\d\d)(\d\d)$/;
553/* Pattern for a duration: [+-]PnnW or [+-]PnnDTnnHnnMnnS */
554var DURATION = /^([+-])?P(\d+W)?(\d+D)?(T)?(\d+H)?(\d+M)?(\d+S)?$/;
555/* Reserved names not suitable for attrbiute names. */
556var RESERVED_NAMES = ['class'];
557
558/* iCalendar lines are split so the max length is no more than 75.
559   Split lines start with a whitespace character.
560   @param  content  (string) the original iCalendar data
561   @return  (string[]) the restored iCalendar data */
562function unfoldLines(content) {
563        var lines = content.replace(/\r\n/g, '\n').split('\n');
564        for (var i = lines.length - 1; i > 0; i--) {
565                var matches = FOLDED.exec(lines[i]);
566                if (matches) {
567                        lines[i - 1] += matches[1];
568                        lines[i] = '';
569                }
570        }
571        return $.map(lines, function(line, i) { // Remove blank lines
572                return (line ? line : null);
573        });
574}
575
576/* Parse a group in the file, delimited by BEGIN:xxx and END:xxx.
577   Recurse if an embedded group encountered.
578   @param  lines      (string[]) the iCalendar data
579   @param  index      (number) the current position within the data
580   @param  owner      (object) the current owner for the new group
581   @param  timezones  (object) collection of defined timezones
582   @return  (number) the updated position after processing this group
583   @throws  errors if group structure is incorrect */
584function parseGroup(lines, index, owner, timezones) {
585        if (index >= lines.length || lines[index].indexOf('BEGIN:') != 0) {
586                throw 'Missing group start';
587        }
588        var group = {};
589        var name = lines[index].substr(6);
590        addEntry(owner, name.toLowerCase(), group);
591        index++;
592        while (index < lines.length && lines[index].indexOf('END:') != 0) {
593                if (lines[index].indexOf('BEGIN:') == 0) { // Recurse for embedded group
594                        index = parseGroup(lines, index, group, timezones);
595                }
596                else {
597                        var entry = parseEntry(lines[index]);
598                        addEntry(group, entry._name, (entry._simple ? entry._value : entry));
599                }
600                index++;
601        }
602        if (name == 'VTIMEZONE') { // Save timezone offset
603                var matches = TZ_OFFSET.exec(group.standard.tzoffsetto);
604                if (matches) {
605                        timezones[group.tzid] = (matches[1] == '-' ? -1 : +1) *
606                                (parseInt(matches[2], 10) * 60 + parseInt(matches[3], 10));
607                }
608        }
609        else {
610                for (var name2 in group) {
611                        resolveTimezones(group[name2], timezones);
612                }
613        }
614        if (lines[index] != 'END:' + name) {
615                throw 'Missing group end ' + name;
616        }
617        return index;
618}
619
620/* Resolve timezone references for dates.
621   @param  value  (any) the current value to check - updated if appropriate
622   @param  timezones  (object) collection of defined timezones */
623function resolveTimezones(value, timezones) {
624        if (!value) {
625                return;
626        }
627        if (value.tzid && value._value) {
628                var offset = timezones[value.tzid];
629                var offsetDate = function(date, tzid) {
630                        date.setMinutes(date.getMinutes() - offset);
631                        date._type = tzid;
632                };
633                if (isArray(value._value)) {
634                        for (var i = 0; i < value._value.length; i++) {
635                                offsetDate(value._value[i], value.tzid);
636                        }
637                }
638                else if (value._value.start && value._value.end) {
639                        offsetDate(value._value.start, value.tzid);
640                        offsetDate(value._value.end, value.tzid);
641                }
642                else {
643                        offsetDate(value._value, value.tzid);
644                }
645        }
646        else if (isArray(value)) {
647                for (var i = 0; i < value.length; i++) {
648                        resolveTimezones(value[i], timezones);
649                }
650        }
651}
652
653/* Add a new entry to an object, making multiple entries into an array.
654   @param  owner  (object) the owning object for the new entry
655   @param  name   (string) the name of the new entry
656   @param  value  (string or object) the new entry value */
657function addEntry(owner, name, value) {
658        if (typeof value == 'string') {
659                value = value.replace(/\\n/g, '\n');
660        }
661        if ($.inArray(name, RESERVED_NAMES) > -1) {
662                name += '_';
663        }
664        if (owner[name]) { // Turn multiple values into an array
665                if (!isArray(owner[name]) || owner['_' + name + 'IsArray']) {
666                        owner[name] = [owner[name]];
667                }
668                owner[name][owner[name].length] = value;
669                if (owner['_' + name + 'IsArray']) {
670                        owner['_' + name + 'IsArray'] = undefined;
671                }
672        }
673        else {
674                owner[name] = value;
675                if (isArray(value)) {
676                        owner['_' + name + 'IsArray'] = true;
677                }
678        }
679}
680
681/* Parse an individual entry.
682   The format is: <name>[;<param>=<pvalue>]...:<value>
683   @param  line  (string) the line to parse
684   @return  (object) the parsed entry with _name and _value
685            attributes, _simple to indicate whether or not
686            other parameters, and other parameters as necessary */
687function parseEntry(line) {
688        var entry = {};
689        var matches = ENTRY.exec(line);
690        if (!matches) {
691                throw 'Missing entry name: ' + line;
692        }
693        entry._name = matches[1].toLowerCase();
694        entry._value = checkDate(matches[3]);
695        entry._simple = true;
696        parseParams(entry, matches[2]);
697        return entry;
698}
699
700/* Parse parameters for an individual entry.
701   The format is: <param>=<pvalue>[;...]
702   @param  owner   (object) the owning object for the parameters,
703                   updated with parameters as attributes, and
704                                   _simple to indicate whether or not other parameters
705   @param  params  (string or string[]) the parameters to parse */
706function parseParams(owner, params) {
707        var param = PARAM.exec(params);
708        while (param) {
709                var values = [];
710                var value = PARAM_VALUE.exec(param[2]);
711                while (value) {
712                        values.push(checkDate(value[1].replace(/^"(.*)"$/, '$1')));
713                        value = PARAM_VALUE.exec(param[2]);
714                }
715                owner[param[1].toLowerCase()] = (values.length > 1 ? values : values[0]);
716                owner._simple = false;
717                param = PARAM.exec(params);
718        }
719}
720
721/* Convert a value into a Date object or array of Date objects if appropriate.
722   @param  value  (string) the value to check
723   @return  (string or Date) the converted value (if appropriate) */
724function checkDate(value) {
725        var matches = DATETIME.exec(value);
726        if (matches) {
727                return makeDate(matches);
728        }
729        matches = DATETIME_RANGE.exec(value);
730        if (matches) {
731                return {start: makeDate(matches), end: makeDate(matches.slice(7))};
732        }
733        matches = DATEONLY.exec(value);
734        if (matches) {
735                return makeDate(matches.concat([0, 0, 0, '']));
736        }
737        return value;
738}
739
740/* Create a date value from matches on a string.
741   @param  matches  (string[]) the component parts of the date
742   @return  (Date) the corresponding date */
743function makeDate(matches) {
744        var date = new Date(matches[1], matches[2] - 1, matches[3],
745                matches[4], matches[5], matches[6]);
746        date._type = (matches[7] ? 'UTC' : 'float');
747        return utcDate(date);
748}
749
750/* Standardise a date to UTC.
751   @param  date  (Date) the date to standardise
752   @return  (Date) the equivalent UTC date */
753function utcDate(date) {
754        date.setMinutes(date.getMinutes() - date.getTimezoneOffset());
755        return date;
756}
757
758/* Calculate the week of the year for a given date
759   according to the ISO 8601 definition.
760   @param  date       (Date) the date to calculate the week for
761   @param  weekStart  (number) the day on which a week starts:
762                      0 = Sun, 1 = Mon, ... (optional, defaults to 1)
763   @return  (number) the week for these parameters (1-53) */
764function getWeekOfYear(date, weekStart) {
765        weekStart = (weekStart || weekStart == 0 ? weekStart : 1);
766        var checkDate = new Date(date.getFullYear(), date.getMonth(), date.getDate(),
767                (date.getTimezoneOffset() / -60));
768        var firstDay = new Date(checkDate.getFullYear(), 1 - 1, 4); // First week always contains 4 Jan
769        var firstDOW = firstDay.getDay(); // Day of week: Sun = 0, Mon = 1, ...
770        firstDay.setDate(4 + weekStart - firstDOW - (weekStart > firstDOW ? 7 : 0)); // Preceding week start
771        if (checkDate < firstDay) { // Adjust first three days in year if necessary
772                checkDate.setDate(checkDate.getDate() - 3); // Generate for previous year
773                return getWeekOfYear(checkDate, weekStart);
774        } else if (checkDate > new Date(checkDate.getFullYear(), 12 - 1, 28)) { // Check last three days in year
775                var firstDay2 = new Date(checkDate.getFullYear() + 1, 1 - 1, 4); // Find first week in next year
776                firstDOW = firstDay2.getDay();
777                firstDay2.setDate(4 + weekStart - firstDOW - (weekStart > firstDOW ? 7 : 0));
778                if (checkDate >= firstDay2) { // Adjust if necessary
779                        return 1;
780                }
781        }
782        return Math.floor(((checkDate - firstDay) /
783                (FREQ_SETTINGS[DY].factor * 1000)) / 7) + 1; // Weeks to given date
784}
785
786/* Determine whether an object is an array.
787   @param  a  (object) the object to test
788   @return  (boolean) true if it is an array, or false if not */
789function isArray(a) {
790        return (a && a.constructor == Array);
791}
792
793})(jQuery);
Note: See TracBrowser for help on using the repository browser.