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 |
|
---|
10 | var PROP_NAME = 'icalendar';
|
---|
11 | var FLASH_ID = 'icalendar-flash-copy';
|
---|
12 |
|
---|
13 | /* iCalendar sharing manager. */
|
---|
14 | function 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 |
|
---|
63 | var 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}];
|
---|
67 | var SE = 0;
|
---|
68 | var MI = 1;
|
---|
69 | var HR = 2;
|
---|
70 | var DY = 3;
|
---|
71 | var MO = 4;
|
---|
72 | var YR = 5;
|
---|
73 | var 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 ? '' : ' ');
|
---|
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! */
|
---|
379 | function 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 */
|
---|
412 | function 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 */
|
---|
429 | function 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 */
|
---|
463 | function 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 */
|
---|
524 | function 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 */
|
---|
538 | var FOLDED = /^\s(.*)$/;
|
---|
539 | /* Pattern for an individual entry: name:value */
|
---|
540 | var ENTRY = /^([A-Za-z0-9-]+)((?:;[A-Za-z0-9-]+=(?:"[^"]+"|[^";:,]+)(?:,(?:"[^"]+"|[^";:,]+))*)*):(.*)$/;
|
---|
541 | /* Pattern for an individual parameter: name=value[,value] */
|
---|
542 | var PARAM = /;([A-Za-z0-9-]+)=((?:"[^"]+"|[^";:,]+)(?:,(?:"[^"]+"|[^";:,]+))*)/g;
|
---|
543 | /* Pattern for an individual parameter value: value | "value" */
|
---|
544 | var PARAM_VALUE = /,?("[^"]+"|[^";:,]+)/g;
|
---|
545 | /* Pattern for a date only field: yyyymmdd */
|
---|
546 | var DATEONLY = /^(\d{4})(\d\d)(\d\d)$/;
|
---|
547 | /* Pattern for a date/time field: yyyymmddThhmmss[Z] */
|
---|
548 | var 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] */
|
---|
550 | var 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 */
|
---|
552 | var TZ_OFFSET = /^([+-])(\d\d)(\d\d)$/;
|
---|
553 | /* Pattern for a duration: [+-]PnnW or [+-]PnnDTnnHnnMnnS */
|
---|
554 | var DURATION = /^([+-])?P(\d+W)?(\d+D)?(T)?(\d+H)?(\d+M)?(\d+S)?$/;
|
---|
555 | /* Reserved names not suitable for attrbiute names. */
|
---|
556 | var 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 */
|
---|
562 | function 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 */
|
---|
584 | function 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 */
|
---|
623 | function 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 */
|
---|
657 | function 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 */
|
---|
687 | function 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 */
|
---|
706 | function 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) */
|
---|
724 | function 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 */
|
---|
743 | function 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 */
|
---|
753 | function 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) */
|
---|
764 | function 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 */
|
---|
789 | function isArray(a) {
|
---|
790 | return (a && a.constructor == Array);
|
---|
791 | }
|
---|
792 |
|
---|
793 | })(jQuery);
|
---|