Wednesday, October 29, 2014

Fullcalendar Upgrade to process Google Calendar API V3 JSON

I posted this in the Google Code issue tracker for FullCalendar. But that thing it overloaded! This may never be seen.

I hope people who need this find it. This is my best stab at implementing GCal API V3 in Fullcalendar's gcal.js. The implementation I am using on my site is slightly different than this. I have removed the old 'gcal' references and I have a custom url in the events.push function in place of the the actual entry.htmlLink value so that the edit link points back to my server. I also have my fullcalendar sources pointing to my server and my server handles all the google oauth v3 authentication and google calendar API v3 calls for events list/create/update/delete for secure communications. Because my implementation is slightly different, I have not tested this exact code below in a production environment, but the processing logic for the GCal API V3 JSON is the same. In this sample code you will also see that I tried to retain backward compatibility with GCal API V1 (even though Gcal API V1 is being deprecated in one month). In searching for upgrading my own system, all my googling finds only request for V3 implenentation. This is really needed!

Thank you Adam Shaw for the amazing FullCalendar app! I hope I can give back a little by contributing this.

I have to give a little bit of credit to this guy's gist for just the right hint to help me figure out how gcal.js was completing the events list processing into the format FullCalendar.je uses. I stumbled on this during my research. Thanks hong-jen kao!
https://gist.github.com/cades/d194af6d2be7398cad75
No explanation or comments in that gist, but looking at it and the json coming back from Google Calendar V3 API - I got it. I didn't use his .map for processing, maybe it's better/faster. I don't know.

I hope whoever needs this is able to modify it to fit their needs.

This is a sample gcal.js replacement - again, not fully tested as my implementation was much more customized, this is my best guess at intended functionality.

Best of luck!

/*!
 * FullCalendar v2.1.1 Google Calendar Plugin
 * Docs & License: http://arshaw.com/fullcalendar/
 * (c) 2013 Adam Shaw
 * Modified by G. Bach 2014-10-28
 * Changed to process GCal API V3 JSON Events list
 */

(function(factory) {
    if (typeof define === 'function' && define.amd) {
        define([ 'jquery' ], factory);
    } else {
        factory(jQuery);
    }
})(function($) {


    var fc = $.fullCalendar;
    var applyAll = fc.applyAll;


    fc.sourceNormalizers.push(function(sourceOptions) {
        if (sourceOptions.dataType == 'gcal'
        || (sourceOptions.dataType === undefined
            && (sourceOptions.url || '').match(/^(http|https):\/\/www.google.com\/calendar\/feeds\//))) {
                sourceOptions.dataType = 'gcal';
                if (sourceOptions.editable === undefined) {
                    sourceOptions.editable = false;
                }
        } else if (sourceOptions.dataType == 'gcalv3'
        || (sourceOptions.dataType === undefined
            && (sourceOptions.url || '').match(/^(http|https):\/\/www.googleapis.com\/calendar\/v3\/calendars\//))) {
                sourceOptions.dataType = 'gcalv3';
                if (sourceOptions.editable === undefined) {
                    sourceOptions.editable = false;
            }
        }
    });


    fc.sourceFetchers.push(function(sourceOptions, start, end, timezone) {
        if (sourceOptions.dataType == 'gcal') {
            return transformOptions(sourceOptions, start, end, timezone);
        } else if (sourceOptions.dataType == 'gcalv3') {
            return transformOptionsV3(sourceOptions, start, end, timezone);
        }
    });


    function transformOptions(sourceOptions, start, end, timezone) {

        var success = sourceOptions.success;
        var data = $.extend({}, sourceOptions.data || {}, {
            singleevents: true,
            'max-results': 9999
        });

        return $.extend({}, sourceOptions, {
            url: sourceOptions.url.replace(/\/basic$/, '/full') + '?alt=json-in-script&callback=?',
            dataType: 'jsonp',
            data: data,
            timezoneParam: 'ctz',
            startParam: 'start-min',
            endParam: 'start-max',
            success: function(data) {
                var events = [];
                if (data.feed.entry) {
                    $.each(data.feed.entry, function(i, entry) {

                        var url;
                        $.each(entry.link, function(i, link) {
                            if (link.type == 'text/html') {
                                url = link.href;
                                if (timezone && timezone != 'local') {
                                    url += (url.indexOf('?') == -1 ? '?' : '&') + 'ctz=' + encodeURIComponent(timezone);
                                }
                            }
                        });

                        events.push({
                            id: entry.gCal$uid.value,
                            title: entry.title.$t,
                            start: entry.gd$when[0].startTime,
                            end: entry.gd$when[0].endTime,
                            url: url,
                            location: entry.gd$where[0].valueString,
                            description: entry.content.$t
                        });

                    });
                }
                var args = [events].concat(Array.prototype.slice.call(arguments, 1));
                var res = applyAll(success, this, args);
                if ($.isArray(res)) {
                    return res;
                }
                return events;
            }
        });

    function transformOptionsV3(sourceOptions, start, end, timezone) {

        var success = sourceOptions.success;
        var data = $.extend({}, sourceOptions.data || {}, {
            singleevents: true,
            'max-results': 9999
        });

        return $.extend({}, sourceOptions, {
            url: sourceOptions.url,
            dataType: 'json',
            data: data,
            startParam: 'start-min',
            endParam: 'start-max',
            success: function(data) {
                var events = [];
                if (data.feed.entry) {
                    $.each(data.feed.entry, function(i, entry) {

                        events.push({
                            id: entry.id,
                            title: entry.summary || '', // must allow default to blank, if it's not set it doesn't exist in the json and will error here
                            start: entry.start.dateTime || entry.start.date,
                            end: entry.end.dateTime || entry.start.date,  // because end.date may be the next day, cause a '2-all-day' event, we use start.date here.
                            url: entry.htmlLink,
                            location: entry.location || '', // must allow default to blank, if it's not set it doesn't exist in the json and will error here
                            description: entry.description || '' // must allow default to blank, if it's not set it doesn't exist in the json and will error here
                        });

                    });
                }
                var args = [events].concat(Array.prototype.slice.call(arguments, 1));
                var res = applyAll(success, this, args);
                if ($.isArray(res)) {
                    return res;
                }
                return events;
            }
        });

    }


    // legacy
    fc.gcalFeed = function(url, sourceOptions) {
        return $.extend({}, sourceOptions, { url: url, dataType: 'gcal' });
    };


});