diff --git a/.gitignore b/.gitignore index 68b9e27..aa6f28c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules/ bower_components/ +.DS_Store diff --git a/README.md b/README.md index db5204f..0a1ec48 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,23 @@ Simply use invoke the object and use the functions... `begin` and `end` need to be formatted in a way that is friendly to `Date()` +Recurring Events +---------------- +Recurring events can be added with the `rrule` object. + +`cal.addEvent(subject, description, location, begin, end, rrule)` + +The `rrule` object has the following properties: + +- `freq` : __Required.__ The frequency of event recurrence. Can be `DAILY`, `WEEKLY`, `MONTHLY`, or `YEARLY`. +- `until` : A date string representing the date on which to end repitition. Must be friendly to `Date()` +- `count` : Alternative to until. Repeat the event `count` times. Must be an integer +- `interval` : The interval of `freq` to recur at. For example, if `freq` is `WEEKLY` and `interval` is `2`, the event will repeat every 2 weeks. Must be an integer. +- `byday` : Which days of the week the event is to occur. An array containing any of `SU`, `MO`, `TU`, `WE`, `TH`, `FR`, `SA`. + +The four properties described above are not exhaustive of recurrence rule capabilities. If extra functionality is required, you can set the `rrule.rule` property to a full recurrence rule string. In this case, none of the four properties described above are necessary. See [this page](http://www.kanzaki.com/docs/ical/rrule.html) for a description of recurrence rules. + + Example ------- * **[Demo](http://htmlpreview.github.io/?https://github.com/nwcell/ics.js/blob/master/demo/demo.html)** @@ -25,7 +42,7 @@ Example Demo ``` diff --git a/ics.deps.min.js b/ics.deps.min.js index 6241080..2386388 100644 --- a/ics.deps.min.js +++ b/ics.deps.min.js @@ -1,2 +1,2 @@ -/*! ics.js March 11, 2014 */ -var saveAs=saveAs||typeof navigator!=="undefined"&&navigator.msSaveOrOpenBlob&&navigator.msSaveOrOpenBlob.bind(navigator)||function(e){"use strict";if(typeof navigator!=="undefined"&&/MSIE [1-9]\./.test(navigator.userAgent)){return}var t=e.document,n=function(){return e.URL||e.webkitURL||e},r=e.URL||e.webkitURL||e,i=t.createElementNS("http://www.w3.org/1999/xhtml","a"),s=!e.externalHost&&"download"in i,o=function(n){var r=t.createEvent("MouseEvents");r.initMouseEvent("click",true,false,e,0,0,0,0,0,false,false,false,false,0,null);n.dispatchEvent(r)},u=e.webkitRequestFileSystem,a=e.requestFileSystem||u||e.mozRequestFileSystem,f=function(t){(e.setImmediate||e.setTimeout)(function(){throw t},0)},l="application/octet-stream",c=0,h=[],p=function(){var e=h.length;while(e--){var t=h[e];if(typeof t==="string"){r.revokeObjectURL(t)}else{t.remove()}}h.length=0},d=function(e,t,n){t=[].concat(t);var r=t.length;while(r--){var i=e["on"+t[r]];if(typeof i==="function"){try{i.call(e,n||e)}catch(s){f(s)}}}},v=function(r,o){var f=this,p=r.type,v=false,m,g,y=function(){var e=n().createObjectURL(r);h.push(e);return e},b=function(){d(f,"writestart progress write writeend".split(" "))},w=function(){if(v||!m){m=y(r)}if(g){g.location.href=m}else{window.open(m,"_blank")}f.readyState=f.DONE;b()},E=function(e){return function(){if(f.readyState!==f.DONE){return e.apply(this,arguments)}}},S={create:true,exclusive:false},x;f.readyState=f.INIT;if(!o){o="download"}if(s){m=y(r);t=e.document;i=t.createElementNS("http://www.w3.org/1999/xhtml","a");i.href=m;i.download=o;var T=t.createEvent("MouseEvents");T.initMouseEvent("click",true,false,e,0,0,0,0,0,false,false,false,false,0,null);i.dispatchEvent(T);f.readyState=f.DONE;b();return}if(e.chrome&&p&&p!==l){x=r.slice||r.webkitSlice;r=x.call(r,0,r.size,l);v=true}if(u&&o!=="download"){o+=".download"}if(p===l||u){g=e}if(!a){w();return}c+=r.size;a(e.TEMPORARY,c,E(function(e){e.root.getDirectory("saved",S,E(function(e){var t=function(){e.getFile(o,S,E(function(e){e.createWriter(E(function(t){t.onwriteend=function(t){g.location.href=e.toURL();h.push(e);f.readyState=f.DONE;d(f,"writeend",t)};t.onerror=function(){var e=t.error;if(e.code!==e.ABORT_ERR){w()}};"writestart progress write abort".split(" ").forEach(function(e){t["on"+e]=f["on"+e]});t.write(r);f.abort=function(){t.abort();f.readyState=f.DONE};f.readyState=f.WRITING}),w)}),w)};e.getFile(o,{create:false},E(function(e){e.remove();t()}),E(function(e){if(e.code===e.NOT_FOUND_ERR){t()}else{w()}}))}),w)}),w)},m=v.prototype,g=function(e,t){return new v(e,t)};m.abort=function(){var e=this;e.readyState=e.DONE;d(e,"abort")};m.readyState=m.INIT=0;m.WRITING=1;m.DONE=2;m.error=m.onwritestart=m.onprogress=m.onwrite=m.onabort=m.onerror=m.onwriteend=null;e.addEventListener("unload",p,false);g.unload=function(){p();e.removeEventListener("unload",p,false)};return g}(typeof self!=="undefined"&&self||typeof window!=="undefined"&&window||this.content);if(typeof module!=="undefined")module.exports=saveAs;if(!(typeof Blob==="function"||typeof Blob==="object")||typeof URL==="undefined")if((typeof Blob==="function"||typeof Blob==="object")&&typeof webkitURL!=="undefined")self.URL=webkitURL;else var Blob=function(e){"use strict";var t=e.BlobBuilder||e.WebKitBlobBuilder||e.MozBlobBuilder||e.MSBlobBuilder||function(e){var t=function(e){return Object.prototype.toString.call(e).match(/^\[object\s(.*)\]$/)[1]},n=function(){this.data=[]},r=function(t,n,r){this.data=t;this.size=t.length;this.type=n;this.encoding=r},i=n.prototype,s=r.prototype,o=e.FileReaderSync,u=function(e){this.code=this[this.name=e]},a=("NOT_FOUND_ERR SECURITY_ERR ABORT_ERR NOT_READABLE_ERR ENCODING_ERR "+"NO_MODIFICATION_ALLOWED_ERR INVALID_STATE_ERR SYNTAX_ERR").split(" "),f=a.length,l=e.URL||e.webkitURL||e,c=l.createObjectURL,h=l.revokeObjectURL,p=l,d=e.btoa,v=e.atob,m=e.ArrayBuffer,g=e.Uint8Array;r.fake=s.fake=true;while(f--){u.prototype[a[f]]=f+1}if(!l.createObjectURL){p=e.URL={}}p.createObjectURL=function(e){var t=e.type,n;if(t===null){t="application/octet-stream"}if(e instanceof r){n="data:"+t;if(e.encoding==="base64"){return n+";base64,"+e.data}else if(e.encoding==="URI"){return n+","+decodeURIComponent(e.data)}if(d){return n+";base64,"+d(e.data)}else{return n+","+encodeURIComponent(e.data)}}else if(c){return c.call(l,e)}};p.revokeObjectURL=function(e){if(e.substring(0,5)!=="data:"&&h){h.call(l,e)}};i.append=function(e){var n=this.data;if(g&&(e instanceof m||e instanceof g)){var i="",s=new g(e),a=0,f=s.length;for(;a1?t:this.data.length),n,this.encoding)};s.toString=function(){return"[object Blob]"};return n}(e);return function(n,r){var i=r?r.type||"":"";var s=new t;if(n){for(var o=0,u=n.length;o-1&&navigator.userAgent.indexOf("MSIE 10")==-1){console.log("Unsupported Browser");return}var e=navigator.appVersion.indexOf("Win")!==-1?"\r\n":"\n";var t=[];var n=["BEGIN:VCALENDAR","VERSION:2.0"].join(e);var r=e+"END:VCALENDAR";return{events:function(){return t},calendar:function(){return n+e+t.join(e)+r},addEvent:function(n,r,i,s,o){if(typeof n==="undefined"||typeof r==="undefined"||typeof i==="undefined"||typeof s==="undefined"||typeof o==="undefined"){return false}var u=new Date(s);var a=new Date(o);var f=("0000"+u.getFullYear().toString()).slice(-4);var l=("00"+(u.getMonth()+1).toString()).slice(-2);var c=("00"+u.getDate().toString()).slice(-2);var h=("00"+u.getHours().toString()).slice(-2);var p=("00"+u.getMinutes().toString()).slice(-2);var d=("00"+u.getMinutes().toString()).slice(-2);var v=("0000"+a.getFullYear().toString()).slice(-4);var m=("00"+(a.getMonth()+1).toString()).slice(-2);var g=("00"+a.getDate().toString()).slice(-2);var y=("00"+a.getHours().toString()).slice(-2);var b=("00"+a.getMinutes().toString()).slice(-2);var w=("00"+a.getMinutes().toString()).slice(-2);var E="";var S="";if(p+d+b+w!=0){E="T"+h+p+d;S="T"+y+b+w}var x=f+l+c+E;var T=v+m+g+S;var N=["BEGIN:VEVENT","CLASS:PUBLIC","DESCRIPTION:"+r,"DTSTART;VALUE=DATE:"+x,"DTEND;VALUE=DATE:"+T,"LOCATION:"+i,"SUMMARY;LANGUAGE=en-us:"+n,"TRANSP:TRANSPARENT","END:VEVENT"].join(e);t.push(N);return N},download:function(i,s){if(t.length<1){return false}s=typeof s!=="undefined"?s:".ics";i=typeof i!=="undefined"?i:"calendar";var o=n+e+t.join(e)+r;var u;if(navigator.userAgent.indexOf("MSIE 10")===-1){u=new Blob([o])}else{var a=new BlobBuilder;a.append(o);u=a.getBlob("text/x-vCalendar;charset="+document.characterSet)}saveAs(u,i+s);return o}}} +/*! ics.js Wed Aug 20 2014 17:23:02 */ +var ics=function(){"use strict";if(navigator.userAgent.indexOf("MSIE")>-1&&-1==navigator.userAgent.indexOf("MSIE 10"))return void console.log("Unsupported Browser");var a=-1!==navigator.appVersion.indexOf("Win")?"\r\n":"\n",b=[],c=["BEGIN:VCALENDAR","VERSION:2.0"].join(a),d=a+"END:VCALENDAR",e=["SU","MO","TU","WE","TH","FR","SA"];return{events:function(){return b},calendar:function(){return c+a+b.join(a)+d},addEvent:function(c,d,f,g,h,i){if("undefined"==typeof c||"undefined"==typeof d||"undefined"==typeof f||"undefined"==typeof g||"undefined"==typeof h)return!1;if(i&&!i.rule){if("YEARLY"!==i.freq&&"MONTHLY"!==i.freq&&"WEEKLY"!==i.freq&&"DAILY"!==i.freq)throw"Recurrence rule frequency must be provided and be one of the following: 'YEARLY', 'MONTHLY', 'WEEKLY', or 'DAILY'";if(i.until&&isNaN(Date.parse(i.until)))throw"Recurrence rule 'until' must be a valid date string";if(i.interval&&isNaN(parseInt(i.interval)))throw"Recurrence rule 'interval' must be an integer";if(i.count&&isNaN(parseInt(i.count)))throw"Recurrence rule 'count' must be an integer";if("undefined"!=typeof i.byday){if("[object Array]"!==Object.prototype.toString.call(i.byday))throw"Recurrence rule 'byday' must be an array";if(i.byday.length>7)throw"Recurrence rule 'byday' array must not be longer than the 7 days in a week";i.byday=i.byday.filter(function(a,b){return i.byday.indexOf(a)==b});for(var j in i.byday)if(e.indexOf(i.byday[j])<0)throw"Recurrence rule 'byday' values must include only the following: 'SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'"}}var k=new Date(g),l=new Date(h),m=("0000"+k.getFullYear().toString()).slice(-4),n=("00"+(k.getMonth()+1).toString()).slice(-2),o=("00"+k.getDate().toString()).slice(-2),p=("00"+k.getHours().toString()).slice(-2),q=("00"+k.getMinutes().toString()).slice(-2),r=("00"+k.getMinutes().toString()).slice(-2),s=("0000"+l.getFullYear().toString()).slice(-4),t=("00"+(l.getMonth()+1).toString()).slice(-2),u=("00"+l.getDate().toString()).slice(-2),v=("00"+l.getHours().toString()).slice(-2),w=("00"+l.getMinutes().toString()).slice(-2),x=("00"+l.getMinutes().toString()).slice(-2),y="",z="";q+r+w+x!==0&&(y="T"+p+q+r,z="T"+v+w+x);var A,B=m+n+o+y,C=s+t+u+z;if(i)if(i.rule)A=i.rule;else{if(A="RRULE:FREQ="+i.freq,i.until){var D=new Date(Date.parse(i.until)).toISOString();A+=";UNTIL="+D.substring(0,D.length-13).replace(/[-]/g,"")+"000000Z"}i.interval&&(A+=";INTERVAL="+i.interval),i.count&&(A+=";COUNT="+i.count),i.byday&&i.byday.length>0&&(A+=";BYDAY="+i.byday.join(","))}var E=(new Date).toISOString(),F=["BEGIN:VEVENT","CLASS:PUBLIC","DESCRIPTION:"+d,"DTSTART:"+B,"DTEND:"+C,"DTSTAMP:"+E.substring(0,E.length-13).replace(/[-]/g,"")+"000000Z","LOCATION:"+f,"SUMMARY;LANGUAGE=en-us:"+c,"TRANSP:TRANSPARENT","END:VEVENT"];return A&&F.splice(4,0,A),F=F.join(a),b.push(F),F},download:function(e,f){if(b.length<1)return!1;f="undefined"!=typeof f?f:".ics",e="undefined"!=typeof e?e:"calendar";var g,h=c+a+b.join(a)+d;if(-1===navigator.userAgent.indexOf("MSIE 10"))g=new Blob([h]);else{var i=new BlobBuilder;i.append(h),g=i.getBlob("text/x-vCalendar;charset="+document.characterSet)}return saveAs(g,e+f),h}}}; \ No newline at end of file diff --git a/ics.js b/ics.js index 083d963..f7ce758 100644 --- a/ics.js +++ b/ics.js @@ -16,6 +16,7 @@ var ics = function() { 'VERSION:2.0' ].join(SEPARATOR); var calendarEnd = SEPARATOR + 'END:VCALENDAR'; + var BYDAY_VALUES = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA']; return { /** @@ -42,7 +43,7 @@ var ics = function() { * @param {string} begin Beginning date of event * @param {string} stop Ending date of event */ - 'addEvent': function(subject, description, location, begin, stop) { + 'addEvent': function(subject, description, location, begin, stop, rrule) { // I'm not in the mood to make these optional... So they are all required if (typeof subject === 'undefined' || typeof description === 'undefined' || @@ -51,7 +52,55 @@ var ics = function() { typeof stop === 'undefined' ) { return false; - }; + } + + // validate rrule + if (rrule) { + if (!rrule.rule) { + if (rrule.freq !== 'YEARLY' && rrule.freq !== 'MONTHLY' && rrule.freq !== 'WEEKLY' && rrule.freq !== 'DAILY') { + throw "Recurrence rule frequency must be provided and be one of the following: 'YEARLY', 'MONTHLY', 'WEEKLY', or 'DAILY'"; + } + + if (rrule.until) { + if (isNaN(Date.parse(rrule.until))) { + throw "Recurrence rule 'until' must be a valid date string"; + } + } + + if (rrule.interval) { + if (isNaN(parseInt(rrule.interval))) { + throw "Recurrence rule 'interval' must be an integer"; + } + } + + if (rrule.count) { + if (isNaN(parseInt(rrule.count))) { + throw "Recurrence rule 'count' must be an integer"; + } + } + + if (typeof rrule.byday !== 'undefined') { + if ( (Object.prototype.toString.call(rrule.byday) !== '[object Array]') ) { + throw "Recurrence rule 'byday' must be an array"; + } + + if (rrule.byday.length > 7) { + throw "Recurrence rule 'byday' array must not be longer than the 7 days in a week"; + } + + // Filter any possible repeats + rrule.byday = rrule.byday.filter(function(elem, pos) { + return rrule.byday.indexOf(elem) == pos; + }); + + for (var d in rrule.byday) { + if (BYDAY_VALUES.indexOf(rrule.byday[d]) < 0) { + throw "Recurrence rule 'byday' values must include only the following: 'SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'"; + } + } + } + } + } //TODO add time and time zone? use moment to format? var start_date = new Date(begin); @@ -62,19 +111,19 @@ var ics = function() { var start_day = ("00" + ((start_date.getDate()).toString())).slice(-2); var start_hours = ("00" + (start_date.getHours().toString())).slice(-2); var start_minutes = ("00" + (start_date.getMinutes().toString())).slice(-2); - var start_seconds = ("00" + (start_date.getMinutes().toString())).slice(-2); + var start_seconds = ("00" + (start_date.getSeconds().toString())).slice(-2); var end_year = ("0000" + (end_date.getFullYear().toString())).slice(-4); var end_month = ("00" + ((end_date.getMonth() + 1).toString())).slice(-2); var end_day = ("00" + ((end_date.getDate()).toString())).slice(-2); var end_hours = ("00" + (end_date.getHours().toString())).slice(-2); var end_minutes = ("00" + (end_date.getMinutes().toString())).slice(-2); - var end_seconds = ("00" + (end_date.getMinutes().toString())).slice(-2); + var end_seconds = ("00" + (end_date.getSeconds().toString())).slice(-2); // Since some calendars don't add 0 second events, we need to remove time if there is none... var start_time = ''; var end_time = ''; - if (start_minutes + start_seconds + end_minutes + end_seconds != 0) { + if (start_minutes + start_seconds + end_minutes + end_seconds !== 0) { start_time = 'T' + start_hours + start_minutes + start_seconds; end_time = 'T' + end_hours + end_minutes + end_seconds; } @@ -82,17 +131,53 @@ var ics = function() { var start = start_year + start_month + start_day + start_time; var end = end_year + end_month + end_day + end_time; + // recurrence rule vars + var rruleString; + if (rrule) { + if (rrule.rule) { + rruleString = rrule.rule; + } else { + rruleString = 'RRULE:FREQ=' + rrule.freq; + + if (rrule.until) { + var uDate = new Date(Date.parse(rrule.until)).toISOString(); + rruleString += ';UNTIL=' + uDate.substring(0, uDate.length - 13).replace(/[-]/g, '') + '000000Z'; + } + + if (rrule.interval) { + rruleString += ';INTERVAL=' + rrule.interval; + } + + if (rrule.count) { + rruleString += ';COUNT=' + rrule.count; + } + + if (rrule.byday && rrule.byday.length > 0) { + rruleString += ';BYDAY=' + rrule.byday.join(','); + } + } + } + + var stamp = new Date().toISOString(); + var calendarEvent = [ 'BEGIN:VEVENT', 'CLASS:PUBLIC', - 'DESCRIPTION:' + description, - 'DTSTART;VALUE=DATE:' + start, - 'DTEND;VALUE=DATE:' + end, + 'DESCRIPTION:' + description.replace('\r\n', '\\n'), + 'DTSTART:' + start, + 'DTEND:' + end, + 'DTSTAMP:' + stamp.substring(0, stamp.length - 13).replace(/[-]/g, '') + '000000Z', 'LOCATION:' + location, 'SUMMARY;LANGUAGE=en-us:' + subject, 'TRANSP:TRANSPARENT', 'END:VEVENT' - ].join(SEPARATOR); + ]; + + if (rruleString) { + calendarEvent.splice(4, 0, rruleString); + } + + calendarEvent = calendarEvent.join(SEPARATOR); calendarEvents.push(calendarEvent); return calendarEvent; diff --git a/ics.min.js b/ics.min.js index ffa9994..2386388 100644 --- a/ics.min.js +++ b/ics.min.js @@ -1,2 +1,2 @@ -/*! ics.js March 11, 2014 */ -var ics=function(){"use strict";if(navigator.userAgent.indexOf("MSIE")>-1&&navigator.userAgent.indexOf("MSIE 10")==-1){console.log("Unsupported Browser");return}var e=navigator.appVersion.indexOf("Win")!==-1?"\r\n":"\n";var t=[];var n=["BEGIN:VCALENDAR","VERSION:2.0"].join(e);var r=e+"END:VCALENDAR";return{events:function(){return t},calendar:function(){return n+e+t.join(e)+r},addEvent:function(n,r,i,s,o){if(typeof n==="undefined"||typeof r==="undefined"||typeof i==="undefined"||typeof s==="undefined"||typeof o==="undefined"){return false}var u=new Date(s);var a=new Date(o);var f=("0000"+u.getFullYear().toString()).slice(-4);var l=("00"+(u.getMonth()+1).toString()).slice(-2);var c=("00"+u.getDate().toString()).slice(-2);var h=("00"+u.getHours().toString()).slice(-2);var p=("00"+u.getMinutes().toString()).slice(-2);var d=("00"+u.getMinutes().toString()).slice(-2);var v=("0000"+a.getFullYear().toString()).slice(-4);var m=("00"+(a.getMonth()+1).toString()).slice(-2);var g=("00"+a.getDate().toString()).slice(-2);var y=("00"+a.getHours().toString()).slice(-2);var b=("00"+a.getMinutes().toString()).slice(-2);var w=("00"+a.getMinutes().toString()).slice(-2);var E="";var S="";if(p+d+b+w!=0){E="T"+h+p+d;S="T"+y+b+w}var x=f+l+c+E;var T=v+m+g+S;var N=["BEGIN:VEVENT","CLASS:PUBLIC","DESCRIPTION:"+r,"DTSTART;VALUE=DATE:"+x,"DTEND;VALUE=DATE:"+T,"LOCATION:"+i,"SUMMARY;LANGUAGE=en-us:"+n,"TRANSP:TRANSPARENT","END:VEVENT"].join(e);t.push(N);return N},download:function(i,s){if(t.length<1){return false}s=typeof s!=="undefined"?s:".ics";i=typeof i!=="undefined"?i:"calendar";var o=n+e+t.join(e)+r;var u;if(navigator.userAgent.indexOf("MSIE 10")===-1){u=new Blob([o])}else{var a=new BlobBuilder;a.append(o);u=a.getBlob("text/x-vCalendar;charset="+document.characterSet)}saveAs(u,i+s);return o}}} +/*! ics.js Wed Aug 20 2014 17:23:02 */ +var ics=function(){"use strict";if(navigator.userAgent.indexOf("MSIE")>-1&&-1==navigator.userAgent.indexOf("MSIE 10"))return void console.log("Unsupported Browser");var a=-1!==navigator.appVersion.indexOf("Win")?"\r\n":"\n",b=[],c=["BEGIN:VCALENDAR","VERSION:2.0"].join(a),d=a+"END:VCALENDAR",e=["SU","MO","TU","WE","TH","FR","SA"];return{events:function(){return b},calendar:function(){return c+a+b.join(a)+d},addEvent:function(c,d,f,g,h,i){if("undefined"==typeof c||"undefined"==typeof d||"undefined"==typeof f||"undefined"==typeof g||"undefined"==typeof h)return!1;if(i&&!i.rule){if("YEARLY"!==i.freq&&"MONTHLY"!==i.freq&&"WEEKLY"!==i.freq&&"DAILY"!==i.freq)throw"Recurrence rule frequency must be provided and be one of the following: 'YEARLY', 'MONTHLY', 'WEEKLY', or 'DAILY'";if(i.until&&isNaN(Date.parse(i.until)))throw"Recurrence rule 'until' must be a valid date string";if(i.interval&&isNaN(parseInt(i.interval)))throw"Recurrence rule 'interval' must be an integer";if(i.count&&isNaN(parseInt(i.count)))throw"Recurrence rule 'count' must be an integer";if("undefined"!=typeof i.byday){if("[object Array]"!==Object.prototype.toString.call(i.byday))throw"Recurrence rule 'byday' must be an array";if(i.byday.length>7)throw"Recurrence rule 'byday' array must not be longer than the 7 days in a week";i.byday=i.byday.filter(function(a,b){return i.byday.indexOf(a)==b});for(var j in i.byday)if(e.indexOf(i.byday[j])<0)throw"Recurrence rule 'byday' values must include only the following: 'SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'"}}var k=new Date(g),l=new Date(h),m=("0000"+k.getFullYear().toString()).slice(-4),n=("00"+(k.getMonth()+1).toString()).slice(-2),o=("00"+k.getDate().toString()).slice(-2),p=("00"+k.getHours().toString()).slice(-2),q=("00"+k.getMinutes().toString()).slice(-2),r=("00"+k.getMinutes().toString()).slice(-2),s=("0000"+l.getFullYear().toString()).slice(-4),t=("00"+(l.getMonth()+1).toString()).slice(-2),u=("00"+l.getDate().toString()).slice(-2),v=("00"+l.getHours().toString()).slice(-2),w=("00"+l.getMinutes().toString()).slice(-2),x=("00"+l.getMinutes().toString()).slice(-2),y="",z="";q+r+w+x!==0&&(y="T"+p+q+r,z="T"+v+w+x);var A,B=m+n+o+y,C=s+t+u+z;if(i)if(i.rule)A=i.rule;else{if(A="RRULE:FREQ="+i.freq,i.until){var D=new Date(Date.parse(i.until)).toISOString();A+=";UNTIL="+D.substring(0,D.length-13).replace(/[-]/g,"")+"000000Z"}i.interval&&(A+=";INTERVAL="+i.interval),i.count&&(A+=";COUNT="+i.count),i.byday&&i.byday.length>0&&(A+=";BYDAY="+i.byday.join(","))}var E=(new Date).toISOString(),F=["BEGIN:VEVENT","CLASS:PUBLIC","DESCRIPTION:"+d,"DTSTART:"+B,"DTEND:"+C,"DTSTAMP:"+E.substring(0,E.length-13).replace(/[-]/g,"")+"000000Z","LOCATION:"+f,"SUMMARY;LANGUAGE=en-us:"+c,"TRANSP:TRANSPARENT","END:VEVENT"];return A&&F.splice(4,0,A),F=F.join(a),b.push(F),F},download:function(e,f){if(b.length<1)return!1;f="undefined"!=typeof f?f:".ics",e="undefined"!=typeof e?e:"calendar";var g,h=c+a+b.join(a)+d;if(-1===navigator.userAgent.indexOf("MSIE 10"))g=new Blob([h]);else{var i=new BlobBuilder;i.append(h),g=i.getBlob("text/x-vCalendar;charset="+document.characterSet)}return saveAs(g,e+f),h}}}; \ No newline at end of file diff --git a/test/spec/test.js b/test/spec/test.js index 4db3f6c..5875e35 100644 --- a/test/spec/test.js +++ b/test/spec/test.js @@ -2,6 +2,10 @@ (function () { 'use strict'; + + var stamp = new Date().toISOString(); + stamp = stamp.substring(0, stamp.length - 13).replace(/[-]/g, '') + '000000Z'; + describe('Load ics Object', function () { describe('Calendar Events Array', function () { it('should be an empty array initially', function () { @@ -21,25 +25,47 @@ cal.addEvent('Christmas', 'Christian holiday celebrating the birth of Jesus Christ', 'Bethlehem', '12/25/2013', '12/25/2013'); assert.isArray(cal.events(), 'calendarEvents not an array'); assert.lengthOf(cal.events(), '1'); - assert.strictEqual(cal.events()[0], 'BEGIN:VEVENT\nCLASS:PUBLIC\nDESCRIPTION:Christian holiday celebrating the birth of Jesus Christ\nDTSTART;VALUE=DATE:20131225\nDTEND;VALUE=DATE:20131225\nLOCATION:Bethlehem\nSUMMARY;LANGUAGE=en-us:Christmas\nTRANSP:TRANSPARENT\nEND:VEVENT'); + assert.strictEqual(cal.events()[0], 'BEGIN:VEVENT\nCLASS:PUBLIC\nDESCRIPTION:Christian holiday celebrating the birth of Jesus Christ\nDTSTART:20131225T000000\nDTEND:20131225T000000\nDTSTAMP:' + stamp + '\nLOCATION:Bethlehem\nSUMMARY;LANGUAGE=en-us:Christmas\nTRANSP:TRANSPARENT\nEND:VEVENT'); }); }); describe('Calendar String', function () { it('should have one event', function () { - assert.strictEqual(cal.calendar(), 'BEGIN:VCALENDAR\nVERSION:2.0\nBEGIN:VEVENT\nCLASS:PUBLIC\nDESCRIPTION:Christian holiday celebrating the birth of Jesus Christ\nDTSTART;VALUE=DATE:20131225\nDTEND;VALUE=DATE:20131225\nLOCATION:Bethlehem\nSUMMARY;LANGUAGE=en-us:Christmas\nTRANSP:TRANSPARENT\nEND:VEVENT\nEND:VCALENDAR', 'calendar does not match'); + assert.strictEqual(cal.calendar(), 'BEGIN:VCALENDAR\nVERSION:2.0\nBEGIN:VEVENT\nCLASS:PUBLIC\nDESCRIPTION:Christian holiday celebrating the birth of Jesus Christ\nDTSTART:20131225T000000\nDTEND:20131225T000000\nDTSTAMP:' + stamp + '\nLOCATION:Bethlehem\nSUMMARY;LANGUAGE=en-us:Christmas\nTRANSP:TRANSPARENT\nEND:VEVENT\nEND:VCALENDAR', 'calendar does not match'); }); }); describe('Calendar String of Single Digit Months', function () { it('should have one event', function () { cal.addEvent('Easter', 'Christian holiday celebrating the resurrection of Jesus Christ', 'Jerusalem', '04/20/2014', '04/20/2014'); - assert.strictEqual(cal.events()[1], 'BEGIN:VEVENT\nCLASS:PUBLIC\nDESCRIPTION:Christian holiday celebrating the resurrection of Jesus Christ\nDTSTART;VALUE=DATE:20140420\nDTEND;VALUE=DATE:20140420\nLOCATION:Jerusalem\nSUMMARY;LANGUAGE=en-us:Easter\nTRANSP:TRANSPARENT\nEND:VEVENT'); + assert.strictEqual(cal.events()[1], 'BEGIN:VEVENT\nCLASS:PUBLIC\nDESCRIPTION:Christian holiday celebrating the resurrection of Jesus Christ\nDTSTART:20140420T000000\nDTEND:20140420T000000\nDTSTAMP:' + stamp + '\nLOCATION:Jerusalem\nSUMMARY;LANGUAGE=en-us:Easter\nTRANSP:TRANSPARENT\nEND:VEVENT'); }); }); describe('Calendar String of Single Digit Day', function () { it('should have one event', function () { cal.addEvent('April Fools Day', 'What isn\'t is', 'America', '4/1/2014', '4/1/2014'); - assert.strictEqual(cal.events()[2], 'BEGIN:VEVENT\nCLASS:PUBLIC\nDESCRIPTION:What isn\'t is\nDTSTART;VALUE=DATE:20140401\nDTEND;VALUE=DATE:20140401\nLOCATION:America\nSUMMARY;LANGUAGE=en-us:April Fools Day\nTRANSP:TRANSPARENT\nEND:VEVENT'); + assert.strictEqual(cal.events()[2], 'BEGIN:VEVENT\nCLASS:PUBLIC\nDESCRIPTION:What isn\'t is\nDTSTART:20140401T000000\nDTEND:20140401T000000\nDTSTAMP:' + stamp + '\nLOCATION:America\nSUMMARY;LANGUAGE=en-us:April Fools Day\nTRANSP:TRANSPARENT\nEND:VEVENT'); }); }); + + describe('Recurring Events', function () { + it('should add recurring events using frequency and until', function () { + cal.addEvent('Soccer Practice', 'Practice kicking the ball in the net! YAYY!!', 'Soccer field', '08/18/2014', '09/18/2014', {freq: 'WEEKLY', until: '08/18/2014'}); + assert.strictEqual(cal.events()[3], 'BEGIN:VEVENT\nCLASS:PUBLIC\nDESCRIPTION:Practice kicking the ball in the net! YAYY!!\nDTSTART:20140818T000000\nRRULE:FREQ=WEEKLY;UNTIL=20140818T000000Z\nDTEND:20140918T000000\nDTSTAMP:' + stamp + '\nLOCATION:Soccer field\nSUMMARY;LANGUAGE=en-us:Soccer Practice\nTRANSP:TRANSPARENT\nEND:VEVENT'); + }); + + it('should add recurring events using interval and count', function () { + cal.addEvent('Soccer Practice', 'Practice kicking the ball in the net! YAYY!!', 'Soccer field', '08/18/2014', '09/18/2014', {freq: 'WEEKLY', interval: 2, count: 10}); + assert.strictEqual(cal.events()[4], 'BEGIN:VEVENT\nCLASS:PUBLIC\nDESCRIPTION:Practice kicking the ball in the net! YAYY!!\nDTSTART:20140818T000000\nRRULE:FREQ=WEEKLY;INTERVAL=2;COUNT=10\nDTEND:20140918T000000\nDTSTAMP:' + stamp + '\nLOCATION:Soccer field\nSUMMARY;LANGUAGE=en-us:Soccer Practice\nTRANSP:TRANSPARENT\nEND:VEVENT'); + }); + + it('should add recurring events using interval and until and byday', function () { + cal.addEvent('Soccer Practice', 'Practice kicking the ball in the net! YAYY!!', 'Soccer field', '08/18/2014', '09/18/2014', {freq: 'WEEKLY', interval: 1, until: '08/18/2014', byday: ['MO','WE','FR']}); + assert.strictEqual(cal.events()[5], 'BEGIN:VEVENT\nCLASS:PUBLIC\nDESCRIPTION:Practice kicking the ball in the net! YAYY!!\nDTSTART:20140818T000000\nRRULE:FREQ=WEEKLY;UNTIL=20140818T000000Z;INTERVAL=1;BYDAY=MO,WE,FR\nDTEND:20140918T000000\nDTSTAMP:' + stamp + '\nLOCATION:Soccer field\nSUMMARY;LANGUAGE=en-us:Soccer Practice\nTRANSP:TRANSPARENT\nEND:VEVENT'); + }); + + it('should add recurring events using interval and until and ignore duplicates in byday array', function () { + cal.addEvent('Soccer Practice', 'Practice kicking the ball in the net! YAYY!!', 'Soccer field', '08/18/2014', '09/18/2014', {freq: 'WEEKLY', interval: 1, until: '08/18/2014', byday: ['MO', 'WE', 'MO']}); + assert.strictEqual(cal.events()[6], 'BEGIN:VEVENT\nCLASS:PUBLIC\nDESCRIPTION:Practice kicking the ball in the net! YAYY!!\nDTSTART:20140818T000000\nRRULE:FREQ=WEEKLY;UNTIL=20140818T000000Z;INTERVAL=1;BYDAY=MO,WE\nDTEND:20140918T000000\nDTSTAMP:' + stamp + '\nLOCATION:Soccer field\nSUMMARY;LANGUAGE=en-us:Soccer Practice\nTRANSP:TRANSPARENT\nEND:VEVENT'); + }); + }); }); })();