1/* global jQuery */ 2/** 3 * AddCalEvent.js 4 5 * @author Mike Sendlakowski, [email protected] 6 * 7 * Adds functionality to a button element to all users to add an event 8 * to various calendar apps 9 * 10 * Usage: 11 * All parameters are optional. However, if you want this to work for Outlook and iCal you must 12 * provide the url that will generate the ics file response 13 * 14 * $(SELECTOR).addcalevent({ 15 * 'onclick': true, 16 * 'apps': [1,2,4], 17 * 'ics': '', 18 * 'data': {} 19 * }); 20 * 21 * onclick: Open the dropdown on click rather than mouseover (default: false) 22 * data: Event data (see below for format). This can also be set individually as a json string directly on each HTML element. 23 * linkText: Array containing localized or customized text for the calendar links (order is critical) 24 * ics: Url that will generate the server-side ics response based on the event details. Not optional if Outlook or iCal is needed. 25 * apps: Array containing the applications available to the user. (default: [] - which is all apps) 26 * 0 = Outlook 27 * 1 = Google 28 * 2 = Yahoo 29 * 3 = Hotmail 30 * 4 = iCal 31 * 32 * Format of event data object: 33 * 34 * title: '', 35 * desc: '', 36 * location: '', 37 * url: '', // only available in Yahoo calendars 38 * organizer: { // only available in Outlook 39 * name: '', 40 * email: '' 41 * }, 42 * time: { 43 * start: '', // 'month day, year 14:30:00' 44 * end: '', // 'month day, year 14:30:00' 45 * zone: '', // '+tt:tt' - plus or minus from UTC time zone (example: Pacific Daylight Time is '-07:00') 46 * allday: false 47 * } 48 * 49 */ 50 51(function(window, $, undefined) { 52 'use strict'; 53 54 // CONSTANTS 55 var EVENT_SHOW = 'AddCalEvent:show', 56 EVENT_ADDED = 'AddCalEvent:add', 57 EVENT_INVALID = 'AddCalEvent:invalid', 58 DEFAULT_EVENT = { 59 title: '', 60 desc: '', 61 location: '', 62 url: '', 63 organizer: {name: '', email: ''}, 64 time: { 65 start: '', 66 end: '', 67 zone: '00:00', 68 allday: false 69 } 70 }; 71 72 $.addcalevent = function addcalevent(options, element) { 73 this.el = $(element); 74 // Flag the object in the event of a failed creation 75 if (!this._create(options)) { 76 this.failed = true; 77 } 78 }; 79 80 $.addcalevent.defaults = { 81 onclick: false, 82 apps: [], 83 ics: '', 84 preVal: true, 85 disabledClass: "dis", 86 linkText: [ 87 'Outlook Calendar', 88 'Google Calendar', 89 'Yahoo Calendar', 90 'Hotmail Calendar', 91 'iCal Calendar' 92 ] 93 }; 94 95 $.addcalevent.prototype = { 96 97 /* 98 ---------------------------- 99 Private methods 100 ---------------------------- 101 */ 102 103 _create: function(options){ 104 this.options = $.extend(true, {}, $.addcalevent.defaults, options); 105 if(this.options.preVal){ 106 this._disable(!this._sanitizeData()); 107 } 108 this._attach(); 109 return true; 110 }, 111 112 // add the basic hook to the button 113 _attach: function() { 114 var self = this; 115 116 // handle clicking on a calendar link 117 this.el.on('click', '.ace_opt', function(event){self._click(this, event);}); 118 119 // handle opening and closing the dropdown - always available for accessiblity 120 this.el.on('click', function(event){self._clickShow(event);}); 121 $(document).on('click', function(){self._hide();}); 122 123 // handle opening and closing the dropdown on mouseover 124 if(!this.options.onclick) { 125 this.el.on('mouseover', function(){self._show();}); 126 //this.el.on('mouseout', function(){self._hide();}); 127 } 128 }, 129 130 // onclick handler for button (not calendar links) 131 _clickShow: function(event) { 132 if(this.open) { 133 this._hide(); 134 } else { 135 this._show(); 136 this.open = true; 137 } 138 event.stopPropagation(); 139 }, 140 141 // show the dropdown 142 _show: function() { 143 var self = this; 144 if(!this.dd) { 145 this._createDropdown(); 146 } 147 this.dd.stop(); 148 this.dd.clearQueue(); 149 150 this.dd.show(); 151 self.dd.css(self.ddCss); 152 // if(this.opening) { // its already started animating, so finish it immediately 153 // this.dd.show(); 154 // self.dd.css(self.ddCss); 155 // } else { 156 // this.dd.show('fast'); 157 // this.opening = true; 158 // this._eventPublisher(EVENT_SHOW); 159 // } 160 }, 161 162 // hide the dropdown 163 _hide: function() { 164 var self = this; 165 if(!this.dd) { // if it doesn't have a dropdown then no need to hide it. duh! 166 return; 167 } 168 this.open = false;
169 this.dd.stop(); 170 this.dd.clearQueue(); 171 this.dd.hide('fast', function(){ 172 self.opening = false; 173 self.dd.css(self.ddCss); // reset the height and opacity, because the jquery animation was losing it when we call stop 174 }); 175 }, 176 177 // create the dropdown 178 _createDropdown: function(showOpts) { 179 var isValid = this._sanitizeData(); 180 this._disable(!isValid); 181 this.dd = $(this._createHtml(isValid)); 182 this.el.append(this.dd); 183 this.ddCss = { 184 height: this.dd.css("height"), 185 opacity: 1 186 } 187 }, 188 189 _disable:function(disable){ 190 this.el.toggleClass(this.options.disabledClass, disable); 191 }, 192 193 // create the html for the dropdown 194 _createHtml: function(isValid) { 195 var styleString = this._getStyle(), 196 optionLinks = ''; 197 for(var i=0,j=this.options.linkText.length;i<j;i++) { 198 if(this._isLinkNeeded(i)) { 199 optionLinks += '<li><a class="ace_opt" data-ace-id="'+ i +'" href="javascript:;">' + this.options.linkText[i] + '</a>'; 200 } 201 } 202 return '<ul class="ace_dd" style="">' + optionLinks + '</ul>'; 203 }, 204 205 // test to see if we need to display this link 206 _isLinkNeeded: function(linkId) { 207 var isOK = false, 208 appLength = this.options.apps.length; 209 if(appLength === 0) { 210 isOK = this._runIcsTest(linkId); 211 } else { 212 for(var y=0,z=this.options.apps.length;y<z;y++) { 213 if(this.options.apps[y] === linkId) { 214 isOK = this._runIcsTest(linkId); 215 break; 216 } 217 } 218 } 219 return isOK; 220 }, 221 222 // only show the outlook and ical links if the ics url is available 223 _runIcsTest: function(linkId) { 224 var isOK = false; 225 if(linkId === 0 || linkId === 4) { 226 if(this.options.ics !== '') { 227 isOK = true; 228 } 229 } else { 230 isOK = true; 231 } 232 return isOK; 233 }, 234 235 // get the absolute position and min-width based on the parent button 236 // could cause issues if the button moves around the page dynamically when it resizes 237 _getStyle: function() { 238 var pos = this.el.position(), 239 left = pos.left + parseFloat(this.el.css('marginLeft')), 240 top = pos.top + this.el.outerHeight() + parseFloat(this.el.css('marginTop')) - 1, 241 minwidth = this.el.outerWidth() - parseFloat(this.el.css('borderLeftWidth')); 242 243 244 return 'min-width:' + minwidth + 'px;top:' + top + 'px;left:' + left + 'px'; 245 }, 246 247 // make sure all event data is available and valid 248 _sanitizeData: function() { 249 var testData = this._getDataFromEl(), 250 isValid = true, 251 zoneRegex = /^[\+\-]?[0-9]{2}:[0-9]{2}/; 252 253 if(!testData) { 254 testData = this.options.data; 255 } 256 this.options.data = $.extend(true, {}, DEFAULT_EVENT, testData); 257 this.options.data.timeObj = {}; 258 259 // is the start date valid? 260 this.options.data.timeObj.start = new Date(this.options.data.time.start); 261 if(isNaN(this.options.data.timeObj.start.getTime())) { 262 isValid = false; 263 this._eventPublisher(EVENT_INVALID, {invalidStartDate: this.options.data.time.start}); 264 } 265 266 // is the end date valid? 267 this.options.data.timeObj.end = new Date(this.options.data.time.end); 268 if(isNaN(this.options.data.timeObj.end.getTime())) { 269 isValid = false; 270 this._eventPublisher(EVENT_INVALID, {invalidEndDate: this.options.data.time.end}); 271 } 272 273 // is the time zone valid? 274 this._lookupTimeZone(); 275 if(this.options.data.time.zone.length < 6) { 276 this.options.data.time.zone = '+' + this.options.data.time.zone; 277 } 278 279 if(!zoneRegex.test(this.options.data.time.zone)) { 280 isValid = false;
281 this._eventPublisher(EVENT_INVALID, {invalidTimeZone: this.options.data.time.zone}); 282 this.options.data.time.zone = '+00:00'; 283 } 284 285 return isValid; 286 }, 287 288 // translates a time zone abbreviation to an offset, as long as 289 // AddCalEventZones.js is loaded. But ideally the zone offset is identified on the server. 290 _lookupTimeZone: function(){ 291 if(AddCalEventZones && AddCalEventZones[this.options.data.time.zone]) { 292 this.options.data.time.zone = AddCalEventZones[this.options.data.time.zone]; 293 } 294 }, 295 296 // get the data from the html element rather than the plugin config 297 _getDataFromEl: function(){ 298 var data = this.el.data('ace'); 299 if(typeof data === 'string') { 300 this._eventPublisher(EVENT_INVALID, {invalidDataString: data}); 301 data = null; 302 } 303 return data; 304 }, 305 306 // update the event options 307 _update: function(options){ 308 this.options = $.extend(true, {}, $.addcalevent.defaults, options); 309 if(this.options.preVal){ 310 this._disable(!this._sanitizeData()); 311 } 312 return true; 313 }, 314 315 // an event link was clicked 316 _click: function(item, event) { 317 var id = $(item).data('ace-id'); 318 this._addEvent(id); 319 event.stopPropagation(); 320 this._hide(); 321 }, 322 323 // create an 'add to calendar' url and open it in a new window 324 // most of the details were taken from this article: 325 // http://richmarr.wordpress.com/2008/01/07/adding-events-to-users-calendars-part-2-web-calendars/ 326 _addEvent: function(calendarLinkType){ 327 var url, 328 sameWindow; 329 switch(calendarLinkType) { 330 case 0: //Outlook 331 case 4: // iCal 332 url = this._getUrl_ics(); 333 sameWindow = true; 334 break; 335 case 1: // Google 336 url = this._getUrl_google(); 337 break; 338 case 2: // Yahoo 339 url = this._getUrl_yahoo(); 340 break; 341 case 3: // Hotmail 342 url = this._getUrl_hotmail(); 343 break; 344 } 345 if(url) { 346 if(sameWindow) { 347 location.href = url; 348 } else { 349 window.open(url, 'calendar'); 350 } 351 } 352 this._eventPublisher(EVENT_ADDED, {calendarLinkType: calendarLinkType, url: url}); 353 }, 354 355 _getUTCTime: function(dateObj) { 356 var newDateObj = this._adjustToUTC(dateObj, this.options.data.time.zone); 357 return this._getDatePart(newDateObj.getFullYear(),4) + this._getDatePart(newDateObj.getMonth()+1,2) + this._getDatePart(newDateObj.getDate(),2) + 'T' + this._getDatePart(newDateObj.getHours(),2) + this._getDatePart(newDateObj.getMinutes(),2) + this._getDatePart(newDateObj.getSeconds(),2) + 'Z'; 358 }, 359 360 // return a new date object based on dateObj and the zone 361 _adjustToUTC: function(dateObj, zone){ 362 var dateOut = new Date(dateObj), 363 hours, mins; 364 365 if(isNaN(dateOut.getTime())) { 366 return new Date(); 367 } 368 369 // adjust to UTC 370 hours = zone.substring(1,3); 371 mins = zone.substring(4,6); 372 if(zone.substring(0,1) === '-') { 373 dateOut.setHours(dateOut.getHours() + (hours-0)); 374 dateOut.setMinutes(dateOut.getMinutes() + (mins-0)); 375 } else { 376 dateOut.setHours(dateOut.getHours() - hours); 377 dateOut.setMinutes(dateOut.getMinutes() - mins); 378 } 379 return dateOut; 380 }, 381 382 _getDatePart: function(part, digits){ 383 part = part.toString(); 384 while(part.length < digits) { 385 part = '0' + part; 386 } 387 return part;
388 }, 389 390 _getDateDiff: function(startDate, endDate) { 391 var diff = Math.floor((endDate - startDate)/60000), 392 hours = Math.floor(diff/60), 393 mins = diff - (hours * 60); 394 return this._getDatePart(hours,2) + this._getDatePart(mins,2); 395 }, 396 397 _getUrl_ics: function() { 398 var data = this.options.data, 399 url = this.options.ics; 400 401 url += (url.indexOf('?') === -1) ? '?' : '&'; 402 url += 'title=' + encodeURIComponent(data.title); 403 url += '&desc=' + encodeURIComponent(data.desc); 404 url += '&start=' + this._getUTCTime(data.timeObj.start); 405 url += '&end=' + this._getUTCTime(data.timeObj.end); 406 url += '&loc=' + encodeURIComponent(data.location); 407 url += '&org=' + encodeURIComponent(data.organizer.name); 408 url += '&url=' + encodeURIComponent(data.url); 409 url += '&offset=' + new Date().getTimezoneOffset(); 410 return url; 411 }, 412 413 _getUrl_google: function() { 414 var data = this.options.data, 415 url = 'http://www.google.com/calendar/event?action=TEMPLATE'; 416 url += '&text=' + encodeURIComponent(data.title); 417 url += '&details=' + encodeURIComponent(data.desc); 418 url += '&location=' + encodeURIComponent(data.location); 419 url += '&dates=' + encodeURIComponent(this._getUTCTime(data.timeObj.start) + '/' + this._getUTCTime(data.timeObj.end)); // time needs to be sent as UTC and let Google handle converting to local 420 url += '&sprop=website:' + encodeURIComponent(data.url); 421 url += '&sprop=name:' + encodeURIComponent(data.organizer.name); 422 return url; 423 }, 424 _getUrl_yahoo: function() { 425 var data = this.options.data, 426 url = 'http://calendar.yahoo.com?v=60'; 427 url += '&TITLE=' + encodeURIComponent(data.title); 428 url += '&DESC=' + encodeURIComponent(data.desc); 429 url += '&URL=' + encodeURIComponent(data.url); 430 url += '&in_loc=' + encodeURIComponent(data.location); 431 url += '&ST=' + this._getUTCTime(data.timeObj.start); 432 url += '&DUR=' + this._getDateDiff(data.timeObj.start, data.timeObj.end); 433 return url; 434 }, 435 _getUrl_hotmail: function() { 436 var data = this.options.data, 437 url = 'http://calendar.live.com/calendar/calendar.aspx?rru=addevent'; 438 url += '&summary=' + encodeURIComponent(data.title); 439 url += '&location=' + encodeURIComponent(data.location); 440 url += '&dtstart=' + this._getUTCTime(data.timeObj.start); 441 url += '&dtend=' + this._getUTCTime(data.timeObj.end); 442 url += '&description=' + encodeURIComponent(data.desc); 443 return url; 444 }, 445 446 _eventPublisher: function(evt, evtObj) { 447 evtObj = evtObj || {}; 448 evtObj.buttonId = this.el.attr('id'); 449 evtObj.data = this.options.data; 450 $(document).trigger(evt, evtObj); 451 if(window.console){ 452 window.console.log(evt); 453 window.console.log(evtObj); 454 } 455 }, 456 457 /* 458 ---------------------------- 459 Public methods 460 ---------------------------- 461 */ 462 463 getEventData: function(returnObj) { 464 this._sanitizeData(); 465 returnObj.data = this.options.data; 466 }, 467 468 update: function(options){ 469 this._update(options); 470 } 471 }; 472 473 474 /* 475 ---------------------------- 476 Function 477 ---------------------------- 478 */ 479 480 $.fn.addcalevent = function addcalevent_init(options) { 481 482 var thisCall = typeof options; 483 484 switch (thisCall) { 485 486 // allow users to call a specific public method 487 case 'string': 488 var args = Array.prototype.slice.call(arguments, 1); 489 try{ 490 this.each(function () { 491 var instance = $.data(this, 'addcalevent'); 492 if (!instance) { 493 throw 'Method ' + options + ' cannot be called until addcalevent is setup'; 494 } 495 if (!$.isFunction(instance[options]) || options.charAt(0) === '_') { 496 throw 'No such public method ' + options + ' for addcalevent'; 497 } 498 // no errors! 499 instance[options].apply(instance, args); 500 }); 501 } catch(err){ 502 if(window.console){ 503 window.console.log(err); 504 } 505 return false;
506 } 507 break; 508 509 // attempt to create 510 case 'undefined': 511 case 'object': 512 this.each(function () { 513 var instance = $.data(this, 'addcalevent'); 514 if (instance) { 515 // update options of current instance 516 instance.update(options); 517 } else { 518 // initialize new instance 519 instance = new $.addcalevent(options, this); 520 // don't attach if instantiation failed 521 if (!instance.failed) { 522 $.data(this, 'addcalevent', instance); 523 } 524 } 525 }); 526 break; 527 528 } 529 return this; 530 }; 531 532})(window, jQuery);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.