GcalEventSource.ts 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. import * as $ from 'jquery'
  2. import { EventSource, Promise, JsonFeedEventSource, warn, applyAll } from 'fullcalendar'
  3. export default class GcalEventSource extends EventSource {
  4. static API_BASE = 'https://www.googleapis.com/calendar/v3/calendars'
  5. // TODO: eventually remove "googleCalendar" prefix (API-breaking)
  6. googleCalendarApiKey: any
  7. googleCalendarId: any
  8. googleCalendarError: any // optional function
  9. ajaxSettings: any
  10. static parse(rawInput, calendar) {
  11. let rawProps
  12. if (typeof rawInput === 'object') { // long form. might fail in applyManualStandardProps
  13. rawProps = rawInput
  14. } else if (typeof rawInput === 'string') { // short form
  15. rawProps = { url: rawInput } // url will be parsed with parseGoogleCalendarId
  16. }
  17. if (rawProps) {
  18. return EventSource.parse.call(this, rawProps, calendar)
  19. }
  20. return false
  21. }
  22. fetch(start, end, timezone) {
  23. let url = this.buildUrl()
  24. let requestParams = this.buildRequestParams(start, end, timezone)
  25. let ajaxSettings = this.ajaxSettings || {}
  26. let onSuccess = ajaxSettings.success
  27. if (!requestParams) { // could have failed
  28. return Promise.reject()
  29. }
  30. this.calendar.pushLoading()
  31. return Promise.construct((onResolve, onReject) => {
  32. $.ajax($.extend(
  33. {}, // destination
  34. JsonFeedEventSource.AJAX_DEFAULTS,
  35. ajaxSettings,
  36. {
  37. url: url,
  38. data: requestParams,
  39. success: (responseData, status, xhr) => {
  40. let rawEventDefs
  41. let successRes
  42. this.calendar.popLoading()
  43. if (responseData.error) {
  44. this.reportError('Google Calendar API: ' + responseData.error.message, responseData.error.errors)
  45. onReject()
  46. } else if (responseData.items) {
  47. rawEventDefs = this.gcalItemsToRawEventDefs(
  48. responseData.items,
  49. requestParams.timeZone
  50. )
  51. successRes = applyAll(onSuccess, this, [ responseData, status, xhr ]) // passthru
  52. if ($.isArray(successRes)) {
  53. rawEventDefs = successRes
  54. }
  55. onResolve(this.parseEventDefs(rawEventDefs))
  56. }
  57. },
  58. error: (xhr, statusText, errorThrown) => {
  59. this.reportError(
  60. 'Google Calendar network failure: ' + statusText,
  61. [ xhr, errorThrown ]
  62. )
  63. this.calendar.popLoading()
  64. onReject()
  65. }
  66. }
  67. ))
  68. })
  69. }
  70. gcalItemsToRawEventDefs(items, gcalTimezone) {
  71. return items.map((item) => {
  72. return this.gcalItemToRawEventDef(item, gcalTimezone)
  73. })
  74. }
  75. gcalItemToRawEventDef(item, gcalTimezone) {
  76. let url = item.htmlLink || null
  77. // make the URLs for each event show times in the correct timezone
  78. if (url && gcalTimezone) {
  79. url = injectQsComponent(url, 'ctz=' + gcalTimezone)
  80. }
  81. let extendedProperties = {}
  82. if (
  83. typeof item.extendedProperties === 'object' &&
  84. typeof item.extendedProperties.shared === 'object'
  85. ) {
  86. extendedProperties = item.extendedProperties.shared
  87. }
  88. return {
  89. id: item.id,
  90. title: item.summary,
  91. start: item.start.dateTime || item.start.date, // try timed. will fall back to all-day
  92. end: item.end.dateTime || item.end.date, // same
  93. url: url,
  94. location: item.location,
  95. description: item.description,
  96. extendedProperties
  97. }
  98. }
  99. buildUrl() {
  100. return GcalEventSource.API_BASE + '/' +
  101. encodeURIComponent(this.googleCalendarId) +
  102. '/events?callback=?' // jsonp
  103. }
  104. buildRequestParams(start, end, timezone) {
  105. let apiKey = this.googleCalendarApiKey || this.calendar.opt('googleCalendarApiKey')
  106. let params
  107. if (!apiKey) {
  108. this.reportError('Specify a googleCalendarApiKey. See http://fullcalendar.io/docs/google_calendar/')
  109. return null
  110. }
  111. // The API expects an ISO8601 datetime with a time and timezone part.
  112. // Since the calendar's timezone offset isn't always known, request the date in UTC and pad it by a day on each
  113. // side, guaranteeing we will receive all events in the desired range, albeit a superset.
  114. // .utc() will set a zone and give it a 00:00:00 time.
  115. if (!start.hasZone()) {
  116. start = start.clone().utc().add(-1, 'day')
  117. }
  118. if (!end.hasZone()) {
  119. end = end.clone().utc().add(1, 'day')
  120. }
  121. params = $.extend(
  122. this.ajaxSettings.data || {},
  123. {
  124. key: apiKey,
  125. timeMin: start.format(),
  126. timeMax: end.format(),
  127. singleEvents: true,
  128. maxResults: 9999
  129. }
  130. )
  131. if (timezone && timezone !== 'local') {
  132. // when sending timezone names to Google, only accepts underscores, not spaces
  133. params.timeZone = timezone.replace(' ', '_')
  134. }
  135. return params
  136. }
  137. reportError(message, apiErrorObjs?) {
  138. let calendar = this.calendar
  139. let calendarOnError = calendar.opt('googleCalendarError')
  140. let errorObjs = apiErrorObjs || [ { message: message } ] // to be passed into error handlers
  141. if (this.googleCalendarError) {
  142. this.googleCalendarError.apply(calendar, errorObjs)
  143. }
  144. if (calendarOnError) {
  145. calendarOnError.apply(calendar, errorObjs)
  146. }
  147. // print error to debug console
  148. warn.apply(null, [ message ].concat(apiErrorObjs || []))
  149. }
  150. getPrimitive() {
  151. return this.googleCalendarId
  152. }
  153. applyManualStandardProps(rawProps) {
  154. let superSuccess = EventSource.prototype.applyManualStandardProps.apply(this, arguments)
  155. let googleCalendarId = rawProps.googleCalendarId
  156. if (googleCalendarId == null && rawProps.url) {
  157. googleCalendarId = parseGoogleCalendarId(rawProps.url)
  158. }
  159. if (googleCalendarId != null) {
  160. this.googleCalendarId = googleCalendarId
  161. return superSuccess
  162. }
  163. return false
  164. }
  165. applyMiscProps(rawProps) {
  166. if (!this.ajaxSettings) {
  167. this.ajaxSettings = {}
  168. }
  169. $.extend(this.ajaxSettings, rawProps)
  170. }
  171. }
  172. GcalEventSource.defineStandardProps({
  173. // manually process...
  174. url: false,
  175. googleCalendarId: false,
  176. // automatically transfer...
  177. googleCalendarApiKey: true,
  178. googleCalendarError: true
  179. })
  180. function parseGoogleCalendarId(url) {
  181. let match
  182. // detect if the ID was specified as a single string.
  183. // will match calendars like "[email protected]" in addition to person email calendars.
  184. if (/^[^\/]+@([^\/\.]+\.)*(google|googlemail|gmail)\.com$/.test(url)) {
  185. return url
  186. } else if (
  187. (match = /^https:\/\/www.googleapis.com\/calendar\/v3\/calendars\/([^\/]*)/.exec(url)) ||
  188. (match = /^https?:\/\/www.google.com\/calendar\/feeds\/([^\/]*)/.exec(url))
  189. ) {
  190. return decodeURIComponent(match[1])
  191. }
  192. }
  193. // Injects a string like "arg=value" into the querystring of a URL
  194. function injectQsComponent(url, component) {
  195. // inject it after the querystring but before the fragment
  196. return url.replace(/(\?.*?)?(#|$)/, function(whole, qs, hash) {
  197. return (qs ? qs + '&' : '?') + component + hash
  198. })
  199. }