username@email.com
2025-04-27 15eb82df2d6ec539e9d4245bfe08d531e8eb6379
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
/*!
 * FullCalendar v2.2.3 Google Calendar Plugin
 * Docs & License: http://arshaw.com/fullcalendar/
 * (c) 2013 Adam Shaw
 */
 
(function(factory) {
    if (typeof define === 'function' && define.amd) {
        define([ 'jquery' ], factory);
    }
    else {
        factory(jQuery);
    }
})(function($) {
 
 
var API_BASE = 'https://www.googleapis.com/calendar/v3/calendars';
var fc = $.fullCalendar;
var applyAll = fc.applyAll;
 
 
fc.sourceNormalizers.push(function(sourceOptions) {
    var googleCalendarId = sourceOptions.googleCalendarId;
    var url = sourceOptions.url;
    var match;
 
    // if the Google Calendar ID hasn't been explicitly defined
    if (!googleCalendarId && url) {
 
        // detect if the ID was specified as a single string.
        // will match calendars like "asdf1234@calendar.google.com" in addition to person email calendars.
        if ((match = /^[^\/]+@([^\/\.]+\.)*(google|googlemail|gmail)\.com$/.test(url))) {
            googleCalendarId = url;
        }
        // try to scrape it out of a V1 or V3 API feed URL
        else if (
            (match = /^https:\/\/www.googleapis.com\/calendar\/v3\/calendars\/([^\/]*)/.exec(url)) ||
            (match = /^https?:\/\/www.google.com\/calendar\/feeds\/([^\/]*)/.exec(url))
        ) {
            googleCalendarId = decodeURIComponent(match[1]);
        }
 
        if (googleCalendarId) {
            sourceOptions.googleCalendarId = googleCalendarId;
        }
    }
 
 
    if (googleCalendarId) { // is this a Google Calendar?
 
        // make each Google Calendar source uneditable by default
        if (sourceOptions.editable == null) {
            sourceOptions.editable = false;
        }
 
        // We want removeEventSource to work, but it won't know about the googleCalendarId primitive.
        // Shoehorn it into the url, which will function as the unique primitive. Won't cause side effects.
        // This hack is obsolete since 2.2.3, but keep it so this plugin file is compatible with old versions.
        sourceOptions.url = googleCalendarId;
    }
});
 
 
fc.sourceFetchers.push(function(sourceOptions, start, end, timezone) {
    if (sourceOptions.googleCalendarId) {
        return transformOptions(sourceOptions, start, end, timezone, this); // `this` is the calendar
    }
});
 
 
function transformOptions(sourceOptions, start, end, timezone, calendar) {
    var url = API_BASE + '/' + encodeURIComponent(sourceOptions.googleCalendarId) + '/events?callback=?'; // jsonp
    var apiKey = sourceOptions.googleCalendarApiKey || calendar.options.googleCalendarApiKey;
    var success = sourceOptions.success;
    var data;
    var timezoneArg; // populated when a specific timezone. escaped to Google's liking
 
    function reportError(message, apiErrorObjs) {
        var errorObjs = apiErrorObjs || [ { message: message } ]; // to be passed into error handlers
        var consoleObj = window.console;
        var consoleWarnFunc = consoleObj ? (consoleObj.warn || consoleObj.log) : null;
 
        // call error handlers
        (sourceOptions.googleCalendarError || $.noop).apply(calendar, errorObjs);
        (calendar.options.googleCalendarError || $.noop).apply(calendar, errorObjs);
 
        // print error to debug console
        if (consoleWarnFunc) {
            consoleWarnFunc.apply(consoleObj, [ message ].concat(apiErrorObjs || []));
        }
    }
 
    if (!apiKey) {
        reportError("Specify a googleCalendarApiKey. See http://fullcalendar.io/docs/google_calendar/");
        return {}; // an empty source to use instead. won't fetch anything.
    }
 
    // The API expects an ISO8601 datetime with a time and timezone part.
    // Since the calendar's timezone offset isn't always known, request the date in UTC and pad it by a day on each
    // side, guaranteeing we will receive all events in the desired range, albeit a superset.
    // .utc() will set a zone and give it a 00:00:00 time.
    if (!start.hasZone()) {
        start = start.clone().utc().add(-1, 'day');
    }
    if (!end.hasZone()) {
        end = end.clone().utc().add(1, 'day');
    }
 
    // when sending timezone names to Google, only accepts underscores, not spaces
    if (timezone && timezone != 'local') {
        timezoneArg = timezone.replace(' ', '_');
    }
 
    data = $.extend({}, sourceOptions.data || {}, {
        key: apiKey,
        timeMin: start.format(),
        timeMax: end.format(),
        timeZone: timezoneArg,
        singleEvents: true,
        maxResults: 9999
    });
 
    return $.extend({}, sourceOptions, {
        googleCalendarId: null, // prevents source-normalizing from happening again
        url: url,
        data: data,
        startParam: false, // `false` omits this parameter. we already included it above
        endParam: false, // same
        timezoneParam: false, // same
        success: function(data) {
            var events = [];
            var successArgs;
            var successRes;
 
            if (data.error) {
                reportError('Google Calendar API: ' + data.error.message, data.error.errors);
            }
            else if (data.items) {
                $.each(data.items, function(i, entry) {
                    var url = entry.htmlLink;
 
                    // make the URLs for each event show times in the correct timezone
                    if (timezoneArg) {
                        url = injectQsComponent(url, 'ctz=' + timezoneArg);
                    }
 
                    events.push({
                        id: entry.id,
                        title: entry.summary,
                        start: entry.start.dateTime || entry.start.date, // try timed. will fall back to all-day
                        end: entry.end.dateTime || entry.end.date, // same
                        url: url,
                        location: entry.location,
                        description: entry.description
                    });
                });
 
                // call the success handler(s) and allow it to return a new events array
                successArgs = [ events ].concat(Array.prototype.slice.call(arguments, 1)); // forward other jq args
                successRes = applyAll(success, this, successArgs);
                if ($.isArray(successRes)) {
                    return successRes;
                }
            }
 
            return events;
        }
    });
}
 
 
// Injects a string like "arg=value" into the querystring of a URL
function injectQsComponent(url, component) {
    // inject it after the querystring but before the fragment
    return url.replace(/(\?.*?)?(#|$)/, function(whole, qs, hash) {
        return (qs ? qs + '&' : '?') + component + hash;
    });
}
 
 
});