Projet

Général

Profil

0003-add-calendar-cell-static-files-16393.patch

Josué Kouka, 18 mai 2017 10:41

Télécharger (1,07 Mo)

Voir les différences:

Subject: [PATCH 3/3] add calendar cell static files (#16393)

 combo/apps/chrono/static/chrono/CHANGELOG.txt      |  1160 ++
 combo/apps/chrono/static/chrono/CONTRIBUTING.txt   |   127 +
 combo/apps/chrono/static/chrono/LICENSE.txt        |    20 +
 combo/apps/chrono/static/chrono/chrono.css         |    95 +
 combo/apps/chrono/static/chrono/chrono.js          |    76 +
 .../chrono/static/chrono/demos/agenda-views.html   |   106 +
 .../static/chrono/demos/background-events.html     |   102 +
 .../chrono/static/chrono/demos/basic-views.html    |   106 +
 combo/apps/chrono/static/chrono/demos/default.html |   100 +
 .../static/chrono/demos/external-dragging.html     |   135 +
 combo/apps/chrono/static/chrono/demos/gcal.html    |    78 +
 combo/apps/chrono/static/chrono/demos/json.html    |    85 +
 .../chrono/static/chrono/demos/json/events.json    |    56 +
 .../chrono/static/chrono/demos/list-views.html     |   115 +
 combo/apps/chrono/static/chrono/demos/locales.html |   143 +
 .../chrono/static/chrono/demos/php/get-events.php  |    50 +
 .../static/chrono/demos/php/get-timezones.php      |     9 +
 .../apps/chrono/static/chrono/demos/php/utils.php  |   130 +
 .../chrono/static/chrono/demos/selectable.html     |   121 +
 combo/apps/chrono/static/chrono/demos/theme.html   |   108 +
 .../apps/chrono/static/chrono/demos/timezones.html |   128 +
 .../chrono/static/chrono/demos/week-numbers.html   |   111 +
 combo/apps/chrono/static/chrono/fullcalendar.css   |  1413 ++
 combo/apps/chrono/static/chrono/fullcalendar.js    | 15591 +++++++++++++++++++
 .../apps/chrono/static/chrono/fullcalendar.min.css |     5 +
 .../apps/chrono/static/chrono/fullcalendar.min.js  |    10 +
 .../chrono/static/chrono/fullcalendar.print.css    |   208 +
 .../static/chrono/fullcalendar.print.min.css       |     5 +
 combo/apps/chrono/static/chrono/gcal.js            |   180 +
 combo/apps/chrono/static/chrono/gcal.min.js        |     6 +
 combo/apps/chrono/static/chrono/locale-all.js      |     5 +
 combo/apps/chrono/static/chrono/locale/af.js       |     1 +
 combo/apps/chrono/static/chrono/locale/ar-dz.js    |     1 +
 combo/apps/chrono/static/chrono/locale/ar-kw.js    |     1 +
 combo/apps/chrono/static/chrono/locale/ar-ly.js    |     1 +
 combo/apps/chrono/static/chrono/locale/ar-ma.js    |     1 +
 combo/apps/chrono/static/chrono/locale/ar-sa.js    |     1 +
 combo/apps/chrono/static/chrono/locale/ar-tn.js    |     1 +
 combo/apps/chrono/static/chrono/locale/ar.js       |     1 +
 combo/apps/chrono/static/chrono/locale/bg.js       |     1 +
 combo/apps/chrono/static/chrono/locale/ca.js       |     1 +
 combo/apps/chrono/static/chrono/locale/cs.js       |     1 +
 combo/apps/chrono/static/chrono/locale/da.js       |     1 +
 combo/apps/chrono/static/chrono/locale/de-at.js    |     1 +
 combo/apps/chrono/static/chrono/locale/de-ch.js    |     1 +
 combo/apps/chrono/static/chrono/locale/de.js       |     1 +
 combo/apps/chrono/static/chrono/locale/el.js       |     1 +
 combo/apps/chrono/static/chrono/locale/en-au.js    |     1 +
 combo/apps/chrono/static/chrono/locale/en-ca.js    |     1 +
 combo/apps/chrono/static/chrono/locale/en-gb.js    |     1 +
 combo/apps/chrono/static/chrono/locale/en-ie.js    |     1 +
 combo/apps/chrono/static/chrono/locale/en-nz.js    |     1 +
 combo/apps/chrono/static/chrono/locale/es-do.js    |     1 +
 combo/apps/chrono/static/chrono/locale/es.js       |     1 +
 combo/apps/chrono/static/chrono/locale/et.js       |     1 +
 combo/apps/chrono/static/chrono/locale/eu.js       |     1 +
 combo/apps/chrono/static/chrono/locale/fa.js       |     1 +
 combo/apps/chrono/static/chrono/locale/fi.js       |     1 +
 combo/apps/chrono/static/chrono/locale/fr-ca.js    |     1 +
 combo/apps/chrono/static/chrono/locale/fr-ch.js    |     1 +
 combo/apps/chrono/static/chrono/locale/fr.js       |     1 +
 combo/apps/chrono/static/chrono/locale/gl.js       |     1 +
 combo/apps/chrono/static/chrono/locale/he.js       |     1 +
 combo/apps/chrono/static/chrono/locale/hi.js       |     1 +
 combo/apps/chrono/static/chrono/locale/hr.js       |     1 +
 combo/apps/chrono/static/chrono/locale/hu.js       |     1 +
 combo/apps/chrono/static/chrono/locale/id.js       |     1 +
 combo/apps/chrono/static/chrono/locale/is.js       |     1 +
 combo/apps/chrono/static/chrono/locale/it.js       |     1 +
 combo/apps/chrono/static/chrono/locale/ja.js       |     1 +
 combo/apps/chrono/static/chrono/locale/kk.js       |     1 +
 combo/apps/chrono/static/chrono/locale/ko.js       |     1 +
 combo/apps/chrono/static/chrono/locale/lb.js       |     1 +
 combo/apps/chrono/static/chrono/locale/lt.js       |     1 +
 combo/apps/chrono/static/chrono/locale/lv.js       |     1 +
 combo/apps/chrono/static/chrono/locale/mk.js       |     1 +
 combo/apps/chrono/static/chrono/locale/ms-my.js    |     1 +
 combo/apps/chrono/static/chrono/locale/ms.js       |     1 +
 combo/apps/chrono/static/chrono/locale/nb.js       |     1 +
 combo/apps/chrono/static/chrono/locale/nl-be.js    |     1 +
 combo/apps/chrono/static/chrono/locale/nl.js       |     1 +
 combo/apps/chrono/static/chrono/locale/nn.js       |     1 +
 combo/apps/chrono/static/chrono/locale/pl.js       |     1 +
 combo/apps/chrono/static/chrono/locale/pt-br.js    |     1 +
 combo/apps/chrono/static/chrono/locale/pt.js       |     1 +
 combo/apps/chrono/static/chrono/locale/ro.js       |     1 +
 combo/apps/chrono/static/chrono/locale/ru.js       |     1 +
 combo/apps/chrono/static/chrono/locale/sk.js       |     1 +
 combo/apps/chrono/static/chrono/locale/sl.js       |     1 +
 combo/apps/chrono/static/chrono/locale/sr-cyrl.js  |     1 +
 combo/apps/chrono/static/chrono/locale/sr.js       |     1 +
 combo/apps/chrono/static/chrono/locale/sv.js       |     1 +
 combo/apps/chrono/static/chrono/locale/th.js       |     1 +
 combo/apps/chrono/static/chrono/locale/tr.js       |     1 +
 combo/apps/chrono/static/chrono/locale/uk.js       |     1 +
 combo/apps/chrono/static/chrono/locale/vi.js       |     1 +
 combo/apps/chrono/static/chrono/locale/zh-cn.js    |     1 +
 combo/apps/chrono/static/chrono/locale/zh-tw.js    |     1 +
 98 files changed, 20651 insertions(+)
 create mode 100644 combo/apps/chrono/static/chrono/CHANGELOG.txt
 create mode 100644 combo/apps/chrono/static/chrono/CONTRIBUTING.txt
 create mode 100644 combo/apps/chrono/static/chrono/LICENSE.txt
 create mode 100644 combo/apps/chrono/static/chrono/chrono.css
 create mode 100644 combo/apps/chrono/static/chrono/chrono.js
 create mode 100644 combo/apps/chrono/static/chrono/demos/agenda-views.html
 create mode 100644 combo/apps/chrono/static/chrono/demos/background-events.html
 create mode 100644 combo/apps/chrono/static/chrono/demos/basic-views.html
 create mode 100644 combo/apps/chrono/static/chrono/demos/default.html
 create mode 100644 combo/apps/chrono/static/chrono/demos/external-dragging.html
 create mode 100644 combo/apps/chrono/static/chrono/demos/gcal.html
 create mode 100644 combo/apps/chrono/static/chrono/demos/json.html
 create mode 100644 combo/apps/chrono/static/chrono/demos/json/events.json
 create mode 100644 combo/apps/chrono/static/chrono/demos/list-views.html
 create mode 100644 combo/apps/chrono/static/chrono/demos/locales.html
 create mode 100644 combo/apps/chrono/static/chrono/demos/php/get-events.php
 create mode 100644 combo/apps/chrono/static/chrono/demos/php/get-timezones.php
 create mode 100644 combo/apps/chrono/static/chrono/demos/php/utils.php
 create mode 100644 combo/apps/chrono/static/chrono/demos/selectable.html
 create mode 100644 combo/apps/chrono/static/chrono/demos/theme.html
 create mode 100644 combo/apps/chrono/static/chrono/demos/timezones.html
 create mode 100644 combo/apps/chrono/static/chrono/demos/week-numbers.html
 create mode 100644 combo/apps/chrono/static/chrono/fullcalendar.css
 create mode 100644 combo/apps/chrono/static/chrono/fullcalendar.js
 create mode 100644 combo/apps/chrono/static/chrono/fullcalendar.min.css
 create mode 100644 combo/apps/chrono/static/chrono/fullcalendar.min.js
 create mode 100644 combo/apps/chrono/static/chrono/fullcalendar.print.css
 create mode 100644 combo/apps/chrono/static/chrono/fullcalendar.print.min.css
 create mode 100644 combo/apps/chrono/static/chrono/gcal.js
 create mode 100644 combo/apps/chrono/static/chrono/gcal.min.js
 create mode 100644 combo/apps/chrono/static/chrono/locale-all.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/af.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/ar-dz.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/ar-kw.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/ar-ly.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/ar-ma.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/ar-sa.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/ar-tn.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/ar.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/bg.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/ca.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/cs.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/da.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/de-at.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/de-ch.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/de.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/el.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/en-au.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/en-ca.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/en-gb.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/en-ie.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/en-nz.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/es-do.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/es.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/et.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/eu.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/fa.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/fi.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/fr-ca.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/fr-ch.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/fr.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/gl.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/he.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/hi.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/hr.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/hu.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/id.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/is.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/it.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/ja.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/kk.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/ko.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/lb.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/lt.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/lv.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/mk.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/ms-my.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/ms.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/nb.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/nl-be.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/nl.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/nn.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/pl.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/pt-br.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/pt.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/ro.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/ru.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/sk.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/sl.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/sr-cyrl.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/sr.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/sv.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/th.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/tr.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/uk.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/vi.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/zh-cn.js
 create mode 100644 combo/apps/chrono/static/chrono/locale/zh-tw.js
combo/apps/chrono/static/chrono/CHANGELOG.txt
1

  
2
v3.4.0 (2017-04-27)
3
-------------------
4

  
5
- composer.js for Composer (PHP package manager) (#3617)
6
- fix toISOString for locales with non-trivial postformatting (#3619)
7
- fix for nested inverse-background events (#3609)
8
- Estonian locale (#3600)
9
- fixed Latvian localization (#3525)
10
- internal refactor of async systems
11

  
12

  
13
v3.3.1 (2017-04-01)
14
-------------------
15

  
16
Bugfixes:
17
- stale calendar title when navigate away from then back to the a view (#3604)
18
- js error when gotoDate immediately after calendar initialization (#3598)
19
- agenda view scrollbars causes misalignment in jquery 3.2.1 (#3612)
20
- navigation bug when trying to navigate to a day of another week (#3610)
21
- dateIncrement not working when duration and dateIncrement have different units
22

  
23

  
24
v3.3.0 (2017-03-23)
25
-------------------
26

  
27
Features:
28
- `visibleRange` - complete control over view's date range (#2847, #3105, #3245)
29
- `validRange` - restrict date range (#429)
30
- `changeView` - pass in a date or visibleRange as second param (#3366)
31
- `dateIncrement` - customize prev/next jump (#2710)
32
- `dateAlignment` - custom view alignment, like start-of-week (#3113)
33
- `dayCount` - force a fixed number-of-days, even with hiddenDays (#2753)
34
- `showNonCurrentDates` - option to hide day cells for prev/next months (#437)
35
- can define a defaultView with a duration/visibleRange/dayCount with needing
36
  to create a custom view in the `views` object. Known as a "Generic View".
37

  
38
Behavior Changes:
39
- when custom view is specified with duration `{days:7}`,
40
  it will no longer align with the start of the week. (#2847)
41
- when `gotoDate` is called on a custom view with a duration of multiple days,
42
  the view will always shift to begin with the given date. (#3515)
43

  
44
Bugfixes:
45
- event rendering when excessive `minTime`/`maxTime` (#2530)
46
- event dragging not shown when excessive `minTime`/`maxTime` (#3055)
47
- excessive `minTime`/`maxTime` not reflected in event fetching (#3514)
48
	- when minTime is negative, or maxTime beyond 24 hours, when event data is requested
49
	  via a function or a feed, the given data params will have time parts.
50
- external event dragging via touchpunch broken (#3544)
51
- can't make an immediate new selection after existing selection, with mouse.
52
  introduced in v3.2.0 (#3558)
53

  
54

  
55
v3.2.0 (2017-02-14)
56
-------------------
57

  
58
Features:
59
- `selectMinDistance`, threshold before a mouse selection begins (#2428)
60

  
61
Bugfixes:
62
- iOS 10, unwanted scrolling while dragging events/selection (#3403)
63
- dayClick triggered when swiping on touch devices (#3332)
64
- dayClick not functioning on Firefix mobile (#3450)
65
- title computed incorrectly for views with no weekends (#2884)
66
- unwanted scrollbars in month-view when non-integer width (#3453, #3444)
67
- incorrect date formatting for locales with non-standlone month/day names (#3478)
68
- date formatting, incorrect omission of trailing period for certain locales (#2504, #3486)
69
- formatRange should collapse same week numbers (#3467)
70
- Taiwanese locale updated (#3426)
71
- Finnish noEventsMessage updated (#3476)
72
- Croatian (hr) buttonText is blank (#3270)
73
- JSON feed PHP example, date range math bug (#3485)
74

  
75

  
76
v3.1.0 (2016-12-05)
77
-------------------
78

  
79
- experimental support for implicitly batched ("debounced") event rendering (#2938)
80
	- `eventRenderWait` (off by default)
81
- new `footer` option, similar to header toolbar (#654, #3299)
82
- event rendering batch methods (#3351):
83
	- `renderEvents`
84
	- `updateEvents`
85
- more granular touch settings (#3377):
86
	- `eventLongPressDelay`
87
	- `selectLongPressDelay`
88
- eventDestroy not called when removing the popover (#3416, #3419)
89
- print stylesheet and gcal extension now offered as minified (#3415)
90
- fc-today in agenda header cells (#3361, #3365)
91
- height-related options in tandem with other options (#3327, #3384)
92
- Kazakh locale (#3394)
93
- Afrikaans locale (#3390)
94
- internal refactor related to timing of rendering and firing handlers.
95
  calls to rerender the current date-range and events from within handlers
96
  might not execute immediately. instead, will execute after handler finishes.
97

  
98

  
99
v3.0.1 (2016-09-26)
100
-------------------
101

  
102
Bugfixes:
103
- list view rendering event times incorrectly (#3334)
104
- list view rendering events/days out of order (#3347)
105
- events with no title rendering as "undefined"
106
- add .fc scope to table print styles (#3343)
107
- "display no events" text fix for German (#3354)
108

  
109

  
110
v3.0.0 (2016-09-04)
111
-------------------
112

  
113
Features:
114
- List View (#560)
115
	- new views: `listDay`, `listWeek`, `listMonth`, `listYear`, and simply `list`
116
	- `listDayFormat`
117
	- `listDayAltFormat`
118
	- `noEventsMessage`
119
- Clickable day/week numbers for easier navigation (#424)
120
	- `navLinks`
121
	- `navLinkDayClick`
122
	- `navLinkWeekClick`
123
- Programmatically allow/disallow user interactions:
124
	- `eventAllow` (#2740)
125
	- `selectAllow` (#2511)
126
- Option to display week numbers in cells (#3024)
127
	- `weekNumbersWithinDays` (set to `true` to activate)
128
- When week calc is ISO, default first day-of-week to Monday (#3255)
129
- Macedonian locale (#2739)
130
- Malay locale
131

  
132
Breaking Changes:
133
- IE8 support dropped
134
- jQuery: minimum support raised to v2.0.0
135
- MomentJS: minimum support raised to v2.9.0
136
- `lang` option renamed to `locale`
137
- dist files have been renamed to be more consistent with MomentJS:
138
	- `lang/` -> `locale/`
139
	- `lang-all.js` -> `locale-all.js`
140
- behavior of moment methods no longer affected by ambiguousness:
141
	- `isSame`
142
	- `isBefore`
143
	- `isAfter`
144
- View-Option-Hashes no longer supported (deprecated in 2.2.4)
145
- removed `weekMode` setting
146
- removed `axisFormat` setting
147
- DOM structure of month/basic-view day cell numbers changed
148

  
149
Bugfixes:
150
- `$.fullCalendar.version` incorrect (#3292)
151

  
152
Build System:
153
- using gulp instead of grunt (faster)
154
- using npm internally for dependencies instead of bower
155
- changed repo directory structure
156

  
157

  
158
v2.9.1 (2016-07-31)
159
-------------------
160

  
161
- multiple definitions for businessHours (#2686)
162
- businessHours for single day doesn't display weekends (#2944)
163
- height/contentHeight can accept a function or 'parent' for dynamic value (#3271)
164
- fix +more popover clipped by overflow (#3232)
165
- fix +more popover positioned incorrectly when scrolled (#3137)
166
- Norwegian Nynorsk translation (#3246)
167
- fix isAnimating JS error (#3285)
168

  
169

  
170
v2.9.0 (2016-07-10)
171
-------------------
172

  
173
- Setters for (almost) all options (#564).
174
  See [docs](http://fullcalendar.io/docs/utilities/dynamic_options/) for more info.
175
- Travis CI improvements (#3266)
176

  
177

  
178
v2.8.0 (2016-06-19)
179
-------------------
180

  
181
- getEventSources method (#3103, #2433)
182
- getEventSourceById method (#3223)
183
- refetchEventSources method (#3103, #1328, #254)
184
- removeEventSources method (#3165, #948)
185
- prevent flicker when refetchEvents is called (#3123, #2558)
186
- fix for removing event sources that share same URL (#3209)
187
- jQuery 3 support (#3197, #3124)
188
- Travis CI integration (#3218)
189
- EditorConfig for promoting consistent code style (#141)
190
- use en dash when formatting ranges (#3077)
191
- height:auto always shows scrollbars in month view on FF (#3202)
192
- new languages:
193
	- Basque (#2992)
194
	- Galician (#194)
195
	- Luxembourgish (#2979)
196

  
197

  
198
v2.7.3 (2016-06-02)
199
-------------------
200

  
201
internal enhancements that plugins can benefit from:
202
- EventEmitter not correctly working with stopListeningTo
203
- normalizeEvent hook for manipulating event data
204

  
205

  
206
v2.7.2 (2016-05-20)
207
-------------------
208

  
209
- fixed desktops/laptops with touch support not accepting mouse events for
210
  dayClick/dragging/resizing (#3154, #3149)
211
- fixed dayClick incorrectly triggered on touch scroll (#3152)
212
- fixed touch event dragging wrongfully beginning upon scrolling document (#3160)
213
- fixed minified JS still contained comments
214
- UI change: mouse users must hover over an event to reveal its resizers
215

  
216

  
217
v2.7.1 (2016-05-01)
218
-------------------
219

  
220
- dayClick not firing on touch devices (#3138)
221
- icons for prev/next not working in MS Edge (#2852)
222
- fix bad languages troubles with firewalls (#3133, #3132)
223
- update all dev dependencies (#3145, #3010, #2901, #251)
224
- git-ignore npm debug logs (#3011)
225
- misc automated test updates (#3139, #3147)
226
- Google Calendar htmlLink not always defined (#2844)
227

  
228

  
229
v2.7.0 (2016-04-23)
230
-------------------
231

  
232
touch device support (#994):
233
	- smoother scrolling
234
	- interactions initiated via "long press":
235
		- event drag-n-drop
236
		- event resize
237
		- time-range selecting
238
	- `longPressDelay`
239

  
240

  
241
v2.6.1 (2016-02-17)
242
-------------------
243

  
244
- make `nowIndicator` positioning refresh on window resize
245

  
246

  
247
v2.6.0 (2016-01-07)
248
-------------------
249

  
250
- current time indicator (#414)
251
- bundled with most recent version of moment (2.11.0)
252
- UMD wrapper around lang files now handles commonjs (#2918)
253
- fix bug where external event dragging would not respect eventOverlap
254
- fix bug where external event dropping would not render the whole-day highlight
255

  
256

  
257
v2.5.0 (2015-11-30)
258
-------------------
259

  
260
- internal timezone refactor. fixes #2396, #2900, #2945, #2711
261
- internal "grid" system refactor. improved API for plugins.
262

  
263

  
264
v2.4.0 (2015-08-16)
265
-------------------
266

  
267
- add new buttons to the header via `customButtons` ([225])
268
- control stacking order of events via `eventOrder` ([364])
269
- control frequency of slot text via `slotLabelInterval` ([946])
270
- `displayEventTime` ([1904])
271
- `on` and `off` methods ([1910])
272
- renamed `axisFormat` to `slotLabelFormat`
273

  
274
[225]: https://code.google.com/p/fullcalendar/issues/detail?id=225
275
[364]: https://code.google.com/p/fullcalendar/issues/detail?id=364
276
[946]: https://code.google.com/p/fullcalendar/issues/detail?id=946
277
[1904]: https://code.google.com/p/fullcalendar/issues/detail?id=1904
278
[1910]: https://code.google.com/p/fullcalendar/issues/detail?id=1910
279

  
280

  
281
v2.3.2 (2015-06-14)
282
-------------------
283

  
284
- minor code adjustment in preparation for plugins
285

  
286

  
287
v2.3.1 (2015-03-08)
288
-------------------
289

  
290
- Fix week view column title for en-gb ([PR220])
291
- Publish to NPM ([2447])
292
- Detangle bower from npm package ([PR179])
293

  
294
[PR220]: https://github.com/arshaw/fullcalendar/pull/220
295
[2447]: https://code.google.com/p/fullcalendar/issues/detail?id=2447
296
[PR179]: https://github.com/arshaw/fullcalendar/pull/179
297

  
298

  
299
v2.3.0 (2015-02-21)
300
-------------------
301

  
302
- internal refactoring in preparation for other views
303
- businessHours now renders on whole-days in addition to timed areas
304
- events in "more" popover not sorted by time ([2385])
305
- avoid using moment's deprecated zone method ([2443])
306
- destroying the calendar sometimes causes all window resize handlers to be unbound ([2432])
307
- multiple calendars on one page, can't accept external elements after navigating ([2433])
308
- accept external events from jqui sortable ([1698])
309
- external jqui drop processed before reverting ([1661])
310
- IE8 fix: month view renders incorrectly ([2428])
311
- IE8 fix: eventLimit:true wouldn't activate "more" link ([2330])
312
- IE8 fix: dragging an event with an href
313
- IE8 fix: invisible element while dragging agenda view events
314
- IE8 fix: erratic external element dragging
315

  
316
[2385]: https://code.google.com/p/fullcalendar/issues/detail?id=2385
317
[2443]: https://code.google.com/p/fullcalendar/issues/detail?id=2443
318
[2432]: https://code.google.com/p/fullcalendar/issues/detail?id=2432
319
[2433]: https://code.google.com/p/fullcalendar/issues/detail?id=2433
320
[1698]: https://code.google.com/p/fullcalendar/issues/detail?id=1698
321
[1661]: https://code.google.com/p/fullcalendar/issues/detail?id=1661
322
[2428]: https://code.google.com/p/fullcalendar/issues/detail?id=2428
323
[2330]: https://code.google.com/p/fullcalendar/issues/detail?id=2330
324

  
325

  
326
v2.2.7 (2015-02-10)
327
-------------------
328

  
329
- view.title wasn't defined in viewRender callback ([2407])
330
- FullCalendar versions >= 2.2.5 brokenness with Moment versions <= 2.8.3 ([2417])
331
- Support Bokmal Norwegian language specifically ([2427])
332

  
333
[2407]: https://code.google.com/p/fullcalendar/issues/detail?id=2407
334
[2417]: https://code.google.com/p/fullcalendar/issues/detail?id=2417
335
[2427]: https://code.google.com/p/fullcalendar/issues/detail?id=2427
336

  
337

  
338
v2.2.6 (2015-01-11)
339
-------------------
340

  
341
- Compatibility with Moment v2.9. Was breaking GCal plugin ([2408])
342
- View object's `title` property mistakenly omitted ([2407])
343
- Single-day views with hiddens days could cause prev/next misbehavior ([2406])
344
- Don't let the current date ever be a hidden day (solves [2395])
345
- Hebrew locale ([2157])
346

  
347
[2408]: https://code.google.com/p/fullcalendar/issues/detail?id=2408
348
[2407]: https://code.google.com/p/fullcalendar/issues/detail?id=2407
349
[2406]: https://code.google.com/p/fullcalendar/issues/detail?id=2406
350
[2395]: https://code.google.com/p/fullcalendar/issues/detail?id=2395
351
[2157]: https://code.google.com/p/fullcalendar/issues/detail?id=2157
352

  
353

  
354
v2.2.5 (2014-12-30)
355
-------------------
356

  
357
- `buttonText` specified for custom views via the `views` option
358
	- bugfix: wrong default value, couldn't override default
359
	- feature: default value taken from locale
360

  
361

  
362
v2.2.4 (2014-12-29)
363
-------------------
364

  
365
- Arbitrary durations for basic/agenda views with the `views` option ([692])
366
- Specify view-specific options using the `views` option. fixes [2283]
367
- Deprecate view-option-hashes
368
- Formalize and expose View API ([1055])
369
- updateEvent method, more intuitive behavior. fixes [2194]
370

  
371
[692]: https://code.google.com/p/fullcalendar/issues/detail?id=692
372
[2283]: https://code.google.com/p/fullcalendar/issues/detail?id=2283
373
[1055]: https://code.google.com/p/fullcalendar/issues/detail?id=1055
374
[2194]: https://code.google.com/p/fullcalendar/issues/detail?id=2194
375

  
376

  
377
v2.2.3 (2014-11-26)
378
-------------------
379

  
380
- removeEventSource with Google Calendar object source, would not remove ([2368])
381
- Events with invalid end dates are still accepted and rendered ([2350], [2237], [2296])
382
- Bug when rendering business hours and navigating away from original view ([2365])
383
- Links to Google Calendar events will use current timezone ([2122])
384
- Google Calendar plugin works with timezone names that have spaces
385
- Google Calendar plugin accepts person email addresses as calendar IDs
386
- Internally use numeric sort instead of alphanumeric sort ([2370])
387

  
388
[2368]: https://code.google.com/p/fullcalendar/issues/detail?id=2368
389
[2350]: https://code.google.com/p/fullcalendar/issues/detail?id=2350
390
[2237]: https://code.google.com/p/fullcalendar/issues/detail?id=2237
391
[2296]: https://code.google.com/p/fullcalendar/issues/detail?id=2296
392
[2365]: https://code.google.com/p/fullcalendar/issues/detail?id=2365
393
[2122]: https://code.google.com/p/fullcalendar/issues/detail?id=2122
394
[2370]: https://code.google.com/p/fullcalendar/issues/detail?id=2370
395

  
396

  
397
v2.2.2 (2014-11-19)
398
-------------------
399

  
400
- Fixes to Google Calendar API V3 code
401
	- wouldn't recognize a lone-string Google Calendar ID if periods before the @ symbol
402
	- removeEventSource wouldn't work when given a Google Calendar ID
403

  
404

  
405
v2.2.1 (2014-11-19)
406
-------------------
407

  
408
- Migrate Google Calendar plugin to use V3 of the API ([1526])
409

  
410
[1526]: https://code.google.com/p/fullcalendar/issues/detail?id=1526
411

  
412

  
413
v2.2.0 (2014-11-14)
414
-------------------
415

  
416
- Background events. Event object's `rendering` property ([144], [1286])
417
- `businessHours` option ([144])
418
- Controlling where events can be dragged/resized and selections can go ([396], [1286], [2253])
419
	- `eventOverlap`, `selectOverlap`, and similar
420
	- `eventConstraint`, `selectConstraint`, and similar
421
- Improvements to dragging and dropping external events ([2004])
422
	- Associating with real event data. used with `eventReceive`
423
	- Associating a `duration`
424
- Performance boost for moment creation
425
	- Be aware, FullCalendar-specific methods now attached directly to global moment.fn
426
	- Helps with [issue 2259][2259]
427
- Reintroduced forgotten `dropAccept` option ([2312])
428

  
429
[144]: https://code.google.com/p/fullcalendar/issues/detail?id=144
430
[396]: https://code.google.com/p/fullcalendar/issues/detail?id=396
431
[1286]: https://code.google.com/p/fullcalendar/issues/detail?id=1286
432
[2004]: https://code.google.com/p/fullcalendar/issues/detail?id=2004
433
[2253]: https://code.google.com/p/fullcalendar/issues/detail?id=2253
434
[2259]: https://code.google.com/p/fullcalendar/issues/detail?id=2259
435
[2312]: https://code.google.com/p/fullcalendar/issues/detail?id=2312
436

  
437

  
438
v2.1.1 (2014-08-29)
439
-------------------
440

  
441
- removeEventSource not working with array ([2203])
442
- mouseout not triggered after mouseover+updateEvent ([829])
443
- agenda event's render with no <a> href, not clickable ([2263])
444

  
445
[2203]: https://code.google.com/p/fullcalendar/issues/detail?id=2203
446
[829]: https://code.google.com/p/fullcalendar/issues/detail?id=829
447
[2263]: https://code.google.com/p/fullcalendar/issues/detail?id=2263
448

  
449

  
450
v2.1.0 (2014-08-25)
451
-------------------
452

  
453
Large code refactor with better OOP, better code reuse, and more comments.
454
**No more reliance on jQuery UI** for event dragging, resizing, or anything else.
455

  
456
Significant changes to HTML/CSS skeleton:
457
- Leverages tables for liquid rendering of days and events. No costly manual repositioning ([809])
458
- **Backwards-incompatibilities**:
459
	- **Many classNames have changed. Custom CSS will likely need to be adjusted.**
460
	- IE7 definitely not supported anymore
461
	- In `eventRender` callback, `element` will not be attached to DOM yet
462
	- Events are styled to be one line by default ([1992]). Can be undone through custom CSS,
463
	  but not recommended (might get gaps [like this][111] in certain situations).
464

  
465
A "more..." link when there are too many events on a day ([304]). Works with month and basic views
466
as well as the all-day section of the agenda views. New options:
467
- `eventLimit`. a number or `true`
468
- `eventLimitClick`. the `"popover`" value will reveal all events in a raised panel (the default)
469
- `eventLimitText`
470
- `dayPopoverFormat`
471

  
472
Changes related to height and scrollbars:
473
- `aspectRatio`/`height`/`contentHeight` values will be honored *no matter what*
474
	- If too many events causing too much vertical space, scrollbars will be used ([728]).
475
	  This is default behavior for month view (**backwards-incompatibility**)
476
	- If too few slots in agenda view, view will stretch to be the correct height ([2196])
477
- `'auto'` value for `height`/`contentHeight` options. If content is too tall, the view will
478
  vertically stretch to accomodate and no scrollbars will be used ([521]).
479
- Tall weeks in month view will borrow height from other weeks ([243])
480
- Automatically scroll the view then dragging/resizing an event ([1025], [2078])
481
- New `fixedWeekCount` option to determines the number of weeks in month view
482
	- Supersedes `weekMode` (**deprecated**). Instead, use a combination of `fixedWeekCount` and
483
	  one of the height options, possibly with an `'auto'` value
484

  
485
Much nicer, glitch-free rendering of calendar *for printers* ([35]). Things you might not expect:
486
- Buttons will become hidden
487
- Agenda views display a flat list of events where the time slots would be
488

  
489
Other issues resolved along the way:
490
- Space on right side of agenda events configurable through CSS ([204])
491
- Problem with window resize ([259])
492
- Events sorting stays consistent across weeks ([510])
493
- Agenda's columns misaligned on wide screens ([511])
494
- Run `selectHelper` through `eventRender` callbacks ([629])
495
- Keyboard access, tabbing ([637])
496
- Run resizing events through `eventRender` ([714])
497
- Resize an event to a different day in agenda views ([736])
498
- Allow selection across days in agenda views ([778])
499
- Mouseenter delegated event not working on event elements ([936])
500
- Agenda event dragging, snapping to different columns is erratic ([1101])
501
- Android browser cuts off Day view at 8 PM with no scroll bar ([1203])
502
- Don't fire `eventMouseover`/`eventMouseout` while dragging/resizing ([1297])
503
- Customize the resize handle text ("=") ([1326])
504
- If agenda event is too short, don't overwrite `.fc-event-time` ([1700])
505
- Zooming calendar causes events to misalign ([1996])
506
- Event destroy callback on event removal ([2017])
507
- Agenda views, when RTL, should have axis on right ([2132])
508
- Make header buttons more accessibile ([2151])
509
- daySelectionMousedown should interpret OSX ctrl+click as a right mouse click ([2169])
510
- Best way to display time text on multi-day events *with times* ([2172])
511
- Eliminate table use for header layout ([2186])
512
- Event delegation used for event-related callbacks (like `eventClick`). Speedier.
513

  
514
[35]: https://code.google.com/p/fullcalendar/issues/detail?id=35
515
[204]: https://code.google.com/p/fullcalendar/issues/detail?id=204
516
[243]: https://code.google.com/p/fullcalendar/issues/detail?id=243
517
[259]: https://code.google.com/p/fullcalendar/issues/detail?id=259
518
[304]: https://code.google.com/p/fullcalendar/issues/detail?id=304
519
[510]: https://code.google.com/p/fullcalendar/issues/detail?id=510
520
[511]: https://code.google.com/p/fullcalendar/issues/detail?id=511
521
[521]: https://code.google.com/p/fullcalendar/issues/detail?id=521
522
[629]: https://code.google.com/p/fullcalendar/issues/detail?id=629
523
[637]: https://code.google.com/p/fullcalendar/issues/detail?id=637
524
[714]: https://code.google.com/p/fullcalendar/issues/detail?id=714
525
[728]: https://code.google.com/p/fullcalendar/issues/detail?id=728
526
[736]: https://code.google.com/p/fullcalendar/issues/detail?id=736
527
[778]: https://code.google.com/p/fullcalendar/issues/detail?id=778
528
[809]: https://code.google.com/p/fullcalendar/issues/detail?id=809
529
[936]: https://code.google.com/p/fullcalendar/issues/detail?id=936
530
[1025]: https://code.google.com/p/fullcalendar/issues/detail?id=1025
531
[1101]: https://code.google.com/p/fullcalendar/issues/detail?id=1101
532
[1203]: https://code.google.com/p/fullcalendar/issues/detail?id=1203
533
[1297]: https://code.google.com/p/fullcalendar/issues/detail?id=1297
534
[1326]: https://code.google.com/p/fullcalendar/issues/detail?id=1326
535
[1700]: https://code.google.com/p/fullcalendar/issues/detail?id=1700
536
[1992]: https://code.google.com/p/fullcalendar/issues/detail?id=1992
537
[1996]: https://code.google.com/p/fullcalendar/issues/detail?id=1996
538
[2017]: https://code.google.com/p/fullcalendar/issues/detail?id=2017
539
[2078]: https://code.google.com/p/fullcalendar/issues/detail?id=2078
540
[2132]: https://code.google.com/p/fullcalendar/issues/detail?id=2132
541
[2151]: https://code.google.com/p/fullcalendar/issues/detail?id=2151
542
[2169]: https://code.google.com/p/fullcalendar/issues/detail?id=2169
543
[2172]: https://code.google.com/p/fullcalendar/issues/detail?id=2172
544
[2186]: https://code.google.com/p/fullcalendar/issues/detail?id=2186
545
[2196]: https://code.google.com/p/fullcalendar/issues/detail?id=2196
546
[111]: https://code.google.com/p/fullcalendar/issues/detail?id=111
547

  
548

  
549
v2.0.3 (2014-08-15)
550
-------------------
551

  
552
- moment-2.8.1 compatibility ([2221])
553
- relative path in bower.json ([PR 117])
554
- upgraded jquery-ui and misc dev dependencies
555

  
556
[2221]: https://code.google.com/p/fullcalendar/issues/detail?id=2221
557
[PR 117]: https://github.com/arshaw/fullcalendar/pull/177
558

  
559

  
560
v2.0.2 (2014-06-24)
561
-------------------
562

  
563
- bug with persisting addEventSource calls ([2191])
564
- bug with persisting removeEvents calls with an array source ([2187])
565
- bug with removeEvents method when called with 0 removes all events ([2082])
566

  
567
[2191]: https://code.google.com/p/fullcalendar/issues/detail?id=2191
568
[2187]: https://code.google.com/p/fullcalendar/issues/detail?id=2187
569
[2082]: https://code.google.com/p/fullcalendar/issues/detail?id=2082
570

  
571

  
572
v2.0.1 (2014-06-15)
573
-------------------
574

  
575
- `delta` parameters reintroduced in `eventDrop` and `eventResize` handlers ([2156])
576
  - **Note**: this changes the argument order for `revertFunc`
577
- wrongfully triggering a windowResize when resizing an agenda view event ([1116])
578
- `this` values in event drag-n-drop/resize handlers consistently the DOM node ([1177])
579
- `displayEventEnd` - v2 workaround to force display of an end time ([2090])
580
- don't modify passed-in eventSource items ([954])
581
- destroy method now removes fc-ltr class ([2033])
582
- weeks of last/next month still visible when weekends are hidden ([2095])
583
- fixed memory leak when destroying calendar with selectable/droppable ([2137])
584
- Icelandic language ([2180])
585
- Bahasa Indonesia language ([PR 172])
586

  
587
[1116]: https://code.google.com/p/fullcalendar/issues/detail?id=1116
588
[1177]: https://code.google.com/p/fullcalendar/issues/detail?id=1177
589
[2090]: https://code.google.com/p/fullcalendar/issues/detail?id=2090
590
[954]: https://code.google.com/p/fullcalendar/issues/detail?id=954
591
[2033]: https://code.google.com/p/fullcalendar/issues/detail?id=2033
592
[2095]: https://code.google.com/p/fullcalendar/issues/detail?id=2095
593
[2137]: https://code.google.com/p/fullcalendar/issues/detail?id=2137
594
[2156]: https://code.google.com/p/fullcalendar/issues/detail?id=2156
595
[2180]: https://code.google.com/p/fullcalendar/issues/detail?id=2180
596
[PR 172]: https://github.com/arshaw/fullcalendar/pull/172
597

  
598

  
599
v2.0.0 (2014-06-01)
600
-------------------
601

  
602
Internationalization support, timezone support, and [MomentJS] integration. Extensive changes, many
603
of which are backwards incompatible.
604

  
605
[Full list of changes][Upgrading-to-v2] | [Affected Issues][Date-Milestone]
606

  
607
An automated testing framework has been set up ([Karma] + [Jasmine]) and tests have been written
608
which cover about half of FullCalendar's functionality. Special thanks to @incre-d, @vidbina, and
609
@sirrocco for the help.
610

  
611
In addition, the main development repo has been repurposed to also include the built distributable
612
JS/CSS for the project and will serve as the new [Bower] endpoint.
613

  
614
[MomentJS]: http://momentjs.com/
615
[Upgrading-to-v2]: http://arshaw.com/fullcalendar/wiki/Upgrading-to-v2/
616
[Date-Milestone]: https://code.google.com/p/fullcalendar/issues/list?can=1&q=milestone%3Ddate
617
[Karma]: http://karma-runner.github.io/
618
[Jasmine]: http://jasmine.github.io/
619
[Bower]: http://bower.io/
620

  
621

  
622
v1.6.4 (2013-09-01)
623
-------------------
624

  
625
- better algorithm for positioning timed agenda events ([1115])
626
- `slotEventOverlap` option to tweak timed agenda event overlapping ([218])
627
- selection bug when slot height is customized ([1035])
628
- supply view argument in `loading` callback ([1018])
629
- fixed week number not displaying in agenda views ([1951])
630
- fixed fullCalendar not initializing with no options ([1356])
631
- NPM's `package.json`, no more warnings or errors ([1762])
632
- building the bower component should output `bower.json` instead of `component.json` ([PR 125])
633
- use bower internally for fetching new versions of jQuery and jQuery UI
634

  
635
[1115]: https://code.google.com/p/fullcalendar/issues/detail?id=1115
636
[218]: https://code.google.com/p/fullcalendar/issues/detail?id=218
637
[1035]: https://code.google.com/p/fullcalendar/issues/detail?id=1035
638
[1018]: https://code.google.com/p/fullcalendar/issues/detail?id=1018
639
[1951]: https://code.google.com/p/fullcalendar/issues/detail?id=1951
640
[1356]: https://code.google.com/p/fullcalendar/issues/detail?id=1356
641
[1762]: https://code.google.com/p/fullcalendar/issues/detail?id=1762
642
[PR 125]: https://github.com/arshaw/fullcalendar/pull/125
643

  
644

  
645
v1.6.3 (2013-08-10)
646
-------------------
647

  
648
- `viewRender` callback ([PR 15])
649
- `viewDestroy` callback ([PR 15])
650
- `eventDestroy` callback ([PR 111])
651
- `handleWindowResize` option ([PR 54])
652
- `eventStartEditable`/`startEditable` options ([PR 49])
653
- `eventDurationEditable`/`durationEditable` options ([PR 49])
654
- specify function for `$.ajax` `data` parameter for JSON event sources ([PR 59])
655
- fixed bug with agenda event dropping in wrong column ([PR 55])
656
- easier event element z-index customization ([PR 58])
657
- classNames on past/future days ([PR 88])
658
- allow `null`/`undefined` event titles ([PR 84])
659
- small optimize for agenda event rendering ([PR 56])
660
- deprecated:
661
	- `viewDisplay`
662
	- `disableDragging`
663
	- `disableResizing`
664
- bundled with latest jQuery (1.10.2) and jQuery UI (1.10.3)
665

  
666
[PR 15]: https://github.com/arshaw/fullcalendar/pull/15
667
[PR 111]: https://github.com/arshaw/fullcalendar/pull/111
668
[PR 54]: https://github.com/arshaw/fullcalendar/pull/54
669
[PR 49]: https://github.com/arshaw/fullcalendar/pull/49
670
[PR 59]: https://github.com/arshaw/fullcalendar/pull/59
671
[PR 55]: https://github.com/arshaw/fullcalendar/pull/55
672
[PR 58]: https://github.com/arshaw/fullcalendar/pull/58
673
[PR 88]: https://github.com/arshaw/fullcalendar/pull/88
674
[PR 84]: https://github.com/arshaw/fullcalendar/pull/84
675
[PR 56]: https://github.com/arshaw/fullcalendar/pull/56
676

  
677

  
678
v1.6.2 (2013-07-18)
679
-------------------
680

  
681
- `hiddenDays` option ([686])
682
- bugfix: when `eventRender` returns `false`, incorrect stacking of events ([762])
683
- bugfix: couldn't change `event.backgroundImage` when calling `updateEvent` (thx @stephenharris)
684

  
685
[686]: https://code.google.com/p/fullcalendar/issues/detail?id=686
686
[762]: https://code.google.com/p/fullcalendar/issues/detail?id=762
687

  
688

  
689
v1.6.1 (2013-04-14)
690
-------------------
691

  
692
- fixed event inner content overflow bug ([1783])
693
- fixed table header className bug [1772]
694
- removed text-shadow on events (better for general use, thx @tkrotoff)
695

  
696
[1783]: https://code.google.com/p/fullcalendar/issues/detail?id=1783
697
[1772]: https://code.google.com/p/fullcalendar/issues/detail?id=1772
698

  
699

  
700
v1.6.0 (2013-03-18)
701
-------------------
702

  
703
- visual facelift, with bootstrap-inspired buttons and colors
704
- simplified HTML/CSS for events and buttons
705
- `dayRender`, for modifying a day cell ([191], thx @althaus)
706
- week numbers on side of calendar ([295])
707
	- `weekNumber`
708
	- `weekNumberCalculation`
709
	- `weekNumberTitle`
710
	- `W` formatting variable
711
- finer snapping granularity for agenda view events ([495], thx @ms-doodle-com)
712
- `eventAfterAllRender` ([753], thx @pdrakeweb)
713
- `eventDataTransform` (thx @joeyspo)
714
- `data-date` attributes on cells (thx @Jae)
715
- expose `$.fullCalendar.dateFormatters`
716
- when clicking fast on buttons, prevent text selection
717
- bundled with latest jQuery (1.9.1) and jQuery UI (1.10.2)
718
- Grunt/Lumbar build system for internal development
719
- build for Bower package manager
720
- build for jQuery plugin site
721

  
722
[191]: https://code.google.com/p/fullcalendar/issues/detail?id=191
723
[295]: https://code.google.com/p/fullcalendar/issues/detail?id=295
724
[495]: https://code.google.com/p/fullcalendar/issues/detail?id=495
725
[753]: https://code.google.com/p/fullcalendar/issues/detail?id=753
726

  
727

  
728
v1.5.4 (2012-09-05)
729
-------------------
730

  
731
- made compatible with jQuery 1.8.* (thx @archaeron)
732
- bundled with jQuery 1.8.1 and jQuery UI 1.8.23
733

  
734

  
735
v1.5.3 (2012-02-06)
736
-------------------
737

  
738
- fixed dragging issue with jQuery UI 1.8.16 ([1168])
739
- bundled with jQuery 1.7.1 and jQuery UI 1.8.17
740

  
741
[1168]: https://code.google.com/p/fullcalendar/issues/detail?id=1168
742

  
743

  
744
v1.5.2 (2011-08-21)
745
-------------------
746

  
747
- correctly process UTC "Z" ISO8601 date strings ([750])
748

  
749
[750]: https://code.google.com/p/fullcalendar/issues/detail?id=750
750

  
751

  
752
v1.5.1 (2011-04-09)
753
-------------------
754

  
755
- more flexible ISO8601 date parsing ([814])
756
- more flexible parsing of UNIX timestamps ([826])
757
- FullCalendar now buildable from source on a Mac ([795])
758
- FullCalendar QA'd in FF4 ([883])
759
- upgraded to jQuery 1.5.2 (which supports IE9) and jQuery UI 1.8.11
760

  
761
[814]: https://code.google.com/p/fullcalendar/issues/detail?id=814
762
[826]: https://code.google.com/p/fullcalendar/issues/detail?id=826
763
[795]: https://code.google.com/p/fullcalendar/issues/detail?id=795
764
[883]: https://code.google.com/p/fullcalendar/issues/detail?id=883
765

  
766

  
767
v1.5 (2011-03-19)
768
-----------------
769

  
770
- slicker default styling for buttons
771
- reworked a lot of the calendar's HTML and accompanying CSS (solves [327] and [395])
772
- more printer-friendly (fullcalendar-print.css)
773
- fullcalendar now inherits styles from jquery-ui themes differently.
774
  styles for buttons are distinct from styles for calendar cells.
775
  (solves [299])
776
- can now color events through FullCalendar options and Event-Object properties ([117])
777
  THIS IS NOW THE PREFERRED METHOD OF COLORING EVENTS (as opposed to using className and CSS)
778
	- FullCalendar options:
779
		- eventColor (changes both background and border)
780
		- eventBackgroundColor
781
		- eventBorderColor
782
		- eventTextColor
783
	- Event-Object options:
784
		- color (changes both background and border)
785
		- backgroundColor
786
		- borderColor
787
		- textColor
788
- can now specify an event source as an *object* with a `url` property (json feed) or
789
  an `events` property (function or array) with additional properties that will
790
  be applied to the entire event source:
791
	- color (changes both background and border)
792
	- backgroudColor
793
	- borderColor
794
	- textColor
795
	- className
796
	- editable
797
	- allDayDefault
798
	- ignoreTimezone
799
	- startParam (for a feed)
800
	- endParam   (for a feed)
801
	- ANY OF THE JQUERY $.ajax OPTIONS
802
	  allows for easily changing from GET to POST and sending additional parameters ([386])
803
	  allows for easily attaching ajax handlers such as `error` ([754])
804
	  allows for turning caching on ([355])
805
- Google Calendar feeds are now specified differently:
806
	- specify a simple string of your feed's URL
807
	- specify an *object* with a `url` property of your feed's URL.
808
	  you can include any of the new Event-Source options in this object.
809
	- the old `$.fullCalendar.gcalFeed` method still works
810
- no more IE7 SSL popup ([504])
811
- remove `cacheParam` - use json event source `cache` option instead
812
- latest jquery/jquery-ui
813

  
814
[327]: https://code.google.com/p/fullcalendar/issues/detail?id=327
815
[395]: https://code.google.com/p/fullcalendar/issues/detail?id=395
816
[299]: https://code.google.com/p/fullcalendar/issues/detail?id=299
817
[117]: https://code.google.com/p/fullcalendar/issues/detail?id=117
818
[386]: https://code.google.com/p/fullcalendar/issues/detail?id=386
819
[754]: https://code.google.com/p/fullcalendar/issues/detail?id=754
820
[355]: https://code.google.com/p/fullcalendar/issues/detail?id=355
821
[504]: https://code.google.com/p/fullcalendar/issues/detail?id=504
822

  
823

  
824
v1.4.11 (2011-02-22)
825
--------------------
826

  
827
- fixed rerenderEvents bug ([790])
828
- fixed bug with faulty dragging of events from all-day slot in agenda views
829
- bundled with jquery 1.5 and jquery-ui 1.8.9
830

  
831
[790]: https://code.google.com/p/fullcalendar/issues/detail?id=790
832

  
833

  
834
v1.4.10 (2011-01-02)
835
--------------------
836

  
837
- fixed bug with resizing event to different week in 5-day month view ([740])
838
- fixed bug with events not sticking after a removeEvents call ([757])
839
- fixed bug with underlying parseTime method, and other uses of parseInt ([688])
840

  
841
[740]: https://code.google.com/p/fullcalendar/issues/detail?id=740
842
[757]: https://code.google.com/p/fullcalendar/issues/detail?id=757
843
[688]: https://code.google.com/p/fullcalendar/issues/detail?id=688
844

  
845

  
846
v1.4.9 (2010-11-16)
847
-------------------
848

  
849
- new algorithm for vertically stacking events ([111])
850
- resizing an event to a different week ([306])
851
- bug: some events not rendered with consecutive calls to addEventSource ([679])
852

  
853
[111]: https://code.google.com/p/fullcalendar/issues/detail?id=111
854
[306]: https://code.google.com/p/fullcalendar/issues/detail?id=306
855
[679]: https://code.google.com/p/fullcalendar/issues/detail?id=679
856

  
857

  
858
v1.4.8 (2010-10-16)
859
-------------------
860

  
861
- ignoreTimezone option (set to `false` to process UTC offsets in ISO8601 dates)
862
- bugfixes
863
	- event refetching not being called under certain conditions ([417], [554])
864
	- event refetching being called multiple times under certain conditions ([586], [616])
865
	- selection cannot be triggered by right mouse button ([558])
866
	- agenda view left axis sized incorrectly ([465])
867
	- IE js error when calendar is too narrow ([517])
868
	- agenda view looks strange when no scrollbars ([235])
869
	- improved parsing of ISO8601 dates with UTC offsets
870
- $.fullCalendar.version
871
- an internal refactor of the code, for easier future development and modularity
872

  
873
[417]: https://code.google.com/p/fullcalendar/issues/detail?id=417
874
[554]: https://code.google.com/p/fullcalendar/issues/detail?id=554
875
[586]: https://code.google.com/p/fullcalendar/issues/detail?id=586
876
[616]: https://code.google.com/p/fullcalendar/issues/detail?id=616
877
[558]: https://code.google.com/p/fullcalendar/issues/detail?id=558
878
[465]: https://code.google.com/p/fullcalendar/issues/detail?id=465
879
[517]: https://code.google.com/p/fullcalendar/issues/detail?id=517
880
[235]: https://code.google.com/p/fullcalendar/issues/detail?id=235
881

  
882

  
883
v1.4.7 (2010-07-05)
884
-------------------
885

  
886
- "dropping" external objects onto the calendar
887
	- droppable (boolean, to turn on/off)
888
	- dropAccept (to filter which events the calendar will accept)
889
	- drop (trigger)
890
- selectable options can now be specified with a View Option Hash
891
- bugfixes
892
	- dragged & reverted events having wrong time text ([406])
893
	- bug rendering events that have an endtime with seconds, but no hours/minutes ([477])
894
	- gotoDate date overflow bug ([429])
895
	- wrong date reported when clicking on edge of last column in agenda views [412]
896
- support newlines in event titles
897
- select/unselect callbacks now passes native js event
898

  
899
[406]: https://code.google.com/p/fullcalendar/issues/detail?id=406
900
[477]: https://code.google.com/p/fullcalendar/issues/detail?id=477
901
[429]: https://code.google.com/p/fullcalendar/issues/detail?id=429
902
[412]: https://code.google.com/p/fullcalendar/issues/detail?id=412
903

  
904

  
905
v1.4.6 (2010-05-31)
906
-------------------
907

  
908
- "selecting" days or timeslots
909
	- options: selectable, selectHelper, unselectAuto, unselectCancel
910
	- callbacks: select, unselect
911
	- methods: select, unselect
912
- when dragging an event, the highlighting reflects the duration of the event
913
- code compressing by Google Closure Compiler
914
- bundled with jQuery 1.4.2 and jQuery UI 1.8.1
915

  
916

  
917
v1.4.5 (2010-02-21)
918
-------------------
919

  
920
- lazyFetching option, which can force the calendar to fetch events on every view/date change
921
- scroll state of agenda views are preserved when switching back to view
922
- bugfixes
923
	- calling methods on an uninitialized fullcalendar throws error
924
	- IE6/7 bug where an entire view becomes invisible ([320])
925
	- error when rendering a hidden calendar (in jquery ui tabs for example) in IE ([340])
926
	- interconnected bugs related to calendar resizing and scrollbars
927
		- when switching views or clicking prev/next, calendar would "blink" ([333])
928
		- liquid-width calendar's events shifted (depending on initial height of browser) ([341])
929
		- more robust underlying algorithm for calendar resizing
930

  
931
[320]: https://code.google.com/p/fullcalendar/issues/detail?id=320
932
[340]: https://code.google.com/p/fullcalendar/issues/detail?id=340
933
[333]: https://code.google.com/p/fullcalendar/issues/detail?id=333
934
[341]: https://code.google.com/p/fullcalendar/issues/detail?id=341
935

  
936

  
937
v1.4.4 (2010-02-03)
938
-------------------
939

  
940
- optimized event rendering in all views (events render in 1/10 the time)
941
- gotoDate() does not force the calendar to unnecessarily rerender
942
- render() method now correctly readjusts height
943

  
944

  
945
v1.4.3 (2009-12-22)
946
-------------------
947

  
948
- added destroy method
949
- Google Calendar event pages respect currentTimezone
950
- caching now handled by jQuery's ajax	
951
- protection from setting aspectRatio to zero
952
- bugfixes
953
	- parseISO8601 and DST caused certain events to display day before
954
	- button positioning problem in IE6
955
	- ajax event source removed after recently being added, events still displayed
956
	- event not displayed when end is an empty string
957
	- dynamically setting calendar height when no events have been fetched, throws error
958

  
959

  
960
v1.4.2 (2009-12-02)
961
-------------------
962

  
963
- eventAfterRender trigger
964
- getDate & getView methods
965
- height & contentHeight options (explicitly sets the pixel height)
966
- minTime & maxTime options (restricts shown hours in agenda view)
967
- getters [for all options] and setters [for height, contentHeight, and aspectRatio ONLY! stay tuned..]
968
- render method now readjusts calendar's size
969
- bugfixes
970
	- lightbox scripts that use iframes (like fancybox)
971
	- day-of-week classNames were off when firstDay=1
972
	- guaranteed space on right side of agenda events (even when stacked)
973
	- accepts ISO8601 dates with a space (instead of 'T')
974

  
975

  
976
v1.4.1 (2009-10-31)
977
-------------------
978

  
979
- can exclude weekends with new 'weekends' option
980
- gcal feed 'currentTimezone' option
981
- bugfixes
982
	- year/month/date option sometimes wouldn't set correctly (depending on current date)
983
	- daylight savings issue caused agenda views to start at 1am (for BST users)
984
- cleanup of gcal.js code
985

  
986

  
987
v1.4 (2009-10-19)
988
-----------------
989

  
990
- agendaWeek and agendaDay views
991
- added some options for agenda views:
992
	- allDaySlot
993
	- allDayText
994
	- firstHour
995
	- slotMinutes
996
	- defaultEventMinutes
997
	- axisFormat
998
- modified some existing options/triggers to work with agenda views:
999
	- dragOpacity and timeFormat can now accept a "View Hash" (a new concept)
1000
	- dayClick now has an allDay parameter
1001
	- eventDrop now has an an allDay parameter
1002
	  (this will affect those who use revertFunc, adjust parameter list)
1003
- added 'prevYear' and 'nextYear' for buttons in header
1004
- minor change for theme users, ui-state-hover not applied to active/inactive buttons
1005
- added event-color-changing example in docs
1006
- better defaults for right-to-left themed button icons
1007

  
1008

  
1009
v1.3.2 (2009-10-13)
1010
-------------------
1011

  
1012
- Bugfixes (please upgrade from 1.3.1!)
1013
	- squashed potential infinite loop when addMonths and addDays
1014
	  is called with an invalid date
1015
	- $.fullCalendar.parseDate() now correctly parses IETF format
1016
	- when switching views, the 'today' button sticks inactive, fixed
1017
- gotoDate now can accept a single Date argument
1018
- documentation for changes in 1.3.1 and 1.3.2 now on website
1019

  
1020

  
1021
v1.3.1 (2009-09-30)
1022
-------------------
1023

  
1024
- Important Bugfixes (please upgrade from 1.3!)
1025
	- When current date was late in the month, for long months, and prev/next buttons
1026
	  were clicked in month-view, some months would be skipped/repeated
1027
	- In certain time zones, daylight savings time would cause certain days
1028
	  to be misnumbered in month-view
1029
- Subtle change in way week interval is chosen when switching from month to basicWeek/basicDay view
1030
- Added 'allDayDefault' option
1031
- Added 'changeView' and 'render' methods
1032

  
1033

  
1034
v1.3 (2009-09-21)
1035
-----------------
1036

  
1037
- different 'views': month/basicWeek/basicDay
1038
- more flexible 'header' system for buttons
1039
- themable by jQuery UI themes
1040
- resizable events (require jQuery UI resizable plugin)
1041
- rescoped & rewritten CSS, enhanced default look
1042
- cleaner css & rendering techniques for right-to-left
1043
- reworked options & API to support multiple views / be consistent with jQuery UI
1044
- refactoring of entire codebase
1045
	- broken into different JS & CSS files, assembled w/ build scripts
1046
	- new test suite for new features, uses firebug-lite
1047
- refactored docs
1048
- Options
1049
	- + date
1050
	- + defaultView
1051
	- + aspectRatio
1052
	- + disableResizing
1053
	- + monthNames      (use instead of $.fullCalendar.monthNames)
1054
	- + monthNamesShort (use instead of $.fullCalendar.monthAbbrevs)
1055
	- + dayNames        (use instead of $.fullCalendar.dayNames)
1056
	- + dayNamesShort   (use instead of $.fullCalendar.dayAbbrevs)
1057
	- + theme
1058
	- + buttonText
1059
	- + buttonIcons
1060
	- x draggable           -> editable/disableDragging
1061
	- x fixedWeeks          -> weekMode
1062
	- x abbrevDayHeadings   -> columnFormat
1063
	- x buttons/title       -> header
1064
	- x eventDragOpacity    -> dragOpacity
1065
	- x eventRevertDuration -> dragRevertDuration
1066
	- x weekStart           -> firstDay
1067
	- x rightToLeft         -> isRTL
1068
	- x showTime (use 'allDay' CalEvent property instead)
1069
- Triggered Actions
1070
	- + eventResizeStart
1071
	- + eventResizeStop
1072
	- + eventResize
1073
	- x monthDisplay -> viewDisplay
1074
	- x resize       -> windowResize
1075
	- 'eventDrop' params changed, can revert if ajax cuts out
1076
- CalEvent Properties
1077
	- x showTime  -> allDay
1078
	- x draggable -> editable
1079
	- 'end' is now INCLUSIVE when allDay=true
1080
	- 'url' now produces a real <a> tag, more native clicking/tab behavior
1081
- Methods:
1082
	- + renderEvent
1083
	- x prevMonth         -> prev
1084
	- x nextMonth         -> next
1085
	- x prevYear/nextYear -> moveDate
1086
	- x refresh           -> rerenderEvents/refetchEvents
1087
	- x removeEvent       -> removeEvents
1088
	- x getEventsByID     -> clientEvents
1089
- Utilities:
1090
	- 'formatDate' format string completely changed (inspired by jQuery UI datepicker + datejs)
1091
	- 'formatDates' added to support date-ranges
1092
- Google Calendar Options:
1093
	- x draggable -> editable
1094
- Bugfixes
1095
	- gcal extension fetched 25 results max, now fetches all
1096

  
1097

  
1098
v1.2.1 (2009-06-29)
1099
-------------------
1100

  
1101
- bugfixes
1102
	- allows and corrects invalid end dates for events
1103
	- doesn't throw an error in IE while rendering when display:none
1104
	- fixed 'loading' callback when used w/ multiple addEventSource calls
1105
	- gcal className can now be an array
1106

  
1107

  
1108
v1.2 (2009-05-31)
1109
-----------------
1110

  
1111
- expanded API
1112
	- 'className' CalEvent attribute
1113
	- 'source' CalEvent attribute
1114
	- dynamically get/add/remove/update events of current month
1115
	- locale improvements: change month/day name text
1116
	- better date formatting ($.fullCalendar.formatDate)
1117
	- multiple 'event sources' allowed
1118
		- dynamically add/remove event sources
1119
- options for prevYear and nextYear buttons
1120
- docs have been reworked (include addition of Google Calendar docs)
1121
- changed behavior of parseDate for number strings
1122
  (now interpets as unix timestamp, not MS times)
1123
- bugfixes
1124
	- rightToLeft month start bug
1125
	- off-by-one errors with month formatting commands
1126
	- events from previous months sticking when clicking prev/next quickly
1127
- Google Calendar API changed to work w/ multiple event sources
1128
	- can also provide 'className' and 'draggable' options
1129
- date utilties moved from $ to $.fullCalendar
1130
- more documentation in source code
1131
- minified version of fullcalendar.js
1132
- test suit (available from svn)
1133
- top buttons now use `<button>` w/ an inner `<span>` for better css cusomization
1134
	- thus CSS has changed. IF UPGRADING FROM PREVIOUS VERSIONS,
1135
	  UPGRADE YOUR FULLCALENDAR.CSS FILE
1136

  
1137

  
1138
v1.1 (2009-05-10)
1139
-----------------
1140

  
1141
- Added the following options:
1142
	- weekStart
1143
	- rightToLeft
1144
	- titleFormat
1145
	- timeFormat
1146
	- cacheParam
1147
	- resize
1148
- Fixed rendering bugs
1149
	- Opera 9.25 (events placement & window resizing)
1150
	- IE6 (window resizing)
1151
- Optimized window resizing for ALL browsers
1152
- Events on same day now sorted by start time (but first by timespan)
1153
- Correct z-index when dragging
1154
- Dragging contained in overflow DIV for IE6
1155
- Modified fullcalendar.css
1156
	- for right-to-left support
1157
	- for variable start-of-week
1158
	- for IE6 resizing bug
1159
	- for THEAD and TBODY (in 1.0, just used TBODY, restructured in 1.1)
1160
	- IF UPGRADING FROM FULLCALENDAR 1.0, YOU MUST UPGRADE FULLCALENDAR.CSS
combo/apps/chrono/static/chrono/CONTRIBUTING.txt
1

  
2
## Reporting Bugs
3

  
4
Each bug report MUST have a [JSFiddle/JSBin] recreation before any work can begin. [further instructions &raquo;](http://fullcalendar.io/wiki/Reporting-Bugs/)
5

  
6

  
7
## Requesting Features
8

  
9
Please search the [Issue Tracker] to see if your feature has already been requested, and if so, subscribe to it. Otherwise, read these [further instructions &raquo;](http://fullcalendar.io/wiki/Requesting-Features/)
10

  
11

  
12
## Contributing Features
13

  
14
The FullCalendar project welcomes [Pull Requests][Using Pull Requests] for new features, but because there are so many feature requests (over 100), and because every new feature requires refinement and maintenance, each PR will be prioritized against the project's other demands and might take a while to make it to an official release.
15

  
16
Furthermore, each new feature should be designed as robustly as possible and be useful beyond the immediate usecase it was initially designed for. Feel free to start a ticket discussing the feature's specs before coding.
17

  
18

  
19
## Contributing Bugfixes
20

  
21
In the description of your [Pull Request][Using Pull Requests], please include recreation steps for the bug as well as a [JSFiddle/JSBin] demo. Communicating the buggy behavior is a requirement before a merge can happen.
22

  
23

  
24
## Contributing Locales
25

  
26
Please edit the original files in the `locale/` directory. DO NOT edit anything in the `dist/` directory. The build system will responsible for merging FullCalendar's `locale/` data with the [MomentJS locale data].
27

  
28

  
29
## Other Ways to Contribute
30

  
31
[Read about other ways to contribute &raquo;](http://fullcalendar.io/wiki/Contributing/)
32

  
33

  
34
## Getting Set Up
35

  
36
You will need [Git][git], [Node][node], and NPM installed. For clarification, please view the [jQuery readme][jq-readme], which requires a similar setup.
37

  
38
Also, you will need the [gulp-cli][gulp-cli] package installed globally (`-g`) on your system:
39

  
40
	npm install -g gulp-cli
41

  
42
Then, clone FullCalendar's git repo:
43

  
44
	git clone git://github.com/fullcalendar/fullcalendar.git
45

  
46
Enter the directory and install FullCalendar's dependencies:
47

  
48
	cd fullcalendar
49
	npm install
50

  
51

  
52
## What to edit
53

  
54
When modifying files, please do not edit the generated or minified files in the `dist/` directory. Please edit the original `src/` files.
55

  
56

  
57
## Development Workflow
58

  
59
After you make code changes, you'll want to compile the JS/CSS so that it can be previewed from the tests and demos. You can either manually rebuild each time you make a change:
60

  
61
	gulp dev
62

  
63
Or, you can run a script that automatically rebuilds whenever you save a source file:
64

  
65
	gulp watch
66

  
67
When you are finished, run the following command to write the distributable files into the `./dist/` directory:
68

  
69
	gulp dist
70

  
71
If you want to clean up the generated files, run:
72

  
73
	gulp clean
74

  
75

  
76
## Style Guide
77

  
78
Please follow the [Google JavaScript Style Guide] as closely as possible. With the following exceptions:
79

  
80
```js
81
if (true) {
82
}
83
else { // please put else, else if, and catch on a separate line
84
}
85

  
86
// please write one-line array literals with a one-space padding inside
87
var a = [ 1, 2, 3 ];
88

  
89
// please write one-line object literals with a one-space padding inside
90
var o = { a: 1, b: 2, c: 3 };
91
```
92

  
93
Other exceptions:
94

  
95
- please ignore anything about Google Closure Compiler or the `goog` library
96
- please do not write JSDoc comments
97

  
98
Notes about whitespace:
99

  
100
- **use *tabs* instead of spaces**
101
- separate functions with *2* blank lines
102
- separate logical blocks within functions with *1* blank line
103

  
104
Run the command line tool to automatically check your style:
105

  
106
	gulp lint
107

  
108

  
109
## Before Submitting your Code
110

  
111
If you have edited code (including **tests** and **translations**) and would like to submit a pull request, please make sure you have done the following:
112

  
113
1. Conformed to the style guide (successfully run `gulp lint`)
114

  
115
2. Written automated tests. View the [Automated Test Readme]
116

  
117

  
118
[JSFiddle/JSBin]: http://fullcalendar.io/wiki/Reporting-Bugs/
119
[Issue Tracker]: https://github.com/fullcalendar/fullcalendar/issues
120
[Using Pull Requests]: https://help.github.com/articles/using-pull-requests/
121
[MomentJS locale data]: https://github.com/moment/moment/tree/develop/locale
122
[git]: http://git-scm.com/
123
[node]: http://nodejs.org/
124
[gulp-cli]: https://github.com/gulpjs/gulp/blob/master/docs/getting-started.md
125
[jq-readme]: https://github.com/jquery/jquery/blob/master/README.md#what-you-need-to-build-your-own-jquery
126
[Google JavaScript Style Guide]: http://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml
127
[Automated Test Readme]: https://github.com/fullcalendar/fullcalendar/wiki/Automated-Tests
combo/apps/chrono/static/chrono/LICENSE.txt
1
Copyright (c) 2015 Adam Shaw
2

  
3
Permission is hereby granted, free of charge, to any person obtaining
4
a copy of this software and associated documentation files (the
5
"Software"), to deal in the Software without restriction, including
6
without limitation the rights to use, copy, modify, merge, publish,
7
distribute, sublicense, and/or sell copies of the Software, and to
8
permit persons to whom the Software is furnished to do so, subject to
9
the following conditions:
10

  
11
The above copyright notice and this permission notice shall be
12
included in all copies or substantial portions of the Software.
13

  
14
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
combo/apps/chrono/static/chrono/chrono.css
1
#calendar {
2
    width: 900px;
3
    margin: 0 auto;
4
}
5

  
6

  
7
page {
8
  max-width: 960px; padding: 0 15px; margin: 40px auto;
9
  @media (max-height: 790px) {
10
    margin-top: 0;
11
  }
12
}
13
.page-header h1 { .text-center; font-weight: 100; }
14

  
15
.input, select {
16
  padding: 2px 5px;
17
}
18
.btn {
19
  padding: .2em .8em;
20
  border-radius: 4px;
21
  border: 1px solid #bcbcbc;
22
	box-shadow: 0 1px 3px rgba(0,0,0,0.12);
23
	background-image: linear-gradient(180deg, rgba(255,255,255,1) 0%,rgba(239,239,239,1) 60%,rgba(225,223,226,1) 100%);
24
  background-repeat: no-repeat; // fix for firefox
25
}
26

  
27
.bubble {
28
  box-shadow: 0 2px 4px rgba(0,0,0,.2); border-radius: 2px;
29
  background: #fff; padding: 15px;
30
  width: 420px;
31
  z-index: 99;
32
  position: absolute;
33

  
34
  .close {
35
    position: absolute; font-size: 24px; line-height: 1;
36
    padding: 0 5px;
37
    right: 5px; top: 5px;
38
  }
39
}
40

  
41
.bubble {
42
  @border-color: #ccc;
43
  border: 1px solid @border-color;
44
  .arrow, .arrow:after {
45
    position: absolute; height: 0; width: 0; font-size: 0; .horizontal-border(transparent, 10px);
46
  }
47
  &-top, &-bottom {
48
    .arrow {
49
      left: 50%; margin-left: -10px;
50
    }
51
    .arrow:after {
52
      content: ''; left: -10px;
53
    }
54
  }
55
  &-top {
56
    .arrow {
57
      border-top: @border-color 10px solid; top: 100%;
58
    }
59
    .arrow:after {
60
      border-top: #FFF  10px solid; bottom: 1px;
61
    }
62
  }
63
  &-bottom {
64
    .arrow {
65
      border-bottom: @border-color 10px solid; bottom: 100%;
66
    }
67
    .arrow:after {
68
      border-bottom: #FFF  10px solid; top: 1px;
69
    }
70
  }
71
}
72

  
73

  
74
.form-group {
75
  .clearfix; padding-bottom: 8px;
76
  &>label {
77
    float: left; width: 4em; text-align: right; padding-right: 5px;
78
  }
79
  &>input, &>.input-wrapper {
80
    margin-left: 4em;
81
    display: block;
82
  }
83
}
84

  
85

  
86
.btn-delete {
87
  margin-top: 5px;
88
  display: none;
89
  .text-danger;
90
  &:hover {
91
    text-decoration: underline;
92
  }
93
}
94

  
95
.usage { margin-top: 10px; }
combo/apps/chrono/static/chrono/chrono.js
1
$(function() { // document ready
2
  var calendar = $('#calendar').fullCalendar({
3
    header: {
4
      left: 'prev,next today',
5
      center: 'title',
6
      right: 'agendaWeek,agendaDay'
7
    },
8
    // slotDuration: chrono.slot_duration,
9
    defaultView: 'agendaWeek',
10
    weekends: false,
11
    defaultTimedEventDuration: '02:00',
12
    allDaySlot: false,
13
    scrollTime: '08:00',
14
    businessHours: {
15
      start: chrono.business_hours_start,
16
      end: chrono.business_hours_end,
17
    },
18
    events: events_source_url,
19
    eventOverlap: function(stillEvent, movingEvent) {
20
      return true;
21
    },
22
    editable: true,
23
    selectable: true,
24
    selectHelper: true,
25
    select: function(start, end) {
26
      if (start.isBefore(moment())){
27
          $('#calendar').fullCalendar('unselect');
28
          return false;
29
      }
30
      var title = chrono.event_default_title;
31
      var eventData;
32
      if (title && title.trim()) {
33
        eventData = {
34
          title: title,
35
          start: start,
36
          end: end
37
        };
38
        calendar.fullCalendar('renderEvent', eventData);
39
      }
40
      calendar.fullCalendar('unselect');
41
    },
42
    eventRender: function(event, element) {
43
      var start = moment(event.start).fromNow();
44
      element.attr('title', start);
45
    },
46
    loading: function() {
47
    },
48
    eventClick: function(calEvent, jsEvent, view){
49
        if (calEvent.source){
50
            console.log(calEvent.source);
51
            return false;
52
        }
53
        params = {
54
            start: calEvent.start.format(),
55
            end: calEvent.end.format()
56
        }
57
        form_url = events_booking_url + '?' + $.param(params)
58
        $.ajax({
59
            type: "POST",
60
            url: events_booking_url,
61
            data: JSON.stringify({
62
                'start': calEvent.start.format(),
63
                'end': calEvent.end.format()
64
            }),
65
            dataType: 'json',
66
            success: function(response){
67
                console.log(response.url);
68
                window.location = response.url;
69
            },
70
            error: function(xhr, status, error){
71
                console.log(JSON.stringify(status));
72
            }
73
        });
74
    }
75
  });
76
});
combo/apps/chrono/static/chrono/demos/agenda-views.html
1
<!DOCTYPE html>
2
<html>
3
<head>
4
<meta charset='utf-8' />
5
<link href='../fullcalendar.min.css' rel='stylesheet' />
6
<link href='../fullcalendar.print.min.css' rel='stylesheet' media='print' />
7
<script src='../lib/moment.min.js'></script>
8
<script src='../lib/jquery.min.js'></script>
9
<script src='../fullcalendar.min.js'></script>
10
<script>
11

  
12
	$(document).ready(function() {
13
		
14
		$('#calendar').fullCalendar({
15
			header: {
16
				left: 'prev,next today',
17
				center: 'title',
18
				right: 'month,agendaWeek,agendaDay,listWeek'
19
			},
20
			defaultDate: '2017-05-12',
21
			navLinks: true, // can click day/week names to navigate views
22
			editable: true,
23
			eventLimit: true, // allow "more" link when too many events
24
			events: [
25
				{
26
					title: 'All Day Event',
27
					start: '2017-05-01'
28
				},
29
				{
30
					title: 'Long Event',
31
					start: '2017-05-07',
32
					end: '2017-05-10'
33
				},
34
				{
35
					id: 999,
36
					title: 'Repeating Event',
37
					start: '2017-05-09T16:00:00'
38
				},
39
				{
40
					id: 999,
41
					title: 'Repeating Event',
42
					start: '2017-05-16T16:00:00'
43
				},
44
				{
45
					title: 'Conference',
46
					start: '2017-05-11',
47
					end: '2017-05-13'
48
				},
49
				{
50
					title: 'Meeting',
51
					start: '2017-05-12T10:30:00',
52
					end: '2017-05-12T12:30:00'
53
				},
54
				{
55
					title: 'Lunch',
56
					start: '2017-05-12T12:00:00'
57
				},
58
				{
59
					title: 'Meeting',
60
					start: '2017-05-12T14:30:00'
61
				},
62
				{
63
					title: 'Happy Hour',
64
					start: '2017-05-12T17:30:00'
65
				},
66
				{
67
					title: 'Dinner',
68
					start: '2017-05-12T20:00:00'
69
				},
70
				{
71
					title: 'Birthday Party',
72
					start: '2017-05-13T07:00:00'
73
				},
74
				{
75
					title: 'Click for Google',
76
					url: 'http://google.com/',
77
					start: '2017-05-28'
78
				}
79
			]
80
		});
81
		
82
	});
83

  
84
</script>
85
<style>
86

  
87
	body {
88
		margin: 40px 10px;
89
		padding: 0;
90
		font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif;
91
		font-size: 14px;
92
	}
93

  
94
	#calendar {
95
		max-width: 900px;
96
		margin: 0 auto;
97
	}
98

  
99
</style>
100
</head>
101
<body>
102

  
103
	<div id='calendar'></div>
104

  
105
</body>
106
</html>
combo/apps/chrono/static/chrono/demos/background-events.html
1
<!DOCTYPE html>
2
<html>
3
<head>
4
<meta charset='utf-8' />
5
<link href='../fullcalendar.min.css' rel='stylesheet' />
6
<link href='../fullcalendar.print.min.css' rel='stylesheet' media='print' />
7
<script src='../lib/moment.min.js'></script>
8
<script src='../lib/jquery.min.js'></script>
9
<script src='../fullcalendar.min.js'></script>
10
<script>
11

  
12
	$(document).ready(function() {
13

  
14
		$('#calendar').fullCalendar({
15
			header: {
16
				left: 'prev,next today',
17
				center: 'title',
18
				right: 'month,agendaWeek,agendaDay,listMonth'
19
			},
20
			defaultDate: '2017-05-12',
21
			navLinks: true, // can click day/week names to navigate views
22
			businessHours: true, // display business hours
23
			editable: true,
24
			events: [
25
				{
26
					title: 'Business Lunch',
27
					start: '2017-05-03T13:00:00',
28
					constraint: 'businessHours'
29
				},
30
				{
31
					title: 'Meeting',
32
					start: '2017-05-13T11:00:00',
33
					constraint: 'availableForMeeting', // defined below
34
					color: '#257e4a'
35
				},
36
				{
37
					title: 'Conference',
38
					start: '2017-05-18',
39
					end: '2017-05-20'
40
				},
41
				{
42
					title: 'Party',
43
					start: '2017-05-29T20:00:00'
44
				},
45

  
46
				// areas where "Meeting" must be dropped
47
				{
48
					id: 'availableForMeeting',
49
					start: '2017-05-11T10:00:00',
50
					end: '2017-05-11T16:00:00',
51
					rendering: 'background'
52
				},
53
				{
54
					id: 'availableForMeeting',
55
					start: '2017-05-13T10:00:00',
56
					end: '2017-05-13T16:00:00',
57
					rendering: 'background'
58
				},
59

  
60
				// red areas where no events can be dropped
61
				{
62
					start: '2017-05-24',
63
					end: '2017-05-28',
64
					overlap: false,
65
					rendering: 'background',
66
					color: '#ff9f89'
67
				},
68
				{
69
					start: '2017-05-06',
70
					end: '2017-05-08',
71
					overlap: false,
72
					rendering: 'background',
73
					color: '#ff9f89'
74
				}
75
			]
76
		});
77
		
78
	});
79

  
80
</script>
81
<style>
82

  
83
	body {
84
		margin: 40px 10px;
85
		padding: 0;
86
		font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif;
87
		font-size: 14px;
88
	}
89

  
90
	#calendar {
91
		max-width: 900px;
92
		margin: 0 auto;
93
	}
94

  
95
</style>
96
</head>
97
<body>
98

  
99
	<div id='calendar'></div>
100

  
101
</body>
102
</html>
combo/apps/chrono/static/chrono/demos/basic-views.html
1
<!DOCTYPE html>
2
<html>
3
<head>
4
<meta charset='utf-8' />
5
<link href='../fullcalendar.min.css' rel='stylesheet' />
6
<link href='../fullcalendar.print.min.css' rel='stylesheet' media='print' />
7
<script src='../lib/moment.min.js'></script>
8
<script src='../lib/jquery.min.js'></script>
9
<script src='../fullcalendar.min.js'></script>
10
<script>
11

  
12
	$(document).ready(function() {
13
		
14
		$('#calendar').fullCalendar({
15
			header: {
16
				left: 'prev,next today',
17
				center: 'title',
18
				right: 'month,basicWeek,basicDay'
19
			},
20
			defaultDate: '2017-05-12',
21
			navLinks: true, // can click day/week names to navigate views
22
			editable: true,
23
			eventLimit: true, // allow "more" link when too many events
24
			events: [
25
				{
26
					title: 'All Day Event',
27
					start: '2017-05-01'
28
				},
29
				{
30
					title: 'Long Event',
31
					start: '2017-05-07',
32
					end: '2017-05-10'
33
				},
34
				{
35
					id: 999,
36
					title: 'Repeating Event',
37
					start: '2017-05-09T16:00:00'
38
				},
39
				{
40
					id: 999,
41
					title: 'Repeating Event',
42
					start: '2017-05-16T16:00:00'
43
				},
44
				{
45
					title: 'Conference',
46
					start: '2017-05-11',
47
					end: '2017-05-13'
48
				},
49
				{
50
					title: 'Meeting',
51
					start: '2017-05-12T10:30:00',
52
					end: '2017-05-12T12:30:00'
53
				},
54
				{
55
					title: 'Lunch',
56
					start: '2017-05-12T12:00:00'
57
				},
58
				{
59
					title: 'Meeting',
60
					start: '2017-05-12T14:30:00'
61
				},
62
				{
63
					title: 'Happy Hour',
64
					start: '2017-05-12T17:30:00'
65
				},
66
				{
67
					title: 'Dinner',
68
					start: '2017-05-12T20:00:00'
69
				},
70
				{
71
					title: 'Birthday Party',
72
					start: '2017-05-13T07:00:00'
73
				},
74
				{
75
					title: 'Click for Google',
76
					url: 'http://google.com/',
77
					start: '2017-05-28'
78
				}
79
			]
80
		});
81
		
82
	});
83

  
84
</script>
85
<style>
86

  
87
	body {
88
		margin: 40px 10px;
89
		padding: 0;
90
		font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif;
91
		font-size: 14px;
92
	}
93

  
94
	#calendar {
95
		max-width: 900px;
96
		margin: 0 auto;
97
	}
98

  
99
</style>
100
</head>
101
<body>
102

  
103
	<div id='calendar'></div>
104

  
105
</body>
106
</html>
combo/apps/chrono/static/chrono/demos/default.html
1
<!DOCTYPE html>
2
<html>
3
<head>
4
<meta charset='utf-8' />
5
<link href='../fullcalendar.min.css' rel='stylesheet' />
6
<link href='../fullcalendar.print.min.css' rel='stylesheet' media='print' />
7
<script src='../lib/moment.min.js'></script>
8
<script src='../lib/jquery.min.js'></script>
9
<script src='../fullcalendar.min.js'></script>
10
<script>
11

  
12
	$(document).ready(function() {
13

  
14
		$('#calendar').fullCalendar({
15
			defaultDate: '2017-05-12',
16
			editable: true,
17
			eventLimit: true, // allow "more" link when too many events
18
			events: [
19
				{
20
					title: 'All Day Event',
21
					start: '2017-05-01'
22
				},
23
				{
24
					title: 'Long Event',
25
					start: '2017-05-07',
26
					end: '2017-05-10'
27
				},
28
				{
29
					id: 999,
30
					title: 'Repeating Event',
31
					start: '2017-05-09T16:00:00'
32
				},
33
				{
34
					id: 999,
35
					title: 'Repeating Event',
36
					start: '2017-05-16T16:00:00'
37
				},
38
				{
39
					title: 'Conference',
40
					start: '2017-05-11',
41
					end: '2017-05-13'
42
				},
43
				{
44
					title: 'Meeting',
45
					start: '2017-05-12T10:30:00',
46
					end: '2017-05-12T12:30:00'
47
				},
48
				{
49
					title: 'Lunch',
50
					start: '2017-05-12T12:00:00'
51
				},
52
				{
53
					title: 'Meeting',
54
					start: '2017-05-12T14:30:00'
55
				},
56
				{
57
					title: 'Happy Hour',
58
					start: '2017-05-12T17:30:00'
59
				},
60
				{
61
					title: 'Dinner',
62
					start: '2017-05-12T20:00:00'
63
				},
64
				{
65
					title: 'Birthday Party',
66
					start: '2017-05-13T07:00:00'
67
				},
68
				{
69
					title: 'Click for Google',
70
					url: 'http://google.com/',
71
					start: '2017-05-28'
72
				}
73
			]
74
		});
75
		
76
	});
77

  
78
</script>
79
<style>
80

  
81
	body {
82
		margin: 40px 10px;
83
		padding: 0;
84
		font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif;
85
		font-size: 14px;
86
	}
87

  
88
	#calendar {
89
		max-width: 900px;
90
		margin: 0 auto;
91
	}
92

  
93
</style>
94
</head>
95
<body>
96

  
97
	<div id='calendar'></div>
98

  
99
</body>
100
</html>
combo/apps/chrono/static/chrono/demos/external-dragging.html
1
<!DOCTYPE html>
2
<html>
3
<head>
4
<meta charset='utf-8' />
5
<link href='../fullcalendar.min.css' rel='stylesheet' />
6
<link href='../fullcalendar.print.min.css' rel='stylesheet' media='print' />
7
<script src='../lib/moment.min.js'></script>
8
<script src='../lib/jquery.min.js'></script>
9
<script src='../lib/jquery-ui.min.js'></script>
10
<script src='../fullcalendar.min.js'></script>
11
<script>
12

  
13
	$(document).ready(function() {
14

  
15

  
16
		/* initialize the external events
17
		-----------------------------------------------------------------*/
18

  
19
		$('#external-events .fc-event').each(function() {
20

  
21
			// store data so the calendar knows to render an event upon drop
22
			$(this).data('event', {
23
				title: $.trim($(this).text()), // use the element's text as the event title
24
				stick: true // maintain when user navigates (see docs on the renderEvent method)
25
			});
26

  
27
			// make the event draggable using jQuery UI
28
			$(this).draggable({
29
				zIndex: 999,
30
				revert: true,      // will cause the event to go back to its
31
				revertDuration: 0  //  original position after the drag
32
			});
33

  
34
		});
35

  
36

  
37
		/* initialize the calendar
38
		-----------------------------------------------------------------*/
39

  
40
		$('#calendar').fullCalendar({
41
			header: {
42
				left: 'prev,next today',
43
				center: 'title',
44
				right: 'month,agendaWeek,agendaDay'
45
			},
46
			editable: true,
47
			droppable: true, // this allows things to be dropped onto the calendar
48
			drop: function() {
49
				// is the "remove after drop" checkbox checked?
50
				if ($('#drop-remove').is(':checked')) {
51
					// if so, remove the element from the "Draggable Events" list
52
					$(this).remove();
53
				}
54
			}
55
		});
56

  
57

  
58
	});
59

  
60
</script>
61
<style>
62

  
63
	body {
64
		margin-top: 40px;
65
		text-align: center;
66
		font-size: 14px;
67
		font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif;
68
	}
69
		
70
	#wrap {
71
		width: 1100px;
72
		margin: 0 auto;
73
	}
74
		
75
	#external-events {
76
		float: left;
77
		width: 150px;
78
		padding: 0 10px;
79
		border: 1px solid #ccc;
80
		background: #eee;
81
		text-align: left;
82
	}
83
		
84
	#external-events h4 {
85
		font-size: 16px;
86
		margin-top: 0;
87
		padding-top: 1em;
88
	}
89
		
90
	#external-events .fc-event {
91
		margin: 10px 0;
92
		cursor: pointer;
93
	}
94
		
95
	#external-events p {
96
		margin: 1.5em 0;
97
		font-size: 11px;
98
		color: #666;
99
	}
100
		
101
	#external-events p input {
102
		margin: 0;
103
		vertical-align: middle;
104
	}
105

  
106
	#calendar {
107
		float: right;
108
		width: 900px;
109
	}
110

  
111
</style>
112
</head>
113
<body>
114
	<div id='wrap'>
115

  
116
		<div id='external-events'>
117
			<h4>Draggable Events</h4>
118
			<div class='fc-event'>My Event 1</div>
119
			<div class='fc-event'>My Event 2</div>
120
			<div class='fc-event'>My Event 3</div>
121
			<div class='fc-event'>My Event 4</div>
122
			<div class='fc-event'>My Event 5</div>
123
			<p>
124
				<input type='checkbox' id='drop-remove' />
125
				<label for='drop-remove'>remove after drop</label>
126
			</p>
127
		</div>
128

  
129
		<div id='calendar'></div>
130

  
131
		<div style='clear:both'></div>
132

  
133
	</div>
134
</body>
135
</html>
combo/apps/chrono/static/chrono/demos/gcal.html
1
<!DOCTYPE html>
2
<html>
3
<head>
4
<meta charset='utf-8' />
5
<link href='../fullcalendar.min.css' rel='stylesheet' />
6
<link href='../fullcalendar.print.min.css' rel='stylesheet' media='print' />
7
<script src='../lib/moment.min.js'></script>
8
<script src='../lib/jquery.min.js'></script>
9
<script src='../fullcalendar.min.js'></script>
10
<script src='../gcal.min.js'></script>
11
<script>
12

  
13
	$(document).ready(function() {
14
	
15
		$('#calendar').fullCalendar({
16

  
17
			header: {
18
				left: 'prev,next today',
19
				center: 'title',
20
				right: 'month,listYear'
21
			},
22

  
23
			displayEventTime: false, // don't show the time column in list view
24

  
25
			// THIS KEY WON'T WORK IN PRODUCTION!!!
26
			// To make your own Google API key, follow the directions here:
27
			// http://fullcalendar.io/docs/google_calendar/
28
			googleCalendarApiKey: 'AIzaSyDcnW6WejpTOCffshGDDb4neIrXVUA1EAE',
29
		
30
			// US Holidays
31
			events: 'en.usa#holiday@group.v.calendar.google.com',
32
			
33
			eventClick: function(event) {
34
				// opens events in a popup window
35
				window.open(event.url, 'gcalevent', 'width=700,height=600');
36
				return false;
37
			},
38
			
39
			loading: function(bool) {
40
				$('#loading').toggle(bool);
41
			}
42
			
43
		});
44
		
45
	});
46

  
47
</script>
48
<style>
49

  
50
	body {
51
		margin: 40px 10px;
52
		padding: 0;
53
		font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif;
54
		font-size: 14px;
55
	}
56
		
57
	#loading {
58
		display: none;
59
		position: absolute;
60
		top: 10px;
61
		right: 10px;
62
	}
63

  
64
	#calendar {
65
		max-width: 900px;
66
		margin: 0 auto;
67
	}
68

  
69
</style>
70
</head>
71
<body>
72

  
73
	<div id='loading'>loading...</div>
74

  
75
	<div id='calendar'></div>
76

  
77
</body>
78
</html>
combo/apps/chrono/static/chrono/demos/json.html
1
<!DOCTYPE html>
2
<html>
3
<head>
4
<meta charset='utf-8' />
5
<link href='../fullcalendar.min.css' rel='stylesheet' />
6
<link href='../fullcalendar.print.min.css' rel='stylesheet' media='print' />
7
<script src='../lib/moment.min.js'></script>
8
<script src='../lib/jquery.min.js'></script>
9
<script src='../fullcalendar.min.js'></script>
10
<script>
11

  
12
	$(document).ready(function() {
13
	
14
		$('#calendar').fullCalendar({
15
			header: {
16
				left: 'prev,next today',
17
				center: 'title',
18
				right: 'month,agendaWeek,agendaDay,listWeek'
19
			},
20
			defaultDate: '2017-05-12',
21
			editable: true,
22
			navLinks: true, // can click day/week names to navigate views
23
			eventLimit: true, // allow "more" link when too many events
24
			events: {
25
				url: 'php/get-events.php',
26
				error: function() {
27
					$('#script-warning').show();
28
				}
29
			},
30
			loading: function(bool) {
31
				$('#loading').toggle(bool);
32
			}
33
		});
34
		
35
	});
36

  
37
</script>
38
<style>
39

  
40
	body {
41
		margin: 0;
42
		padding: 0;
43
		font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif;
44
		font-size: 14px;
45
	}
46

  
47
	#script-warning {
48
		display: none;
49
		background: #eee;
50
		border-bottom: 1px solid #ddd;
51
		padding: 0 10px;
52
		line-height: 40px;
53
		text-align: center;
54
		font-weight: bold;
55
		font-size: 12px;
56
		color: red;
57
	}
58

  
59
	#loading {
60
		display: none;
61
		position: absolute;
62
		top: 10px;
63
		right: 10px;
64
	}
65

  
66
	#calendar {
67
		max-width: 900px;
68
		margin: 40px auto;
69
		padding: 0 10px;
70
	}
71

  
72
</style>
73
</head>
74
<body>
75

  
76
	<div id='script-warning'>
77
		<code>php/get-events.php</code> must be running.
78
	</div>
79

  
80
	<div id='loading'>loading...</div>
81

  
82
	<div id='calendar'></div>
83

  
84
</body>
85
</html>
combo/apps/chrono/static/chrono/demos/json/events.json
1
[
2
  {
3
    "title": "All Day Event",
4
    "start": "2017-05-01"
5
  },
6
  {
7
    "title": "Long Event",
8
    "start": "2017-05-07",
9
    "end": "2017-05-10"
10
  },
11
  {
12
    "id": "999",
13
    "title": "Repeating Event",
14
    "start": "2017-05-09T16:00:00-05:00"
15
  },
16
  {
17
    "id": "999",
18
    "title": "Repeating Event",
19
    "start": "2017-05-16T16:00:00-05:00"
20
  },
21
  {
22
    "title": "Conference",
23
    "start": "2017-05-11",
24
    "end": "2017-05-13"
25
  },
26
  {
27
    "title": "Meeting",
28
    "start": "2017-05-12T10:30:00-05:00",
29
    "end": "2017-05-12T12:30:00-05:00"
30
  },
31
  {
32
    "title": "Lunch",
33
    "start": "2017-05-12T12:00:00-05:00"
34
  },
35
  {
36
    "title": "Meeting",
37
    "start": "2017-05-12T14:30:00-05:00"
38
  },
39
  {
40
    "title": "Happy Hour",
41
    "start": "2017-05-12T17:30:00-05:00"
42
  },
43
  {
44
    "title": "Dinner",
45
    "start": "2017-05-12T20:00:00"
46
  },
47
  {
48
    "title": "Birthday Party",
49
    "start": "2017-05-13T07:00:00-05:00"
50
  },
51
  {
52
    "title": "Click for Google",
53
    "url": "http://google.com/",
54
    "start": "2017-05-28"
55
  }
56
]
combo/apps/chrono/static/chrono/demos/list-views.html
1
<!DOCTYPE html>
2
<html>
3
<head>
4
<meta charset='utf-8' />
5
<link href='../fullcalendar.min.css' rel='stylesheet' />
6
<link href='../fullcalendar.print.min.css' rel='stylesheet' media='print' />
7
<script src='../lib/moment.min.js'></script>
8
<script src='../lib/jquery.min.js'></script>
9
<script src='../fullcalendar.min.js'></script>
10
<script>
11

  
12
	$(document).ready(function() {
13
		
14
		$('#calendar').fullCalendar({
15
			header: {
16
				left: 'prev,next today',
17
				center: 'title',
18
				right: 'listDay,listWeek,month'
19
			},
20

  
21
			// customize the button names,
22
			// otherwise they'd all just say "list"
23
			views: {
24
				listDay: { buttonText: 'list day' },
25
				listWeek: { buttonText: 'list week' }
26
			},
27

  
28
			defaultView: 'listWeek',
29
			defaultDate: '2017-05-12',
30
			navLinks: true, // can click day/week names to navigate views
31
			editable: true,
32
			eventLimit: true, // allow "more" link when too many events
33
			events: [
34
				{
35
					title: 'All Day Event',
36
					start: '2017-05-01'
37
				},
38
				{
39
					title: 'Long Event',
40
					start: '2017-05-07',
41
					end: '2017-05-10'
42
				},
43
				{
44
					id: 999,
45
					title: 'Repeating Event',
46
					start: '2017-05-09T16:00:00'
47
				},
48
				{
49
					id: 999,
50
					title: 'Repeating Event',
51
					start: '2017-05-16T16:00:00'
52
				},
53
				{
54
					title: 'Conference',
55
					start: '2017-05-11',
56
					end: '2017-05-13'
57
				},
58
				{
59
					title: 'Meeting',
60
					start: '2017-05-12T10:30:00',
61
					end: '2017-05-12T12:30:00'
62
				},
63
				{
64
					title: 'Lunch',
65
					start: '2017-05-12T12:00:00'
66
				},
67
				{
68
					title: 'Meeting',
69
					start: '2017-05-12T14:30:00'
70
				},
71
				{
72
					title: 'Happy Hour',
73
					start: '2017-05-12T17:30:00'
74
				},
75
				{
76
					title: 'Dinner',
77
					start: '2017-05-12T20:00:00'
78
				},
79
				{
80
					title: 'Birthday Party',
81
					start: '2017-05-13T07:00:00'
82
				},
83
				{
84
					title: 'Click for Google',
85
					url: 'http://google.com/',
86
					start: '2017-05-28'
87
				}
88
			]
89
		});
90
		
91
	});
92

  
93
</script>
94
<style>
95

  
96
	body {
97
		margin: 40px 10px;
98
		padding: 0;
99
		font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif;
100
		font-size: 14px;
101
	}
102

  
103
	#calendar {
104
		max-width: 900px;
105
		margin: 0 auto;
106
	}
107

  
108
</style>
109
</head>
110
<body>
111

  
112
	<div id='calendar'></div>
113

  
114
</body>
115
</html>
combo/apps/chrono/static/chrono/demos/locales.html
1
<!DOCTYPE html>
2
<html>
3
<head>
4
<meta charset='utf-8' />
5
<link href='../fullcalendar.min.css' rel='stylesheet' />
6
<link href='../fullcalendar.print.min.css' rel='stylesheet' media='print' />
7
<script src='../lib/moment.min.js'></script>
8
<script src='../lib/jquery.min.js'></script>
9
<script src='../fullcalendar.min.js'></script>
10
<script src='../locale-all.js'></script>
11
<script>
12

  
13
	$(document).ready(function() {
14
		var initialLocaleCode = 'en';
15

  
16
		$('#calendar').fullCalendar({
17
			header: {
18
				left: 'prev,next today',
19
				center: 'title',
20
				right: 'month,agendaWeek,agendaDay,listMonth'
21
			},
22
			defaultDate: '2017-05-12',
23
			locale: initialLocaleCode,
24
			buttonIcons: false, // show the prev/next text
25
			weekNumbers: true,
26
			navLinks: true, // can click day/week names to navigate views
27
			editable: true,
28
			eventLimit: true, // allow "more" link when too many events
29
			events: [
30
				{
31
					title: 'All Day Event',
32
					start: '2017-05-01'
33
				},
34
				{
35
					title: 'Long Event',
36
					start: '2017-05-07',
37
					end: '2017-05-10'
38
				},
39
				{
40
					id: 999,
41
					title: 'Repeating Event',
42
					start: '2017-05-09T16:00:00'
43
				},
44
				{
45
					id: 999,
46
					title: 'Repeating Event',
47
					start: '2017-05-16T16:00:00'
48
				},
49
				{
50
					title: 'Conference',
51
					start: '2017-05-11',
52
					end: '2017-05-13'
53
				},
54
				{
55
					title: 'Meeting',
56
					start: '2017-05-12T10:30:00',
57
					end: '2017-05-12T12:30:00'
58
				},
59
				{
60
					title: 'Lunch',
61
					start: '2017-05-12T12:00:00'
62
				},
63
				{
64
					title: 'Meeting',
65
					start: '2017-05-12T14:30:00'
66
				},
67
				{
68
					title: 'Happy Hour',
69
					start: '2017-05-12T17:30:00'
70
				},
71
				{
72
					title: 'Dinner',
73
					start: '2017-05-12T20:00:00'
74
				},
75
				{
76
					title: 'Birthday Party',
77
					start: '2017-05-13T07:00:00'
78
				},
79
				{
80
					title: 'Click for Google',
81
					url: 'http://google.com/',
82
					start: '2017-05-28'
83
				}
84
			]
85
		});
86

  
87
		// build the locale selector's options
88
		$.each($.fullCalendar.locales, function(localeCode) {
89
			$('#locale-selector').append(
90
				$('<option/>')
91
					.attr('value', localeCode)
92
					.prop('selected', localeCode == initialLocaleCode)
93
					.text(localeCode)
94
			);
95
		});
96

  
97
		// when the selected option changes, dynamically change the calendar option
98
		$('#locale-selector').on('change', function() {
99
			if (this.value) {
100
				$('#calendar').fullCalendar('option', 'locale', this.value);
101
			}
102
		});
103
	});
104

  
105
</script>
106
<style>
107

  
108
	body {
109
		margin: 0;
110
		padding: 0;
111
		font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif;
112
		font-size: 14px;
113
	}
114

  
115
	#top {
116
		background: #eee;
117
		border-bottom: 1px solid #ddd;
118
		padding: 0 10px;
119
		line-height: 40px;
120
		font-size: 12px;
121
	}
122

  
123
	#calendar {
124
		max-width: 900px;
125
		margin: 40px auto;
126
		padding: 0 10px;
127
	}
128

  
129
</style>
130
</head>
131
<body>
132

  
133
	<div id='top'>
134

  
135
		Locales:
136
		<select id='locale-selector'></select>
137

  
138
	</div>
139

  
140
	<div id='calendar'></div>
141

  
142
</body>
143
</html>
combo/apps/chrono/static/chrono/demos/php/get-events.php
1
<?php
2

  
3
//--------------------------------------------------------------------------------------------------
4
// This script reads event data from a JSON file and outputs those events which are within the range
5
// supplied by the "start" and "end" GET parameters.
6
//
7
// An optional "timezone" GET parameter will force all ISO8601 date stings to a given timezone.
8
//
9
// Requires PHP 5.2.0 or higher.
10
//--------------------------------------------------------------------------------------------------
11

  
12
// Require our Event class and datetime utilities
13
require dirname(__FILE__) . '/utils.php';
14

  
15
// Short-circuit if the client did not give us a date range.
16
if (!isset($_GET['start']) || !isset($_GET['end'])) {
17
	die("Please provide a date range.");
18
}
19

  
20
// Parse the start/end parameters.
21
// These are assumed to be ISO8601 strings with no time nor timezone, like "2013-12-29".
22
// Since no timezone will be present, they will parsed as UTC.
23
$range_start = parseDateTime($_GET['start']);
24
$range_end = parseDateTime($_GET['end']);
25

  
26
// Parse the timezone parameter if it is present.
27
$timezone = null;
28
if (isset($_GET['timezone'])) {
29
	$timezone = new DateTimeZone($_GET['timezone']);
30
}
31

  
32
// Read and parse our events JSON file into an array of event data arrays.
33
$json = file_get_contents(dirname(__FILE__) . '/../json/events.json');
34
$input_arrays = json_decode($json, true);
35

  
36
// Accumulate an output array of event data arrays.
37
$output_arrays = array();
38
foreach ($input_arrays as $array) {
39

  
40
	// Convert the input array into a useful Event object
41
	$event = new Event($array, $timezone);
42

  
43
	// If the event is in-bounds, add it to the output
44
	if ($event->isWithinDayRange($range_start, $range_end)) {
45
		$output_arrays[] = $event->toArray();
46
	}
47
}
48

  
49
// Send JSON to the client.
50
echo json_encode($output_arrays);
combo/apps/chrono/static/chrono/demos/php/get-timezones.php
1
<?php
2

  
3
//--------------------------------------------------------------------------------------------------
4
// This script outputs a JSON array of all timezones (like "America/Chicago") that PHP supports.
5
//
6
// Requires PHP 5.2.0 or higher.
7
//--------------------------------------------------------------------------------------------------
8

  
9
echo json_encode(DateTimeZone::listIdentifiers());
combo/apps/chrono/static/chrono/demos/php/utils.php
1
<?php
2

  
3
//--------------------------------------------------------------------------------------------------
4
// Utilities for our event-fetching scripts.
5
//
6
// Requires PHP 5.2.0 or higher.
7
//--------------------------------------------------------------------------------------------------
8

  
9
// PHP will fatal error if we attempt to use the DateTime class without this being set.
10
date_default_timezone_set('UTC');
11

  
12

  
13
class Event {
14

  
15
	// Tests whether the given ISO8601 string has a time-of-day or not
16
	const ALL_DAY_REGEX = '/^\d{4}-\d\d-\d\d$/'; // matches strings like "2013-12-29"
17

  
18
	public $title;
19
	public $allDay; // a boolean
20
	public $start; // a DateTime
21
	public $end; // a DateTime, or null
22
	public $properties = array(); // an array of other misc properties
23

  
24

  
25
	// Constructs an Event object from the given array of key=>values.
26
	// You can optionally force the timezone of the parsed dates.
27
	public function __construct($array, $timezone=null) {
28

  
29
		$this->title = $array['title'];
30

  
31
		if (isset($array['allDay'])) {
32
			// allDay has been explicitly specified
33
			$this->allDay = (bool)$array['allDay'];
34
		}
35
		else {
36
			// Guess allDay based off of ISO8601 date strings
37
			$this->allDay = preg_match(self::ALL_DAY_REGEX, $array['start']) &&
38
				(!isset($array['end']) || preg_match(self::ALL_DAY_REGEX, $array['end']));
39
		}
40

  
41
		if ($this->allDay) {
42
			// If dates are allDay, we want to parse them in UTC to avoid DST issues.
43
			$timezone = null;
44
		}
45

  
46
		// Parse dates
47
		$this->start = parseDateTime($array['start'], $timezone);
48
		$this->end = isset($array['end']) ? parseDateTime($array['end'], $timezone) : null;
49

  
50
		// Record misc properties
51
		foreach ($array as $name => $value) {
52
			if (!in_array($name, array('title', 'allDay', 'start', 'end'))) {
53
				$this->properties[$name] = $value;
54
			}
55
		}
56
	}
57

  
58

  
59
	// Returns whether the date range of our event intersects with the given all-day range.
60
	// $rangeStart and $rangeEnd are assumed to be dates in UTC with 00:00:00 time.
61
	public function isWithinDayRange($rangeStart, $rangeEnd) {
62

  
63
		// Normalize our event's dates for comparison with the all-day range.
64
		$eventStart = stripTime($this->start);
65

  
66
		if (isset($this->end)) {
67
			$eventEnd = stripTime($this->end); // normalize
68
		}
69
		else {
70
			$eventEnd = $eventStart; // consider this a zero-duration event
71
		}
72

  
73
		// Check if the two whole-day ranges intersect.
74
		return $eventStart < $rangeEnd && $eventEnd >= $rangeStart;
75
	}
76

  
77

  
78
	// Converts this Event object back to a plain data array, to be used for generating JSON
79
	public function toArray() {
80

  
81
		// Start with the misc properties (don't worry, PHP won't affect the original array)
82
		$array = $this->properties;
83

  
84
		$array['title'] = $this->title;
85

  
86
		// Figure out the date format. This essentially encodes allDay into the date string.
87
		if ($this->allDay) {
88
			$format = 'Y-m-d'; // output like "2013-12-29"
89
		}
90
		else {
91
			$format = 'c'; // full ISO8601 output, like "2013-12-29T09:00:00+08:00"
92
		}
93

  
94
		// Serialize dates into strings
95
		$array['start'] = $this->start->format($format);
96
		if (isset($this->end)) {
97
			$array['end'] = $this->end->format($format);
98
		}
99

  
100
		return $array;
101
	}
102

  
103
}
104

  
105

  
106
// Date Utilities
107
//----------------------------------------------------------------------------------------------
108

  
109

  
110
// Parses a string into a DateTime object, optionally forced into the given timezone.
111
function parseDateTime($string, $timezone=null) {
112
	$date = new DateTime(
113
		$string,
114
		$timezone ? $timezone : new DateTimeZone('UTC')
115
			// Used only when the string is ambiguous.
116
			// Ignored if string has a timezone offset in it.
117
	);
118
	if ($timezone) {
119
		// If our timezone was ignored above, force it.
120
		$date->setTimezone($timezone);
121
	}
122
	return $date;
123
}
124

  
125

  
126
// Takes the year/month/date values of the given DateTime and converts them to a new DateTime,
127
// but in UTC.
128
function stripTime($datetime) {
129
	return new DateTime($datetime->format('Y-m-d'));
130
}
combo/apps/chrono/static/chrono/demos/selectable.html
1
<!DOCTYPE html>
2
<html>
3
<head>
4
<meta charset='utf-8' />
5
<link href='../fullcalendar.min.css' rel='stylesheet' />
6
<link href='../fullcalendar.print.min.css' rel='stylesheet' media='print' />
7
<script src='../lib/moment.min.js'></script>
8
<script src='../lib/jquery.min.js'></script>
9
<script src='../fullcalendar.min.js'></script>
10
<script>
11

  
12
	$(document).ready(function() {
13
		
14
		$('#calendar').fullCalendar({
15
			header: {
16
				left: 'prev,next today',
17
				center: 'title',
18
				right: 'month,agendaWeek,agendaDay'
19
			},
20
			defaultDate: '2017-05-12',
21
			navLinks: true, // can click day/week names to navigate views
22
			selectable: true,
23
			selectHelper: true,
24
			select: function(start, end) {
25
				var title = prompt('Event Title:');
26
				var eventData;
27
				if (title) {
28
					eventData = {
29
						title: title,
30
						start: start,
31
						end: end
32
					};
33
					$('#calendar').fullCalendar('renderEvent', eventData, true); // stick? = true
34
				}
35
				$('#calendar').fullCalendar('unselect');
36
			},
37
			editable: true,
38
			eventLimit: true, // allow "more" link when too many events
39
			events: [
40
				{
41
					title: 'All Day Event',
42
					start: '2017-05-01'
43
				},
44
				{
45
					title: 'Long Event',
46
					start: '2017-05-07',
47
					end: '2017-05-10'
48
				},
49
				{
50
					id: 999,
51
					title: 'Repeating Event',
52
					start: '2017-05-09T16:00:00'
53
				},
54
				{
55
					id: 999,
56
					title: 'Repeating Event',
57
					start: '2017-05-16T16:00:00'
58
				},
59
				{
60
					title: 'Conference',
61
					start: '2017-05-11',
62
					end: '2017-05-13'
63
				},
64
				{
65
					title: 'Meeting',
66
					start: '2017-05-12T10:30:00',
67
					end: '2017-05-12T12:30:00'
68
				},
69
				{
70
					title: 'Lunch',
71
					start: '2017-05-12T12:00:00'
72
				},
73
				{
74
					title: 'Meeting',
75
					start: '2017-05-12T14:30:00'
76
				},
77
				{
78
					title: 'Happy Hour',
79
					start: '2017-05-12T17:30:00'
80
				},
81
				{
82
					title: 'Dinner',
83
					start: '2017-05-12T20:00:00'
84
				},
85
				{
86
					title: 'Birthday Party',
87
					start: '2017-05-13T07:00:00'
88
				},
89
				{
90
					title: 'Click for Google',
91
					url: 'http://google.com/',
92
					start: '2017-05-28'
93
				}
94
			]
95
		});
96
		
97
	});
98

  
99
</script>
100
<style>
101

  
102
	body {
103
		margin: 40px 10px;
104
		padding: 0;
105
		font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif;
106
		font-size: 14px;
107
	}
108

  
109
	#calendar {
110
		max-width: 900px;
111
		margin: 0 auto;
112
	}
113

  
114
</style>
115
</head>
116
<body>
117

  
118
	<div id='calendar'></div>
119

  
120
</body>
121
</html>
combo/apps/chrono/static/chrono/demos/theme.html
1
<!DOCTYPE html>
2
<html>
3
<head>
4
<meta charset='utf-8' />
5
<link rel='stylesheet' href='../lib/cupertino/jquery-ui.min.css' />
6
<link href='../fullcalendar.min.css' rel='stylesheet' />
7
<link href='../fullcalendar.print.min.css' rel='stylesheet' media='print' />
8
<script src='../lib/moment.min.js'></script>
9
<script src='../lib/jquery.min.js'></script>
10
<script src='../fullcalendar.min.js'></script>
11
<script>
12

  
13
	$(document).ready(function() {
14

  
15
		$('#calendar').fullCalendar({
16
			theme: true,
17
			header: {
18
				left: 'prev,next today',
19
				center: 'title',
20
				right: 'month,agendaWeek,agendaDay,listMonth'
21
			},
22
			defaultDate: '2017-05-12',
23
			navLinks: true, // can click day/week names to navigate views
24
			editable: true,
25
			eventLimit: true, // allow "more" link when too many events
26
			events: [
27
				{
28
					title: 'All Day Event',
29
					start: '2017-05-01'
30
				},
31
				{
32
					title: 'Long Event',
33
					start: '2017-05-07',
34
					end: '2017-05-10'
35
				},
36
				{
37
					id: 999,
38
					title: 'Repeating Event',
39
					start: '2017-05-09T16:00:00'
40
				},
41
				{
42
					id: 999,
43
					title: 'Repeating Event',
44
					start: '2017-05-16T16:00:00'
45
				},
46
				{
47
					title: 'Conference',
48
					start: '2017-05-11',
49
					end: '2017-05-13'
50
				},
51
				{
52
					title: 'Meeting',
53
					start: '2017-05-12T10:30:00',
54
					end: '2017-05-12T12:30:00'
55
				},
56
				{
57
					title: 'Lunch',
58
					start: '2017-05-12T12:00:00'
59
				},
60
				{
61
					title: 'Meeting',
62
					start: '2017-05-12T14:30:00'
63
				},
64
				{
65
					title: 'Happy Hour',
66
					start: '2017-05-12T17:30:00'
67
				},
68
				{
69
					title: 'Dinner',
70
					start: '2017-05-12T20:00:00'
71
				},
72
				{
73
					title: 'Birthday Party',
74
					start: '2017-05-13T07:00:00'
75
				},
76
				{
77
					title: 'Click for Google',
78
					url: 'http://google.com/',
79
					start: '2017-05-28'
80
				}
81
			]
82
		});
83
		
84
	});
85

  
86
</script>
87
<style>
88

  
89
	body {
90
		margin: 40px 10px;
91
		padding: 0;
92
		font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif;
93
		font-size: 14px;
94
	}
95

  
96
	#calendar {
97
		max-width: 900px;
98
		margin: 0 auto;
99
	}
100

  
101
</style>
102
</head>
103
<body>
104

  
105
	<div id='calendar'></div>
106

  
107
</body>
108
</html>
combo/apps/chrono/static/chrono/demos/timezones.html
1
<!DOCTYPE html>
2
<html>
3
<head>
4
<meta charset='utf-8' />
5
<link href='../fullcalendar.min.css' rel='stylesheet' />
6
<link href='../fullcalendar.print.min.css' rel='stylesheet' media='print' />
7
<script src='../lib/moment.min.js'></script>
8
<script src='../lib/jquery.min.js'></script>
9
<script src='../fullcalendar.min.js'></script>
10
<script>
11

  
12
	$(document).ready(function() {
13

  
14
		$('#calendar').fullCalendar({
15
			header: {
16
				left: 'prev,next today',
17
				center: 'title',
18
				right: 'month,agendaWeek,agendaDay,listWeek'
19
			},
20
			defaultDate: '2017-05-12',
21
			navLinks: true, // can click day/week names to navigate views
22
			editable: true,
23
			selectable: true,
24
			eventLimit: true, // allow "more" link when too many events
25
			events: {
26
				url: 'php/get-events.php',
27
				error: function() {
28
					$('#script-warning').show();
29
				}
30
			},
31
			loading: function(bool) {
32
				$('#loading').toggle(bool);
33
			},
34
			eventRender: function(event, el) {
35
				// render the timezone offset below the event title
36
				if (event.start.hasZone()) {
37
					el.find('.fc-title').after(
38
						$('<div class="tzo"/>').text(event.start.format('Z'))
39
					);
40
				}
41
			},
42
			dayClick: function(date) {
43
				console.log('dayClick', date.format());
44
			},
45
			select: function(startDate, endDate) {
46
				console.log('select', startDate.format(), endDate.format());
47
			}
48
		});
49

  
50
		// load the list of available timezones, build the <select> options
51
		$.getJSON('php/get-timezones.php', function(timezones) {
52
			$.each(timezones, function(i, timezone) {
53
				if (timezone != 'UTC') { // UTC is already in the list
54
					$('#timezone-selector').append(
55
						$("<option/>").text(timezone).attr('value', timezone)
56
					);
57
				}
58
			});
59
		});
60

  
61
		// when the timezone selector changes, dynamically change the calendar option
62
		$('#timezone-selector').on('change', function() {
63
			$('#calendar').fullCalendar('option', 'timezone', this.value || false);
64
		});
65
	});
66

  
67
</script>
68
<style>
69

  
70
	body {
71
		margin: 0;
72
		padding: 0;
73
		font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif;
74
		font-size: 14px;
75
	}
76

  
77
	#top {
78
		background: #eee;
79
		border-bottom: 1px solid #ddd;
80
		padding: 0 10px;
81
		line-height: 40px;
82
		font-size: 12px;
83
	}
84
	.left { float: left }
85
	.right { float: right }
86
	.clear { clear: both }
87

  
88
	#script-warning, #loading { display: none }
89
	#script-warning { font-weight: bold; color: red }
90

  
91
	#calendar {
92
		max-width: 900px;
93
		margin: 40px auto;
94
		padding: 0 10px;
95
	}
96

  
97
	.tzo {
98
		color: #000;
99
	}
100

  
101
</style>
102
</head>
103
<body>
104

  
105
	<div id='top'>
106

  
107
		<div class='left'>
108
			Timezone:
109
			<select id='timezone-selector'>
110
				<option value='' selected>none</option>
111
				<option value='local'>local</option>
112
				<option value='UTC'>UTC</option>
113
			</select>
114
		</div>
115

  
116
		<div class='right'>
117
			<span id='loading'>loading...</span>
118
			<span id='script-warning'><code>php/get-events.php</code> must be running.</span>
119
		</div>
120

  
121
		<div class='clear'></div>
122

  
123
	</div>
124

  
125
	<div id='calendar'></div>
126

  
127
</body>
128
</html>
combo/apps/chrono/static/chrono/demos/week-numbers.html
1
<!DOCTYPE html>
2
<html>
3
<head>
4
<meta charset='utf-8' />
5
<link href='../fullcalendar.min.css' rel='stylesheet' />
6
<link href='../fullcalendar.print.min.css' rel='stylesheet' media='print' />
7
<script src='../lib/moment.min.js'></script>
8
<script src='../lib/jquery.min.js'></script>
9
<script src='../fullcalendar.min.js'></script>
10
<script>
11

  
12
	$(document).ready(function() {
13
		
14
		$('#calendar').fullCalendar({
15
			header: {
16
				left: 'prev,next today',
17
				center: 'title',
18
				right: 'month,agendaWeek,agendaDay,listWeek'
19
			},
20
			defaultDate: '2017-05-12',
21
			navLinks: true, // can click day/week names to navigate views
22

  
23
			weekNumbers: true,
24
			weekNumbersWithinDays: true,
25
			weekNumberCalculation: 'ISO',
26

  
27
			editable: true,
28
			eventLimit: true, // allow "more" link when too many events
29
			events: [
30
				{
31
					title: 'All Day Event',
32
					start: '2017-05-01'
33
				},
34
				{
35
					title: 'Long Event',
36
					start: '2017-05-07',
37
					end: '2017-05-10'
38
				},
39
				{
40
					id: 999,
41
					title: 'Repeating Event',
42
					start: '2017-05-09T16:00:00'
43
				},
44
				{
45
					id: 999,
46
					title: 'Repeating Event',
47
					start: '2017-05-16T16:00:00'
48
				},
49
				{
50
					title: 'Conference',
51
					start: '2017-05-11',
52
					end: '2017-05-13'
53
				},
54
				{
55
					title: 'Meeting',
56
					start: '2017-05-12T10:30:00',
57
					end: '2017-05-12T12:30:00'
58
				},
59
				{
60
					title: 'Lunch',
61
					start: '2017-05-12T12:00:00'
62
				},
63
				{
64
					title: 'Meeting',
65
					start: '2017-05-12T14:30:00'
66
				},
67
				{
68
					title: 'Happy Hour',
69
					start: '2017-05-12T17:30:00'
70
				},
71
				{
72
					title: 'Dinner',
73
					start: '2017-05-12T20:00:00'
74
				},
75
				{
76
					title: 'Birthday Party',
77
					start: '2017-05-13T07:00:00'
78
				},
79
				{
80
					title: 'Click for Google',
81
					url: 'http://google.com/',
82
					start: '2017-05-28'
83
				}
84
			]
85
		});
86
		
87
	});
88

  
89
</script>
90
<style>
91

  
92
	body {
93
		margin: 40px 10px;
94
		padding: 0;
95
		font-family: "Lucida Grande",Helvetica,Arial,Verdana,sans-serif;
96
		font-size: 14px;
97
	}
98

  
99
	#calendar {
100
		max-width: 900px;
101
		margin: 0 auto;
102
	}
103

  
104
</style>
105
</head>
106
<body>
107

  
108
	<div id='calendar'></div>
109

  
110
</body>
111
</html>
combo/apps/chrono/static/chrono/fullcalendar.css
1
/*!
2
 * FullCalendar v3.4.0 Stylesheet
3
 * Docs & License: https://fullcalendar.io/
4
 * (c) 2017 Adam Shaw
5
 */
6

  
7

  
8
.fc {
9
	direction: ltr;
10
	text-align: left;
11
}
12

  
13
.fc-rtl {
14
	text-align: right;
15
}
16

  
17
body .fc { /* extra precedence to overcome jqui */
18
	font-size: 1em;
19
}
20

  
21

  
22
/* Colors
23
--------------------------------------------------------------------------------------------------*/
24

  
25
.fc-unthemed th,
26
.fc-unthemed td,
27
.fc-unthemed thead,
28
.fc-unthemed tbody,
29
.fc-unthemed .fc-divider,
30
.fc-unthemed .fc-row,
31
.fc-unthemed .fc-content, /* for gutter border */
32
.fc-unthemed .fc-popover,
33
.fc-unthemed .fc-list-view,
34
.fc-unthemed .fc-list-heading td {
35
	border-color: #ddd;
36
}
37

  
38
.fc-unthemed .fc-popover {
39
	background-color: #fff;
40
}
41

  
42
.fc-unthemed .fc-divider,
43
.fc-unthemed .fc-popover .fc-header,
44
.fc-unthemed .fc-list-heading td {
45
	background: #eee;
46
}
47

  
48
.fc-unthemed .fc-popover .fc-header .fc-close {
49
	color: #666;
50
}
51

  
52
.fc-unthemed td.fc-today {
53
	background: #fcf8e3;
54
}
55

  
56
.fc-highlight { /* when user is selecting cells */
57
	background: #bce8f1;
58
	opacity: .3;
59
}
60

  
61
.fc-bgevent { /* default look for background events */
62
	background: rgb(143, 223, 130);
63
	opacity: .3;
64
}
65

  
66
.fc-nonbusiness { /* default look for non-business-hours areas */
67
	/* will inherit .fc-bgevent's styles */
68
	background: #d7d7d7;
69
}
70

  
71
.fc-unthemed .fc-disabled-day {
72
	background: #d7d7d7;
73
	opacity: .3;
74
}
75

  
76
.ui-widget .fc-disabled-day { /* themed */
77
	background-image: none;
78
}
79

  
80

  
81
/* Icons (inline elements with styled text that mock arrow icons)
82
--------------------------------------------------------------------------------------------------*/
83

  
84
.fc-icon {
85
	display: inline-block;
86
	height: 1em;
87
	line-height: 1em;
88
	font-size: 1em;
89
	text-align: center;
90
	overflow: hidden;
91
	font-family: "Courier New", Courier, monospace;
92

  
93
	/* don't allow browser text-selection */
94
	-webkit-touch-callout: none;
95
	-webkit-user-select: none;
96
	-khtml-user-select: none;
97
	-moz-user-select: none;
98
	-ms-user-select: none;
99
	user-select: none;
100
	}
101

  
102
/*
103
Acceptable font-family overrides for individual icons:
104
	"Arial", sans-serif
105
	"Times New Roman", serif
106

  
107
NOTE: use percentage font sizes or else old IE chokes
108
*/
109

  
110
.fc-icon:after {
111
	position: relative;
112
}
113

  
114
.fc-icon-left-single-arrow:after {
115
	content: "\02039";
116
	font-weight: bold;
117
	font-size: 200%;
118
	top: -7%;
119
}
120

  
121
.fc-icon-right-single-arrow:after {
122
	content: "\0203A";
123
	font-weight: bold;
124
	font-size: 200%;
125
	top: -7%;
126
}
127

  
128
.fc-icon-left-double-arrow:after {
129
	content: "\000AB";
130
	font-size: 160%;
131
	top: -7%;
132
}
133

  
134
.fc-icon-right-double-arrow:after {
135
	content: "\000BB";
136
	font-size: 160%;
137
	top: -7%;
138
}
139

  
140
.fc-icon-left-triangle:after {
141
	content: "\25C4";
142
	font-size: 125%;
143
	top: 3%;
144
}
145

  
146
.fc-icon-right-triangle:after {
147
	content: "\25BA";
148
	font-size: 125%;
149
	top: 3%;
150
}
151

  
152
.fc-icon-down-triangle:after {
153
	content: "\25BC";
154
	font-size: 125%;
155
	top: 2%;
156
}
157

  
158
.fc-icon-x:after {
159
	content: "\000D7";
160
	font-size: 200%;
161
	top: 6%;
162
}
163

  
164

  
165
/* Buttons (styled <button> tags, normalized to work cross-browser)
166
--------------------------------------------------------------------------------------------------*/
167

  
168
.fc button {
169
	/* force height to include the border and padding */
170
	-moz-box-sizing: border-box;
171
	-webkit-box-sizing: border-box;
172
	box-sizing: border-box;
173

  
174
	/* dimensions */
175
	margin: 0;
176
	height: 2.1em;
177
	padding: 0 .6em;
178

  
179
	/* text & cursor */
180
	font-size: 1em; /* normalize */
181
	white-space: nowrap;
182
	cursor: pointer;
183
}
184

  
185
/* Firefox has an annoying inner border */
186
.fc button::-moz-focus-inner { margin: 0; padding: 0; }
187
	
188
.fc-state-default { /* non-theme */
189
	border: 1px solid;
190
}
191

  
192
.fc-state-default.fc-corner-left { /* non-theme */
193
	border-top-left-radius: 4px;
194
	border-bottom-left-radius: 4px;
195
}
196

  
197
.fc-state-default.fc-corner-right { /* non-theme */
198
	border-top-right-radius: 4px;
199
	border-bottom-right-radius: 4px;
200
}
201

  
202
/* icons in buttons */
203

  
204
.fc button .fc-icon { /* non-theme */
205
	position: relative;
206
	top: -0.05em; /* seems to be a good adjustment across browsers */
207
	margin: 0 .2em;
208
	vertical-align: middle;
209
}
210
	
211
/*
212
  button states
213
  borrowed from twitter bootstrap (http://twitter.github.com/bootstrap/)
214
*/
215

  
216
.fc-state-default {
217
	background-color: #f5f5f5;
218
	background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6);
219
	background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6));
220
	background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6);
221
	background-image: -o-linear-gradient(top, #ffffff, #e6e6e6);
222
	background-image: linear-gradient(to bottom, #ffffff, #e6e6e6);
223
	background-repeat: repeat-x;
224
	border-color: #e6e6e6 #e6e6e6 #bfbfbf;
225
	border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
226
	color: #333;
227
	text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75);
228
	box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
229
}
230

  
231
.fc-state-hover,
232
.fc-state-down,
233
.fc-state-active,
234
.fc-state-disabled {
235
	color: #333333;
236
	background-color: #e6e6e6;
237
}
238

  
239
.fc-state-hover {
240
	color: #333333;
241
	text-decoration: none;
242
	background-position: 0 -15px;
243
	-webkit-transition: background-position 0.1s linear;
244
	   -moz-transition: background-position 0.1s linear;
245
	     -o-transition: background-position 0.1s linear;
246
	        transition: background-position 0.1s linear;
247
}
248

  
249
.fc-state-down,
250
.fc-state-active {
251
	background-color: #cccccc;
252
	background-image: none;
253
	box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05);
254
}
255

  
256
.fc-state-disabled {
257
	cursor: default;
258
	background-image: none;
259
	opacity: 0.65;
260
	box-shadow: none;
261
}
262

  
263

  
264
/* Buttons Groups
265
--------------------------------------------------------------------------------------------------*/
266

  
267
.fc-button-group {
268
	display: inline-block;
269
}
270

  
271
/*
272
every button that is not first in a button group should scootch over one pixel and cover the
273
previous button's border...
274
*/
275

  
276
.fc .fc-button-group > * { /* extra precedence b/c buttons have margin set to zero */
277
	float: left;
278
	margin: 0 0 0 -1px;
279
}
280

  
281
.fc .fc-button-group > :first-child { /* same */
282
	margin-left: 0;
283
}
284

  
285

  
286
/* Popover
287
--------------------------------------------------------------------------------------------------*/
288

  
289
.fc-popover {
290
	position: absolute;
291
	box-shadow: 0 2px 6px rgba(0,0,0,.15);
292
}
293

  
294
.fc-popover .fc-header { /* TODO: be more consistent with fc-head/fc-body */
295
	padding: 2px 4px;
296
}
297

  
298
.fc-popover .fc-header .fc-title {
299
	margin: 0 2px;
300
}
301

  
302
.fc-popover .fc-header .fc-close {
303
	cursor: pointer;
304
}
305

  
306
.fc-ltr .fc-popover .fc-header .fc-title,
307
.fc-rtl .fc-popover .fc-header .fc-close {
308
	float: left;
309
}
310

  
311
.fc-rtl .fc-popover .fc-header .fc-title,
312
.fc-ltr .fc-popover .fc-header .fc-close {
313
	float: right;
314
}
315

  
316
/* unthemed */
317

  
318
.fc-unthemed .fc-popover {
319
	border-width: 1px;
320
	border-style: solid;
321
}
322

  
323
.fc-unthemed .fc-popover .fc-header .fc-close {
324
	font-size: .9em;
325
	margin-top: 2px;
326
}
327

  
328
/* jqui themed */
329

  
330
.fc-popover > .ui-widget-header + .ui-widget-content {
331
	border-top: 0; /* where they meet, let the header have the border */
332
}
333

  
334

  
335
/* Misc Reusable Components
336
--------------------------------------------------------------------------------------------------*/
337

  
338
.fc-divider {
339
	border-style: solid;
340
	border-width: 1px;
341
}
342

  
343
hr.fc-divider {
344
	height: 0;
345
	margin: 0;
346
	padding: 0 0 2px; /* height is unreliable across browsers, so use padding */
347
	border-width: 1px 0;
348
}
349

  
350
.fc-clear {
351
	clear: both;
352
}
353

  
354
.fc-bg,
355
.fc-bgevent-skeleton,
356
.fc-highlight-skeleton,
357
.fc-helper-skeleton {
358
	/* these element should always cling to top-left/right corners */
359
	position: absolute;
360
	top: 0;
361
	left: 0;
362
	right: 0;
363
}
364

  
365
.fc-bg {
366
	bottom: 0; /* strech bg to bottom edge */
367
}
368

  
369
.fc-bg table {
370
	height: 100%; /* strech bg to bottom edge */
371
}
372

  
373

  
374
/* Tables
375
--------------------------------------------------------------------------------------------------*/
376

  
377
.fc table {
378
	width: 100%;
379
	box-sizing: border-box; /* fix scrollbar issue in firefox */
380
	table-layout: fixed;
381
	border-collapse: collapse;
382
	border-spacing: 0;
383
	font-size: 1em; /* normalize cross-browser */
384
}
385

  
386
.fc th {
387
	text-align: center;
388
}
389

  
390
.fc th,
391
.fc td {
392
	border-style: solid;
393
	border-width: 1px;
394
	padding: 0;
395
	vertical-align: top;
396
}
397

  
398
.fc td.fc-today {
399
	border-style: double; /* overcome neighboring borders */
400
}
401

  
402

  
403
/* Internal Nav Links
404
--------------------------------------------------------------------------------------------------*/
405

  
406
a[data-goto] {
407
	cursor: pointer;
408
}
409

  
410
a[data-goto]:hover {
411
	text-decoration: underline;
412
}
413

  
414

  
415
/* Fake Table Rows
416
--------------------------------------------------------------------------------------------------*/
417

  
418
.fc .fc-row { /* extra precedence to overcome themes w/ .ui-widget-content forcing a 1px border */
419
	/* no visible border by default. but make available if need be (scrollbar width compensation) */
420
	border-style: solid;
421
	border-width: 0;
422
}
423

  
424
.fc-row table {
425
	/* don't put left/right border on anything within a fake row.
426
	   the outer tbody will worry about this */
427
	border-left: 0 hidden transparent;
428
	border-right: 0 hidden transparent;
429

  
430
	/* no bottom borders on rows */
431
	border-bottom: 0 hidden transparent; 
432
}
433

  
434
.fc-row:first-child table {
435
	border-top: 0 hidden transparent; /* no top border on first row */
436
}
437

  
438

  
439
/* Day Row (used within the header and the DayGrid)
440
--------------------------------------------------------------------------------------------------*/
441

  
442
.fc-row {
443
	position: relative;
444
}
445

  
446
.fc-row .fc-bg {
447
	z-index: 1;
448
}
449

  
450
/* highlighting cells & background event skeleton */
451

  
452
.fc-row .fc-bgevent-skeleton,
453
.fc-row .fc-highlight-skeleton {
454
	bottom: 0; /* stretch skeleton to bottom of row */
455
}
456

  
457
.fc-row .fc-bgevent-skeleton table,
458
.fc-row .fc-highlight-skeleton table {
459
	height: 100%; /* stretch skeleton to bottom of row */
460
}
461

  
462
.fc-row .fc-highlight-skeleton td,
463
.fc-row .fc-bgevent-skeleton td {
464
	border-color: transparent;
465
}
466

  
467
.fc-row .fc-bgevent-skeleton {
468
	z-index: 2;
469

  
470
}
471

  
472
.fc-row .fc-highlight-skeleton {
473
	z-index: 3;
474
}
475

  
476
/*
477
row content (which contains day/week numbers and events) as well as "helper" (which contains
478
temporary rendered events).
479
*/
480

  
481
.fc-row .fc-content-skeleton {
482
	position: relative;
483
	z-index: 4;
484
	padding-bottom: 2px; /* matches the space above the events */
485
}
486

  
487
.fc-row .fc-helper-skeleton {
488
	z-index: 5;
489
}
490

  
491
.fc-row .fc-content-skeleton td,
492
.fc-row .fc-helper-skeleton td {
493
	/* see-through to the background below */
494
	background: none; /* in case <td>s are globally styled */
495
	border-color: transparent;
496

  
497
	/* don't put a border between events and/or the day number */
498
	border-bottom: 0;
499
}
500

  
501
.fc-row .fc-content-skeleton tbody td, /* cells with events inside (so NOT the day number cell) */
502
.fc-row .fc-helper-skeleton tbody td {
503
	/* don't put a border between event cells */
504
	border-top: 0;
505
}
506

  
507

  
508
/* Scrolling Container
509
--------------------------------------------------------------------------------------------------*/
510

  
511
.fc-scroller {
512
	-webkit-overflow-scrolling: touch;
513
}
514

  
515
/* TODO: move to agenda/basic */
516
.fc-scroller > .fc-day-grid,
517
.fc-scroller > .fc-time-grid {
518
	position: relative; /* re-scope all positions */
519
	width: 100%; /* hack to force re-sizing this inner element when scrollbars appear/disappear */
520
}
521

  
522

  
523
/* Global Event Styles
524
--------------------------------------------------------------------------------------------------*/
525

  
526
.fc-event {
527
	position: relative; /* for resize handle and other inner positioning */
528
	display: block; /* make the <a> tag block */
529
	font-size: .85em;
530
	line-height: 1.3;
531
	border-radius: 3px;
532
	border: 1px solid #3a87ad; /* default BORDER color */
533
	font-weight: normal; /* undo jqui's ui-widget-header bold */
534
}
535

  
536
.fc-event,
537
.fc-event-dot {
538
	background-color: #3a87ad; /* default BACKGROUND color */
539
}
540

  
541
/* overpower some of bootstrap's and jqui's styles on <a> tags */
542
.fc-event,
543
.fc-event:hover,
544
.ui-widget .fc-event {
545
	color: #fff; /* default TEXT color */
546
	text-decoration: none; /* if <a> has an href */
547
}
548

  
549
.fc-event[href],
550
.fc-event.fc-draggable {
551
	cursor: pointer; /* give events with links and draggable events a hand mouse pointer */
552
}
553

  
554
.fc-not-allowed, /* causes a "warning" cursor. applied on body */
555
.fc-not-allowed .fc-event { /* to override an event's custom cursor */
556
	cursor: not-allowed;
557
}
558

  
559
.fc-event .fc-bg { /* the generic .fc-bg already does position */
560
	z-index: 1;
561
	background: #fff;
562
	opacity: .25;
563
}
564

  
565
.fc-event .fc-content {
566
	position: relative;
567
	z-index: 2;
568
}
569

  
570
/* resizer (cursor AND touch devices) */
571

  
572
.fc-event .fc-resizer {
573
	position: absolute;
574
	z-index: 4;
575
}
576

  
577
/* resizer (touch devices) */
578

  
579
.fc-event .fc-resizer {
580
	display: none;
581
}
582

  
583
.fc-event.fc-allow-mouse-resize .fc-resizer,
584
.fc-event.fc-selected .fc-resizer {
585
	/* only show when hovering or selected (with touch) */
586
	display: block;
587
}
588

  
589
/* hit area */
590

  
591
.fc-event.fc-selected .fc-resizer:before {
592
	/* 40x40 touch area */
593
	content: "";
594
	position: absolute;
595
	z-index: 9999; /* user of this util can scope within a lower z-index */
596
	top: 50%;
597
	left: 50%;
598
	width: 40px;
599
	height: 40px;
600
	margin-left: -20px;
601
	margin-top: -20px;
602
}
603

  
604

  
605
/* Event Selection (only for touch devices)
606
--------------------------------------------------------------------------------------------------*/
607

  
608
.fc-event.fc-selected {
609
	z-index: 9999 !important; /* overcomes inline z-index */
610
	box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
611
}
612

  
613
.fc-event.fc-selected.fc-dragging {
614
	box-shadow: 0 2px 7px rgba(0, 0, 0, 0.3);
615
}
616

  
617

  
618
/* Horizontal Events
619
--------------------------------------------------------------------------------------------------*/
620

  
621
/* bigger touch area when selected */
622
.fc-h-event.fc-selected:before {
623
	content: "";
624
	position: absolute;
625
	z-index: 3; /* below resizers */
626
	top: -10px;
627
	bottom: -10px;
628
	left: 0;
629
	right: 0;
630
}
631

  
632
/* events that are continuing to/from another week. kill rounded corners and butt up against edge */
633

  
634
.fc-ltr .fc-h-event.fc-not-start,
635
.fc-rtl .fc-h-event.fc-not-end {
636
	margin-left: 0;
637
	border-left-width: 0;
638
	padding-left: 1px; /* replace the border with padding */
639
	border-top-left-radius: 0;
640
	border-bottom-left-radius: 0;
641
}
642

  
643
.fc-ltr .fc-h-event.fc-not-end,
644
.fc-rtl .fc-h-event.fc-not-start {
645
	margin-right: 0;
646
	border-right-width: 0;
647
	padding-right: 1px; /* replace the border with padding */
648
	border-top-right-radius: 0;
649
	border-bottom-right-radius: 0;
650
}
651

  
652
/* resizer (cursor AND touch devices) */
653

  
654
/* left resizer  */
655
.fc-ltr .fc-h-event .fc-start-resizer,
656
.fc-rtl .fc-h-event .fc-end-resizer {
657
	cursor: w-resize;
658
	left: -1px; /* overcome border */
659
}
660

  
661
/* right resizer */
662
.fc-ltr .fc-h-event .fc-end-resizer,
663
.fc-rtl .fc-h-event .fc-start-resizer {
664
	cursor: e-resize;
665
	right: -1px; /* overcome border */
666
}
667

  
668
/* resizer (mouse devices) */
669

  
670
.fc-h-event.fc-allow-mouse-resize .fc-resizer {
671
	width: 7px;
672
	top: -1px; /* overcome top border */
673
	bottom: -1px; /* overcome bottom border */
674
}
675

  
676
/* resizer (touch devices) */
677

  
678
.fc-h-event.fc-selected .fc-resizer {
679
	/* 8x8 little dot */
680
	border-radius: 4px;
681
	border-width: 1px;
682
	width: 6px;
683
	height: 6px;
684
	border-style: solid;
685
	border-color: inherit;
686
	background: #fff;
687
	/* vertically center */
688
	top: 50%;
689
	margin-top: -4px;
690
}
691

  
692
/* left resizer  */
693
.fc-ltr .fc-h-event.fc-selected .fc-start-resizer,
694
.fc-rtl .fc-h-event.fc-selected .fc-end-resizer {
695
	margin-left: -4px; /* centers the 8x8 dot on the left edge */
696
}
697

  
698
/* right resizer */
699
.fc-ltr .fc-h-event.fc-selected .fc-end-resizer,
700
.fc-rtl .fc-h-event.fc-selected .fc-start-resizer {
701
	margin-right: -4px; /* centers the 8x8 dot on the right edge */
702
}
703

  
704

  
705
/* DayGrid events
706
----------------------------------------------------------------------------------------------------
707
We use the full "fc-day-grid-event" class instead of using descendants because the event won't
708
be a descendant of the grid when it is being dragged.
709
*/
710

  
711
.fc-day-grid-event {
712
	margin: 1px 2px 0; /* spacing between events and edges */
713
	padding: 0 1px;
714
}
715

  
716
tr:first-child > td > .fc-day-grid-event {
717
	margin-top: 2px; /* a little bit more space before the first event */
718
}
719

  
720
.fc-day-grid-event.fc-selected:after {
721
	content: "";
722
	position: absolute;
723
	z-index: 1; /* same z-index as fc-bg, behind text */
724
	/* overcome the borders */
725
	top: -1px;
726
	right: -1px;
727
	bottom: -1px;
728
	left: -1px;
729
	/* darkening effect */
730
	background: #000;
731
	opacity: .25;
732
}
733

  
734
.fc-day-grid-event .fc-content { /* force events to be one-line tall */
735
	white-space: nowrap;
736
	overflow: hidden;
737
}
738

  
739
.fc-day-grid-event .fc-time {
740
	font-weight: bold;
741
}
742

  
743
/* resizer (cursor devices) */
744

  
745
/* left resizer  */
746
.fc-ltr .fc-day-grid-event.fc-allow-mouse-resize .fc-start-resizer,
747
.fc-rtl .fc-day-grid-event.fc-allow-mouse-resize .fc-end-resizer {
748
	margin-left: -2px; /* to the day cell's edge */
749
}
750

  
751
/* right resizer */
752
.fc-ltr .fc-day-grid-event.fc-allow-mouse-resize .fc-end-resizer,
753
.fc-rtl .fc-day-grid-event.fc-allow-mouse-resize .fc-start-resizer {
754
	margin-right: -2px; /* to the day cell's edge */
755
}
756

  
757

  
758
/* Event Limiting
759
--------------------------------------------------------------------------------------------------*/
760

  
761
/* "more" link that represents hidden events */
762

  
763
a.fc-more {
764
	margin: 1px 3px;
765
	font-size: .85em;
766
	cursor: pointer;
767
	text-decoration: none;
768
}
769

  
770
a.fc-more:hover {
771
	text-decoration: underline;
772
}
773

  
774
.fc-limited { /* rows and cells that are hidden because of a "more" link */
775
	display: none;
776
}
777

  
778
/* popover that appears when "more" link is clicked */
779

  
780
.fc-day-grid .fc-row {
781
	z-index: 1; /* make the "more" popover one higher than this */
782
}
783

  
784
.fc-more-popover {
785
	z-index: 2;
786
	width: 220px;
787
}
788

  
789
.fc-more-popover .fc-event-container {
790
	padding: 10px;
791
}
792

  
793

  
794
/* Now Indicator
795
--------------------------------------------------------------------------------------------------*/
796

  
797
.fc-now-indicator {
798
	position: absolute;
799
	border: 0 solid red;
800
}
801

  
802

  
803
/* Utilities
804
--------------------------------------------------------------------------------------------------*/
805

  
806
.fc-unselectable {
807
	-webkit-user-select: none;
808
	 -khtml-user-select: none;
809
	   -moz-user-select: none;
810
	    -ms-user-select: none;
811
	        user-select: none;
812
	-webkit-touch-callout: none;
813
	-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
814
}
815

  
816

  
817

  
818
/* Toolbar
819
--------------------------------------------------------------------------------------------------*/
820

  
821
.fc-toolbar {
822
	text-align: center;
823
}
824

  
825
.fc-toolbar.fc-header-toolbar {
826
	margin-bottom: 1em;
827
}
828

  
829
.fc-toolbar.fc-footer-toolbar {
830
	margin-top: 1em;
831
}
832

  
833
.fc-toolbar .fc-left {
834
	float: left;
835
}
836

  
837
.fc-toolbar .fc-right {
838
	float: right;
839
}
840

  
841
.fc-toolbar .fc-center {
842
	display: inline-block;
843
}
844

  
845
/* the things within each left/right/center section */
846
.fc .fc-toolbar > * > * { /* extra precedence to override button border margins */
847
	float: left;
848
	margin-left: .75em;
849
}
850

  
851
/* the first thing within each left/center/right section */
852
.fc .fc-toolbar > * > :first-child { /* extra precedence to override button border margins */
853
	margin-left: 0;
854
}
855
	
856
/* title text */
857

  
858
.fc-toolbar h2 {
859
	margin: 0;
860
}
861

  
862
/* button layering (for border precedence) */
863

  
864
.fc-toolbar button {
865
	position: relative;
866
}
867

  
868
.fc-toolbar .fc-state-hover,
869
.fc-toolbar .ui-state-hover {
870
	z-index: 2;
871
}
872
	
873
.fc-toolbar .fc-state-down {
874
	z-index: 3;
875
}
876

  
877
.fc-toolbar .fc-state-active,
878
.fc-toolbar .ui-state-active {
879
	z-index: 4;
880
}
881

  
882
.fc-toolbar button:focus {
883
	z-index: 5;
884
}
885

  
886

  
887
/* View Structure
888
--------------------------------------------------------------------------------------------------*/
889

  
890
/* undo twitter bootstrap's box-sizing rules. normalizes positioning techniques */
891
/* don't do this for the toolbar because we'll want bootstrap to style those buttons as some pt */
892
.fc-view-container *,
893
.fc-view-container *:before,
894
.fc-view-container *:after {
895
	-webkit-box-sizing: content-box;
896
	   -moz-box-sizing: content-box;
897
	        box-sizing: content-box;
898
}
899

  
900
.fc-view, /* scope positioning and z-index's for everything within the view */
901
.fc-view > table { /* so dragged elements can be above the view's main element */
902
	position: relative;
903
	z-index: 1;
904
}
905

  
906

  
907

  
908
/* BasicView
909
--------------------------------------------------------------------------------------------------*/
910

  
911
/* day row structure */
912

  
913
.fc-basicWeek-view .fc-content-skeleton,
914
.fc-basicDay-view .fc-content-skeleton {
915
	/* there may be week numbers in these views, so no padding-top */
916
	padding-bottom: 1em; /* ensure a space at bottom of cell for user selecting/clicking */
917
}
918

  
919
.fc-basic-view .fc-body .fc-row {
920
	min-height: 4em; /* ensure that all rows are at least this tall */
921
}
922

  
923
/* a "rigid" row will take up a constant amount of height because content-skeleton is absolute */
924

  
925
.fc-row.fc-rigid {
926
	overflow: hidden;
927
}
928

  
929
.fc-row.fc-rigid .fc-content-skeleton {
930
	position: absolute;
931
	top: 0;
932
	left: 0;
933
	right: 0;
934
}
935

  
936
/* week and day number styling */
937

  
938
.fc-day-top.fc-other-month {
939
	opacity: 0.3;
940
}
941

  
942
.fc-basic-view .fc-week-number,
943
.fc-basic-view .fc-day-number {
944
	padding: 2px;
945
}
946

  
947
.fc-basic-view th.fc-week-number,
948
.fc-basic-view th.fc-day-number {
949
	padding: 0 2px; /* column headers can't have as much v space */
950
}
951

  
952
.fc-ltr .fc-basic-view .fc-day-top .fc-day-number { float: right; }
953
.fc-rtl .fc-basic-view .fc-day-top .fc-day-number { float: left; }
954

  
955
.fc-ltr .fc-basic-view .fc-day-top .fc-week-number { float: left; border-radius: 0 0 3px 0; }
956
.fc-rtl .fc-basic-view .fc-day-top .fc-week-number { float: right; border-radius: 0 0 0 3px; }
957

  
958
.fc-basic-view .fc-day-top .fc-week-number {
959
	min-width: 1.5em;
960
	text-align: center;
961
	background-color: #f2f2f2;
962
	color: #808080;
963
}
964

  
965
/* when week/day number have own column */
966

  
967
.fc-basic-view td.fc-week-number {
968
	text-align: center;
969
}
970

  
971
.fc-basic-view td.fc-week-number > * {
972
	/* work around the way we do column resizing and ensure a minimum width */
973
	display: inline-block;
974
	min-width: 1.25em;
975
}
976

  
977

  
978
/* AgendaView all-day area
979
--------------------------------------------------------------------------------------------------*/
980

  
981
.fc-agenda-view .fc-day-grid {
982
	position: relative;
983
	z-index: 2; /* so the "more.." popover will be over the time grid */
984
}
985

  
986
.fc-agenda-view .fc-day-grid .fc-row {
987
	min-height: 3em; /* all-day section will never get shorter than this */
988
}
989

  
990
.fc-agenda-view .fc-day-grid .fc-row .fc-content-skeleton {
991
	padding-bottom: 1em; /* give space underneath events for clicking/selecting days */
992
}
993

  
994

  
995
/* TimeGrid axis running down the side (for both the all-day area and the slot area)
996
--------------------------------------------------------------------------------------------------*/
997

  
998
.fc .fc-axis { /* .fc to overcome default cell styles */
999
	vertical-align: middle;
1000
	padding: 0 4px;
1001
	white-space: nowrap;
1002
}
1003

  
1004
.fc-ltr .fc-axis {
1005
	text-align: right;
1006
}
1007

  
1008
.fc-rtl .fc-axis {
1009
	text-align: left;
1010
}
1011

  
1012
.ui-widget td.fc-axis {
1013
	font-weight: normal; /* overcome jqui theme making it bold */
1014
}
1015

  
1016

  
1017
/* TimeGrid Structure
1018
--------------------------------------------------------------------------------------------------*/
1019

  
1020
.fc-time-grid-container, /* so scroll container's z-index is below all-day */
1021
.fc-time-grid { /* so slats/bg/content/etc positions get scoped within here */
1022
	position: relative;
1023
	z-index: 1;
1024
}
1025

  
1026
.fc-time-grid {
1027
	min-height: 100%; /* so if height setting is 'auto', .fc-bg stretches to fill height */
1028
}
1029

  
1030
.fc-time-grid table { /* don't put outer borders on slats/bg/content/etc */
1031
	border: 0 hidden transparent;
1032
}
1033

  
1034
.fc-time-grid > .fc-bg {
1035
	z-index: 1;
1036
}
1037

  
1038
.fc-time-grid .fc-slats,
1039
.fc-time-grid > hr { /* the <hr> AgendaView injects when grid is shorter than scroller */
1040
	position: relative;
1041
	z-index: 2;
1042
}
1043

  
1044
.fc-time-grid .fc-content-col {
1045
	position: relative; /* because now-indicator lives directly inside */
1046
}
1047

  
1048
.fc-time-grid .fc-content-skeleton {
1049
	position: absolute;
1050
	z-index: 3;
1051
	top: 0;
1052
	left: 0;
1053
	right: 0;
1054
}
1055

  
1056
/* divs within a cell within the fc-content-skeleton */
1057

  
1058
.fc-time-grid .fc-business-container {
1059
	position: relative;
1060
	z-index: 1;
1061
}
1062

  
1063
.fc-time-grid .fc-bgevent-container {
1064
	position: relative;
1065
	z-index: 2;
1066
}
1067

  
1068
.fc-time-grid .fc-highlight-container {
1069
	position: relative;
1070
	z-index: 3;
1071
}
1072

  
1073
.fc-time-grid .fc-event-container {
1074
	position: relative;
1075
	z-index: 4;
1076
}
1077

  
1078
.fc-time-grid .fc-now-indicator-line {
1079
	z-index: 5;
1080
}
1081

  
1082
.fc-time-grid .fc-helper-container { /* also is fc-event-container */
1083
	position: relative;
1084
	z-index: 6;
1085
}
1086

  
1087

  
1088
/* TimeGrid Slats (lines that run horizontally)
1089
--------------------------------------------------------------------------------------------------*/
1090

  
1091
.fc-time-grid .fc-slats td {
1092
	height: 1.5em;
1093
	border-bottom: 0; /* each cell is responsible for its top border */
1094
}
1095

  
1096
.fc-time-grid .fc-slats .fc-minor td {
1097
	border-top-style: dotted;
1098
}
1099

  
1100
.fc-time-grid .fc-slats .ui-widget-content { /* for jqui theme */
1101
	background: none; /* see through to fc-bg */
1102
}
1103

  
1104

  
1105
/* TimeGrid Highlighting Slots
1106
--------------------------------------------------------------------------------------------------*/
1107

  
1108
.fc-time-grid .fc-highlight-container { /* a div within a cell within the fc-highlight-skeleton */
1109
	position: relative; /* scopes the left/right of the fc-highlight to be in the column */
1110
}
1111

  
1112
.fc-time-grid .fc-highlight {
1113
	position: absolute;
1114
	left: 0;
1115
	right: 0;
1116
	/* top and bottom will be in by JS */
1117
}
1118

  
1119

  
1120
/* TimeGrid Event Containment
1121
--------------------------------------------------------------------------------------------------*/
1122

  
1123
.fc-ltr .fc-time-grid .fc-event-container { /* space on the sides of events for LTR (default) */
1124
	margin: 0 2.5% 0 2px;
1125
}
1126

  
1127
.fc-rtl .fc-time-grid .fc-event-container { /* space on the sides of events for RTL */
1128
	margin: 0 2px 0 2.5%;
1129
}
1130

  
1131
.fc-time-grid .fc-event,
1132
.fc-time-grid .fc-bgevent {
1133
	position: absolute;
1134
	z-index: 1; /* scope inner z-index's */
1135
}
1136

  
1137
.fc-time-grid .fc-bgevent {
1138
	/* background events always span full width */
1139
	left: 0;
1140
	right: 0;
1141
}
1142

  
1143

  
1144
/* Generic Vertical Event
1145
--------------------------------------------------------------------------------------------------*/
1146

  
1147
.fc-v-event.fc-not-start { /* events that are continuing from another day */
1148
	/* replace space made by the top border with padding */
1149
	border-top-width: 0;
1150
	padding-top: 1px;
1151

  
1152
	/* remove top rounded corners */
1153
	border-top-left-radius: 0;
1154
	border-top-right-radius: 0;
1155
}
1156

  
1157
.fc-v-event.fc-not-end {
1158
	/* replace space made by the top border with padding */
1159
	border-bottom-width: 0;
1160
	padding-bottom: 1px;
1161

  
1162
	/* remove bottom rounded corners */
1163
	border-bottom-left-radius: 0;
1164
	border-bottom-right-radius: 0;
1165
}
1166

  
1167

  
1168
/* TimeGrid Event Styling
1169
----------------------------------------------------------------------------------------------------
1170
We use the full "fc-time-grid-event" class instead of using descendants because the event won't
1171
be a descendant of the grid when it is being dragged.
1172
*/
1173

  
1174
.fc-time-grid-event {
1175
	overflow: hidden; /* don't let the bg flow over rounded corners */
1176
}
1177

  
1178
.fc-time-grid-event.fc-selected {
1179
	/* need to allow touch resizers to extend outside event's bounding box */
1180
	/* common fc-selected styles hide the fc-bg, so don't need this anyway */
1181
	overflow: visible;
1182
}
1183

  
1184
.fc-time-grid-event.fc-selected .fc-bg {
1185
	display: none; /* hide semi-white background, to appear darker */
1186
}
1187

  
1188
.fc-time-grid-event .fc-content {
1189
	overflow: hidden; /* for when .fc-selected */
1190
}
1191

  
1192
.fc-time-grid-event .fc-time,
1193
.fc-time-grid-event .fc-title {
1194
	padding: 0 1px;
1195
}
1196

  
1197
.fc-time-grid-event .fc-time {
1198
	font-size: .85em;
1199
	white-space: nowrap;
1200
}
1201

  
1202
/* short mode, where time and title are on the same line */
1203

  
1204
.fc-time-grid-event.fc-short .fc-content {
1205
	/* don't wrap to second line (now that contents will be inline) */
1206
	white-space: nowrap;
1207
}
1208

  
1209
.fc-time-grid-event.fc-short .fc-time,
1210
.fc-time-grid-event.fc-short .fc-title {
1211
	/* put the time and title on the same line */
1212
	display: inline-block;
1213
	vertical-align: top;
1214
}
1215

  
1216
.fc-time-grid-event.fc-short .fc-time span {
1217
	display: none; /* don't display the full time text... */
1218
}
1219

  
1220
.fc-time-grid-event.fc-short .fc-time:before {
1221
	content: attr(data-start); /* ...instead, display only the start time */
1222
}
1223

  
1224
.fc-time-grid-event.fc-short .fc-time:after {
1225
	content: "\000A0-\000A0"; /* seperate with a dash, wrapped in nbsp's */
1226
}
1227

  
1228
.fc-time-grid-event.fc-short .fc-title {
1229
	font-size: .85em; /* make the title text the same size as the time */
1230
	padding: 0; /* undo padding from above */
1231
}
1232

  
1233
/* resizer (cursor device) */
1234

  
1235
.fc-time-grid-event.fc-allow-mouse-resize .fc-resizer {
1236
	left: 0;
1237
	right: 0;
1238
	bottom: 0;
1239
	height: 8px;
1240
	overflow: hidden;
1241
	line-height: 8px;
1242
	font-size: 11px;
1243
	font-family: monospace;
1244
	text-align: center;
1245
	cursor: s-resize;
1246
}
1247

  
1248
.fc-time-grid-event.fc-allow-mouse-resize .fc-resizer:after {
1249
	content: "=";
1250
}
1251

  
1252
/* resizer (touch device) */
1253

  
1254
.fc-time-grid-event.fc-selected .fc-resizer {
1255
	/* 10x10 dot */
1256
	border-radius: 5px;
1257
	border-width: 1px;
1258
	width: 8px;
1259
	height: 8px;
1260
	border-style: solid;
1261
	border-color: inherit;
1262
	background: #fff;
1263
	/* horizontally center */
1264
	left: 50%;
1265
	margin-left: -5px;
1266
	/* center on the bottom edge */
1267
	bottom: -5px;
1268
}
1269

  
1270

  
1271
/* Now Indicator
1272
--------------------------------------------------------------------------------------------------*/
1273

  
1274
.fc-time-grid .fc-now-indicator-line {
1275
	border-top-width: 1px;
1276
	left: 0;
1277
	right: 0;
1278
}
1279

  
1280
/* arrow on axis */
1281

  
1282
.fc-time-grid .fc-now-indicator-arrow {
1283
	margin-top: -5px; /* vertically center on top coordinate */
1284
}
1285

  
1286
.fc-ltr .fc-time-grid .fc-now-indicator-arrow {
1287
	left: 0;
1288
	/* triangle pointing right... */
1289
	border-width: 5px 0 5px 6px;
1290
	border-top-color: transparent;
1291
	border-bottom-color: transparent;
1292
}
1293

  
1294
.fc-rtl .fc-time-grid .fc-now-indicator-arrow {
1295
	right: 0;
1296
	/* triangle pointing left... */
1297
	border-width: 5px 6px 5px 0;
1298
	border-top-color: transparent;
1299
	border-bottom-color: transparent;
1300
}
1301

  
1302

  
1303

  
1304
/* List View
1305
--------------------------------------------------------------------------------------------------*/
1306

  
1307
/* possibly reusable */
1308

  
1309
.fc-event-dot {
1310
	display: inline-block;
1311
	width: 10px;
1312
	height: 10px;
1313
	border-radius: 5px;
1314
}
1315

  
1316
/* view wrapper */
1317

  
1318
.fc-rtl .fc-list-view {
1319
	direction: rtl; /* unlike core views, leverage browser RTL */
1320
}
1321

  
1322
.fc-list-view {
1323
	border-width: 1px;
1324
	border-style: solid;
1325
}
1326

  
1327
/* table resets */
1328

  
1329
.fc .fc-list-table {
1330
	table-layout: auto; /* for shrinkwrapping cell content */
1331
}
1332

  
1333
.fc-list-table td {
1334
	border-width: 1px 0 0;
1335
	padding: 8px 14px;
1336
}
1337

  
1338
.fc-list-table tr:first-child td {
1339
	border-top-width: 0;
1340
}
1341

  
1342
/* day headings with the list */
1343

  
1344
.fc-list-heading {
1345
	border-bottom-width: 1px;
1346
}
1347

  
1348
.fc-list-heading td {
1349
	font-weight: bold;
1350
}
1351

  
1352
.fc-ltr .fc-list-heading-main { float: left; }
1353
.fc-ltr .fc-list-heading-alt { float: right; }
1354

  
1355
.fc-rtl .fc-list-heading-main { float: right; }
1356
.fc-rtl .fc-list-heading-alt { float: left; }
1357

  
1358
/* event list items */
1359

  
1360
.fc-list-item.fc-has-url {
1361
	cursor: pointer; /* whole row will be clickable */
1362
}
1363

  
1364
.fc-list-item:hover td {
1365
	background-color: #f5f5f5;
1366
}
1367

  
1368
.fc-list-item-marker,
1369
.fc-list-item-time {
1370
	white-space: nowrap;
1371
	width: 1px;
1372
}
1373

  
1374
/* make the dot closer to the event title */
1375
.fc-ltr .fc-list-item-marker { padding-right: 0; }
1376
.fc-rtl .fc-list-item-marker { padding-left: 0; }
1377

  
1378
.fc-list-item-title a {
1379
	/* every event title cell has an <a> tag */
1380
	text-decoration: none;
1381
	color: inherit;
1382
}
1383

  
1384
.fc-list-item-title a[href]:hover {
1385
	/* hover effect only on titles with hrefs */
1386
	text-decoration: underline;
1387
}
1388

  
1389
/* message when no events */
1390

  
1391
.fc-list-empty-wrap2 {
1392
	position: absolute;
1393
	top: 0;
1394
	left: 0;
1395
	right: 0;
1396
	bottom: 0;
1397
}
1398

  
1399
.fc-list-empty-wrap1 {
1400
	width: 100%;
1401
	height: 100%;
1402
	display: table;
1403
}
1404

  
1405
.fc-list-empty {
1406
	display: table-cell;
1407
	vertical-align: middle;
1408
	text-align: center;
1409
}
1410

  
1411
.fc-unthemed .fc-list-empty { /* theme will provide own background */
1412
	background-color: #eee;
1413
}
combo/apps/chrono/static/chrono/fullcalendar.js
1
/*!
2
 * FullCalendar v3.4.0
3
 * Docs & License: https://fullcalendar.io/
4
 * (c) 2017 Adam Shaw
5
 */
6

  
7
(function(factory) {
8
	if (typeof define === 'function' && define.amd) {
9
		define([ 'jquery', 'moment' ], factory);
10
	}
11
	else if (typeof exports === 'object') { // Node/CommonJS
12
		module.exports = factory(require('jquery'), require('moment'));
13
	}
14
	else {
15
		factory(jQuery, moment);
16
	}
17
})(function($, moment) {
18

  
19
;;
20

  
21
var FC = $.fullCalendar = {
22
	version: "3.4.0",
23
	// When introducing internal API incompatibilities (where fullcalendar plugins would break),
24
	// the minor version of the calendar should be upped (ex: 2.7.2 -> 2.8.0)
25
	// and the below integer should be incremented.
26
	internalApiVersion: 9
27
};
28
var fcViews = FC.views = {};
29

  
30

  
31
$.fn.fullCalendar = function(options) {
32
	var args = Array.prototype.slice.call(arguments, 1); // for a possible method call
33
	var res = this; // what this function will return (this jQuery object by default)
34

  
35
	this.each(function(i, _element) { // loop each DOM element involved
36
		var element = $(_element);
37
		var calendar = element.data('fullCalendar'); // get the existing calendar object (if any)
38
		var singleRes; // the returned value of this single method call
39

  
40
		// a method call
41
		if (typeof options === 'string') {
42
			if (calendar && $.isFunction(calendar[options])) {
43
				singleRes = calendar[options].apply(calendar, args);
44
				if (!i) {
45
					res = singleRes; // record the first method call result
46
				}
47
				if (options === 'destroy') { // for the destroy method, must remove Calendar object data
48
					element.removeData('fullCalendar');
49
				}
50
			}
51
		}
52
		// a new calendar initialization
53
		else if (!calendar) { // don't initialize twice
54
			calendar = new Calendar(element, options);
55
			element.data('fullCalendar', calendar);
56
			calendar.render();
57
		}
58
	});
59

  
60
	return res;
61
};
62

  
63

  
64
var complexOptions = [ // names of options that are objects whose properties should be combined
65
	'header',
66
	'footer',
67
	'buttonText',
68
	'buttonIcons',
69
	'themeButtonIcons'
70
];
71

  
72

  
73
// Merges an array of option objects into a single object
74
function mergeOptions(optionObjs) {
75
	return mergeProps(optionObjs, complexOptions);
76
}
77

  
78
;;
79

  
80
// exports
81
FC.intersectRanges = intersectRanges;
82
FC.applyAll = applyAll;
83
FC.debounce = debounce;
84
FC.isInt = isInt;
85
FC.htmlEscape = htmlEscape;
86
FC.cssToStr = cssToStr;
87
FC.proxy = proxy;
88
FC.capitaliseFirstLetter = capitaliseFirstLetter;
89

  
90

  
91
/* FullCalendar-specific DOM Utilities
92
----------------------------------------------------------------------------------------------------------------------*/
93

  
94

  
95
// Given the scrollbar widths of some other container, create borders/margins on rowEls in order to match the left
96
// and right space that was offset by the scrollbars. A 1-pixel border first, then margin beyond that.
97
function compensateScroll(rowEls, scrollbarWidths) {
98
	if (scrollbarWidths.left) {
99
		rowEls.css({
100
			'border-left-width': 1,
101
			'margin-left': scrollbarWidths.left - 1
102
		});
103
	}
104
	if (scrollbarWidths.right) {
105
		rowEls.css({
106
			'border-right-width': 1,
107
			'margin-right': scrollbarWidths.right - 1
108
		});
109
	}
110
}
111

  
112

  
113
// Undoes compensateScroll and restores all borders/margins
114
function uncompensateScroll(rowEls) {
115
	rowEls.css({
116
		'margin-left': '',
117
		'margin-right': '',
118
		'border-left-width': '',
119
		'border-right-width': ''
120
	});
121
}
122

  
123

  
124
// Make the mouse cursor express that an event is not allowed in the current area
125
function disableCursor() {
126
	$('body').addClass('fc-not-allowed');
127
}
128

  
129

  
130
// Returns the mouse cursor to its original look
131
function enableCursor() {
132
	$('body').removeClass('fc-not-allowed');
133
}
134

  
135

  
136
// Given a total available height to fill, have `els` (essentially child rows) expand to accomodate.
137
// By default, all elements that are shorter than the recommended height are expanded uniformly, not considering
138
// any other els that are already too tall. if `shouldRedistribute` is on, it considers these tall rows and 
139
// reduces the available height.
140
function distributeHeight(els, availableHeight, shouldRedistribute) {
141

  
142
	// *FLOORING NOTE*: we floor in certain places because zoom can give inaccurate floating-point dimensions,
143
	// and it is better to be shorter than taller, to avoid creating unnecessary scrollbars.
144

  
145
	var minOffset1 = Math.floor(availableHeight / els.length); // for non-last element
146
	var minOffset2 = Math.floor(availableHeight - minOffset1 * (els.length - 1)); // for last element *FLOORING NOTE*
147
	var flexEls = []; // elements that are allowed to expand. array of DOM nodes
148
	var flexOffsets = []; // amount of vertical space it takes up
149
	var flexHeights = []; // actual css height
150
	var usedHeight = 0;
151

  
152
	undistributeHeight(els); // give all elements their natural height
153

  
154
	// find elements that are below the recommended height (expandable).
155
	// important to query for heights in a single first pass (to avoid reflow oscillation).
156
	els.each(function(i, el) {
157
		var minOffset = i === els.length - 1 ? minOffset2 : minOffset1;
158
		var naturalOffset = $(el).outerHeight(true);
159

  
160
		if (naturalOffset < minOffset) {
161
			flexEls.push(el);
162
			flexOffsets.push(naturalOffset);
163
			flexHeights.push($(el).height());
164
		}
165
		else {
166
			// this element stretches past recommended height (non-expandable). mark the space as occupied.
167
			usedHeight += naturalOffset;
168
		}
169
	});
170

  
171
	// readjust the recommended height to only consider the height available to non-maxed-out rows.
172
	if (shouldRedistribute) {
173
		availableHeight -= usedHeight;
174
		minOffset1 = Math.floor(availableHeight / flexEls.length);
175
		minOffset2 = Math.floor(availableHeight - minOffset1 * (flexEls.length - 1)); // *FLOORING NOTE*
176
	}
177

  
178
	// assign heights to all expandable elements
179
	$(flexEls).each(function(i, el) {
180
		var minOffset = i === flexEls.length - 1 ? minOffset2 : minOffset1;
181
		var naturalOffset = flexOffsets[i];
182
		var naturalHeight = flexHeights[i];
183
		var newHeight = minOffset - (naturalOffset - naturalHeight); // subtract the margin/padding
184

  
185
		if (naturalOffset < minOffset) { // we check this again because redistribution might have changed things
186
			$(el).height(newHeight);
187
		}
188
	});
189
}
190

  
191

  
192
// Undoes distrubuteHeight, restoring all els to their natural height
193
function undistributeHeight(els) {
194
	els.height('');
195
}
196

  
197

  
198
// Given `els`, a jQuery set of <td> cells, find the cell with the largest natural width and set the widths of all the
199
// cells to be that width.
200
// PREREQUISITE: if you want a cell to take up width, it needs to have a single inner element w/ display:inline
201
function matchCellWidths(els) {
202
	var maxInnerWidth = 0;
203

  
204
	els.find('> *').each(function(i, innerEl) {
205
		var innerWidth = $(innerEl).outerWidth();
206
		if (innerWidth > maxInnerWidth) {
207
			maxInnerWidth = innerWidth;
208
		}
209
	});
210

  
211
	maxInnerWidth++; // sometimes not accurate of width the text needs to stay on one line. insurance
212

  
213
	els.width(maxInnerWidth);
214

  
215
	return maxInnerWidth;
216
}
217

  
218

  
219
// Given one element that resides inside another,
220
// Subtracts the height of the inner element from the outer element.
221
function subtractInnerElHeight(outerEl, innerEl) {
222
	var both = outerEl.add(innerEl);
223
	var diff;
224

  
225
	// effin' IE8/9/10/11 sometimes returns 0 for dimensions. this weird hack was the only thing that worked
226
	both.css({
227
		position: 'relative', // cause a reflow, which will force fresh dimension recalculation
228
		left: -1 // ensure reflow in case the el was already relative. negative is less likely to cause new scroll
229
	});
230
	diff = outerEl.outerHeight() - innerEl.outerHeight(); // grab the dimensions
231
	both.css({ position: '', left: '' }); // undo hack
232

  
233
	return diff;
234
}
235

  
236

  
237
/* Element Geom Utilities
238
----------------------------------------------------------------------------------------------------------------------*/
239

  
240
FC.getOuterRect = getOuterRect;
241
FC.getClientRect = getClientRect;
242
FC.getContentRect = getContentRect;
243
FC.getScrollbarWidths = getScrollbarWidths;
244

  
245

  
246
// borrowed from https://github.com/jquery/jquery-ui/blob/1.11.0/ui/core.js#L51
247
function getScrollParent(el) {
248
	var position = el.css('position'),
249
		scrollParent = el.parents().filter(function() {
250
			var parent = $(this);
251
			return (/(auto|scroll)/).test(
252
				parent.css('overflow') + parent.css('overflow-y') + parent.css('overflow-x')
253
			);
254
		}).eq(0);
255

  
256
	return position === 'fixed' || !scrollParent.length ? $(el[0].ownerDocument || document) : scrollParent;
257
}
258

  
259

  
260
// Queries the outer bounding area of a jQuery element.
261
// Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive).
262
// Origin is optional.
263
function getOuterRect(el, origin) {
264
	var offset = el.offset();
265
	var left = offset.left - (origin ? origin.left : 0);
266
	var top = offset.top - (origin ? origin.top : 0);
267

  
268
	return {
269
		left: left,
270
		right: left + el.outerWidth(),
271
		top: top,
272
		bottom: top + el.outerHeight()
273
	};
274
}
275

  
276

  
277
// Queries the area within the margin/border/scrollbars of a jQuery element. Does not go within the padding.
278
// Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive).
279
// Origin is optional.
280
// WARNING: given element can't have borders
281
// NOTE: should use clientLeft/clientTop, but very unreliable cross-browser.
282
function getClientRect(el, origin) {
283
	var offset = el.offset();
284
	var scrollbarWidths = getScrollbarWidths(el);
285
	var left = offset.left + getCssFloat(el, 'border-left-width') + scrollbarWidths.left - (origin ? origin.left : 0);
286
	var top = offset.top + getCssFloat(el, 'border-top-width') + scrollbarWidths.top - (origin ? origin.top : 0);
287

  
288
	return {
289
		left: left,
290
		right: left + el[0].clientWidth, // clientWidth includes padding but NOT scrollbars
291
		top: top,
292
		bottom: top + el[0].clientHeight // clientHeight includes padding but NOT scrollbars
293
	};
294
}
295

  
296

  
297
// Queries the area within the margin/border/padding of a jQuery element. Assumed not to have scrollbars.
298
// Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive).
299
// Origin is optional.
300
function getContentRect(el, origin) {
301
	var offset = el.offset(); // just outside of border, margin not included
302
	var left = offset.left + getCssFloat(el, 'border-left-width') + getCssFloat(el, 'padding-left') -
303
		(origin ? origin.left : 0);
304
	var top = offset.top + getCssFloat(el, 'border-top-width') + getCssFloat(el, 'padding-top') -
305
		(origin ? origin.top : 0);
306

  
307
	return {
308
		left: left,
309
		right: left + el.width(),
310
		top: top,
311
		bottom: top + el.height()
312
	};
313
}
314

  
315

  
316
// Returns the computed left/right/top/bottom scrollbar widths for the given jQuery element.
317
// WARNING: given element can't have borders (which will cause offsetWidth/offsetHeight to be larger).
318
// NOTE: should use clientLeft/clientTop, but very unreliable cross-browser.
319
function getScrollbarWidths(el) {
320
	var leftRightWidth = el[0].offsetWidth - el[0].clientWidth;
321
	var bottomWidth = el[0].offsetHeight - el[0].clientHeight;
322
	var widths;
323

  
324
	leftRightWidth = sanitizeScrollbarWidth(leftRightWidth);
325
	bottomWidth = sanitizeScrollbarWidth(bottomWidth);
326

  
327
	widths = { left: 0, right: 0, top: 0, bottom: bottomWidth };
328

  
329
	if (getIsLeftRtlScrollbars() && el.css('direction') == 'rtl') { // is the scrollbar on the left side?
330
		widths.left = leftRightWidth;
331
	}
332
	else {
333
		widths.right = leftRightWidth;
334
	}
335

  
336
	return widths;
337
}
338

  
339

  
340
// The scrollbar width computations in getScrollbarWidths are sometimes flawed when it comes to
341
// retina displays, rounding, and IE11. Massage them into a usable value.
342
function sanitizeScrollbarWidth(width) {
343
	width = Math.max(0, width); // no negatives
344
	width = Math.round(width);
345
	return width;
346
}
347

  
348

  
349
// Logic for determining if, when the element is right-to-left, the scrollbar appears on the left side
350

  
351
var _isLeftRtlScrollbars = null;
352

  
353
function getIsLeftRtlScrollbars() { // responsible for caching the computation
354
	if (_isLeftRtlScrollbars === null) {
355
		_isLeftRtlScrollbars = computeIsLeftRtlScrollbars();
356
	}
357
	return _isLeftRtlScrollbars;
358
}
359

  
360
function computeIsLeftRtlScrollbars() { // creates an offscreen test element, then removes it
361
	var el = $('<div><div/></div>')
362
		.css({
363
			position: 'absolute',
364
			top: -1000,
365
			left: 0,
366
			border: 0,
367
			padding: 0,
368
			overflow: 'scroll',
369
			direction: 'rtl'
370
		})
371
		.appendTo('body');
372
	var innerEl = el.children();
373
	var res = innerEl.offset().left > el.offset().left; // is the inner div shifted to accommodate a left scrollbar?
374
	el.remove();
375
	return res;
376
}
377

  
378

  
379
// Retrieves a jQuery element's computed CSS value as a floating-point number.
380
// If the queried value is non-numeric (ex: IE can return "medium" for border width), will just return zero.
381
function getCssFloat(el, prop) {
382
	return parseFloat(el.css(prop)) || 0;
383
}
384

  
385

  
386
/* Mouse / Touch Utilities
387
----------------------------------------------------------------------------------------------------------------------*/
388

  
389
FC.preventDefault = preventDefault;
390

  
391

  
392
// Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac)
393
function isPrimaryMouseButton(ev) {
394
	return ev.which == 1 && !ev.ctrlKey;
395
}
396

  
397

  
398
function getEvX(ev) {
399
	var touches = ev.originalEvent.touches;
400

  
401
	// on mobile FF, pageX for touch events is present, but incorrect,
402
	// so, look at touch coordinates first.
403
	if (touches && touches.length) {
404
		return touches[0].pageX;
405
	}
406

  
407
	return ev.pageX;
408
}
409

  
410

  
411
function getEvY(ev) {
412
	var touches = ev.originalEvent.touches;
413

  
414
	// on mobile FF, pageX for touch events is present, but incorrect,
415
	// so, look at touch coordinates first.
416
	if (touches && touches.length) {
417
		return touches[0].pageY;
418
	}
419

  
420
	return ev.pageY;
421
}
422

  
423

  
424
function getEvIsTouch(ev) {
425
	return /^touch/.test(ev.type);
426
}
427

  
428

  
429
function preventSelection(el) {
430
	el.addClass('fc-unselectable')
431
		.on('selectstart', preventDefault);
432
}
433

  
434

  
435
function allowSelection(el) {
436
	el.removeClass('fc-unselectable')
437
		.off('selectstart', preventDefault);
438
}
439

  
440

  
441
// Stops a mouse/touch event from doing it's native browser action
442
function preventDefault(ev) {
443
	ev.preventDefault();
444
}
445

  
446

  
447
/* General Geometry Utils
448
----------------------------------------------------------------------------------------------------------------------*/
449

  
450
FC.intersectRects = intersectRects;
451

  
452
// Returns a new rectangle that is the intersection of the two rectangles. If they don't intersect, returns false
453
function intersectRects(rect1, rect2) {
454
	var res = {
455
		left: Math.max(rect1.left, rect2.left),
456
		right: Math.min(rect1.right, rect2.right),
457
		top: Math.max(rect1.top, rect2.top),
458
		bottom: Math.min(rect1.bottom, rect2.bottom)
459
	};
460

  
461
	if (res.left < res.right && res.top < res.bottom) {
462
		return res;
463
	}
464
	return false;
465
}
466

  
467

  
468
// Returns a new point that will have been moved to reside within the given rectangle
469
function constrainPoint(point, rect) {
470
	return {
471
		left: Math.min(Math.max(point.left, rect.left), rect.right),
472
		top: Math.min(Math.max(point.top, rect.top), rect.bottom)
473
	};
474
}
475

  
476

  
477
// Returns a point that is the center of the given rectangle
478
function getRectCenter(rect) {
479
	return {
480
		left: (rect.left + rect.right) / 2,
481
		top: (rect.top + rect.bottom) / 2
482
	};
483
}
484

  
485

  
486
// Subtracts point2's coordinates from point1's coordinates, returning a delta
487
function diffPoints(point1, point2) {
488
	return {
489
		left: point1.left - point2.left,
490
		top: point1.top - point2.top
491
	};
492
}
493

  
494

  
495
/* Object Ordering by Field
496
----------------------------------------------------------------------------------------------------------------------*/
497

  
498
FC.parseFieldSpecs = parseFieldSpecs;
499
FC.compareByFieldSpecs = compareByFieldSpecs;
500
FC.compareByFieldSpec = compareByFieldSpec;
501
FC.flexibleCompare = flexibleCompare;
502

  
503

  
504
function parseFieldSpecs(input) {
505
	var specs = [];
506
	var tokens = [];
507
	var i, token;
508

  
509
	if (typeof input === 'string') {
510
		tokens = input.split(/\s*,\s*/);
511
	}
512
	else if (typeof input === 'function') {
513
		tokens = [ input ];
514
	}
515
	else if ($.isArray(input)) {
516
		tokens = input;
517
	}
518

  
519
	for (i = 0; i < tokens.length; i++) {
520
		token = tokens[i];
521

  
522
		if (typeof token === 'string') {
523
			specs.push(
524
				token.charAt(0) == '-' ?
525
					{ field: token.substring(1), order: -1 } :
526
					{ field: token, order: 1 }
527
			);
528
		}
529
		else if (typeof token === 'function') {
530
			specs.push({ func: token });
531
		}
532
	}
533

  
534
	return specs;
535
}
536

  
537

  
538
function compareByFieldSpecs(obj1, obj2, fieldSpecs) {
539
	var i;
540
	var cmp;
541

  
542
	for (i = 0; i < fieldSpecs.length; i++) {
543
		cmp = compareByFieldSpec(obj1, obj2, fieldSpecs[i]);
544
		if (cmp) {
545
			return cmp;
546
		}
547
	}
548

  
549
	return 0;
550
}
551

  
552

  
553
function compareByFieldSpec(obj1, obj2, fieldSpec) {
554
	if (fieldSpec.func) {
555
		return fieldSpec.func(obj1, obj2);
556
	}
557
	return flexibleCompare(obj1[fieldSpec.field], obj2[fieldSpec.field]) *
558
		(fieldSpec.order || 1);
559
}
560

  
561

  
562
function flexibleCompare(a, b) {
563
	if (!a && !b) {
564
		return 0;
565
	}
566
	if (b == null) {
567
		return -1;
568
	}
569
	if (a == null) {
570
		return 1;
571
	}
572
	if ($.type(a) === 'string' || $.type(b) === 'string') {
573
		return String(a).localeCompare(String(b));
574
	}
575
	return a - b;
576
}
577

  
578

  
579
/* FullCalendar-specific Misc Utilities
580
----------------------------------------------------------------------------------------------------------------------*/
581

  
582

  
583
// Computes the intersection of the two ranges. Will return fresh date clones in a range.
584
// Returns undefined if no intersection.
585
// Expects all dates to be normalized to the same timezone beforehand.
586
// TODO: move to date section?
587
function intersectRanges(subjectRange, constraintRange) {
588
	var subjectStart = subjectRange.start;
589
	var subjectEnd = subjectRange.end;
590
	var constraintStart = constraintRange.start;
591
	var constraintEnd = constraintRange.end;
592
	var segStart, segEnd;
593
	var isStart, isEnd;
594

  
595
	if (subjectEnd > constraintStart && subjectStart < constraintEnd) { // in bounds at all?
596

  
597
		if (subjectStart >= constraintStart) {
598
			segStart = subjectStart.clone();
599
			isStart = true;
600
		}
601
		else {
602
			segStart = constraintStart.clone();
603
			isStart =  false;
604
		}
605

  
606
		if (subjectEnd <= constraintEnd) {
607
			segEnd = subjectEnd.clone();
608
			isEnd = true;
609
		}
610
		else {
611
			segEnd = constraintEnd.clone();
612
			isEnd = false;
613
		}
614

  
615
		return {
616
			start: segStart,
617
			end: segEnd,
618
			isStart: isStart,
619
			isEnd: isEnd
620
		};
621
	}
622
}
623

  
624

  
625
/* Date Utilities
626
----------------------------------------------------------------------------------------------------------------------*/
627

  
628
FC.computeGreatestUnit = computeGreatestUnit;
629
FC.divideRangeByDuration = divideRangeByDuration;
630
FC.divideDurationByDuration = divideDurationByDuration;
631
FC.multiplyDuration = multiplyDuration;
632
FC.durationHasTime = durationHasTime;
633

  
634
var dayIDs = [ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ];
635
var unitsDesc = [ 'year', 'month', 'week', 'day', 'hour', 'minute', 'second', 'millisecond' ]; // descending
636

  
637

  
638
// Diffs the two moments into a Duration where full-days are recorded first, then the remaining time.
639
// Moments will have their timezones normalized.
640
function diffDayTime(a, b) {
641
	return moment.duration({
642
		days: a.clone().stripTime().diff(b.clone().stripTime(), 'days'),
643
		ms: a.time() - b.time() // time-of-day from day start. disregards timezone
644
	});
645
}
646

  
647

  
648
// Diffs the two moments via their start-of-day (regardless of timezone). Produces whole-day durations.
649
function diffDay(a, b) {
650
	return moment.duration({
651
		days: a.clone().stripTime().diff(b.clone().stripTime(), 'days')
652
	});
653
}
654

  
655

  
656
// Diffs two moments, producing a duration, made of a whole-unit-increment of the given unit. Uses rounding.
657
function diffByUnit(a, b, unit) {
658
	return moment.duration(
659
		Math.round(a.diff(b, unit, true)), // returnFloat=true
660
		unit
661
	);
662
}
663

  
664

  
665
// Computes the unit name of the largest whole-unit period of time.
666
// For example, 48 hours will be "days" whereas 49 hours will be "hours".
667
// Accepts start/end, a range object, or an original duration object.
668
function computeGreatestUnit(start, end) {
669
	var i, unit;
670
	var val;
671

  
672
	for (i = 0; i < unitsDesc.length; i++) {
673
		unit = unitsDesc[i];
674
		val = computeRangeAs(unit, start, end);
675

  
676
		if (val >= 1 && isInt(val)) {
677
			break;
678
		}
679
	}
680

  
681
	return unit; // will be "milliseconds" if nothing else matches
682
}
683

  
684

  
685
// like computeGreatestUnit, but has special abilities to interpret the source input for clues
686
function computeDurationGreatestUnit(duration, durationInput) {
687
	var unit = computeGreatestUnit(duration);
688

  
689
	// prevent days:7 from being interpreted as a week
690
	if (unit === 'week' && typeof durationInput === 'object' && durationInput.days) {
691
		unit = 'day';
692
	}
693

  
694
	return unit;
695
}
696

  
697

  
698
// Computes the number of units (like "hours") in the given range.
699
// Range can be a {start,end} object, separate start/end args, or a Duration.
700
// Results are based on Moment's .as() and .diff() methods, so results can depend on internal handling
701
// of month-diffing logic (which tends to vary from version to version).
702
function computeRangeAs(unit, start, end) {
703

  
704
	if (end != null) { // given start, end
705
		return end.diff(start, unit, true);
706
	}
707
	else if (moment.isDuration(start)) { // given duration
708
		return start.as(unit);
709
	}
710
	else { // given { start, end } range object
711
		return start.end.diff(start.start, unit, true);
712
	}
713
}
714

  
715

  
716
// Intelligently divides a range (specified by a start/end params) by a duration
717
function divideRangeByDuration(start, end, dur) {
718
	var months;
719

  
720
	if (durationHasTime(dur)) {
721
		return (end - start) / dur;
722
	}
723
	months = dur.asMonths();
724
	if (Math.abs(months) >= 1 && isInt(months)) {
725
		return end.diff(start, 'months', true) / months;
726
	}
727
	return end.diff(start, 'days', true) / dur.asDays();
728
}
729

  
730

  
731
// Intelligently divides one duration by another
732
function divideDurationByDuration(dur1, dur2) {
733
	var months1, months2;
734

  
735
	if (durationHasTime(dur1) || durationHasTime(dur2)) {
736
		return dur1 / dur2;
737
	}
738
	months1 = dur1.asMonths();
739
	months2 = dur2.asMonths();
740
	if (
741
		Math.abs(months1) >= 1 && isInt(months1) &&
742
		Math.abs(months2) >= 1 && isInt(months2)
743
	) {
744
		return months1 / months2;
745
	}
746
	return dur1.asDays() / dur2.asDays();
747
}
748

  
749

  
750
// Intelligently multiplies a duration by a number
751
function multiplyDuration(dur, n) {
752
	var months;
753

  
754
	if (durationHasTime(dur)) {
755
		return moment.duration(dur * n);
756
	}
757
	months = dur.asMonths();
758
	if (Math.abs(months) >= 1 && isInt(months)) {
759
		return moment.duration({ months: months * n });
760
	}
761
	return moment.duration({ days: dur.asDays() * n });
762
}
763

  
764

  
765
function cloneRange(range) {
766
	return {
767
		start: range.start.clone(),
768
		end: range.end.clone()
769
	};
770
}
771

  
772

  
773
// Trims the beginning and end of inner range to be completely within outerRange.
774
// Returns a new range object.
775
function constrainRange(innerRange, outerRange) {
776
	innerRange = cloneRange(innerRange);
777

  
778
	if (outerRange.start) {
779
		// needs to be inclusively before outerRange's end
780
		innerRange.start = constrainDate(innerRange.start, outerRange);
781
	}
782

  
783
	if (outerRange.end) {
784
		innerRange.end = minMoment(innerRange.end, outerRange.end);
785
	}
786

  
787
	return innerRange;
788
}
789

  
790

  
791
// If the given date is not within the given range, move it inside.
792
// (If it's past the end, make it one millisecond before the end).
793
// Always returns a new moment.
794
function constrainDate(date, range) {
795
	date = date.clone();
796

  
797
	if (range.start) {
798
		date = maxMoment(date, range.start);
799
	}
800

  
801
	if (range.end && date >= range.end) {
802
		date = range.end.clone().subtract(1);
803
	}
804

  
805
	return date;
806
}
807

  
808

  
809
function isDateWithinRange(date, range) {
810
	return (!range.start || date >= range.start) &&
811
		(!range.end || date < range.end);
812
}
813

  
814

  
815
// TODO: deal with repeat code in intersectRanges
816
// constraintRange can have unspecified start/end, an open-ended range.
817
function doRangesIntersect(subjectRange, constraintRange) {
818
	return (!constraintRange.start || subjectRange.end >= constraintRange.start) &&
819
		(!constraintRange.end || subjectRange.start < constraintRange.end);
820
}
821

  
822

  
823
function isRangeWithinRange(innerRange, outerRange) {
824
	return (!outerRange.start || innerRange.start >= outerRange.start) &&
825
		(!outerRange.end || innerRange.end <= outerRange.end);
826
}
827

  
828

  
829
function isRangesEqual(range0, range1) {
830
	return ((range0.start && range1.start && range0.start.isSame(range1.start)) || (!range0.start && !range1.start)) &&
831
		((range0.end && range1.end && range0.end.isSame(range1.end)) || (!range0.end && !range1.end));
832
}
833

  
834

  
835
// Returns the moment that's earlier in time. Always a copy.
836
function minMoment(mom1, mom2) {
837
	return (mom1.isBefore(mom2) ? mom1 : mom2).clone();
838
}
839

  
840

  
841
// Returns the moment that's later in time. Always a copy.
842
function maxMoment(mom1, mom2) {
843
	return (mom1.isAfter(mom2) ? mom1 : mom2).clone();
844
}
845

  
846

  
847
// Returns a boolean about whether the given duration has any time parts (hours/minutes/seconds/ms)
848
function durationHasTime(dur) {
849
	return Boolean(dur.hours() || dur.minutes() || dur.seconds() || dur.milliseconds());
850
}
851

  
852

  
853
function isNativeDate(input) {
854
	return  Object.prototype.toString.call(input) === '[object Date]' || input instanceof Date;
855
}
856

  
857

  
858
// Returns a boolean about whether the given input is a time string, like "06:40:00" or "06:00"
859
function isTimeString(str) {
860
	return /^\d+\:\d+(?:\:\d+\.?(?:\d{3})?)?$/.test(str);
861
}
862

  
863

  
864
/* Logging and Debug
865
----------------------------------------------------------------------------------------------------------------------*/
866

  
867
FC.log = function() {
868
	var console = window.console;
869

  
870
	if (console && console.log) {
871
		return console.log.apply(console, arguments);
872
	}
873
};
874

  
875
FC.warn = function() {
876
	var console = window.console;
877

  
878
	if (console && console.warn) {
879
		return console.warn.apply(console, arguments);
880
	}
881
	else {
882
		return FC.log.apply(FC, arguments);
883
	}
884
};
885

  
886

  
887
/* General Utilities
888
----------------------------------------------------------------------------------------------------------------------*/
889

  
890
var hasOwnPropMethod = {}.hasOwnProperty;
891

  
892

  
893
// Merges an array of objects into a single object.
894
// The second argument allows for an array of property names who's object values will be merged together.
895
function mergeProps(propObjs, complexProps) {
896
	var dest = {};
897
	var i, name;
898
	var complexObjs;
899
	var j, val;
900
	var props;
901

  
902
	if (complexProps) {
903
		for (i = 0; i < complexProps.length; i++) {
904
			name = complexProps[i];
905
			complexObjs = [];
906

  
907
			// collect the trailing object values, stopping when a non-object is discovered
908
			for (j = propObjs.length - 1; j >= 0; j--) {
909
				val = propObjs[j][name];
910

  
911
				if (typeof val === 'object') {
912
					complexObjs.unshift(val);
913
				}
914
				else if (val !== undefined) {
915
					dest[name] = val; // if there were no objects, this value will be used
916
					break;
917
				}
918
			}
919

  
920
			// if the trailing values were objects, use the merged value
921
			if (complexObjs.length) {
922
				dest[name] = mergeProps(complexObjs);
923
			}
924
		}
925
	}
926

  
927
	// copy values into the destination, going from last to first
928
	for (i = propObjs.length - 1; i >= 0; i--) {
929
		props = propObjs[i];
930

  
931
		for (name in props) {
932
			if (!(name in dest)) { // if already assigned by previous props or complex props, don't reassign
933
				dest[name] = props[name];
934
			}
935
		}
936
	}
937

  
938
	return dest;
939
}
940

  
941

  
942
// Create an object that has the given prototype. Just like Object.create
943
function createObject(proto) {
944
	var f = function() {};
945
	f.prototype = proto;
946
	return new f();
947
}
948
FC.createObject = createObject;
949

  
950

  
951
function copyOwnProps(src, dest) {
952
	for (var name in src) {
953
		if (hasOwnProp(src, name)) {
954
			dest[name] = src[name];
955
		}
956
	}
957
}
958

  
959

  
960
function hasOwnProp(obj, name) {
961
	return hasOwnPropMethod.call(obj, name);
962
}
963

  
964

  
965
// Is the given value a non-object non-function value?
966
function isAtomic(val) {
967
	return /undefined|null|boolean|number|string/.test($.type(val));
968
}
969

  
970

  
971
function applyAll(functions, thisObj, args) {
972
	if ($.isFunction(functions)) {
973
		functions = [ functions ];
974
	}
975
	if (functions) {
976
		var i;
977
		var ret;
978
		for (i=0; i<functions.length; i++) {
979
			ret = functions[i].apply(thisObj, args) || ret;
980
		}
981
		return ret;
982
	}
983
}
984

  
985

  
986
function firstDefined() {
987
	for (var i=0; i<arguments.length; i++) {
988
		if (arguments[i] !== undefined) {
989
			return arguments[i];
990
		}
991
	}
992
}
993

  
994

  
995
function htmlEscape(s) {
996
	return (s + '').replace(/&/g, '&amp;')
997
		.replace(/</g, '&lt;')
998
		.replace(/>/g, '&gt;')
999
		.replace(/'/g, '&#039;')
1000
		.replace(/"/g, '&quot;')
1001
		.replace(/\n/g, '<br />');
1002
}
1003

  
1004

  
1005
function stripHtmlEntities(text) {
1006
	return text.replace(/&.*?;/g, '');
1007
}
1008

  
1009

  
1010
// Given a hash of CSS properties, returns a string of CSS.
1011
// Uses property names as-is (no camel-case conversion). Will not make statements for null/undefined values.
1012
function cssToStr(cssProps) {
1013
	var statements = [];
1014

  
1015
	$.each(cssProps, function(name, val) {
1016
		if (val != null) {
1017
			statements.push(name + ':' + val);
1018
		}
1019
	});
1020

  
1021
	return statements.join(';');
1022
}
1023

  
1024

  
1025
// Given an object hash of HTML attribute names to values,
1026
// generates a string that can be injected between < > in HTML
1027
function attrsToStr(attrs) {
1028
	var parts = [];
1029

  
1030
	$.each(attrs, function(name, val) {
1031
		if (val != null) {
1032
			parts.push(name + '="' + htmlEscape(val) + '"');
1033
		}
1034
	});
1035

  
1036
	return parts.join(' ');
1037
}
1038

  
1039

  
1040
function capitaliseFirstLetter(str) {
1041
	return str.charAt(0).toUpperCase() + str.slice(1);
1042
}
1043

  
1044

  
1045
function compareNumbers(a, b) { // for .sort()
1046
	return a - b;
1047
}
1048

  
1049

  
1050
function isInt(n) {
1051
	return n % 1 === 0;
1052
}
1053

  
1054

  
1055
// Returns a method bound to the given object context.
1056
// Just like one of the jQuery.proxy signatures, but without the undesired behavior of treating the same method with
1057
// different contexts as identical when binding/unbinding events.
1058
function proxy(obj, methodName) {
1059
	var method = obj[methodName];
1060

  
1061
	return function() {
1062
		return method.apply(obj, arguments);
1063
	};
1064
}
1065

  
1066

  
1067
// Returns a function, that, as long as it continues to be invoked, will not
1068
// be triggered. The function will be called after it stops being called for
1069
// N milliseconds. If `immediate` is passed, trigger the function on the
1070
// leading edge, instead of the trailing.
1071
// https://github.com/jashkenas/underscore/blob/1.6.0/underscore.js#L714
1072
function debounce(func, wait, immediate) {
1073
	var timeout, args, context, timestamp, result;
1074

  
1075
	var later = function() {
1076
		var last = +new Date() - timestamp;
1077
		if (last < wait) {
1078
			timeout = setTimeout(later, wait - last);
1079
		}
1080
		else {
1081
			timeout = null;
1082
			if (!immediate) {
1083
				result = func.apply(context, args);
1084
				context = args = null;
1085
			}
1086
		}
1087
	};
1088

  
1089
	return function() {
1090
		context = this;
1091
		args = arguments;
1092
		timestamp = +new Date();
1093
		var callNow = immediate && !timeout;
1094
		if (!timeout) {
1095
			timeout = setTimeout(later, wait);
1096
		}
1097
		if (callNow) {
1098
			result = func.apply(context, args);
1099
			context = args = null;
1100
		}
1101
		return result;
1102
	};
1103
}
1104

  
1105
;;
1106

  
1107
/*
1108
GENERAL NOTE on moments throughout the *entire rest* of the codebase:
1109
All moments are assumed to be ambiguously-zoned unless otherwise noted,
1110
with the NOTABLE EXCEOPTION of start/end dates that live on *Event Objects*.
1111
Ambiguously-TIMED moments are assumed to be ambiguously-zoned by nature.
1112
*/
1113

  
1114
var ambigDateOfMonthRegex = /^\s*\d{4}-\d\d$/;
1115
var ambigTimeOrZoneRegex =
1116
	/^\s*\d{4}-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?)?$/;
1117
var newMomentProto = moment.fn; // where we will attach our new methods
1118
var oldMomentProto = $.extend({}, newMomentProto); // copy of original moment methods
1119

  
1120
// tell momentjs to transfer these properties upon clone
1121
var momentProperties = moment.momentProperties;
1122
momentProperties.push('_fullCalendar');
1123
momentProperties.push('_ambigTime');
1124
momentProperties.push('_ambigZone');
1125

  
1126

  
1127
// Creating
1128
// -------------------------------------------------------------------------------------------------
1129

  
1130
// Creates a new moment, similar to the vanilla moment(...) constructor, but with
1131
// extra features (ambiguous time, enhanced formatting). When given an existing moment,
1132
// it will function as a clone (and retain the zone of the moment). Anything else will
1133
// result in a moment in the local zone.
1134
FC.moment = function() {
1135
	return makeMoment(arguments);
1136
};
1137

  
1138
// Sames as FC.moment, but forces the resulting moment to be in the UTC timezone.
1139
FC.moment.utc = function() {
1140
	var mom = makeMoment(arguments, true);
1141

  
1142
	// Force it into UTC because makeMoment doesn't guarantee it
1143
	// (if given a pre-existing moment for example)
1144
	if (mom.hasTime()) { // don't give ambiguously-timed moments a UTC zone
1145
		mom.utc();
1146
	}
1147

  
1148
	return mom;
1149
};
1150

  
1151
// Same as FC.moment, but when given an ISO8601 string, the timezone offset is preserved.
1152
// ISO8601 strings with no timezone offset will become ambiguously zoned.
1153
FC.moment.parseZone = function() {
1154
	return makeMoment(arguments, true, true);
1155
};
1156

  
1157
// Builds an enhanced moment from args. When given an existing moment, it clones. When given a
1158
// native Date, or called with no arguments (the current time), the resulting moment will be local.
1159
// Anything else needs to be "parsed" (a string or an array), and will be affected by:
1160
//    parseAsUTC - if there is no zone information, should we parse the input in UTC?
1161
//    parseZone - if there is zone information, should we force the zone of the moment?
1162
function makeMoment(args, parseAsUTC, parseZone) {
1163
	var input = args[0];
1164
	var isSingleString = args.length == 1 && typeof input === 'string';
1165
	var isAmbigTime;
1166
	var isAmbigZone;
1167
	var ambigMatch;
1168
	var mom;
1169

  
1170
	if (moment.isMoment(input) || isNativeDate(input) || input === undefined) {
1171
		mom = moment.apply(null, args);
1172
	}
1173
	else { // "parsing" is required
1174
		isAmbigTime = false;
1175
		isAmbigZone = false;
1176

  
1177
		if (isSingleString) {
1178
			if (ambigDateOfMonthRegex.test(input)) {
1179
				// accept strings like '2014-05', but convert to the first of the month
1180
				input += '-01';
1181
				args = [ input ]; // for when we pass it on to moment's constructor
1182
				isAmbigTime = true;
1183
				isAmbigZone = true;
1184
			}
1185
			else if ((ambigMatch = ambigTimeOrZoneRegex.exec(input))) {
1186
				isAmbigTime = !ambigMatch[5]; // no time part?
1187
				isAmbigZone = true;
1188
			}
1189
		}
1190
		else if ($.isArray(input)) {
1191
			// arrays have no timezone information, so assume ambiguous zone
1192
			isAmbigZone = true;
1193
		}
1194
		// otherwise, probably a string with a format
1195

  
1196
		if (parseAsUTC || isAmbigTime) {
1197
			mom = moment.utc.apply(moment, args);
1198
		}
1199
		else {
1200
			mom = moment.apply(null, args);
1201
		}
1202

  
1203
		if (isAmbigTime) {
1204
			mom._ambigTime = true;
1205
			mom._ambigZone = true; // ambiguous time always means ambiguous zone
1206
		}
1207
		else if (parseZone) { // let's record the inputted zone somehow
1208
			if (isAmbigZone) {
1209
				mom._ambigZone = true;
1210
			}
1211
			else if (isSingleString) {
1212
				mom.utcOffset(input); // if not a valid zone, will assign UTC
1213
			}
1214
		}
1215
	}
1216

  
1217
	mom._fullCalendar = true; // flag for extended functionality
1218

  
1219
	return mom;
1220
}
1221

  
1222

  
1223
// Week Number
1224
// -------------------------------------------------------------------------------------------------
1225

  
1226

  
1227
// Returns the week number, considering the locale's custom week number calcuation
1228
// `weeks` is an alias for `week`
1229
newMomentProto.week = newMomentProto.weeks = function(input) {
1230
	var weekCalc = this._locale._fullCalendar_weekCalc;
1231

  
1232
	if (input == null && typeof weekCalc === 'function') { // custom function only works for getter
1233
		return weekCalc(this);
1234
	}
1235
	else if (weekCalc === 'ISO') {
1236
		return oldMomentProto.isoWeek.apply(this, arguments); // ISO getter/setter
1237
	}
1238

  
1239
	return oldMomentProto.week.apply(this, arguments); // local getter/setter
1240
};
1241

  
1242

  
1243
// Time-of-day
1244
// -------------------------------------------------------------------------------------------------
1245

  
1246
// GETTER
1247
// Returns a Duration with the hours/minutes/seconds/ms values of the moment.
1248
// If the moment has an ambiguous time, a duration of 00:00 will be returned.
1249
//
1250
// SETTER
1251
// You can supply a Duration, a Moment, or a Duration-like argument.
1252
// When setting the time, and the moment has an ambiguous time, it then becomes unambiguous.
1253
newMomentProto.time = function(time) {
1254

  
1255
	// Fallback to the original method (if there is one) if this moment wasn't created via FullCalendar.
1256
	// `time` is a generic enough method name where this precaution is necessary to avoid collisions w/ other plugins.
1257
	if (!this._fullCalendar) {
1258
		return oldMomentProto.time.apply(this, arguments);
1259
	}
1260

  
1261
	if (time == null) { // getter
1262
		return moment.duration({
1263
			hours: this.hours(),
1264
			minutes: this.minutes(),
1265
			seconds: this.seconds(),
1266
			milliseconds: this.milliseconds()
1267
		});
1268
	}
1269
	else { // setter
1270

  
1271
		this._ambigTime = false; // mark that the moment now has a time
1272

  
1273
		if (!moment.isDuration(time) && !moment.isMoment(time)) {
1274
			time = moment.duration(time);
1275
		}
1276

  
1277
		// The day value should cause overflow (so 24 hours becomes 00:00:00 of next day).
1278
		// Only for Duration times, not Moment times.
1279
		var dayHours = 0;
1280
		if (moment.isDuration(time)) {
1281
			dayHours = Math.floor(time.asDays()) * 24;
1282
		}
1283

  
1284
		// We need to set the individual fields.
1285
		// Can't use startOf('day') then add duration. In case of DST at start of day.
1286
		return this.hours(dayHours + time.hours())
1287
			.minutes(time.minutes())
1288
			.seconds(time.seconds())
1289
			.milliseconds(time.milliseconds());
1290
	}
1291
};
1292

  
1293
// Converts the moment to UTC, stripping out its time-of-day and timezone offset,
1294
// but preserving its YMD. A moment with a stripped time will display no time
1295
// nor timezone offset when .format() is called.
1296
newMomentProto.stripTime = function() {
1297

  
1298
	if (!this._ambigTime) {
1299

  
1300
		this.utc(true); // keepLocalTime=true (for keeping *date* value)
1301

  
1302
		// set time to zero
1303
		this.set({
1304
			hours: 0,
1305
			minutes: 0,
1306
			seconds: 0,
1307
			ms: 0
1308
		});
1309

  
1310
		// Mark the time as ambiguous. This needs to happen after the .utc() call, which might call .utcOffset(),
1311
		// which clears all ambig flags.
1312
		this._ambigTime = true;
1313
		this._ambigZone = true; // if ambiguous time, also ambiguous timezone offset
1314
	}
1315

  
1316
	return this; // for chaining
1317
};
1318

  
1319
// Returns if the moment has a non-ambiguous time (boolean)
1320
newMomentProto.hasTime = function() {
1321
	return !this._ambigTime;
1322
};
1323

  
1324

  
1325
// Timezone
1326
// -------------------------------------------------------------------------------------------------
1327

  
1328
// Converts the moment to UTC, stripping out its timezone offset, but preserving its
1329
// YMD and time-of-day. A moment with a stripped timezone offset will display no
1330
// timezone offset when .format() is called.
1331
newMomentProto.stripZone = function() {
1332
	var wasAmbigTime;
1333

  
1334
	if (!this._ambigZone) {
1335

  
1336
		wasAmbigTime = this._ambigTime;
1337

  
1338
		this.utc(true); // keepLocalTime=true (for keeping date and time values)
1339

  
1340
		// the above call to .utc()/.utcOffset() unfortunately might clear the ambig flags, so restore
1341
		this._ambigTime = wasAmbigTime || false;
1342

  
1343
		// Mark the zone as ambiguous. This needs to happen after the .utc() call, which might call .utcOffset(),
1344
		// which clears the ambig flags.
1345
		this._ambigZone = true;
1346
	}
1347

  
1348
	return this; // for chaining
1349
};
1350

  
1351
// Returns of the moment has a non-ambiguous timezone offset (boolean)
1352
newMomentProto.hasZone = function() {
1353
	return !this._ambigZone;
1354
};
1355

  
1356

  
1357
// implicitly marks a zone
1358
newMomentProto.local = function(keepLocalTime) {
1359

  
1360
	// for when converting from ambiguously-zoned to local,
1361
	// keep the time values when converting from UTC -> local
1362
	oldMomentProto.local.call(this, this._ambigZone || keepLocalTime);
1363

  
1364
	// ensure non-ambiguous
1365
	// this probably already happened via local() -> utcOffset(), but don't rely on Moment's internals
1366
	this._ambigTime = false;
1367
	this._ambigZone = false;
1368

  
1369
	return this; // for chaining
1370
};
1371

  
1372

  
1373
// implicitly marks a zone
1374
newMomentProto.utc = function(keepLocalTime) {
1375

  
1376
	oldMomentProto.utc.call(this, keepLocalTime);
1377

  
1378
	// ensure non-ambiguous
1379
	// this probably already happened via utc() -> utcOffset(), but don't rely on Moment's internals
1380
	this._ambigTime = false;
1381
	this._ambigZone = false;
1382

  
1383
	return this;
1384
};
1385

  
1386

  
1387
// implicitly marks a zone (will probably get called upon .utc() and .local())
1388
newMomentProto.utcOffset = function(tzo) {
1389

  
1390
	if (tzo != null) { // setter
1391
		// these assignments needs to happen before the original zone method is called.
1392
		// I forget why, something to do with a browser crash.
1393
		this._ambigTime = false;
1394
		this._ambigZone = false;
1395
	}
1396

  
1397
	return oldMomentProto.utcOffset.apply(this, arguments);
1398
};
1399

  
1400

  
1401
// Formatting
1402
// -------------------------------------------------------------------------------------------------
1403

  
1404
newMomentProto.format = function() {
1405

  
1406
	if (this._fullCalendar && arguments[0]) { // an enhanced moment? and a format string provided?
1407
		return formatDate(this, arguments[0]); // our extended formatting
1408
	}
1409
	if (this._ambigTime) {
1410
		return oldMomentFormat(englishMoment(this), 'YYYY-MM-DD');
1411
	}
1412
	if (this._ambigZone) {
1413
		return oldMomentFormat(englishMoment(this), 'YYYY-MM-DD[T]HH:mm:ss');
1414
	}
1415
	if (this._fullCalendar) { // enhanced non-ambig moment?
1416
		// moment.format() doesn't ensure english, but we want to.
1417
		return oldMomentFormat(englishMoment(this));
1418
	}
1419

  
1420
	return oldMomentProto.format.apply(this, arguments);
1421
};
1422

  
1423
newMomentProto.toISOString = function() {
1424

  
1425
	if (this._ambigTime) {
1426
		return oldMomentFormat(englishMoment(this), 'YYYY-MM-DD');
1427
	}
1428
	if (this._ambigZone) {
1429
		return oldMomentFormat(englishMoment(this), 'YYYY-MM-DD[T]HH:mm:ss');
1430
	}
1431
	if (this._fullCalendar) { // enhanced non-ambig moment?
1432
		// depending on browser, moment might not output english. ensure english.
1433
		// https://github.com/moment/moment/blob/2.18.1/src/lib/moment/format.js#L22
1434
		return oldMomentProto.toISOString.apply(englishMoment(this), arguments);
1435
	}
1436

  
1437
	return oldMomentProto.toISOString.apply(this, arguments);
1438
};
1439

  
1440
function englishMoment(mom) {
1441
	if (mom.locale() !== 'en') {
1442
		return mom.clone().locale('en');
1443
	}
1444
	return mom;
1445
}
1446

  
1447
;;
1448
(function() {
1449

  
1450
// exports
1451
FC.formatDate = formatDate;
1452
FC.formatRange = formatRange;
1453
FC.oldMomentFormat = oldMomentFormat;
1454
FC.queryMostGranularFormatUnit = queryMostGranularFormatUnit;
1455

  
1456

  
1457
// Config
1458
// ---------------------------------------------------------------------------------------------------------------------
1459

  
1460
/*
1461
Inserted between chunks in the fake ("intermediate") formatting string.
1462
Important that it passes as whitespace (\s) because moment often identifies non-standalone months
1463
via a regexp with an \s.
1464
*/
1465
var PART_SEPARATOR = '\u000b'; // vertical tab
1466

  
1467
/*
1468
Inserted as the first character of a literal-text chunk to indicate that the literal text is not actually literal text,
1469
but rather, a "special" token that has custom rendering (see specialTokens map).
1470
*/
1471
var SPECIAL_TOKEN_MARKER = '\u001f'; // information separator 1
1472

  
1473
/*
1474
Inserted at the beginning and end of a span of text that must have non-zero numeric characters.
1475
Handling of these markers is done in a post-processing step at the very end of text rendering.
1476
*/
1477
var MAYBE_MARKER = '\u001e'; // information separator 2
1478
var MAYBE_REGEXP = new RegExp(MAYBE_MARKER + '([^' + MAYBE_MARKER + ']*)' + MAYBE_MARKER, 'g'); // must be global
1479

  
1480
/*
1481
Addition formatting tokens we want recognized
1482
*/
1483
var specialTokens = {
1484
	t: function(date) { // "a" or "p"
1485
		return oldMomentFormat(date, 'a').charAt(0);
1486
	},
1487
	T: function(date) { // "A" or "P"
1488
		return oldMomentFormat(date, 'A').charAt(0);
1489
	}
1490
};
1491

  
1492
/*
1493
The first characters of formatting tokens for units that are 1 day or larger.
1494
`value` is for ranking relative size (lower means bigger).
1495
`unit` is a normalized unit, used for comparing moments.
1496
*/
1497
var largeTokenMap = {
1498
	Y: { value: 1, unit: 'year' },
1499
	M: { value: 2, unit: 'month' },
1500
	W: { value: 3, unit: 'week' }, // ISO week
1501
	w: { value: 3, unit: 'week' }, // local week
1502
	D: { value: 4, unit: 'day' }, // day of month
1503
	d: { value: 4, unit: 'day' } // day of week
1504
};
1505

  
1506

  
1507
// Single Date Formatting
1508
// ---------------------------------------------------------------------------------------------------------------------
1509

  
1510
/*
1511
Formats `date` with a Moment formatting string, but allow our non-zero areas and special token
1512
*/
1513
function formatDate(date, formatStr) {
1514
	return renderFakeFormatString(
1515
		getParsedFormatString(formatStr).fakeFormatString,
1516
		date
1517
	);
1518
}
1519

  
1520
/*
1521
Call this if you want Moment's original format method to be used
1522
*/
1523
function oldMomentFormat(mom, formatStr) {
1524
	return oldMomentProto.format.call(mom, formatStr); // oldMomentProto defined in moment-ext.js
1525
}
1526

  
1527

  
1528
// Date Range Formatting
1529
// -------------------------------------------------------------------------------------------------
1530
// TODO: make it work with timezone offset
1531

  
1532
/*
1533
Using a formatting string meant for a single date, generate a range string, like
1534
"Sep 2 - 9 2013", that intelligently inserts a separator where the dates differ.
1535
If the dates are the same as far as the format string is concerned, just return a single
1536
rendering of one date, without any separator.
1537
*/
1538
function formatRange(date1, date2, formatStr, separator, isRTL) {
1539
	var localeData;
1540

  
1541
	date1 = FC.moment.parseZone(date1);
1542
	date2 = FC.moment.parseZone(date2);
1543

  
1544
	localeData = date1.localeData();
1545

  
1546
	// Expand localized format strings, like "LL" -> "MMMM D YYYY".
1547
	// BTW, this is not important for `formatDate` because it is impossible to put custom tokens
1548
	// or non-zero areas in Moment's localized format strings.
1549
	formatStr = localeData.longDateFormat(formatStr) || formatStr;
1550

  
1551
	return renderParsedFormat(
1552
		getParsedFormatString(formatStr),
1553
		date1,
1554
		date2,
1555
		separator || ' - ',
1556
		isRTL
1557
	);
1558
}
1559

  
1560
/*
1561
Renders a range with an already-parsed format string.
1562
*/
1563
function renderParsedFormat(parsedFormat, date1, date2, separator, isRTL) {
1564
	var sameUnits = parsedFormat.sameUnits;
1565
	var unzonedDate1 = date1.clone().stripZone(); // for same-unit comparisons
1566
	var unzonedDate2 = date2.clone().stripZone(); // "
1567

  
1568
	var renderedParts1 = renderFakeFormatStringParts(parsedFormat.fakeFormatString, date1);
1569
	var renderedParts2 = renderFakeFormatStringParts(parsedFormat.fakeFormatString, date2);
1570

  
1571
	var leftI;
1572
	var leftStr = '';
1573
	var rightI;
1574
	var rightStr = '';
1575
	var middleI;
1576
	var middleStr1 = '';
1577
	var middleStr2 = '';
1578
	var middleStr = '';
1579

  
1580
	// Start at the leftmost side of the formatting string and continue until you hit a token
1581
	// that is not the same between dates.
1582
	for (
1583
		leftI = 0;
1584
		leftI < sameUnits.length && (!sameUnits[leftI] || unzonedDate1.isSame(unzonedDate2, sameUnits[leftI]));
1585
		leftI++
1586
	) {
1587
		leftStr += renderedParts1[leftI];
1588
	}
1589

  
1590
	// Similarly, start at the rightmost side of the formatting string and move left
1591
	for (
1592
		rightI = sameUnits.length - 1;
1593
		rightI > leftI && (!sameUnits[rightI] || unzonedDate1.isSame(unzonedDate2, sameUnits[rightI]));
1594
		rightI--
1595
	) {
1596
		// If current chunk is on the boundary of unique date-content, and is a special-case
1597
		// date-formatting postfix character, then don't consume it. Consider it unique date-content.
1598
		// TODO: make configurable
1599
		if (rightI - 1 === leftI && renderedParts1[rightI] === '.') {
1600
			break;
1601
		}
1602

  
1603
		rightStr = renderedParts1[rightI] + rightStr;
1604
	}
1605

  
1606
	// The area in the middle is different for both of the dates.
1607
	// Collect them distinctly so we can jam them together later.
1608
	for (middleI = leftI; middleI <= rightI; middleI++) {
1609
		middleStr1 += renderedParts1[middleI];
1610
		middleStr2 += renderedParts2[middleI];
1611
	}
1612

  
1613
	if (middleStr1 || middleStr2) {
1614
		if (isRTL) {
1615
			middleStr = middleStr2 + separator + middleStr1;
1616
		}
1617
		else {
1618
			middleStr = middleStr1 + separator + middleStr2;
1619
		}
1620
	}
1621

  
1622
	return processMaybeMarkers(
1623
		leftStr + middleStr + rightStr
1624
	);
1625
}
1626

  
1627

  
1628
// Format String Parsing
1629
// ---------------------------------------------------------------------------------------------------------------------
1630

  
1631
var parsedFormatStrCache = {};
1632

  
1633
/*
1634
Returns a parsed format string, leveraging a cache.
1635
*/
1636
function getParsedFormatString(formatStr) {
1637
	return parsedFormatStrCache[formatStr] ||
1638
		(parsedFormatStrCache[formatStr] = parseFormatString(formatStr));
1639
}
1640

  
1641
/*
1642
Parses a format string into the following:
1643
- fakeFormatString: a momentJS formatting string, littered with special control characters that get post-processed.
1644
- sameUnits: for every part in fakeFormatString, if the part is a token, the value will be a unit string (like "day"),
1645
  that indicates how similar a range's start & end must be in order to share the same formatted text.
1646
  If not a token, then the value is null.
1647
  Always a flat array (not nested liked "chunks").
1648
*/
1649
function parseFormatString(formatStr) {
1650
	var chunks = chunkFormatString(formatStr);
1651
	
1652
	return {
1653
		fakeFormatString: buildFakeFormatString(chunks),
1654
		sameUnits: buildSameUnits(chunks)
1655
	};
1656
}
1657

  
1658
/*
1659
Break the formatting string into an array of chunks.
1660
A 'maybe' chunk will have nested chunks.
1661
*/
1662
function chunkFormatString(formatStr) {
1663
	var chunks = [];
1664
	var match;
1665

  
1666
	// TODO: more descrimination
1667
	// \4 is a backreference to the first character of a multi-character set.
1668
	var chunker = /\[([^\]]*)\]|\(([^\)]*)\)|(LTS|LT|(\w)\4*o?)|([^\w\[\(]+)/g;
1669

  
1670
	while ((match = chunker.exec(formatStr))) {
1671
		if (match[1]) { // a literal string inside [ ... ]
1672
			chunks.push.apply(chunks, // append
1673
				splitStringLiteral(match[1])
1674
			);
1675
		}
1676
		else if (match[2]) { // non-zero formatting inside ( ... )
1677
			chunks.push({ maybe: chunkFormatString(match[2]) });
1678
		}
1679
		else if (match[3]) { // a formatting token
1680
			chunks.push({ token: match[3] });
1681
		}
1682
		else if (match[5]) { // an unenclosed literal string
1683
			chunks.push.apply(chunks, // append
1684
				splitStringLiteral(match[5])
1685
			);
1686
		}
1687
	}
1688

  
1689
	return chunks;
1690
}
1691

  
1692
/*
1693
Potentially splits a literal-text string into multiple parts. For special cases.
1694
*/
1695
function splitStringLiteral(s) {
1696
	if (s === '. ') {
1697
		return [ '.', ' ' ]; // for locales with periods bound to the end of each year/month/date
1698
	}
1699
	else {
1700
		return [ s ];
1701
	}
1702
}
1703

  
1704
/*
1705
Given chunks parsed from a real format string, generate a fake (aka "intermediate") format string with special control
1706
characters that will eventually be given to moment for formatting, and then post-processed.
1707
*/
1708
function buildFakeFormatString(chunks) {
1709
	var parts = [];
1710
	var i, chunk;
1711

  
1712
	for (i = 0; i < chunks.length; i++) {
1713
		chunk = chunks[i];
1714

  
1715
		if (typeof chunk === 'string') {
1716
			parts.push('[' + chunk + ']');
1717
		}
1718
		else if (chunk.token) {
1719
			if (chunk.token in specialTokens) {
1720
				parts.push(
1721
					SPECIAL_TOKEN_MARKER + // useful during post-processing
1722
					'[' + chunk.token + ']' // preserve as literal text
1723
				);
1724
			}
1725
			else {
1726
				parts.push(chunk.token); // unprotected text implies a format string
1727
			}
1728
		}
1729
		else if (chunk.maybe) {
1730
			parts.push(
1731
				MAYBE_MARKER + // useful during post-processing
1732
				buildFakeFormatString(chunk.maybe) +
1733
				MAYBE_MARKER
1734
			);
1735
		}
1736
	}
1737

  
1738
	return parts.join(PART_SEPARATOR);
1739
}
1740

  
1741
/*
1742
Given parsed chunks from a real formatting string, generates an array of unit strings (like "day") that indicate
1743
in which regard two dates must be similar in order to share range formatting text.
1744
The `chunks` can be nested (because of "maybe" chunks), however, the returned array will be flat.
1745
*/
1746
function buildSameUnits(chunks) {
1747
	var units = [];
1748
	var i, chunk;
1749
	var tokenInfo;
1750

  
1751
	for (i = 0; i < chunks.length; i++) {
1752
		chunk = chunks[i];
1753

  
1754
		if (chunk.token) {
1755
			tokenInfo = largeTokenMap[chunk.token.charAt(0)];
1756
			units.push(tokenInfo ? tokenInfo.unit : 'second'); // default to a very strict same-second
1757
		}
1758
		else if (chunk.maybe) {
1759
			units.push.apply(units, // append
1760
				buildSameUnits(chunk.maybe)
1761
			);
1762
		}
1763
		else {
1764
			units.push(null);
1765
		}
1766
	}
1767

  
1768
	return units;
1769
}
1770

  
1771

  
1772
// Rendering to text
1773
// ---------------------------------------------------------------------------------------------------------------------
1774

  
1775
/*
1776
Formats a date with a fake format string, post-processes the control characters, then returns.
1777
*/
1778
function renderFakeFormatString(fakeFormatString, date) {
1779
	return processMaybeMarkers(
1780
		renderFakeFormatStringParts(fakeFormatString, date).join('')
1781
	);
1782
}
1783

  
1784
/*
1785
Formats a date into parts that will have been post-processed, EXCEPT for the "maybe" markers.
1786
*/
1787
function renderFakeFormatStringParts(fakeFormatString, date) {
1788
	var parts = [];
1789
	var fakeRender = oldMomentFormat(date, fakeFormatString);
1790
	var fakeParts = fakeRender.split(PART_SEPARATOR);
1791
	var i, fakePart;
1792

  
1793
	for (i = 0; i < fakeParts.length; i++) {
1794
		fakePart = fakeParts[i];
1795

  
1796
		if (fakePart.charAt(0) === SPECIAL_TOKEN_MARKER) {
1797
			parts.push(
1798
				// the literal string IS the token's name.
1799
				// call special token's registered function.
1800
				specialTokens[fakePart.substring(1)](date)
1801
			);
1802
		}
1803
		else {
1804
			parts.push(fakePart);
1805
		}
1806
	}
1807

  
1808
	return parts;
1809
}
1810

  
1811
/*
1812
Accepts an almost-finally-formatted string and processes the "maybe" control characters, returning a new string.
1813
*/
1814
function processMaybeMarkers(s) {
1815
	return s.replace(MAYBE_REGEXP, function(m0, m1) { // regex assumed to have 'g' flag
1816
		if (m1.match(/[1-9]/)) { // any non-zero numeric characters?
1817
			return m1;
1818
		}
1819
		else {
1820
			return '';
1821
		}
1822
	});
1823
}
1824

  
1825

  
1826
// Misc Utils
1827
// -------------------------------------------------------------------------------------------------
1828

  
1829
/*
1830
Returns a unit string, either 'year', 'month', 'day', or null for the most granular formatting token in the string.
1831
*/
1832
function queryMostGranularFormatUnit(formatStr) {
1833
	var chunks = chunkFormatString(formatStr);
1834
	var i, chunk;
1835
	var candidate;
1836
	var best;
1837

  
1838
	for (i = 0; i < chunks.length; i++) {
1839
		chunk = chunks[i];
1840

  
1841
		if (chunk.token) {
1842
			candidate = largeTokenMap[chunk.token.charAt(0)];
1843
			if (candidate) {
1844
				if (!best || candidate.value > best.value) {
1845
					best = candidate;
1846
				}
1847
			}
1848
		}
1849
	}
1850

  
1851
	if (best) {
1852
		return best.unit;
1853
	}
1854

  
1855
	return null;
1856
};
1857

  
1858
})();
1859

  
1860
// quick local references
1861
var formatDate = FC.formatDate;
1862
var formatRange = FC.formatRange;
1863
var oldMomentFormat = FC.oldMomentFormat;
1864

  
1865
;;
1866

  
1867
FC.Class = Class; // export
1868

  
1869
// Class that all other classes will inherit from
1870
function Class() { }
1871

  
1872

  
1873
// Called on a class to create a subclass.
1874
// Last argument contains instance methods. Any argument before the last are considered mixins.
1875
Class.extend = function() {
1876
	var len = arguments.length;
1877
	var i;
1878
	var members;
1879

  
1880
	for (i = 0; i < len; i++) {
1881
		members = arguments[i];
1882
		if (i < len - 1) { // not the last argument?
1883
			mixIntoClass(this, members);
1884
		}
1885
	}
1886

  
1887
	return extendClass(this, members || {}); // members will be undefined if no arguments
1888
};
1889

  
1890

  
1891
// Adds new member variables/methods to the class's prototype.
1892
// Can be called with another class, or a plain object hash containing new members.
1893
Class.mixin = function(members) {
1894
	mixIntoClass(this, members);
1895
};
1896

  
1897

  
1898
function extendClass(superClass, members) {
1899
	var subClass;
1900

  
1901
	// ensure a constructor for the subclass, forwarding all arguments to the super-constructor if it doesn't exist
1902
	if (hasOwnProp(members, 'constructor')) {
1903
		subClass = members.constructor;
1904
	}
1905
	if (typeof subClass !== 'function') {
1906
		subClass = members.constructor = function() {
1907
			superClass.apply(this, arguments);
1908
		};
1909
	}
1910

  
1911
	// build the base prototype for the subclass, which is an new object chained to the superclass's prototype
1912
	subClass.prototype = createObject(superClass.prototype);
1913

  
1914
	// copy each member variable/method onto the the subclass's prototype
1915
	copyOwnProps(members, subClass.prototype);
1916

  
1917
	// copy over all class variables/methods to the subclass, such as `extend` and `mixin`
1918
	copyOwnProps(superClass, subClass);
1919

  
1920
	return subClass;
1921
}
1922

  
1923

  
1924
function mixIntoClass(theClass, members) {
1925
	copyOwnProps(members, theClass.prototype);
1926
}
1927
;;
1928

  
1929
var Model = Class.extend(EmitterMixin, ListenerMixin, {
1930

  
1931
	_props: null,
1932
	_watchers: null,
1933
	_globalWatchArgs: null,
1934

  
1935
	constructor: function() {
1936
		this._watchers = {};
1937
		this._props = {};
1938
		this.applyGlobalWatchers();
1939
	},
1940

  
1941
	applyGlobalWatchers: function() {
1942
		var argSets = this._globalWatchArgs || [];
1943
		var i;
1944

  
1945
		for (i = 0; i < argSets.length; i++) {
1946
			this.watch.apply(this, argSets[i]);
1947
		}
1948
	},
1949

  
1950
	has: function(name) {
1951
		return name in this._props;
1952
	},
1953

  
1954
	get: function(name) {
1955
		if (name === undefined) {
1956
			return this._props;
1957
		}
1958

  
1959
		return this._props[name];
1960
	},
1961

  
1962
	set: function(name, val) {
1963
		var newProps;
1964

  
1965
		if (typeof name === 'string') {
1966
			newProps = {};
1967
			newProps[name] = val === undefined ? null : val;
1968
		}
1969
		else {
1970
			newProps = name;
1971
		}
1972

  
1973
		this.setProps(newProps);
1974
	},
1975

  
1976
	reset: function(newProps) {
1977
		var oldProps = this._props;
1978
		var changeset = {}; // will have undefined's to signal unsets
1979
		var name;
1980

  
1981
		for (name in oldProps) {
1982
			changeset[name] = undefined;
1983
		}
1984

  
1985
		for (name in newProps) {
1986
			changeset[name] = newProps[name];
1987
		}
1988

  
1989
		this.setProps(changeset);
1990
	},
1991

  
1992
	unset: function(name) { // accepts a string or array of strings
1993
		var newProps = {};
1994
		var names;
1995
		var i;
1996

  
1997
		if (typeof name === 'string') {
1998
			names = [ name ];
1999
		}
2000
		else {
2001
			names = name;
2002
		}
2003

  
2004
		for (i = 0; i < names.length; i++) {
2005
			newProps[names[i]] = undefined;
2006
		}
2007

  
2008
		this.setProps(newProps);
2009
	},
2010

  
2011
	setProps: function(newProps) {
2012
		var changedProps = {};
2013
		var changedCnt = 0;
2014
		var name, val;
2015

  
2016
		for (name in newProps) {
2017
			val = newProps[name];
2018

  
2019
			// a change in value?
2020
			// if an object, don't check equality, because might have been mutated internally.
2021
			// TODO: eventually enforce immutability.
2022
			if (
2023
				typeof val === 'object' ||
2024
				val !== this._props[name]
2025
			) {
2026
				changedProps[name] = val;
2027
				changedCnt++;
2028
			}
2029
		}
2030

  
2031
		if (changedCnt) {
2032

  
2033
			this.trigger('before:batchChange', changedProps);
2034

  
2035
			for (name in changedProps) {
2036
				val = changedProps[name];
2037

  
2038
				this.trigger('before:change', name, val);
2039
				this.trigger('before:change:' + name, val);
2040
			}
2041

  
2042
			for (name in changedProps) {
2043
				val = changedProps[name];
2044

  
2045
				if (val === undefined) {
2046
					delete this._props[name];
2047
				}
2048
				else {
2049
					this._props[name] = val;
2050
				}
2051

  
2052
				this.trigger('change:' + name, val);
2053
				this.trigger('change', name, val);
2054
			}
2055

  
2056
			this.trigger('batchChange', changedProps);
2057
		}
2058
	},
2059

  
2060
	watch: function(name, depList, startFunc, stopFunc) {
2061
		var _this = this;
2062

  
2063
		this.unwatch(name);
2064

  
2065
		this._watchers[name] = this._watchDeps(depList, function(deps) {
2066
			var res = startFunc.call(_this, deps);
2067

  
2068
			if (res && res.then) {
2069
				_this.unset(name); // put in an unset state while resolving
2070
				res.then(function(val) {
2071
					_this.set(name, val);
2072
				});
2073
			}
2074
			else {
2075
				_this.set(name, res);
2076
			}
2077
		}, function() {
2078
			_this.unset(name);
2079

  
2080
			if (stopFunc) {
2081
				stopFunc.call(_this);
2082
			}
2083
		});
2084
	},
2085

  
2086
	unwatch: function(name) {
2087
		var watcher = this._watchers[name];
2088

  
2089
		if (watcher) {
2090
			delete this._watchers[name];
2091
			watcher.teardown();
2092
		}
2093
	},
2094

  
2095
	_watchDeps: function(depList, startFunc, stopFunc) {
2096
		var _this = this;
2097
		var queuedChangeCnt = 0;
2098
		var depCnt = depList.length;
2099
		var satisfyCnt = 0;
2100
		var values = {}; // what's passed as the `deps` arguments
2101
		var bindTuples = []; // array of [ eventName, handlerFunc ] arrays
2102
		var isCallingStop = false;
2103

  
2104
		function onBeforeDepChange(depName, val, isOptional) {
2105
			queuedChangeCnt++;
2106
			if (queuedChangeCnt === 1) { // first change to cause a "stop" ?
2107
				if (satisfyCnt === depCnt) { // all deps previously satisfied?
2108
					isCallingStop = true;
2109
					stopFunc();
2110
					isCallingStop = false;
2111
				}
2112
			}
2113
		}
2114

  
2115
		function onDepChange(depName, val, isOptional) {
2116

  
2117
			if (val === undefined) { // unsetting a value?
2118

  
2119
				// required dependency that was previously set?
2120
				if (!isOptional && values[depName] !== undefined) {
2121
					satisfyCnt--;
2122
				}
2123

  
2124
				delete values[depName];
2125
			}
2126
			else { // setting a value?
2127

  
2128
				// required dependency that was previously unset?
2129
				if (!isOptional && values[depName] === undefined) {
2130
					satisfyCnt++;
2131
				}
2132

  
2133
				values[depName] = val;
2134
			}
2135

  
2136
			queuedChangeCnt--;
2137
			if (!queuedChangeCnt) { // last change to cause a "start"?
2138

  
2139
				// now finally satisfied or satisfied all along?
2140
				if (satisfyCnt === depCnt) {
2141

  
2142
					// if the stopFunc initiated another value change, ignore it.
2143
					// it will be processed by another change event anyway.
2144
					if (!isCallingStop) {
2145
						startFunc(values);
2146
					}
2147
				}
2148
			}
2149
		}
2150

  
2151
		// intercept for .on() that remembers handlers
2152
		function bind(eventName, handler) {
2153
			_this.on(eventName, handler);
2154
			bindTuples.push([ eventName, handler ]);
2155
		}
2156

  
2157
		// listen to dependency changes
2158
		depList.forEach(function(depName) {
2159
			var isOptional = false;
2160

  
2161
			if (depName.charAt(0) === '?') { // TODO: more DRY
2162
				depName = depName.substring(1);
2163
				isOptional = true;
2164
			}
2165

  
2166
			bind('before:change:' + depName, function(val) {
2167
				onBeforeDepChange(depName, val, isOptional);
2168
			});
2169

  
2170
			bind('change:' + depName, function(val) {
2171
				onDepChange(depName, val, isOptional);
2172
			});
2173
		});
2174

  
2175
		// process current dependency values
2176
		depList.forEach(function(depName) {
2177
			var isOptional = false;
2178

  
2179
			if (depName.charAt(0) === '?') { // TODO: more DRY
2180
				depName = depName.substring(1);
2181
				isOptional = true;
2182
			}
2183

  
2184
			if (_this.has(depName)) {
2185
				values[depName] = _this.get(depName);
2186
				satisfyCnt++;
2187
			}
2188
			else if (isOptional) {
2189
				satisfyCnt++;
2190
			}
2191
		});
2192

  
2193
		// initially satisfied
2194
		if (satisfyCnt === depCnt) {
2195
			startFunc(values);
2196
		}
2197

  
2198
		return {
2199
			teardown: function() {
2200
				// remove all handlers
2201
				for (var i = 0; i < bindTuples.length; i++) {
2202
					_this.off(bindTuples[i][0], bindTuples[i][1]);
2203
				}
2204
				bindTuples = null;
2205

  
2206
				// was satisfied, so call stopFunc
2207
				if (satisfyCnt === depCnt) {
2208
					stopFunc();
2209
				}
2210
			},
2211
			flash: function() {
2212
				if (satisfyCnt === depCnt) {
2213
					stopFunc();
2214
					startFunc(values);
2215
				}
2216
			}
2217
		};
2218
	},
2219

  
2220
	flash: function(name) {
2221
		var watcher = this._watchers[name];
2222

  
2223
		if (watcher) {
2224
			watcher.flash();
2225
		}
2226
	}
2227

  
2228
});
2229

  
2230

  
2231
Model.watch = function(/* same arguments as this.watch() */) {
2232
	var proto = this.prototype;
2233

  
2234
	if (!proto._globalWatchArgs) {
2235
		proto._globalWatchArgs = [];
2236
	}
2237

  
2238
	proto._globalWatchArgs.push(arguments);
2239
};
2240

  
2241

  
2242
FC.Model = Model;
2243

  
2244

  
2245
;;
2246

  
2247
var Promise = {
2248

  
2249
	construct: function(executor) {
2250
		var deferred = $.Deferred();
2251
		var promise = deferred.promise();
2252

  
2253
		if (typeof executor === 'function') {
2254
			executor(
2255
				function(val) { // resolve
2256
					deferred.resolve(val);
2257
					attachImmediatelyResolvingThen(promise, val);
2258
				},
2259
				function() { // reject
2260
					deferred.reject();
2261
					attachImmediatelyRejectingThen(promise);
2262
				}
2263
			);
2264
		}
2265

  
2266
		return promise;
2267
	},
2268

  
2269
	resolve: function(val) {
2270
		var deferred = $.Deferred().resolve(val);
2271
		var promise = deferred.promise();
2272

  
2273
		attachImmediatelyResolvingThen(promise, val);
2274

  
2275
		return promise;
2276
	},
2277

  
2278
	reject: function() {
2279
		var deferred = $.Deferred().reject();
2280
		var promise = deferred.promise();
2281

  
2282
		attachImmediatelyRejectingThen(promise);
2283

  
2284
		return promise;
2285
	}
2286

  
2287
};
2288

  
2289

  
2290
function attachImmediatelyResolvingThen(promise, val) {
2291
	promise.then = function(onResolve) {
2292
		if (typeof onResolve === 'function') {
2293
			onResolve(val);
2294
		}
2295
		return promise; // for chaining
2296
	};
2297
}
2298

  
2299

  
2300
function attachImmediatelyRejectingThen(promise) {
2301
	promise.then = function(onResolve, onReject) {
2302
		if (typeof onReject === 'function') {
2303
			onReject();
2304
		}
2305
		return promise; // for chaining
2306
	};
2307
}
2308

  
2309

  
2310
FC.Promise = Promise;
2311

  
2312
;;
2313

  
2314
var TaskQueue = Class.extend(EmitterMixin, {
2315

  
2316
	q: null,
2317
	isPaused: false,
2318
	isRunning: false,
2319

  
2320

  
2321
	constructor: function() {
2322
		this.q = [];
2323
	},
2324

  
2325

  
2326
	queue: function(/* taskFunc, taskFunc... */) {
2327
		this.q.push.apply(this.q, arguments); // append
2328
		this.tryStart();
2329
	},
2330

  
2331

  
2332
	pause: function() {
2333
		this.isPaused = true;
2334
	},
2335

  
2336

  
2337
	resume: function() {
2338
		this.isPaused = false;
2339
		this.tryStart();
2340
	},
2341

  
2342

  
2343
	tryStart: function() {
2344
		if (!this.isRunning && this.canRunNext()) {
2345
			this.isRunning = true;
2346
			this.trigger('start');
2347
			this.runNext();
2348
		}
2349
	},
2350

  
2351

  
2352
	canRunNext: function() {
2353
		return !this.isPaused && this.q.length;
2354
	},
2355

  
2356

  
2357
	runNext: function() { // does not check canRunNext
2358
		this.runTask(this.q.shift());
2359
	},
2360

  
2361

  
2362
	runTask: function(task) {
2363
		this.runTaskFunc(task);
2364
	},
2365

  
2366

  
2367
	runTaskFunc: function(taskFunc) {
2368
		var _this = this;
2369
		var res = taskFunc();
2370

  
2371
		if (res && res.then) {
2372
			res.then(done);
2373
		}
2374
		else {
2375
			done();
2376
		}
2377

  
2378
		function done() {
2379
			if (_this.canRunNext()) {
2380
				_this.runNext();
2381
			}
2382
			else {
2383
				_this.isRunning = false;
2384
				_this.trigger('stop');
2385
			}
2386
		}
2387
	}
2388

  
2389
});
2390

  
2391
FC.TaskQueue = TaskQueue;
2392

  
2393
;;
2394

  
2395
var RenderQueue = TaskQueue.extend({
2396

  
2397
	waitsByNamespace: null,
2398
	waitNamespace: null,
2399
	waitId: null,
2400

  
2401

  
2402
	constructor: function(waitsByNamespace) {
2403
		TaskQueue.call(this); // super-constructor
2404

  
2405
		this.waitsByNamespace = waitsByNamespace || {};
2406
	},
2407

  
2408

  
2409
	queue: function(taskFunc, namespace, type) {
2410
		var task = {
2411
			func: taskFunc,
2412
			namespace: namespace,
2413
			type: type
2414
		};
2415
		var waitMs;
2416

  
2417
		if (namespace) {
2418
			waitMs = this.waitsByNamespace[namespace];
2419
		}
2420

  
2421
		if (this.waitNamespace) {
2422
			if (namespace === this.waitNamespace && waitMs != null) {
2423
				this.delayWait(waitMs);
2424
			}
2425
			else {
2426
				this.clearWait();
2427
				this.tryStart();
2428
			}
2429
		}
2430

  
2431
		if (this.compoundTask(task)) { // appended to queue?
2432

  
2433
			if (!this.waitNamespace && waitMs != null) {
2434
				this.startWait(namespace, waitMs);
2435
			}
2436
			else {
2437
				this.tryStart();
2438
			}
2439
		}
2440
	},
2441

  
2442

  
2443
	startWait: function(namespace, waitMs) {
2444
		this.waitNamespace = namespace;
2445
		this.spawnWait(waitMs);
2446
	},
2447

  
2448

  
2449
	delayWait: function(waitMs) {
2450
		clearTimeout(this.waitId);
2451
		this.spawnWait(waitMs);
2452
	},
2453

  
2454

  
2455
	spawnWait: function(waitMs) {
2456
		var _this = this;
2457

  
2458
		this.waitId = setTimeout(function() {
2459
			_this.waitNamespace = null;
2460
			_this.tryStart();
2461
		}, waitMs);
2462
	},
2463

  
2464

  
2465
	clearWait: function() {
2466
		if (this.waitNamespace) {
2467
			clearTimeout(this.waitId);
2468
			this.waitId = null;
2469
			this.waitNamespace = null;
2470
		}
2471
	},
2472

  
2473

  
2474
	canRunNext: function() {
2475
		if (!TaskQueue.prototype.canRunNext.apply(this, arguments)) {
2476
			return false;
2477
		}
2478

  
2479
		// waiting for a certain namespace to stop receiving tasks?
2480
		if (this.waitNamespace) {
2481

  
2482
			// if there was a different namespace task in the meantime,
2483
			// that forces all previously-waiting tasks to suddenly execute.
2484
			// TODO: find a way to do this in constant time.
2485
			for (var q = this.q, i = 0; i < q.length; i++) {
2486
				if (q[i].namespace !== this.waitNamespace) {
2487
					return true; // allow execution
2488
				}
2489
			}
2490

  
2491
			return false;
2492
		}
2493

  
2494
		return true;
2495
	},
2496

  
2497

  
2498
	runTask: function(task) {
2499
		this.runTaskFunc(task.func);
2500
	},
2501

  
2502

  
2503
	compoundTask: function(newTask) {
2504
		var q = this.q;
2505
		var shouldAppend = true;
2506
		var i, task;
2507

  
2508
		if (newTask.namespace) {
2509

  
2510
			if (newTask.type === 'destroy' || newTask.type === 'init') {
2511

  
2512
				// remove all add/remove ops with same namespace, regardless of order
2513
				for (i = q.length - 1; i >= 0; i--) {
2514
					task = q[i];
2515

  
2516
					if (
2517
						task.namespace === newTask.namespace &&
2518
						(task.type === 'add' || task.type === 'remove')
2519
					) {
2520
						q.splice(i, 1); // remove task
2521
					}
2522
				}
2523

  
2524
				if (newTask.type === 'destroy') {
2525
					// eat away final init/destroy operation
2526
					if (q.length) {
2527
						task = q[q.length - 1]; // last task
2528

  
2529
						if (task.namespace === newTask.namespace) {
2530

  
2531
							// the init and our destroy cancel each other out
2532
							if (task.type === 'init') {
2533
								shouldAppend = false;
2534
								q.pop();
2535
							}
2536
							// prefer to use the destroy operation that's already present
2537
							else if (task.type === 'destroy') {
2538
								shouldAppend = false;
2539
							}
2540
						}
2541
					}
2542
				}
2543
				else if (newTask.type === 'init') {
2544
					// eat away final init operation
2545
					if (q.length) {
2546
						task = q[q.length - 1]; // last task
2547

  
2548
						if (
2549
							task.namespace === newTask.namespace &&
2550
							task.type === 'init'
2551
						) {
2552
							// our init operation takes precedence
2553
							q.pop();
2554
						}
2555
					}
2556
				}
2557
			}
2558
		}
2559

  
2560
		if (shouldAppend) {
2561
			q.push(newTask);
2562
		}
2563

  
2564
		return shouldAppend;
2565
	}
2566

  
2567
});
2568

  
2569
FC.RenderQueue = RenderQueue;
2570

  
2571
;;
2572

  
2573
var EmitterMixin = FC.EmitterMixin = {
2574

  
2575
	// jQuery-ification via $(this) allows a non-DOM object to have
2576
	// the same event handling capabilities (including namespaces).
2577

  
2578

  
2579
	on: function(types, handler) {
2580
		$(this).on(types, this._prepareIntercept(handler));
2581
		return this; // for chaining
2582
	},
2583

  
2584

  
2585
	one: function(types, handler) {
2586
		$(this).one(types, this._prepareIntercept(handler));
2587
		return this; // for chaining
2588
	},
2589

  
2590

  
2591
	_prepareIntercept: function(handler) {
2592
		// handlers are always called with an "event" object as their first param.
2593
		// sneak the `this` context and arguments into the extra parameter object
2594
		// and forward them on to the original handler.
2595
		var intercept = function(ev, extra) {
2596
			return handler.apply(
2597
				extra.context || this,
2598
				extra.args || []
2599
			);
2600
		};
2601

  
2602
		// mimick jQuery's internal "proxy" system (risky, I know)
2603
		// causing all functions with the same .guid to appear to be the same.
2604
		// https://github.com/jquery/jquery/blob/2.2.4/src/core.js#L448
2605
		// this is needed for calling .off with the original non-intercept handler.
2606
		if (!handler.guid) {
2607
			handler.guid = $.guid++;
2608
		}
2609
		intercept.guid = handler.guid;
2610

  
2611
		return intercept;
2612
	},
2613

  
2614

  
2615
	off: function(types, handler) {
2616
		$(this).off(types, handler);
2617

  
2618
		return this; // for chaining
2619
	},
2620

  
2621

  
2622
	trigger: function(types) {
2623
		var args = Array.prototype.slice.call(arguments, 1); // arguments after the first
2624

  
2625
		// pass in "extra" info to the intercept
2626
		$(this).triggerHandler(types, { args: args });
2627

  
2628
		return this; // for chaining
2629
	},
2630

  
2631

  
2632
	triggerWith: function(types, context, args) {
2633

  
2634
		// `triggerHandler` is less reliant on the DOM compared to `trigger`.
2635
		// pass in "extra" info to the intercept.
2636
		$(this).triggerHandler(types, { context: context, args: args });
2637

  
2638
		return this; // for chaining
2639
	}
2640

  
2641
};
2642

  
2643
;;
2644

  
2645
/*
2646
Utility methods for easily listening to events on another object,
2647
and more importantly, easily unlistening from them.
2648
*/
2649
var ListenerMixin = FC.ListenerMixin = (function() {
2650
	var guid = 0;
2651
	var ListenerMixin = {
2652

  
2653
		listenerId: null,
2654

  
2655
		/*
2656
		Given an `other` object that has on/off methods, bind the given `callback` to an event by the given name.
2657
		The `callback` will be called with the `this` context of the object that .listenTo is being called on.
2658
		Can be called:
2659
			.listenTo(other, eventName, callback)
2660
		OR
2661
			.listenTo(other, {
2662
				eventName1: callback1,
2663
				eventName2: callback2
2664
			})
2665
		*/
2666
		listenTo: function(other, arg, callback) {
2667
			if (typeof arg === 'object') { // given dictionary of callbacks
2668
				for (var eventName in arg) {
2669
					if (arg.hasOwnProperty(eventName)) {
2670
						this.listenTo(other, eventName, arg[eventName]);
2671
					}
2672
				}
2673
			}
2674
			else if (typeof arg === 'string') {
2675
				other.on(
2676
					arg + '.' + this.getListenerNamespace(), // use event namespacing to identify this object
2677
					$.proxy(callback, this) // always use `this` context
2678
						// the usually-undesired jQuery guid behavior doesn't matter,
2679
						// because we always unbind via namespace
2680
				);
2681
			}
2682
		},
2683

  
2684
		/*
2685
		Causes the current object to stop listening to events on the `other` object.
2686
		`eventName` is optional. If omitted, will stop listening to ALL events on `other`.
2687
		*/
2688
		stopListeningTo: function(other, eventName) {
2689
			other.off((eventName || '') + '.' + this.getListenerNamespace());
2690
		},
2691

  
2692
		/*
2693
		Returns a string, unique to this object, to be used for event namespacing
2694
		*/
2695
		getListenerNamespace: function() {
2696
			if (this.listenerId == null) {
2697
				this.listenerId = guid++;
2698
			}
2699
			return '_listener' + this.listenerId;
2700
		}
2701

  
2702
	};
2703
	return ListenerMixin;
2704
})();
2705
;;
2706

  
2707
/* A rectangular panel that is absolutely positioned over other content
2708
------------------------------------------------------------------------------------------------------------------------
2709
Options:
2710
	- className (string)
2711
	- content (HTML string or jQuery element set)
2712
	- parentEl
2713
	- top
2714
	- left
2715
	- right (the x coord of where the right edge should be. not a "CSS" right)
2716
	- autoHide (boolean)
2717
	- show (callback)
2718
	- hide (callback)
2719
*/
2720

  
2721
var Popover = Class.extend(ListenerMixin, {
2722

  
2723
	isHidden: true,
2724
	options: null,
2725
	el: null, // the container element for the popover. generated by this object
2726
	margin: 10, // the space required between the popover and the edges of the scroll container
2727

  
2728

  
2729
	constructor: function(options) {
2730
		this.options = options || {};
2731
	},
2732

  
2733

  
2734
	// Shows the popover on the specified position. Renders it if not already
2735
	show: function() {
2736
		if (this.isHidden) {
2737
			if (!this.el) {
2738
				this.render();
2739
			}
2740
			this.el.show();
2741
			this.position();
2742
			this.isHidden = false;
2743
			this.trigger('show');
2744
		}
2745
	},
2746

  
2747

  
2748
	// Hides the popover, through CSS, but does not remove it from the DOM
2749
	hide: function() {
2750
		if (!this.isHidden) {
2751
			this.el.hide();
2752
			this.isHidden = true;
2753
			this.trigger('hide');
2754
		}
2755
	},
2756

  
2757

  
2758
	// Creates `this.el` and renders content inside of it
2759
	render: function() {
2760
		var _this = this;
2761
		var options = this.options;
2762

  
2763
		this.el = $('<div class="fc-popover"/>')
2764
			.addClass(options.className || '')
2765
			.css({
2766
				// position initially to the top left to avoid creating scrollbars
2767
				top: 0,
2768
				left: 0
2769
			})
2770
			.append(options.content)
2771
			.appendTo(options.parentEl);
2772

  
2773
		// when a click happens on anything inside with a 'fc-close' className, hide the popover
2774
		this.el.on('click', '.fc-close', function() {
2775
			_this.hide();
2776
		});
2777

  
2778
		if (options.autoHide) {
2779
			this.listenTo($(document), 'mousedown', this.documentMousedown);
2780
		}
2781
	},
2782

  
2783

  
2784
	// Triggered when the user clicks *anywhere* in the document, for the autoHide feature
2785
	documentMousedown: function(ev) {
2786
		// only hide the popover if the click happened outside the popover
2787
		if (this.el && !$(ev.target).closest(this.el).length) {
2788
			this.hide();
2789
		}
2790
	},
2791

  
2792

  
2793
	// Hides and unregisters any handlers
2794
	removeElement: function() {
2795
		this.hide();
2796

  
2797
		if (this.el) {
2798
			this.el.remove();
2799
			this.el = null;
2800
		}
2801

  
2802
		this.stopListeningTo($(document), 'mousedown');
2803
	},
2804

  
2805

  
2806
	// Positions the popover optimally, using the top/left/right options
2807
	position: function() {
2808
		var options = this.options;
2809
		var origin = this.el.offsetParent().offset();
2810
		var width = this.el.outerWidth();
2811
		var height = this.el.outerHeight();
2812
		var windowEl = $(window);
2813
		var viewportEl = getScrollParent(this.el);
2814
		var viewportTop;
2815
		var viewportLeft;
2816
		var viewportOffset;
2817
		var top; // the "position" (not "offset") values for the popover
2818
		var left; //
2819

  
2820
		// compute top and left
2821
		top = options.top || 0;
2822
		if (options.left !== undefined) {
2823
			left = options.left;
2824
		}
2825
		else if (options.right !== undefined) {
2826
			left = options.right - width; // derive the left value from the right value
2827
		}
2828
		else {
2829
			left = 0;
2830
		}
2831

  
2832
		if (viewportEl.is(window) || viewportEl.is(document)) { // normalize getScrollParent's result
2833
			viewportEl = windowEl;
2834
			viewportTop = 0; // the window is always at the top left
2835
			viewportLeft = 0; // (and .offset() won't work if called here)
2836
		}
2837
		else {
2838
			viewportOffset = viewportEl.offset();
2839
			viewportTop = viewportOffset.top;
2840
			viewportLeft = viewportOffset.left;
2841
		}
2842

  
2843
		// if the window is scrolled, it causes the visible area to be further down
2844
		viewportTop += windowEl.scrollTop();
2845
		viewportLeft += windowEl.scrollLeft();
2846

  
2847
		// constrain to the view port. if constrained by two edges, give precedence to top/left
2848
		if (options.viewportConstrain !== false) {
2849
			top = Math.min(top, viewportTop + viewportEl.outerHeight() - height - this.margin);
2850
			top = Math.max(top, viewportTop + this.margin);
2851
			left = Math.min(left, viewportLeft + viewportEl.outerWidth() - width - this.margin);
2852
			left = Math.max(left, viewportLeft + this.margin);
2853
		}
2854

  
2855
		this.el.css({
2856
			top: top - origin.top,
2857
			left: left - origin.left
2858
		});
2859
	},
2860

  
2861

  
2862
	// Triggers a callback. Calls a function in the option hash of the same name.
2863
	// Arguments beyond the first `name` are forwarded on.
2864
	// TODO: better code reuse for this. Repeat code
2865
	trigger: function(name) {
2866
		if (this.options[name]) {
2867
			this.options[name].apply(this, Array.prototype.slice.call(arguments, 1));
2868
		}
2869
	}
2870

  
2871
});
2872

  
2873
;;
2874

  
2875
/*
2876
A cache for the left/right/top/bottom/width/height values for one or more elements.
2877
Works with both offset (from topleft document) and position (from offsetParent).
2878

  
2879
options:
2880
- els
2881
- isHorizontal
2882
- isVertical
2883
*/
2884
var CoordCache = FC.CoordCache = Class.extend({
2885

  
2886
	els: null, // jQuery set (assumed to be siblings)
2887
	forcedOffsetParentEl: null, // options can override the natural offsetParent
2888
	origin: null, // {left,top} position of offsetParent of els
2889
	boundingRect: null, // constrain cordinates to this rectangle. {left,right,top,bottom} or null
2890
	isHorizontal: false, // whether to query for left/right/width
2891
	isVertical: false, // whether to query for top/bottom/height
2892

  
2893
	// arrays of coordinates (offsets from topleft of document)
2894
	lefts: null,
2895
	rights: null,
2896
	tops: null,
2897
	bottoms: null,
2898

  
2899

  
2900
	constructor: function(options) {
2901
		this.els = $(options.els);
2902
		this.isHorizontal = options.isHorizontal;
2903
		this.isVertical = options.isVertical;
2904
		this.forcedOffsetParentEl = options.offsetParent ? $(options.offsetParent) : null;
2905
	},
2906

  
2907

  
2908
	// Queries the els for coordinates and stores them.
2909
	// Call this method before using and of the get* methods below.
2910
	build: function() {
2911
		var offsetParentEl = this.forcedOffsetParentEl;
2912
		if (!offsetParentEl && this.els.length > 0) {
2913
			offsetParentEl = this.els.eq(0).offsetParent();
2914
		}
2915

  
2916
		this.origin = offsetParentEl ?
2917
			offsetParentEl.offset() :
2918
			null;
2919

  
2920
		this.boundingRect = this.queryBoundingRect();
2921

  
2922
		if (this.isHorizontal) {
2923
			this.buildElHorizontals();
2924
		}
2925
		if (this.isVertical) {
2926
			this.buildElVerticals();
2927
		}
2928
	},
2929

  
2930

  
2931
	// Destroys all internal data about coordinates, freeing memory
2932
	clear: function() {
2933
		this.origin = null;
2934
		this.boundingRect = null;
2935
		this.lefts = null;
2936
		this.rights = null;
2937
		this.tops = null;
2938
		this.bottoms = null;
2939
	},
2940

  
2941

  
2942
	// When called, if coord caches aren't built, builds them
2943
	ensureBuilt: function() {
2944
		if (!this.origin) {
2945
			this.build();
2946
		}
2947
	},
2948

  
2949

  
2950
	// Populates the left/right internal coordinate arrays
2951
	buildElHorizontals: function() {
2952
		var lefts = [];
2953
		var rights = [];
2954

  
2955
		this.els.each(function(i, node) {
2956
			var el = $(node);
2957
			var left = el.offset().left;
2958
			var width = el.outerWidth();
2959

  
2960
			lefts.push(left);
2961
			rights.push(left + width);
2962
		});
2963

  
2964
		this.lefts = lefts;
2965
		this.rights = rights;
2966
	},
2967

  
2968

  
2969
	// Populates the top/bottom internal coordinate arrays
2970
	buildElVerticals: function() {
2971
		var tops = [];
2972
		var bottoms = [];
2973

  
2974
		this.els.each(function(i, node) {
2975
			var el = $(node);
2976
			var top = el.offset().top;
2977
			var height = el.outerHeight();
2978

  
2979
			tops.push(top);
2980
			bottoms.push(top + height);
2981
		});
2982

  
2983
		this.tops = tops;
2984
		this.bottoms = bottoms;
2985
	},
2986

  
2987

  
2988
	// Given a left offset (from document left), returns the index of the el that it horizontally intersects.
2989
	// If no intersection is made, returns undefined.
2990
	getHorizontalIndex: function(leftOffset) {
2991
		this.ensureBuilt();
2992

  
2993
		var lefts = this.lefts;
2994
		var rights = this.rights;
2995
		var len = lefts.length;
2996
		var i;
2997

  
2998
		for (i = 0; i < len; i++) {
2999
			if (leftOffset >= lefts[i] && leftOffset < rights[i]) {
3000
				return i;
3001
			}
3002
		}
3003
	},
3004

  
3005

  
3006
	// Given a top offset (from document top), returns the index of the el that it vertically intersects.
3007
	// If no intersection is made, returns undefined.
3008
	getVerticalIndex: function(topOffset) {
3009
		this.ensureBuilt();
3010

  
3011
		var tops = this.tops;
3012
		var bottoms = this.bottoms;
3013
		var len = tops.length;
3014
		var i;
3015

  
3016
		for (i = 0; i < len; i++) {
3017
			if (topOffset >= tops[i] && topOffset < bottoms[i]) {
3018
				return i;
3019
			}
3020
		}
3021
	},
3022

  
3023

  
3024
	// Gets the left offset (from document left) of the element at the given index
3025
	getLeftOffset: function(leftIndex) {
3026
		this.ensureBuilt();
3027
		return this.lefts[leftIndex];
3028
	},
3029

  
3030

  
3031
	// Gets the left position (from offsetParent left) of the element at the given index
3032
	getLeftPosition: function(leftIndex) {
3033
		this.ensureBuilt();
3034
		return this.lefts[leftIndex] - this.origin.left;
3035
	},
3036

  
3037

  
3038
	// Gets the right offset (from document left) of the element at the given index.
3039
	// This value is NOT relative to the document's right edge, like the CSS concept of "right" would be.
3040
	getRightOffset: function(leftIndex) {
3041
		this.ensureBuilt();
3042
		return this.rights[leftIndex];
3043
	},
3044

  
3045

  
3046
	// Gets the right position (from offsetParent left) of the element at the given index.
3047
	// This value is NOT relative to the offsetParent's right edge, like the CSS concept of "right" would be.
3048
	getRightPosition: function(leftIndex) {
3049
		this.ensureBuilt();
3050
		return this.rights[leftIndex] - this.origin.left;
3051
	},
3052

  
3053

  
3054
	// Gets the width of the element at the given index
3055
	getWidth: function(leftIndex) {
3056
		this.ensureBuilt();
3057
		return this.rights[leftIndex] - this.lefts[leftIndex];
3058
	},
3059

  
3060

  
3061
	// Gets the top offset (from document top) of the element at the given index
3062
	getTopOffset: function(topIndex) {
3063
		this.ensureBuilt();
3064
		return this.tops[topIndex];
3065
	},
3066

  
3067

  
3068
	// Gets the top position (from offsetParent top) of the element at the given position
3069
	getTopPosition: function(topIndex) {
3070
		this.ensureBuilt();
3071
		return this.tops[topIndex] - this.origin.top;
3072
	},
3073

  
3074
	// Gets the bottom offset (from the document top) of the element at the given index.
3075
	// This value is NOT relative to the offsetParent's bottom edge, like the CSS concept of "bottom" would be.
3076
	getBottomOffset: function(topIndex) {
3077
		this.ensureBuilt();
3078
		return this.bottoms[topIndex];
3079
	},
3080

  
3081

  
3082
	// Gets the bottom position (from the offsetParent top) of the element at the given index.
3083
	// This value is NOT relative to the offsetParent's bottom edge, like the CSS concept of "bottom" would be.
3084
	getBottomPosition: function(topIndex) {
3085
		this.ensureBuilt();
3086
		return this.bottoms[topIndex] - this.origin.top;
3087
	},
3088

  
3089

  
3090
	// Gets the height of the element at the given index
3091
	getHeight: function(topIndex) {
3092
		this.ensureBuilt();
3093
		return this.bottoms[topIndex] - this.tops[topIndex];
3094
	},
3095

  
3096

  
3097
	// Bounding Rect
3098
	// TODO: decouple this from CoordCache
3099

  
3100
	// Compute and return what the elements' bounding rectangle is, from the user's perspective.
3101
	// Right now, only returns a rectangle if constrained by an overflow:scroll element.
3102
	// Returns null if there are no elements
3103
	queryBoundingRect: function() {
3104
		var scrollParentEl;
3105

  
3106
		if (this.els.length > 0) {
3107
			scrollParentEl = getScrollParent(this.els.eq(0));
3108

  
3109
			if (!scrollParentEl.is(document)) {
3110
				return getClientRect(scrollParentEl);
3111
			}
3112
		}
3113

  
3114
		return null;
3115
	},
3116

  
3117
	isPointInBounds: function(leftOffset, topOffset) {
3118
		return this.isLeftInBounds(leftOffset) && this.isTopInBounds(topOffset);
3119
	},
3120

  
3121
	isLeftInBounds: function(leftOffset) {
3122
		return !this.boundingRect || (leftOffset >= this.boundingRect.left && leftOffset < this.boundingRect.right);
3123
	},
3124

  
3125
	isTopInBounds: function(topOffset) {
3126
		return !this.boundingRect || (topOffset >= this.boundingRect.top && topOffset < this.boundingRect.bottom);
3127
	}
3128

  
3129
});
3130

  
3131
;;
3132

  
3133
/* Tracks a drag's mouse movement, firing various handlers
3134
----------------------------------------------------------------------------------------------------------------------*/
3135
// TODO: use Emitter
3136

  
3137
var DragListener = FC.DragListener = Class.extend(ListenerMixin, {
3138

  
3139
	options: null,
3140
	subjectEl: null,
3141

  
3142
	// coordinates of the initial mousedown
3143
	originX: null,
3144
	originY: null,
3145

  
3146
	// the wrapping element that scrolls, or MIGHT scroll if there's overflow.
3147
	// TODO: do this for wrappers that have overflow:hidden as well.
3148
	scrollEl: null,
3149

  
3150
	isInteracting: false,
3151
	isDistanceSurpassed: false,
3152
	isDelayEnded: false,
3153
	isDragging: false,
3154
	isTouch: false,
3155
	isGeneric: false, // initiated by 'dragstart' (jqui)
3156

  
3157
	delay: null,
3158
	delayTimeoutId: null,
3159
	minDistance: null,
3160

  
3161
	shouldCancelTouchScroll: true,
3162
	scrollAlwaysKills: false,
3163

  
3164

  
3165
	constructor: function(options) {
3166
		this.options = options || {};
3167
	},
3168

  
3169

  
3170
	// Interaction (high-level)
3171
	// -----------------------------------------------------------------------------------------------------------------
3172

  
3173

  
3174
	startInteraction: function(ev, extraOptions) {
3175

  
3176
		if (ev.type === 'mousedown') {
3177
			if (GlobalEmitter.get().shouldIgnoreMouse()) {
3178
				return;
3179
			}
3180
			else if (!isPrimaryMouseButton(ev)) {
3181
				return;
3182
			}
3183
			else {
3184
				ev.preventDefault(); // prevents native selection in most browsers
3185
			}
3186
		}
3187

  
3188
		if (!this.isInteracting) {
3189

  
3190
			// process options
3191
			extraOptions = extraOptions || {};
3192
			this.delay = firstDefined(extraOptions.delay, this.options.delay, 0);
3193
			this.minDistance = firstDefined(extraOptions.distance, this.options.distance, 0);
3194
			this.subjectEl = this.options.subjectEl;
3195

  
3196
			preventSelection($('body'));
3197

  
3198
			this.isInteracting = true;
3199
			this.isTouch = getEvIsTouch(ev);
3200
			this.isGeneric = ev.type === 'dragstart';
3201
			this.isDelayEnded = false;
3202
			this.isDistanceSurpassed = false;
3203

  
3204
			this.originX = getEvX(ev);
3205
			this.originY = getEvY(ev);
3206
			this.scrollEl = getScrollParent($(ev.target));
3207

  
3208
			this.bindHandlers();
3209
			this.initAutoScroll();
3210
			this.handleInteractionStart(ev);
3211
			this.startDelay(ev);
3212

  
3213
			if (!this.minDistance) {
3214
				this.handleDistanceSurpassed(ev);
3215
			}
3216
		}
3217
	},
3218

  
3219

  
3220
	handleInteractionStart: function(ev) {
3221
		this.trigger('interactionStart', ev);
3222
	},
3223

  
3224

  
3225
	endInteraction: function(ev, isCancelled) {
3226
		if (this.isInteracting) {
3227
			this.endDrag(ev);
3228

  
3229
			if (this.delayTimeoutId) {
3230
				clearTimeout(this.delayTimeoutId);
3231
				this.delayTimeoutId = null;
3232
			}
3233

  
3234
			this.destroyAutoScroll();
3235
			this.unbindHandlers();
3236

  
3237
			this.isInteracting = false;
3238
			this.handleInteractionEnd(ev, isCancelled);
3239

  
3240
			allowSelection($('body'));
3241
		}
3242
	},
3243

  
3244

  
3245
	handleInteractionEnd: function(ev, isCancelled) {
3246
		this.trigger('interactionEnd', ev, isCancelled || false);
3247
	},
3248

  
3249

  
3250
	// Binding To DOM
3251
	// -----------------------------------------------------------------------------------------------------------------
3252

  
3253

  
3254
	bindHandlers: function() {
3255
		// some browsers (Safari in iOS 10) don't allow preventDefault on touch events that are bound after touchstart,
3256
		// so listen to the GlobalEmitter singleton, which is always bound, instead of the document directly.
3257
		var globalEmitter = GlobalEmitter.get();
3258

  
3259
		if (this.isGeneric) {
3260
			this.listenTo($(document), { // might only work on iOS because of GlobalEmitter's bind :(
3261
				drag: this.handleMove,
3262
				dragstop: this.endInteraction
3263
			});
3264
		}
3265
		else if (this.isTouch) {
3266
			this.listenTo(globalEmitter, {
3267
				touchmove: this.handleTouchMove,
3268
				touchend: this.endInteraction,
3269
				scroll: this.handleTouchScroll
3270
			});
3271
		}
3272
		else {
3273
			this.listenTo(globalEmitter, {
3274
				mousemove: this.handleMouseMove,
3275
				mouseup: this.endInteraction
3276
			});
3277
		}
3278

  
3279
		this.listenTo(globalEmitter, {
3280
			selectstart: preventDefault, // don't allow selection while dragging
3281
			contextmenu: preventDefault // long taps would open menu on Chrome dev tools
3282
		});
3283
	},
3284

  
3285

  
3286
	unbindHandlers: function() {
3287
		this.stopListeningTo(GlobalEmitter.get());
3288
		this.stopListeningTo($(document)); // for isGeneric
3289
	},
3290

  
3291

  
3292
	// Drag (high-level)
3293
	// -----------------------------------------------------------------------------------------------------------------
3294

  
3295

  
3296
	// extraOptions ignored if drag already started
3297
	startDrag: function(ev, extraOptions) {
3298
		this.startInteraction(ev, extraOptions); // ensure interaction began
3299

  
3300
		if (!this.isDragging) {
3301
			this.isDragging = true;
3302
			this.handleDragStart(ev);
3303
		}
3304
	},
3305

  
3306

  
3307
	handleDragStart: function(ev) {
3308
		this.trigger('dragStart', ev);
3309
	},
3310

  
3311

  
3312
	handleMove: function(ev) {
3313
		var dx = getEvX(ev) - this.originX;
3314
		var dy = getEvY(ev) - this.originY;
3315
		var minDistance = this.minDistance;
3316
		var distanceSq; // current distance from the origin, squared
3317

  
3318
		if (!this.isDistanceSurpassed) {
3319
			distanceSq = dx * dx + dy * dy;
3320
			if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem
3321
				this.handleDistanceSurpassed(ev);
3322
			}
3323
		}
3324

  
3325
		if (this.isDragging) {
3326
			this.handleDrag(dx, dy, ev);
3327
		}
3328
	},
3329

  
3330

  
3331
	// Called while the mouse is being moved and when we know a legitimate drag is taking place
3332
	handleDrag: function(dx, dy, ev) {
3333
		this.trigger('drag', dx, dy, ev);
3334
		this.updateAutoScroll(ev); // will possibly cause scrolling
3335
	},
3336

  
3337

  
3338
	endDrag: function(ev) {
3339
		if (this.isDragging) {
3340
			this.isDragging = false;
3341
			this.handleDragEnd(ev);
3342
		}
3343
	},
3344

  
3345

  
3346
	handleDragEnd: function(ev) {
3347
		this.trigger('dragEnd', ev);
3348
	},
3349

  
3350

  
3351
	// Delay
3352
	// -----------------------------------------------------------------------------------------------------------------
3353

  
3354

  
3355
	startDelay: function(initialEv) {
3356
		var _this = this;
3357

  
3358
		if (this.delay) {
3359
			this.delayTimeoutId = setTimeout(function() {
3360
				_this.handleDelayEnd(initialEv);
3361
			}, this.delay);
3362
		}
3363
		else {
3364
			this.handleDelayEnd(initialEv);
3365
		}
3366
	},
3367

  
3368

  
3369
	handleDelayEnd: function(initialEv) {
3370
		this.isDelayEnded = true;
3371

  
3372
		if (this.isDistanceSurpassed) {
3373
			this.startDrag(initialEv);
3374
		}
3375
	},
3376

  
3377

  
3378
	// Distance
3379
	// -----------------------------------------------------------------------------------------------------------------
3380

  
3381

  
3382
	handleDistanceSurpassed: function(ev) {
3383
		this.isDistanceSurpassed = true;
3384

  
3385
		if (this.isDelayEnded) {
3386
			this.startDrag(ev);
3387
		}
3388
	},
3389

  
3390

  
3391
	// Mouse / Touch
3392
	// -----------------------------------------------------------------------------------------------------------------
3393

  
3394

  
3395
	handleTouchMove: function(ev) {
3396

  
3397
		// prevent inertia and touchmove-scrolling while dragging
3398
		if (this.isDragging && this.shouldCancelTouchScroll) {
3399
			ev.preventDefault();
3400
		}
3401

  
3402
		this.handleMove(ev);
3403
	},
3404

  
3405

  
3406
	handleMouseMove: function(ev) {
3407
		this.handleMove(ev);
3408
	},
3409

  
3410

  
3411
	// Scrolling (unrelated to auto-scroll)
3412
	// -----------------------------------------------------------------------------------------------------------------
3413

  
3414

  
3415
	handleTouchScroll: function(ev) {
3416
		// if the drag is being initiated by touch, but a scroll happens before
3417
		// the drag-initiating delay is over, cancel the drag
3418
		if (!this.isDragging || this.scrollAlwaysKills) {
3419
			this.endInteraction(ev, true); // isCancelled=true
3420
		}
3421
	},
3422

  
3423

  
3424
	// Utils
3425
	// -----------------------------------------------------------------------------------------------------------------
3426

  
3427

  
3428
	// Triggers a callback. Calls a function in the option hash of the same name.
3429
	// Arguments beyond the first `name` are forwarded on.
3430
	trigger: function(name) {
3431
		if (this.options[name]) {
3432
			this.options[name].apply(this, Array.prototype.slice.call(arguments, 1));
3433
		}
3434
		// makes _methods callable by event name. TODO: kill this
3435
		if (this['_' + name]) {
3436
			this['_' + name].apply(this, Array.prototype.slice.call(arguments, 1));
3437
		}
3438
	}
3439

  
3440

  
3441
});
3442

  
3443
;;
3444
/*
3445
this.scrollEl is set in DragListener
3446
*/
3447
DragListener.mixin({
3448

  
3449
	isAutoScroll: false,
3450

  
3451
	scrollBounds: null, // { top, bottom, left, right }
3452
	scrollTopVel: null, // pixels per second
3453
	scrollLeftVel: null, // pixels per second
3454
	scrollIntervalId: null, // ID of setTimeout for scrolling animation loop
3455

  
3456
	// defaults
3457
	scrollSensitivity: 30, // pixels from edge for scrolling to start
3458
	scrollSpeed: 200, // pixels per second, at maximum speed
3459
	scrollIntervalMs: 50, // millisecond wait between scroll increment
3460

  
3461

  
3462
	initAutoScroll: function() {
3463
		var scrollEl = this.scrollEl;
3464

  
3465
		this.isAutoScroll =
3466
			this.options.scroll &&
3467
			scrollEl &&
3468
			!scrollEl.is(window) &&
3469
			!scrollEl.is(document);
3470

  
3471
		if (this.isAutoScroll) {
3472
			// debounce makes sure rapid calls don't happen
3473
			this.listenTo(scrollEl, 'scroll', debounce(this.handleDebouncedScroll, 100));
3474
		}
3475
	},
3476

  
3477

  
3478
	destroyAutoScroll: function() {
3479
		this.endAutoScroll(); // kill any animation loop
3480

  
3481
		// remove the scroll handler if there is a scrollEl
3482
		if (this.isAutoScroll) {
3483
			this.stopListeningTo(this.scrollEl, 'scroll'); // will probably get removed by unbindHandlers too :(
3484
		}
3485
	},
3486

  
3487

  
3488
	// Computes and stores the bounding rectangle of scrollEl
3489
	computeScrollBounds: function() {
3490
		if (this.isAutoScroll) {
3491
			this.scrollBounds = getOuterRect(this.scrollEl);
3492
			// TODO: use getClientRect in future. but prevents auto scrolling when on top of scrollbars
3493
		}
3494
	},
3495

  
3496

  
3497
	// Called when the dragging is in progress and scrolling should be updated
3498
	updateAutoScroll: function(ev) {
3499
		var sensitivity = this.scrollSensitivity;
3500
		var bounds = this.scrollBounds;
3501
		var topCloseness, bottomCloseness;
3502
		var leftCloseness, rightCloseness;
3503
		var topVel = 0;
3504
		var leftVel = 0;
3505

  
3506
		if (bounds) { // only scroll if scrollEl exists
3507

  
3508
			// compute closeness to edges. valid range is from 0.0 - 1.0
3509
			topCloseness = (sensitivity - (getEvY(ev) - bounds.top)) / sensitivity;
3510
			bottomCloseness = (sensitivity - (bounds.bottom - getEvY(ev))) / sensitivity;
3511
			leftCloseness = (sensitivity - (getEvX(ev) - bounds.left)) / sensitivity;
3512
			rightCloseness = (sensitivity - (bounds.right - getEvX(ev))) / sensitivity;
3513

  
3514
			// translate vertical closeness into velocity.
3515
			// mouse must be completely in bounds for velocity to happen.
3516
			if (topCloseness >= 0 && topCloseness <= 1) {
3517
				topVel = topCloseness * this.scrollSpeed * -1; // negative. for scrolling up
3518
			}
3519
			else if (bottomCloseness >= 0 && bottomCloseness <= 1) {
3520
				topVel = bottomCloseness * this.scrollSpeed;
3521
			}
3522

  
3523
			// translate horizontal closeness into velocity
3524
			if (leftCloseness >= 0 && leftCloseness <= 1) {
3525
				leftVel = leftCloseness * this.scrollSpeed * -1; // negative. for scrolling left
3526
			}
3527
			else if (rightCloseness >= 0 && rightCloseness <= 1) {
3528
				leftVel = rightCloseness * this.scrollSpeed;
3529
			}
3530
		}
3531

  
3532
		this.setScrollVel(topVel, leftVel);
3533
	},
3534

  
3535

  
3536
	// Sets the speed-of-scrolling for the scrollEl
3537
	setScrollVel: function(topVel, leftVel) {
3538

  
3539
		this.scrollTopVel = topVel;
3540
		this.scrollLeftVel = leftVel;
3541

  
3542
		this.constrainScrollVel(); // massages into realistic values
3543

  
3544
		// if there is non-zero velocity, and an animation loop hasn't already started, then START
3545
		if ((this.scrollTopVel || this.scrollLeftVel) && !this.scrollIntervalId) {
3546
			this.scrollIntervalId = setInterval(
3547
				proxy(this, 'scrollIntervalFunc'), // scope to `this`
3548
				this.scrollIntervalMs
3549
			);
3550
		}
3551
	},
3552

  
3553

  
3554
	// Forces scrollTopVel and scrollLeftVel to be zero if scrolling has already gone all the way
3555
	constrainScrollVel: function() {
3556
		var el = this.scrollEl;
3557

  
3558
		if (this.scrollTopVel < 0) { // scrolling up?
3559
			if (el.scrollTop() <= 0) { // already scrolled all the way up?
3560
				this.scrollTopVel = 0;
3561
			}
3562
		}
3563
		else if (this.scrollTopVel > 0) { // scrolling down?
3564
			if (el.scrollTop() + el[0].clientHeight >= el[0].scrollHeight) { // already scrolled all the way down?
3565
				this.scrollTopVel = 0;
3566
			}
3567
		}
3568

  
3569
		if (this.scrollLeftVel < 0) { // scrolling left?
3570
			if (el.scrollLeft() <= 0) { // already scrolled all the left?
3571
				this.scrollLeftVel = 0;
3572
			}
3573
		}
3574
		else if (this.scrollLeftVel > 0) { // scrolling right?
3575
			if (el.scrollLeft() + el[0].clientWidth >= el[0].scrollWidth) { // already scrolled all the way right?
3576
				this.scrollLeftVel = 0;
3577
			}
3578
		}
3579
	},
3580

  
3581

  
3582
	// This function gets called during every iteration of the scrolling animation loop
3583
	scrollIntervalFunc: function() {
3584
		var el = this.scrollEl;
3585
		var frac = this.scrollIntervalMs / 1000; // considering animation frequency, what the vel should be mult'd by
3586

  
3587
		// change the value of scrollEl's scroll
3588
		if (this.scrollTopVel) {
3589
			el.scrollTop(el.scrollTop() + this.scrollTopVel * frac);
3590
		}
3591
		if (this.scrollLeftVel) {
3592
			el.scrollLeft(el.scrollLeft() + this.scrollLeftVel * frac);
3593
		}
3594

  
3595
		this.constrainScrollVel(); // since the scroll values changed, recompute the velocities
3596

  
3597
		// if scrolled all the way, which causes the vels to be zero, stop the animation loop
3598
		if (!this.scrollTopVel && !this.scrollLeftVel) {
3599
			this.endAutoScroll();
3600
		}
3601
	},
3602

  
3603

  
3604
	// Kills any existing scrolling animation loop
3605
	endAutoScroll: function() {
3606
		if (this.scrollIntervalId) {
3607
			clearInterval(this.scrollIntervalId);
3608
			this.scrollIntervalId = null;
3609

  
3610
			this.handleScrollEnd();
3611
		}
3612
	},
3613

  
3614

  
3615
	// Get called when the scrollEl is scrolled (NOTE: this is delayed via debounce)
3616
	handleDebouncedScroll: function() {
3617
		// recompute all coordinates, but *only* if this is *not* part of our scrolling animation
3618
		if (!this.scrollIntervalId) {
3619
			this.handleScrollEnd();
3620
		}
3621
	},
3622

  
3623

  
3624
	// Called when scrolling has stopped, whether through auto scroll, or the user scrolling
3625
	handleScrollEnd: function() {
3626
	}
3627

  
3628
});
3629
;;
3630

  
3631
/* Tracks mouse movements over a component and raises events about which hit the mouse is over.
3632
------------------------------------------------------------------------------------------------------------------------
3633
options:
3634
- subjectEl
3635
- subjectCenter
3636
*/
3637

  
3638
var HitDragListener = DragListener.extend({
3639

  
3640
	component: null, // converts coordinates to hits
3641
		// methods: hitsNeeded, hitsNotNeeded, queryHit
3642

  
3643
	origHit: null, // the hit the mouse was over when listening started
3644
	hit: null, // the hit the mouse is over
3645
	coordAdjust: null, // delta that will be added to the mouse coordinates when computing collisions
3646

  
3647

  
3648
	constructor: function(component, options) {
3649
		DragListener.call(this, options); // call the super-constructor
3650

  
3651
		this.component = component;
3652
	},
3653

  
3654

  
3655
	// Called when drag listening starts (but a real drag has not necessarily began).
3656
	// ev might be undefined if dragging was started manually.
3657
	handleInteractionStart: function(ev) {
3658
		var subjectEl = this.subjectEl;
3659
		var subjectRect;
3660
		var origPoint;
3661
		var point;
3662

  
3663
		this.component.hitsNeeded();
3664
		this.computeScrollBounds(); // for autoscroll
3665

  
3666
		if (ev) {
3667
			origPoint = { left: getEvX(ev), top: getEvY(ev) };
3668
			point = origPoint;
3669

  
3670
			// constrain the point to bounds of the element being dragged
3671
			if (subjectEl) {
3672
				subjectRect = getOuterRect(subjectEl); // used for centering as well
3673
				point = constrainPoint(point, subjectRect);
3674
			}
3675

  
3676
			this.origHit = this.queryHit(point.left, point.top);
3677

  
3678
			// treat the center of the subject as the collision point?
3679
			if (subjectEl && this.options.subjectCenter) {
3680

  
3681
				// only consider the area the subject overlaps the hit. best for large subjects.
3682
				// TODO: skip this if hit didn't supply left/right/top/bottom
3683
				if (this.origHit) {
3684
					subjectRect = intersectRects(this.origHit, subjectRect) ||
3685
						subjectRect; // in case there is no intersection
3686
				}
3687

  
3688
				point = getRectCenter(subjectRect);
3689
			}
3690

  
3691
			this.coordAdjust = diffPoints(point, origPoint); // point - origPoint
3692
		}
3693
		else {
3694
			this.origHit = null;
3695
			this.coordAdjust = null;
3696
		}
3697

  
3698
		// call the super-method. do it after origHit has been computed
3699
		DragListener.prototype.handleInteractionStart.apply(this, arguments);
3700
	},
3701

  
3702

  
3703
	// Called when the actual drag has started
3704
	handleDragStart: function(ev) {
3705
		var hit;
3706

  
3707
		DragListener.prototype.handleDragStart.apply(this, arguments); // call the super-method
3708

  
3709
		// might be different from this.origHit if the min-distance is large
3710
		hit = this.queryHit(getEvX(ev), getEvY(ev));
3711

  
3712
		// report the initial hit the mouse is over
3713
		// especially important if no min-distance and drag starts immediately
3714
		if (hit) {
3715
			this.handleHitOver(hit);
3716
		}
3717
	},
3718

  
3719

  
3720
	// Called when the drag moves
3721
	handleDrag: function(dx, dy, ev) {
3722
		var hit;
3723

  
3724
		DragListener.prototype.handleDrag.apply(this, arguments); // call the super-method
3725

  
3726
		hit = this.queryHit(getEvX(ev), getEvY(ev));
3727

  
3728
		if (!isHitsEqual(hit, this.hit)) { // a different hit than before?
3729
			if (this.hit) {
3730
				this.handleHitOut();
3731
			}
3732
			if (hit) {
3733
				this.handleHitOver(hit);
3734
			}
3735
		}
3736
	},
3737

  
3738

  
3739
	// Called when dragging has been stopped
3740
	handleDragEnd: function() {
3741
		this.handleHitDone();
3742
		DragListener.prototype.handleDragEnd.apply(this, arguments); // call the super-method
3743
	},
3744

  
3745

  
3746
	// Called when a the mouse has just moved over a new hit
3747
	handleHitOver: function(hit) {
3748
		var isOrig = isHitsEqual(hit, this.origHit);
3749

  
3750
		this.hit = hit;
3751

  
3752
		this.trigger('hitOver', this.hit, isOrig, this.origHit);
3753
	},
3754

  
3755

  
3756
	// Called when the mouse has just moved out of a hit
3757
	handleHitOut: function() {
3758
		if (this.hit) {
3759
			this.trigger('hitOut', this.hit);
3760
			this.handleHitDone();
3761
			this.hit = null;
3762
		}
3763
	},
3764

  
3765

  
3766
	// Called after a hitOut. Also called before a dragStop
3767
	handleHitDone: function() {
3768
		if (this.hit) {
3769
			this.trigger('hitDone', this.hit);
3770
		}
3771
	},
3772

  
3773

  
3774
	// Called when the interaction ends, whether there was a real drag or not
3775
	handleInteractionEnd: function() {
3776
		DragListener.prototype.handleInteractionEnd.apply(this, arguments); // call the super-method
3777

  
3778
		this.origHit = null;
3779
		this.hit = null;
3780

  
3781
		this.component.hitsNotNeeded();
3782
	},
3783

  
3784

  
3785
	// Called when scrolling has stopped, whether through auto scroll, or the user scrolling
3786
	handleScrollEnd: function() {
3787
		DragListener.prototype.handleScrollEnd.apply(this, arguments); // call the super-method
3788

  
3789
		// hits' absolute positions will be in new places after a user's scroll.
3790
		// HACK for recomputing.
3791
		if (this.isDragging) {
3792
			this.component.releaseHits();
3793
			this.component.prepareHits();
3794
		}
3795
	},
3796

  
3797

  
3798
	// Gets the hit underneath the coordinates for the given mouse event
3799
	queryHit: function(left, top) {
3800

  
3801
		if (this.coordAdjust) {
3802
			left += this.coordAdjust.left;
3803
			top += this.coordAdjust.top;
3804
		}
3805

  
3806
		return this.component.queryHit(left, top);
3807
	}
3808

  
3809
});
3810

  
3811

  
3812
// Returns `true` if the hits are identically equal. `false` otherwise. Must be from the same component.
3813
// Two null values will be considered equal, as two "out of the component" states are the same.
3814
function isHitsEqual(hit0, hit1) {
3815

  
3816
	if (!hit0 && !hit1) {
3817
		return true;
3818
	}
3819

  
3820
	if (hit0 && hit1) {
3821
		return hit0.component === hit1.component &&
3822
			isHitPropsWithin(hit0, hit1) &&
3823
			isHitPropsWithin(hit1, hit0); // ensures all props are identical
3824
	}
3825

  
3826
	return false;
3827
}
3828

  
3829

  
3830
// Returns true if all of subHit's non-standard properties are within superHit
3831
function isHitPropsWithin(subHit, superHit) {
3832
	for (var propName in subHit) {
3833
		if (!/^(component|left|right|top|bottom)$/.test(propName)) {
3834
			if (subHit[propName] !== superHit[propName]) {
3835
				return false;
3836
			}
3837
		}
3838
	}
3839
	return true;
3840
}
3841

  
3842
;;
3843

  
3844
/*
3845
Listens to document and window-level user-interaction events, like touch events and mouse events,
3846
and fires these events as-is to whoever is observing a GlobalEmitter.
3847
Best when used as a singleton via GlobalEmitter.get()
3848

  
3849
Normalizes mouse/touch events. For examples:
3850
- ignores the the simulated mouse events that happen after a quick tap: mousemove+mousedown+mouseup+click
3851
- compensates for various buggy scenarios where a touchend does not fire
3852
*/
3853

  
3854
FC.touchMouseIgnoreWait = 500;
3855

  
3856
var GlobalEmitter = Class.extend(ListenerMixin, EmitterMixin, {
3857

  
3858
	isTouching: false,
3859
	mouseIgnoreDepth: 0,
3860
	handleScrollProxy: null,
3861

  
3862

  
3863
	bind: function() {
3864
		var _this = this;
3865

  
3866
		this.listenTo($(document), {
3867
			touchstart: this.handleTouchStart,
3868
			touchcancel: this.handleTouchCancel,
3869
			touchend: this.handleTouchEnd,
3870
			mousedown: this.handleMouseDown,
3871
			mousemove: this.handleMouseMove,
3872
			mouseup: this.handleMouseUp,
3873
			click: this.handleClick,
3874
			selectstart: this.handleSelectStart,
3875
			contextmenu: this.handleContextMenu
3876
		});
3877

  
3878
		// because we need to call preventDefault
3879
		// because https://www.chromestatus.com/features/5093566007214080
3880
		// TODO: investigate performance because this is a global handler
3881
		window.addEventListener(
3882
			'touchmove',
3883
			this.handleTouchMoveProxy = function(ev) {
3884
				_this.handleTouchMove($.Event(ev));
3885
			},
3886
			{ passive: false } // allows preventDefault()
3887
		);
3888

  
3889
		// attach a handler to get called when ANY scroll action happens on the page.
3890
		// this was impossible to do with normal on/off because 'scroll' doesn't bubble.
3891
		// http://stackoverflow.com/a/32954565/96342
3892
		window.addEventListener(
3893
			'scroll',
3894
			this.handleScrollProxy = function(ev) {
3895
				_this.handleScroll($.Event(ev));
3896
			},
3897
			true // useCapture
3898
		);
3899
	},
3900

  
3901
	unbind: function() {
3902
		this.stopListeningTo($(document));
3903

  
3904
		window.removeEventListener(
3905
			'touchmove',
3906
			this.handleTouchMoveProxy
3907
		);
3908

  
3909
		window.removeEventListener(
3910
			'scroll',
3911
			this.handleScrollProxy,
3912
			true // useCapture
3913
		);
3914
	},
3915

  
3916

  
3917
	// Touch Handlers
3918
	// -----------------------------------------------------------------------------------------------------------------
3919

  
3920
	handleTouchStart: function(ev) {
3921

  
3922
		// if a previous touch interaction never ended with a touchend, then implicitly end it,
3923
		// but since a new touch interaction is about to begin, don't start the mouse ignore period.
3924
		this.stopTouch(ev, true); // skipMouseIgnore=true
3925

  
3926
		this.isTouching = true;
3927
		this.trigger('touchstart', ev);
3928
	},
3929

  
3930
	handleTouchMove: function(ev) {
3931
		if (this.isTouching) {
3932
			this.trigger('touchmove', ev);
3933
		}
3934
	},
3935

  
3936
	handleTouchCancel: function(ev) {
3937
		if (this.isTouching) {
3938
			this.trigger('touchcancel', ev);
3939

  
3940
			// Have touchcancel fire an artificial touchend. That way, handlers won't need to listen to both.
3941
			// If touchend fires later, it won't have any effect b/c isTouching will be false.
3942
			this.stopTouch(ev);
3943
		}
3944
	},
3945

  
3946
	handleTouchEnd: function(ev) {
3947
		this.stopTouch(ev);
3948
	},
3949

  
3950

  
3951
	// Mouse Handlers
3952
	// -----------------------------------------------------------------------------------------------------------------
3953

  
3954
	handleMouseDown: function(ev) {
3955
		if (!this.shouldIgnoreMouse()) {
3956
			this.trigger('mousedown', ev);
3957
		}
3958
	},
3959

  
3960
	handleMouseMove: function(ev) {
3961
		if (!this.shouldIgnoreMouse()) {
3962
			this.trigger('mousemove', ev);
3963
		}
3964
	},
3965

  
3966
	handleMouseUp: function(ev) {
3967
		if (!this.shouldIgnoreMouse()) {
3968
			this.trigger('mouseup', ev);
3969
		}
3970
	},
3971

  
3972
	handleClick: function(ev) {
3973
		if (!this.shouldIgnoreMouse()) {
3974
			this.trigger('click', ev);
3975
		}
3976
	},
3977

  
3978

  
3979
	// Misc Handlers
3980
	// -----------------------------------------------------------------------------------------------------------------
3981

  
3982
	handleSelectStart: function(ev) {
3983
		this.trigger('selectstart', ev);
3984
	},
3985

  
3986
	handleContextMenu: function(ev) {
3987
		this.trigger('contextmenu', ev);
3988
	},
3989

  
3990
	handleScroll: function(ev) {
3991
		this.trigger('scroll', ev);
3992
	},
3993

  
3994

  
3995
	// Utils
3996
	// -----------------------------------------------------------------------------------------------------------------
3997

  
3998
	stopTouch: function(ev, skipMouseIgnore) {
3999
		if (this.isTouching) {
4000
			this.isTouching = false;
4001
			this.trigger('touchend', ev);
4002

  
4003
			if (!skipMouseIgnore) {
4004
				this.startTouchMouseIgnore();
4005
			}
4006
		}
4007
	},
4008

  
4009
	startTouchMouseIgnore: function() {
4010
		var _this = this;
4011
		var wait = FC.touchMouseIgnoreWait;
4012

  
4013
		if (wait) {
4014
			this.mouseIgnoreDepth++;
4015
			setTimeout(function() {
4016
				_this.mouseIgnoreDepth--;
4017
			}, wait);
4018
		}
4019
	},
4020

  
4021
	shouldIgnoreMouse: function() {
4022
		return this.isTouching || Boolean(this.mouseIgnoreDepth);
4023
	}
4024

  
4025
});
4026

  
4027

  
4028
// Singleton
4029
// ---------------------------------------------------------------------------------------------------------------------
4030

  
4031
(function() {
4032
	var globalEmitter = null;
4033
	var neededCount = 0;
4034

  
4035

  
4036
	// gets the singleton
4037
	GlobalEmitter.get = function() {
4038

  
4039
		if (!globalEmitter) {
4040
			globalEmitter = new GlobalEmitter();
4041
			globalEmitter.bind();
4042
		}
4043

  
4044
		return globalEmitter;
4045
	};
4046

  
4047

  
4048
	// called when an object knows it will need a GlobalEmitter in the near future.
4049
	GlobalEmitter.needed = function() {
4050
		GlobalEmitter.get(); // ensures globalEmitter
4051
		neededCount++;
4052
	};
4053

  
4054

  
4055
	// called when the object that originally called needed() doesn't need a GlobalEmitter anymore.
4056
	GlobalEmitter.unneeded = function() {
4057
		neededCount--;
4058

  
4059
		if (!neededCount) { // nobody else needs it
4060
			globalEmitter.unbind();
4061
			globalEmitter = null;
4062
		}
4063
	};
4064

  
4065
})();
4066

  
4067
;;
4068

  
4069
/* Creates a clone of an element and lets it track the mouse as it moves
4070
----------------------------------------------------------------------------------------------------------------------*/
4071

  
4072
var MouseFollower = Class.extend(ListenerMixin, {
4073

  
4074
	options: null,
4075

  
4076
	sourceEl: null, // the element that will be cloned and made to look like it is dragging
4077
	el: null, // the clone of `sourceEl` that will track the mouse
4078
	parentEl: null, // the element that `el` (the clone) will be attached to
4079

  
4080
	// the initial position of el, relative to the offset parent. made to match the initial offset of sourceEl
4081
	top0: null,
4082
	left0: null,
4083

  
4084
	// the absolute coordinates of the initiating touch/mouse action
4085
	y0: null,
4086
	x0: null,
4087

  
4088
	// the number of pixels the mouse has moved from its initial position
4089
	topDelta: null,
4090
	leftDelta: null,
4091

  
4092
	isFollowing: false,
4093
	isHidden: false,
4094
	isAnimating: false, // doing the revert animation?
4095

  
4096
	constructor: function(sourceEl, options) {
4097
		this.options = options = options || {};
4098
		this.sourceEl = sourceEl;
4099
		this.parentEl = options.parentEl ? $(options.parentEl) : sourceEl.parent(); // default to sourceEl's parent
4100
	},
4101

  
4102

  
4103
	// Causes the element to start following the mouse
4104
	start: function(ev) {
4105
		if (!this.isFollowing) {
4106
			this.isFollowing = true;
4107

  
4108
			this.y0 = getEvY(ev);
4109
			this.x0 = getEvX(ev);
4110
			this.topDelta = 0;
4111
			this.leftDelta = 0;
4112

  
4113
			if (!this.isHidden) {
4114
				this.updatePosition();
4115
			}
4116

  
4117
			if (getEvIsTouch(ev)) {
4118
				this.listenTo($(document), 'touchmove', this.handleMove);
4119
			}
4120
			else {
4121
				this.listenTo($(document), 'mousemove', this.handleMove);
4122
			}
4123
		}
4124
	},
4125

  
4126

  
4127
	// Causes the element to stop following the mouse. If shouldRevert is true, will animate back to original position.
4128
	// `callback` gets invoked when the animation is complete. If no animation, it is invoked immediately.
4129
	stop: function(shouldRevert, callback) {
4130
		var _this = this;
4131
		var revertDuration = this.options.revertDuration;
4132

  
4133
		function complete() { // might be called by .animate(), which might change `this` context
4134
			_this.isAnimating = false;
4135
			_this.removeElement();
4136

  
4137
			_this.top0 = _this.left0 = null; // reset state for future updatePosition calls
4138

  
4139
			if (callback) {
4140
				callback();
4141
			}
4142
		}
4143

  
4144
		if (this.isFollowing && !this.isAnimating) { // disallow more than one stop animation at a time
4145
			this.isFollowing = false;
4146

  
4147
			this.stopListeningTo($(document));
4148

  
4149
			if (shouldRevert && revertDuration && !this.isHidden) { // do a revert animation?
4150
				this.isAnimating = true;
4151
				this.el.animate({
4152
					top: this.top0,
4153
					left: this.left0
4154
				}, {
4155
					duration: revertDuration,
4156
					complete: complete
4157
				});
4158
			}
4159
			else {
4160
				complete();
4161
			}
4162
		}
4163
	},
4164

  
4165

  
4166
	// Gets the tracking element. Create it if necessary
4167
	getEl: function() {
4168
		var el = this.el;
4169

  
4170
		if (!el) {
4171
			el = this.el = this.sourceEl.clone()
4172
				.addClass(this.options.additionalClass || '')
4173
				.css({
4174
					position: 'absolute',
4175
					visibility: '', // in case original element was hidden (commonly through hideEvents())
4176
					display: this.isHidden ? 'none' : '', // for when initially hidden
4177
					margin: 0,
4178
					right: 'auto', // erase and set width instead
4179
					bottom: 'auto', // erase and set height instead
4180
					width: this.sourceEl.width(), // explicit height in case there was a 'right' value
4181
					height: this.sourceEl.height(), // explicit width in case there was a 'bottom' value
4182
					opacity: this.options.opacity || '',
4183
					zIndex: this.options.zIndex
4184
				});
4185

  
4186
			// we don't want long taps or any mouse interaction causing selection/menus.
4187
			// would use preventSelection(), but that prevents selectstart, causing problems.
4188
			el.addClass('fc-unselectable');
4189

  
4190
			el.appendTo(this.parentEl);
4191
		}
4192

  
4193
		return el;
4194
	},
4195

  
4196

  
4197
	// Removes the tracking element if it has already been created
4198
	removeElement: function() {
4199
		if (this.el) {
4200
			this.el.remove();
4201
			this.el = null;
4202
		}
4203
	},
4204

  
4205

  
4206
	// Update the CSS position of the tracking element
4207
	updatePosition: function() {
4208
		var sourceOffset;
4209
		var origin;
4210

  
4211
		this.getEl(); // ensure this.el
4212

  
4213
		// make sure origin info was computed
4214
		if (this.top0 === null) {
4215
			sourceOffset = this.sourceEl.offset();
4216
			origin = this.el.offsetParent().offset();
4217
			this.top0 = sourceOffset.top - origin.top;
4218
			this.left0 = sourceOffset.left - origin.left;
4219
		}
4220

  
4221
		this.el.css({
4222
			top: this.top0 + this.topDelta,
4223
			left: this.left0 + this.leftDelta
4224
		});
4225
	},
4226

  
4227

  
4228
	// Gets called when the user moves the mouse
4229
	handleMove: function(ev) {
4230
		this.topDelta = getEvY(ev) - this.y0;
4231
		this.leftDelta = getEvX(ev) - this.x0;
4232

  
4233
		if (!this.isHidden) {
4234
			this.updatePosition();
4235
		}
4236
	},
4237

  
4238

  
4239
	// Temporarily makes the tracking element invisible. Can be called before following starts
4240
	hide: function() {
4241
		if (!this.isHidden) {
4242
			this.isHidden = true;
4243
			if (this.el) {
4244
				this.el.hide();
4245
			}
4246
		}
4247
	},
4248

  
4249

  
4250
	// Show the tracking element after it has been temporarily hidden
4251
	show: function() {
4252
		if (this.isHidden) {
4253
			this.isHidden = false;
4254
			this.updatePosition();
4255
			this.getEl().show();
4256
		}
4257
	}
4258

  
4259
});
4260

  
4261
;;
4262

  
4263
/* An abstract class comprised of a "grid" of areas that each represent a specific datetime
4264
----------------------------------------------------------------------------------------------------------------------*/
4265

  
4266
var Grid = FC.Grid = Class.extend(ListenerMixin, {
4267

  
4268
	// self-config, overridable by subclasses
4269
	hasDayInteractions: true, // can user click/select ranges of time?
4270

  
4271
	view: null, // a View object
4272
	isRTL: null, // shortcut to the view's isRTL option
4273

  
4274
	start: null,
4275
	end: null,
4276

  
4277
	el: null, // the containing element
4278
	elsByFill: null, // a hash of jQuery element sets used for rendering each fill. Keyed by fill name.
4279

  
4280
	// derived from options
4281
	eventTimeFormat: null,
4282
	displayEventTime: null,
4283
	displayEventEnd: null,
4284

  
4285
	minResizeDuration: null, // TODO: hack. set by subclasses. minumum event resize duration
4286

  
4287
	// if defined, holds the unit identified (ex: "year" or "month") that determines the level of granularity
4288
	// of the date areas. if not defined, assumes to be day and time granularity.
4289
	// TODO: port isTimeScale into same system?
4290
	largeUnit: null,
4291

  
4292
	dayClickListener: null,
4293
	daySelectListener: null,
4294
	segDragListener: null,
4295
	segResizeListener: null,
4296
	externalDragListener: null,
4297

  
4298

  
4299
	constructor: function(view) {
4300
		this.view = view;
4301
		this.isRTL = view.opt('isRTL');
4302
		this.elsByFill = {};
4303

  
4304
		this.dayClickListener = this.buildDayClickListener();
4305
		this.daySelectListener = this.buildDaySelectListener();
4306
	},
4307

  
4308

  
4309
	/* Options
4310
	------------------------------------------------------------------------------------------------------------------*/
4311

  
4312

  
4313
	// Generates the format string used for event time text, if not explicitly defined by 'timeFormat'
4314
	computeEventTimeFormat: function() {
4315
		return this.view.opt('smallTimeFormat');
4316
	},
4317

  
4318

  
4319
	// Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventTime'.
4320
	// Only applies to non-all-day events.
4321
	computeDisplayEventTime: function() {
4322
		return true;
4323
	},
4324

  
4325

  
4326
	// Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventEnd'
4327
	computeDisplayEventEnd: function() {
4328
		return true;
4329
	},
4330

  
4331

  
4332
	/* Dates
4333
	------------------------------------------------------------------------------------------------------------------*/
4334

  
4335

  
4336
	// Tells the grid about what period of time to display.
4337
	// Any date-related internal data should be generated.
4338
	setRange: function(range) {
4339
		this.start = range.start.clone();
4340
		this.end = range.end.clone();
4341

  
4342
		this.rangeUpdated();
4343
		this.processRangeOptions();
4344
	},
4345

  
4346

  
4347
	// Called when internal variables that rely on the range should be updated
4348
	rangeUpdated: function() {
4349
	},
4350

  
4351

  
4352
	// Updates values that rely on options and also relate to range
4353
	processRangeOptions: function() {
4354
		var view = this.view;
4355
		var displayEventTime;
4356
		var displayEventEnd;
4357

  
4358
		this.eventTimeFormat =
4359
			view.opt('eventTimeFormat') ||
4360
			view.opt('timeFormat') || // deprecated
4361
			this.computeEventTimeFormat();
4362

  
4363
		displayEventTime = view.opt('displayEventTime');
4364
		if (displayEventTime == null) {
4365
			displayEventTime = this.computeDisplayEventTime(); // might be based off of range
4366
		}
4367

  
4368
		displayEventEnd = view.opt('displayEventEnd');
4369
		if (displayEventEnd == null) {
4370
			displayEventEnd = this.computeDisplayEventEnd(); // might be based off of range
4371
		}
4372

  
4373
		this.displayEventTime = displayEventTime;
4374
		this.displayEventEnd = displayEventEnd;
4375
	},
4376

  
4377

  
4378
	// Converts a span (has unzoned start/end and any other grid-specific location information)
4379
	// into an array of segments (pieces of events whose format is decided by the grid).
4380
	spanToSegs: function(span) {
4381
		// subclasses must implement
4382
	},
4383

  
4384

  
4385
	// Diffs the two dates, returning a duration, based on granularity of the grid
4386
	// TODO: port isTimeScale into this system?
4387
	diffDates: function(a, b) {
4388
		if (this.largeUnit) {
4389
			return diffByUnit(a, b, this.largeUnit);
4390
		}
4391
		else {
4392
			return diffDayTime(a, b);
4393
		}
4394
	},
4395

  
4396

  
4397
	/* Hit Area
4398
	------------------------------------------------------------------------------------------------------------------*/
4399

  
4400
	hitsNeededDepth: 0, // necessary because multiple callers might need the same hits
4401

  
4402
	hitsNeeded: function() {
4403
		if (!(this.hitsNeededDepth++)) {
4404
			this.prepareHits();
4405
		}
4406
	},
4407

  
4408
	hitsNotNeeded: function() {
4409
		if (this.hitsNeededDepth && !(--this.hitsNeededDepth)) {
4410
			this.releaseHits();
4411
		}
4412
	},
4413

  
4414

  
4415
	// Called before one or more queryHit calls might happen. Should prepare any cached coordinates for queryHit
4416
	prepareHits: function() {
4417
	},
4418

  
4419

  
4420
	// Called when queryHit calls have subsided. Good place to clear any coordinate caches.
4421
	releaseHits: function() {
4422
	},
4423

  
4424

  
4425
	// Given coordinates from the topleft of the document, return data about the date-related area underneath.
4426
	// Can return an object with arbitrary properties (although top/right/left/bottom are encouraged).
4427
	// Must have a `grid` property, a reference to this current grid. TODO: avoid this
4428
	// The returned object will be processed by getHitSpan and getHitEl.
4429
	queryHit: function(leftOffset, topOffset) {
4430
	},
4431

  
4432

  
4433
	// like getHitSpan, but returns null if the resulting span's range is invalid
4434
	getSafeHitSpan: function(hit) {
4435
		var hitSpan = this.getHitSpan(hit);
4436

  
4437
		if (!isRangeWithinRange(hitSpan, this.view.activeRange)) {
4438
			return null;
4439
		}
4440

  
4441
		return hitSpan;
4442
	},
4443

  
4444

  
4445
	// Given position-level information about a date-related area within the grid,
4446
	// should return an object with at least a start/end date. Can provide other information as well.
4447
	getHitSpan: function(hit) {
4448
	},
4449

  
4450

  
4451
	// Given position-level information about a date-related area within the grid,
4452
	// should return a jQuery element that best represents it. passed to dayClick callback.
4453
	getHitEl: function(hit) {
4454
	},
4455

  
4456

  
4457
	/* Rendering
4458
	------------------------------------------------------------------------------------------------------------------*/
4459

  
4460

  
4461
	// Sets the container element that the grid should render inside of.
4462
	// Does other DOM-related initializations.
4463
	setElement: function(el) {
4464
		this.el = el;
4465

  
4466
		if (this.hasDayInteractions) {
4467
			preventSelection(el);
4468

  
4469
			this.bindDayHandler('touchstart', this.dayTouchStart);
4470
			this.bindDayHandler('mousedown', this.dayMousedown);
4471
		}
4472

  
4473
		// attach event-element-related handlers. in Grid.events
4474
		// same garbage collection note as above.
4475
		this.bindSegHandlers();
4476

  
4477
		this.bindGlobalHandlers();
4478
	},
4479

  
4480

  
4481
	bindDayHandler: function(name, handler) {
4482
		var _this = this;
4483

  
4484
		// attach a handler to the grid's root element.
4485
		// jQuery will take care of unregistering them when removeElement gets called.
4486
		this.el.on(name, function(ev) {
4487
			if (
4488
				!$(ev.target).is(
4489
					_this.segSelector + ',' + // directly on an event element
4490
					_this.segSelector + ' *,' + // within an event element
4491
					'.fc-more,' + // a "more.." link
4492
					'a[data-goto]' // a clickable nav link
4493
				)
4494
			) {
4495
				return handler.call(_this, ev);
4496
			}
4497
		});
4498
	},
4499

  
4500

  
4501
	// Removes the grid's container element from the DOM. Undoes any other DOM-related attachments.
4502
	// DOES NOT remove any content beforehand (doesn't clear events or call unrenderDates), unlike View
4503
	removeElement: function() {
4504
		this.unbindGlobalHandlers();
4505
		this.clearDragListeners();
4506

  
4507
		this.el.remove();
4508

  
4509
		// NOTE: we don't null-out this.el for the same reasons we don't do it within View::removeElement
4510
	},
4511

  
4512

  
4513
	// Renders the basic structure of grid view before any content is rendered
4514
	renderSkeleton: function() {
4515
		// subclasses should implement
4516
	},
4517

  
4518

  
4519
	// Renders the grid's date-related content (like areas that represent days/times).
4520
	// Assumes setRange has already been called and the skeleton has already been rendered.
4521
	renderDates: function() {
4522
		// subclasses should implement
4523
	},
4524

  
4525

  
4526
	// Unrenders the grid's date-related content
4527
	unrenderDates: function() {
4528
		// subclasses should implement
4529
	},
4530

  
4531

  
4532
	/* Handlers
4533
	------------------------------------------------------------------------------------------------------------------*/
4534

  
4535

  
4536
	// Binds DOM handlers to elements that reside outside the grid, such as the document
4537
	bindGlobalHandlers: function() {
4538
		this.listenTo($(document), {
4539
			dragstart: this.externalDragStart, // jqui
4540
			sortstart: this.externalDragStart // jqui
4541
		});
4542
	},
4543

  
4544

  
4545
	// Unbinds DOM handlers from elements that reside outside the grid
4546
	unbindGlobalHandlers: function() {
4547
		this.stopListeningTo($(document));
4548
	},
4549

  
4550

  
4551
	// Process a mousedown on an element that represents a day. For day clicking and selecting.
4552
	dayMousedown: function(ev) {
4553
		var view = this.view;
4554

  
4555
		// HACK
4556
		// This will still work even though bindDayHandler doesn't use GlobalEmitter.
4557
		if (GlobalEmitter.get().shouldIgnoreMouse()) {
4558
			return;
4559
		}
4560

  
4561
		this.dayClickListener.startInteraction(ev);
4562

  
4563
		if (view.opt('selectable')) {
4564
			this.daySelectListener.startInteraction(ev, {
4565
				distance: view.opt('selectMinDistance')
4566
			});
4567
		}
4568
	},
4569

  
4570

  
4571
	dayTouchStart: function(ev) {
4572
		var view = this.view;
4573
		var selectLongPressDelay;
4574

  
4575
		// On iOS (and Android?) when a new selection is initiated overtop another selection,
4576
		// the touchend never fires because the elements gets removed mid-touch-interaction (my theory).
4577
		// HACK: simply don't allow this to happen.
4578
		// ALSO: prevent selection when an *event* is already raised.
4579
		if (view.isSelected || view.selectedEvent) {
4580
			return;
4581
		}
4582

  
4583
		selectLongPressDelay = view.opt('selectLongPressDelay');
4584
		if (selectLongPressDelay == null) {
4585
			selectLongPressDelay = view.opt('longPressDelay'); // fallback
4586
		}
4587

  
4588
		this.dayClickListener.startInteraction(ev);
4589

  
4590
		if (view.opt('selectable')) {
4591
			this.daySelectListener.startInteraction(ev, {
4592
				delay: selectLongPressDelay
4593
			});
4594
		}
4595
	},
4596

  
4597

  
4598
	// Creates a listener that tracks the user's drag across day elements, for day clicking.
4599
	buildDayClickListener: function() {
4600
		var _this = this;
4601
		var view = this.view;
4602
		var dayClickHit; // null if invalid dayClick
4603

  
4604
		var dragListener = new HitDragListener(this, {
4605
			scroll: view.opt('dragScroll'),
4606
			interactionStart: function() {
4607
				dayClickHit = dragListener.origHit;
4608
			},
4609
			hitOver: function(hit, isOrig, origHit) {
4610
				// if user dragged to another cell at any point, it can no longer be a dayClick
4611
				if (!isOrig) {
4612
					dayClickHit = null;
4613
				}
4614
			},
4615
			hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
4616
				dayClickHit = null;
4617
			},
4618
			interactionEnd: function(ev, isCancelled) {
4619
				var hitSpan;
4620

  
4621
				if (!isCancelled && dayClickHit) {
4622
					hitSpan = _this.getSafeHitSpan(dayClickHit);
4623

  
4624
					if (hitSpan) {
4625
						view.triggerDayClick(hitSpan, _this.getHitEl(dayClickHit), ev);
4626
					}
4627
				}
4628
			}
4629
		});
4630

  
4631
		// because dayClickListener won't be called with any time delay, "dragging" will begin immediately,
4632
		// which will kill any touchmoving/scrolling. Prevent this.
4633
		dragListener.shouldCancelTouchScroll = false;
4634

  
4635
		dragListener.scrollAlwaysKills = true;
4636

  
4637
		return dragListener;
4638
	},
4639

  
4640

  
4641
	// Creates a listener that tracks the user's drag across day elements, for day selecting.
4642
	buildDaySelectListener: function() {
4643
		var _this = this;
4644
		var view = this.view;
4645
		var selectionSpan; // null if invalid selection
4646

  
4647
		var dragListener = new HitDragListener(this, {
4648
			scroll: view.opt('dragScroll'),
4649
			interactionStart: function() {
4650
				selectionSpan = null;
4651
			},
4652
			dragStart: function() {
4653
				view.unselect(); // since we could be rendering a new selection, we want to clear any old one
4654
			},
4655
			hitOver: function(hit, isOrig, origHit) {
4656
				var origHitSpan;
4657
				var hitSpan;
4658

  
4659
				if (origHit) { // click needs to have started on a hit
4660

  
4661
					origHitSpan = _this.getSafeHitSpan(origHit);
4662
					hitSpan = _this.getSafeHitSpan(hit);
4663

  
4664
					if (origHitSpan && hitSpan) {
4665
						selectionSpan = _this.computeSelection(origHitSpan, hitSpan);
4666
					}
4667
					else {
4668
						selectionSpan = null;
4669
					}
4670

  
4671
					if (selectionSpan) {
4672
						_this.renderSelection(selectionSpan);
4673
					}
4674
					else if (selectionSpan === false) {
4675
						disableCursor();
4676
					}
4677
				}
4678
			},
4679
			hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
4680
				selectionSpan = null;
4681
				_this.unrenderSelection();
4682
			},
4683
			hitDone: function() { // called after a hitOut OR before a dragEnd
4684
				enableCursor();
4685
			},
4686
			interactionEnd: function(ev, isCancelled) {
4687
				if (!isCancelled && selectionSpan) {
4688
					// the selection will already have been rendered. just report it
4689
					view.reportSelection(selectionSpan, ev);
4690
				}
4691
			}
4692
		});
4693

  
4694
		return dragListener;
4695
	},
4696

  
4697

  
4698
	// Kills all in-progress dragging.
4699
	// Useful for when public API methods that result in re-rendering are invoked during a drag.
4700
	// Also useful for when touch devices misbehave and don't fire their touchend.
4701
	clearDragListeners: function() {
4702
		this.dayClickListener.endInteraction();
4703
		this.daySelectListener.endInteraction();
4704

  
4705
		if (this.segDragListener) {
4706
			this.segDragListener.endInteraction(); // will clear this.segDragListener
4707
		}
4708
		if (this.segResizeListener) {
4709
			this.segResizeListener.endInteraction(); // will clear this.segResizeListener
4710
		}
4711
		if (this.externalDragListener) {
4712
			this.externalDragListener.endInteraction(); // will clear this.externalDragListener
4713
		}
4714
	},
4715

  
4716

  
4717
	/* Event Helper
4718
	------------------------------------------------------------------------------------------------------------------*/
4719
	// TODO: should probably move this to Grid.events, like we did event dragging / resizing
4720

  
4721

  
4722
	// Renders a mock event at the given event location, which contains zoned start/end properties.
4723
	// Returns all mock event elements.
4724
	renderEventLocationHelper: function(eventLocation, sourceSeg) {
4725
		var fakeEvent = this.fabricateHelperEvent(eventLocation, sourceSeg);
4726

  
4727
		return this.renderHelper(fakeEvent, sourceSeg); // do the actual rendering
4728
	},
4729

  
4730

  
4731
	// Builds a fake event given zoned event date properties and a segment is should be inspired from.
4732
	// The range's end can be null, in which case the mock event that is rendered will have a null end time.
4733
	// `sourceSeg` is the internal segment object involved in the drag. If null, something external is dragging.
4734
	fabricateHelperEvent: function(eventLocation, sourceSeg) {
4735
		var fakeEvent = sourceSeg ? createObject(sourceSeg.event) : {}; // mask the original event object if possible
4736

  
4737
		fakeEvent.start = eventLocation.start.clone();
4738
		fakeEvent.end = eventLocation.end ? eventLocation.end.clone() : null;
4739
		fakeEvent.allDay = null; // force it to be freshly computed by normalizeEventDates
4740
		this.view.calendar.normalizeEventDates(fakeEvent);
4741

  
4742
		// this extra className will be useful for differentiating real events from mock events in CSS
4743
		fakeEvent.className = (fakeEvent.className || []).concat('fc-helper');
4744

  
4745
		// if something external is being dragged in, don't render a resizer
4746
		if (!sourceSeg) {
4747
			fakeEvent.editable = false;
4748
		}
4749

  
4750
		return fakeEvent;
4751
	},
4752

  
4753

  
4754
	// Renders a mock event. Given zoned event date properties.
4755
	// Must return all mock event elements.
4756
	renderHelper: function(eventLocation, sourceSeg) {
4757
		// subclasses must implement
4758
	},
4759

  
4760

  
4761
	// Unrenders a mock event
4762
	unrenderHelper: function() {
4763
		// subclasses must implement
4764
	},
4765

  
4766

  
4767
	/* Selection
4768
	------------------------------------------------------------------------------------------------------------------*/
4769

  
4770

  
4771
	// Renders a visual indication of a selection. Will highlight by default but can be overridden by subclasses.
4772
	// Given a span (unzoned start/end and other misc data)
4773
	renderSelection: function(span) {
4774
		this.renderHighlight(span);
4775
	},
4776

  
4777

  
4778
	// Unrenders any visual indications of a selection. Will unrender a highlight by default.
4779
	unrenderSelection: function() {
4780
		this.unrenderHighlight();
4781
	},
4782

  
4783

  
4784
	// Given the first and last date-spans of a selection, returns another date-span object.
4785
	// Subclasses can override and provide additional data in the span object. Will be passed to renderSelection().
4786
	// Will return false if the selection is invalid and this should be indicated to the user.
4787
	// Will return null/undefined if a selection invalid but no error should be reported.
4788
	computeSelection: function(span0, span1) {
4789
		var span = this.computeSelectionSpan(span0, span1);
4790

  
4791
		if (span && !this.view.calendar.isSelectionSpanAllowed(span)) {
4792
			return false;
4793
		}
4794

  
4795
		return span;
4796
	},
4797

  
4798

  
4799
	// Given two spans, must return the combination of the two.
4800
	// TODO: do this separation of concerns (combining VS validation) for event dnd/resize too.
4801
	computeSelectionSpan: function(span0, span1) {
4802
		var dates = [ span0.start, span0.end, span1.start, span1.end ];
4803

  
4804
		dates.sort(compareNumbers); // sorts chronologically. works with Moments
4805

  
4806
		return { start: dates[0].clone(), end: dates[3].clone() };
4807
	},
4808

  
4809

  
4810
	/* Highlight
4811
	------------------------------------------------------------------------------------------------------------------*/
4812

  
4813

  
4814
	// Renders an emphasis on the given date range. Given a span (unzoned start/end and other misc data)
4815
	renderHighlight: function(span) {
4816
		this.renderFill('highlight', this.spanToSegs(span));
4817
	},
4818

  
4819

  
4820
	// Unrenders the emphasis on a date range
4821
	unrenderHighlight: function() {
4822
		this.unrenderFill('highlight');
4823
	},
4824

  
4825

  
4826
	// Generates an array of classNames for rendering the highlight. Used by the fill system.
4827
	highlightSegClasses: function() {
4828
		return [ 'fc-highlight' ];
4829
	},
4830

  
4831

  
4832
	/* Business Hours
4833
	------------------------------------------------------------------------------------------------------------------*/
4834

  
4835

  
4836
	renderBusinessHours: function() {
4837
	},
4838

  
4839

  
4840
	unrenderBusinessHours: function() {
4841
	},
4842

  
4843

  
4844
	/* Now Indicator
4845
	------------------------------------------------------------------------------------------------------------------*/
4846

  
4847

  
4848
	getNowIndicatorUnit: function() {
4849
	},
4850

  
4851

  
4852
	renderNowIndicator: function(date) {
4853
	},
4854

  
4855

  
4856
	unrenderNowIndicator: function() {
4857
	},
4858

  
4859

  
4860
	/* Fill System (highlight, background events, business hours)
4861
	--------------------------------------------------------------------------------------------------------------------
4862
	TODO: remove this system. like we did in TimeGrid
4863
	*/
4864

  
4865

  
4866
	// Renders a set of rectangles over the given segments of time.
4867
	// MUST RETURN a subset of segs, the segs that were actually rendered.
4868
	// Responsible for populating this.elsByFill. TODO: better API for expressing this requirement
4869
	renderFill: function(type, segs) {
4870
		// subclasses must implement
4871
	},
4872

  
4873

  
4874
	// Unrenders a specific type of fill that is currently rendered on the grid
4875
	unrenderFill: function(type) {
4876
		var el = this.elsByFill[type];
4877

  
4878
		if (el) {
4879
			el.remove();
4880
			delete this.elsByFill[type];
4881
		}
4882
	},
4883

  
4884

  
4885
	// Renders and assigns an `el` property for each fill segment. Generic enough to work with different types.
4886
	// Only returns segments that successfully rendered.
4887
	// To be harnessed by renderFill (implemented by subclasses).
4888
	// Analagous to renderFgSegEls.
4889
	renderFillSegEls: function(type, segs) {
4890
		var _this = this;
4891
		var segElMethod = this[type + 'SegEl'];
4892
		var html = '';
4893
		var renderedSegs = [];
4894
		var i;
4895

  
4896
		if (segs.length) {
4897

  
4898
			// build a large concatenation of segment HTML
4899
			for (i = 0; i < segs.length; i++) {
4900
				html += this.fillSegHtml(type, segs[i]);
4901
			}
4902

  
4903
			// Grab individual elements from the combined HTML string. Use each as the default rendering.
4904
			// Then, compute the 'el' for each segment.
4905
			$(html).each(function(i, node) {
4906
				var seg = segs[i];
4907
				var el = $(node);
4908

  
4909
				// allow custom filter methods per-type
4910
				if (segElMethod) {
4911
					el = segElMethod.call(_this, seg, el);
4912
				}
4913

  
4914
				if (el) { // custom filters did not cancel the render
4915
					el = $(el); // allow custom filter to return raw DOM node
4916

  
4917
					// correct element type? (would be bad if a non-TD were inserted into a table for example)
4918
					if (el.is(_this.fillSegTag)) {
4919
						seg.el = el;
4920
						renderedSegs.push(seg);
4921
					}
4922
				}
4923
			});
4924
		}
4925

  
4926
		return renderedSegs;
4927
	},
4928

  
4929

  
4930
	fillSegTag: 'div', // subclasses can override
4931

  
4932

  
4933
	// Builds the HTML needed for one fill segment. Generic enough to work with different types.
4934
	fillSegHtml: function(type, seg) {
4935

  
4936
		// custom hooks per-type
4937
		var classesMethod = this[type + 'SegClasses'];
4938
		var cssMethod = this[type + 'SegCss'];
4939

  
4940
		var classes = classesMethod ? classesMethod.call(this, seg) : [];
4941
		var css = cssToStr(cssMethod ? cssMethod.call(this, seg) : {});
4942

  
4943
		return '<' + this.fillSegTag +
4944
			(classes.length ? ' class="' + classes.join(' ') + '"' : '') +
4945
			(css ? ' style="' + css + '"' : '') +
4946
			' />';
4947
	},
4948

  
4949

  
4950

  
4951
	/* Generic rendering utilities for subclasses
4952
	------------------------------------------------------------------------------------------------------------------*/
4953

  
4954

  
4955
	// Computes HTML classNames for a single-day element
4956
	getDayClasses: function(date, noThemeHighlight) {
4957
		var view = this.view;
4958
		var classes = [];
4959
		var today;
4960

  
4961
		if (!isDateWithinRange(date, view.activeRange)) {
4962
			classes.push('fc-disabled-day'); // TODO: jQuery UI theme?
4963
		}
4964
		else {
4965
			classes.push('fc-' + dayIDs[date.day()]);
4966

  
4967
			if (
4968
				view.currentRangeAs('months') == 1 && // TODO: somehow get into MonthView
4969
				date.month() != view.currentRange.start.month()
4970
			) {
4971
				classes.push('fc-other-month');
4972
			}
4973

  
4974
			today = view.calendar.getNow();
4975

  
4976
			if (date.isSame(today, 'day')) {
4977
				classes.push('fc-today');
4978

  
4979
				if (noThemeHighlight !== true) {
4980
					classes.push(view.highlightStateClass);
4981
				}
4982
			}
4983
			else if (date < today) {
4984
				classes.push('fc-past');
4985
			}
4986
			else {
4987
				classes.push('fc-future');
4988
			}
4989
		}
4990

  
4991
		return classes;
4992
	}
4993

  
4994
});
4995

  
4996
;;
4997

  
4998
/* Event-rendering and event-interaction methods for the abstract Grid class
4999
----------------------------------------------------------------------------------------------------------------------
5000

  
5001
Data Types:
5002
	event - { title, id, start, (end), whatever }
5003
	location - { start, (end), allDay }
5004
	rawEventRange - { start, end }
5005
	eventRange - { start, end, isStart, isEnd }
5006
	eventSpan - { start, end, isStart, isEnd, whatever }
5007
	eventSeg - { event, whatever }
5008
	seg - { whatever }
5009
*/
5010

  
5011
Grid.mixin({
5012

  
5013
	// self-config, overridable by subclasses
5014
	segSelector: '.fc-event-container > *', // what constitutes an event element?
5015

  
5016
	mousedOverSeg: null, // the segment object the user's mouse is over. null if over nothing
5017
	isDraggingSeg: false, // is a segment being dragged? boolean
5018
	isResizingSeg: false, // is a segment being resized? boolean
5019
	isDraggingExternal: false, // jqui-dragging an external element? boolean
5020
	segs: null, // the *event* segments currently rendered in the grid. TODO: rename to `eventSegs`
5021

  
5022

  
5023
	// Renders the given events onto the grid
5024
	renderEvents: function(events) {
5025
		var bgEvents = [];
5026
		var fgEvents = [];
5027
		var i;
5028

  
5029
		for (i = 0; i < events.length; i++) {
5030
			(isBgEvent(events[i]) ? bgEvents : fgEvents).push(events[i]);
5031
		}
5032

  
5033
		this.segs = [].concat( // record all segs
5034
			this.renderBgEvents(bgEvents),
5035
			this.renderFgEvents(fgEvents)
5036
		);
5037
	},
5038

  
5039

  
5040
	renderBgEvents: function(events) {
5041
		var segs = this.eventsToSegs(events);
5042

  
5043
		// renderBgSegs might return a subset of segs, segs that were actually rendered
5044
		return this.renderBgSegs(segs) || segs;
5045
	},
5046

  
5047

  
5048
	renderFgEvents: function(events) {
5049
		var segs = this.eventsToSegs(events);
5050

  
5051
		// renderFgSegs might return a subset of segs, segs that were actually rendered
5052
		return this.renderFgSegs(segs) || segs;
5053
	},
5054

  
5055

  
5056
	// Unrenders all events currently rendered on the grid
5057
	unrenderEvents: function() {
5058
		this.handleSegMouseout(); // trigger an eventMouseout if user's mouse is over an event
5059
		this.clearDragListeners();
5060

  
5061
		this.unrenderFgSegs();
5062
		this.unrenderBgSegs();
5063

  
5064
		this.segs = null;
5065
	},
5066

  
5067

  
5068
	// Retrieves all rendered segment objects currently rendered on the grid
5069
	getEventSegs: function() {
5070
		return this.segs || [];
5071
	},
5072

  
5073

  
5074
	/* Foreground Segment Rendering
5075
	------------------------------------------------------------------------------------------------------------------*/
5076

  
5077

  
5078
	// Renders foreground event segments onto the grid. May return a subset of segs that were rendered.
5079
	renderFgSegs: function(segs) {
5080
		// subclasses must implement
5081
	},
5082

  
5083

  
5084
	// Unrenders all currently rendered foreground segments
5085
	unrenderFgSegs: function() {
5086
		// subclasses must implement
5087
	},
5088

  
5089

  
5090
	// Renders and assigns an `el` property for each foreground event segment.
5091
	// Only returns segments that successfully rendered.
5092
	// A utility that subclasses may use.
5093
	renderFgSegEls: function(segs, disableResizing) {
5094
		var view = this.view;
5095
		var html = '';
5096
		var renderedSegs = [];
5097
		var i;
5098

  
5099
		if (segs.length) { // don't build an empty html string
5100

  
5101
			// build a large concatenation of event segment HTML
5102
			for (i = 0; i < segs.length; i++) {
5103
				html += this.fgSegHtml(segs[i], disableResizing);
5104
			}
5105

  
5106
			// Grab individual elements from the combined HTML string. Use each as the default rendering.
5107
			// Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false.
5108
			$(html).each(function(i, node) {
5109
				var seg = segs[i];
5110
				var el = view.resolveEventEl(seg.event, $(node));
5111

  
5112
				if (el) {
5113
					el.data('fc-seg', seg); // used by handlers
5114
					seg.el = el;
5115
					renderedSegs.push(seg);
5116
				}
5117
			});
5118
		}
5119

  
5120
		return renderedSegs;
5121
	},
5122

  
5123

  
5124
	// Generates the HTML for the default rendering of a foreground event segment. Used by renderFgSegEls()
5125
	fgSegHtml: function(seg, disableResizing) {
5126
		// subclasses should implement
5127
	},
5128

  
5129

  
5130
	/* Background Segment Rendering
5131
	------------------------------------------------------------------------------------------------------------------*/
5132

  
5133

  
5134
	// Renders the given background event segments onto the grid.
5135
	// Returns a subset of the segs that were actually rendered.
5136
	renderBgSegs: function(segs) {
5137
		return this.renderFill('bgEvent', segs);
5138
	},
5139

  
5140

  
5141
	// Unrenders all the currently rendered background event segments
5142
	unrenderBgSegs: function() {
5143
		this.unrenderFill('bgEvent');
5144
	},
5145

  
5146

  
5147
	// Renders a background event element, given the default rendering. Called by the fill system.
5148
	bgEventSegEl: function(seg, el) {
5149
		return this.view.resolveEventEl(seg.event, el); // will filter through eventRender
5150
	},
5151

  
5152

  
5153
	// Generates an array of classNames to be used for the default rendering of a background event.
5154
	// Called by fillSegHtml.
5155
	bgEventSegClasses: function(seg) {
5156
		var event = seg.event;
5157
		var source = event.source || {};
5158

  
5159
		return [ 'fc-bgevent' ].concat(
5160
			event.className,
5161
			source.className || []
5162
		);
5163
	},
5164

  
5165

  
5166
	// Generates a semicolon-separated CSS string to be used for the default rendering of a background event.
5167
	// Called by fillSegHtml.
5168
	bgEventSegCss: function(seg) {
5169
		return {
5170
			'background-color': this.getSegSkinCss(seg)['background-color']
5171
		};
5172
	},
5173

  
5174

  
5175
	// Generates an array of classNames to be used for the rendering business hours overlay. Called by the fill system.
5176
	// Called by fillSegHtml.
5177
	businessHoursSegClasses: function(seg) {
5178
		return [ 'fc-nonbusiness', 'fc-bgevent' ];
5179
	},
5180

  
5181

  
5182
	/* Business Hours
5183
	------------------------------------------------------------------------------------------------------------------*/
5184

  
5185

  
5186
	// Compute business hour segs for the grid's current date range.
5187
	// Caller must ask if whole-day business hours are needed.
5188
	// If no `businessHours` configuration value is specified, assumes the calendar default.
5189
	buildBusinessHourSegs: function(wholeDay, businessHours) {
5190
		return this.eventsToSegs(
5191
			this.buildBusinessHourEvents(wholeDay, businessHours)
5192
		);
5193
	},
5194

  
5195

  
5196
	// Compute business hour *events* for the grid's current date range.
5197
	// Caller must ask if whole-day business hours are needed.
5198
	// If no `businessHours` configuration value is specified, assumes the calendar default.
5199
	buildBusinessHourEvents: function(wholeDay, businessHours) {
5200
		var calendar = this.view.calendar;
5201
		var events;
5202

  
5203
		if (businessHours == null) {
5204
			// fallback
5205
			// access from calendawr. don't access from view. doesn't update with dynamic options.
5206
			businessHours = calendar.opt('businessHours');
5207
		}
5208

  
5209
		events = calendar.computeBusinessHourEvents(wholeDay, businessHours);
5210

  
5211
		// HACK. Eventually refactor business hours "events" system.
5212
		// If no events are given, but businessHours is activated, this means the entire visible range should be
5213
		// marked as *not* business-hours, via inverse-background rendering.
5214
		if (!events.length && businessHours) {
5215
			events = [
5216
				$.extend({}, BUSINESS_HOUR_EVENT_DEFAULTS, {
5217
					start: this.view.activeRange.end, // guaranteed out-of-range
5218
					end: this.view.activeRange.end,   // "
5219
					dow: null
5220
				})
5221
			];
5222
		}
5223

  
5224
		return events;
5225
	},
5226

  
5227

  
5228
	/* Handlers
5229
	------------------------------------------------------------------------------------------------------------------*/
5230

  
5231

  
5232
	// Attaches event-element-related handlers for *all* rendered event segments of the view.
5233
	bindSegHandlers: function() {
5234
		this.bindSegHandlersToEl(this.el);
5235
	},
5236

  
5237

  
5238
	// Attaches event-element-related handlers to an arbitrary container element. leverages bubbling.
5239
	bindSegHandlersToEl: function(el) {
5240
		this.bindSegHandlerToEl(el, 'touchstart', this.handleSegTouchStart);
5241
		this.bindSegHandlerToEl(el, 'mouseenter', this.handleSegMouseover);
5242
		this.bindSegHandlerToEl(el, 'mouseleave', this.handleSegMouseout);
5243
		this.bindSegHandlerToEl(el, 'mousedown', this.handleSegMousedown);
5244
		this.bindSegHandlerToEl(el, 'click', this.handleSegClick);
5245
	},
5246

  
5247

  
5248
	// Executes a handler for any a user-interaction on a segment.
5249
	// Handler gets called with (seg, ev), and with the `this` context of the Grid
5250
	bindSegHandlerToEl: function(el, name, handler) {
5251
		var _this = this;
5252

  
5253
		el.on(name, this.segSelector, function(ev) {
5254
			var seg = $(this).data('fc-seg'); // grab segment data. put there by View::renderEvents
5255

  
5256
			// only call the handlers if there is not a drag/resize in progress
5257
			if (seg && !_this.isDraggingSeg && !_this.isResizingSeg) {
5258
				return handler.call(_this, seg, ev); // context will be the Grid
5259
			}
5260
		});
5261
	},
5262

  
5263

  
5264
	handleSegClick: function(seg, ev) {
5265
		var res = this.view.publiclyTrigger('eventClick', seg.el[0], seg.event, ev); // can return `false` to cancel
5266
		if (res === false) {
5267
			ev.preventDefault();
5268
		}
5269
	},
5270

  
5271

  
5272
	// Updates internal state and triggers handlers for when an event element is moused over
5273
	handleSegMouseover: function(seg, ev) {
5274
		if (
5275
			!GlobalEmitter.get().shouldIgnoreMouse() &&
5276
			!this.mousedOverSeg
5277
		) {
5278
			this.mousedOverSeg = seg;
5279
			if (this.view.isEventResizable(seg.event)) {
5280
				seg.el.addClass('fc-allow-mouse-resize');
5281
			}
5282
			this.view.publiclyTrigger('eventMouseover', seg.el[0], seg.event, ev);
5283
		}
5284
	},
5285

  
5286

  
5287
	// Updates internal state and triggers handlers for when an event element is moused out.
5288
	// Can be given no arguments, in which case it will mouseout the segment that was previously moused over.
5289
	handleSegMouseout: function(seg, ev) {
5290
		ev = ev || {}; // if given no args, make a mock mouse event
5291

  
5292
		if (this.mousedOverSeg) {
5293
			seg = seg || this.mousedOverSeg; // if given no args, use the currently moused-over segment
5294
			this.mousedOverSeg = null;
5295
			if (this.view.isEventResizable(seg.event)) {
5296
				seg.el.removeClass('fc-allow-mouse-resize');
5297
			}
5298
			this.view.publiclyTrigger('eventMouseout', seg.el[0], seg.event, ev);
5299
		}
5300
	},
5301

  
5302

  
5303
	handleSegMousedown: function(seg, ev) {
5304
		var isResizing = this.startSegResize(seg, ev, { distance: 5 });
5305

  
5306
		if (!isResizing && this.view.isEventDraggable(seg.event)) {
5307
			this.buildSegDragListener(seg)
5308
				.startInteraction(ev, {
5309
					distance: 5
5310
				});
5311
		}
5312
	},
5313

  
5314

  
5315
	handleSegTouchStart: function(seg, ev) {
5316
		var view = this.view;
5317
		var event = seg.event;
5318
		var isSelected = view.isEventSelected(event);
5319
		var isDraggable = view.isEventDraggable(event);
5320
		var isResizable = view.isEventResizable(event);
5321
		var isResizing = false;
5322
		var dragListener;
5323
		var eventLongPressDelay;
5324

  
5325
		if (isSelected && isResizable) {
5326
			// only allow resizing of the event is selected
5327
			isResizing = this.startSegResize(seg, ev);
5328
		}
5329

  
5330
		if (!isResizing && (isDraggable || isResizable)) { // allowed to be selected?
5331

  
5332
			eventLongPressDelay = view.opt('eventLongPressDelay');
5333
			if (eventLongPressDelay == null) {
5334
				eventLongPressDelay = view.opt('longPressDelay'); // fallback
5335
			}
5336

  
5337
			dragListener = isDraggable ?
5338
				this.buildSegDragListener(seg) :
5339
				this.buildSegSelectListener(seg); // seg isn't draggable, but still needs to be selected
5340

  
5341
			dragListener.startInteraction(ev, { // won't start if already started
5342
				delay: isSelected ? 0 : eventLongPressDelay // do delay if not already selected
5343
			});
5344
		}
5345
	},
5346

  
5347

  
5348
	// returns boolean whether resizing actually started or not.
5349
	// assumes the seg allows resizing.
5350
	// `dragOptions` are optional.
5351
	startSegResize: function(seg, ev, dragOptions) {
5352
		if ($(ev.target).is('.fc-resizer')) {
5353
			this.buildSegResizeListener(seg, $(ev.target).is('.fc-start-resizer'))
5354
				.startInteraction(ev, dragOptions);
5355
			return true;
5356
		}
5357
		return false;
5358
	},
5359

  
5360

  
5361

  
5362
	/* Event Dragging
5363
	------------------------------------------------------------------------------------------------------------------*/
5364

  
5365

  
5366
	// Builds a listener that will track user-dragging on an event segment.
5367
	// Generic enough to work with any type of Grid.
5368
	// Has side effect of setting/unsetting `segDragListener`
5369
	buildSegDragListener: function(seg) {
5370
		var _this = this;
5371
		var view = this.view;
5372
		var el = seg.el;
5373
		var event = seg.event;
5374
		var isDragging;
5375
		var mouseFollower; // A clone of the original element that will move with the mouse
5376
		var dropLocation; // zoned event date properties
5377

  
5378
		if (this.segDragListener) {
5379
			return this.segDragListener;
5380
		}
5381

  
5382
		// Tracks mouse movement over the *view's* coordinate map. Allows dragging and dropping between subcomponents
5383
		// of the view.
5384
		var dragListener = this.segDragListener = new HitDragListener(view, {
5385
			scroll: view.opt('dragScroll'),
5386
			subjectEl: el,
5387
			subjectCenter: true,
5388
			interactionStart: function(ev) {
5389
				seg.component = _this; // for renderDrag
5390
				isDragging = false;
5391
				mouseFollower = new MouseFollower(seg.el, {
5392
					additionalClass: 'fc-dragging',
5393
					parentEl: view.el,
5394
					opacity: dragListener.isTouch ? null : view.opt('dragOpacity'),
5395
					revertDuration: view.opt('dragRevertDuration'),
5396
					zIndex: 2 // one above the .fc-view
5397
				});
5398
				mouseFollower.hide(); // don't show until we know this is a real drag
5399
				mouseFollower.start(ev);
5400
			},
5401
			dragStart: function(ev) {
5402
				if (dragListener.isTouch && !view.isEventSelected(event)) {
5403
					// if not previously selected, will fire after a delay. then, select the event
5404
					view.selectEvent(event);
5405
				}
5406
				isDragging = true;
5407
				_this.handleSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
5408
				_this.segDragStart(seg, ev);
5409
				view.hideEvent(event); // hide all event segments. our mouseFollower will take over
5410
			},
5411
			hitOver: function(hit, isOrig, origHit) {
5412
				var isAllowed = true;
5413
				var origHitSpan;
5414
				var hitSpan;
5415
				var dragHelperEls;
5416

  
5417
				// starting hit could be forced (DayGrid.limit)
5418
				if (seg.hit) {
5419
					origHit = seg.hit;
5420
				}
5421

  
5422
				// hit might not belong to this grid, so query origin grid
5423
				origHitSpan = origHit.component.getSafeHitSpan(origHit);
5424
				hitSpan = hit.component.getSafeHitSpan(hit);
5425

  
5426
				if (origHitSpan && hitSpan) {
5427
					dropLocation = _this.computeEventDrop(origHitSpan, hitSpan, event);
5428
					isAllowed = dropLocation && _this.isEventLocationAllowed(dropLocation, event);
5429
				}
5430
				else {
5431
					isAllowed = false;
5432
				}
5433

  
5434
				if (!isAllowed) {
5435
					dropLocation = null;
5436
					disableCursor();
5437
				}
5438

  
5439
				// if a valid drop location, have the subclass render a visual indication
5440
				if (dropLocation && (dragHelperEls = view.renderDrag(dropLocation, seg))) {
5441

  
5442
					dragHelperEls.addClass('fc-dragging');
5443
					if (!dragListener.isTouch) {
5444
						_this.applyDragOpacity(dragHelperEls);
5445
					}
5446

  
5447
					mouseFollower.hide(); // if the subclass is already using a mock event "helper", hide our own
5448
				}
5449
				else {
5450
					mouseFollower.show(); // otherwise, have the helper follow the mouse (no snapping)
5451
				}
5452

  
5453
				if (isOrig) {
5454
					dropLocation = null; // needs to have moved hits to be a valid drop
5455
				}
5456
			},
5457
			hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
5458
				view.unrenderDrag(); // unrender whatever was done in renderDrag
5459
				mouseFollower.show(); // show in case we are moving out of all hits
5460
				dropLocation = null;
5461
			},
5462
			hitDone: function() { // Called after a hitOut OR before a dragEnd
5463
				enableCursor();
5464
			},
5465
			interactionEnd: function(ev) {
5466
				delete seg.component; // prevent side effects
5467

  
5468
				// do revert animation if hasn't changed. calls a callback when finished (whether animation or not)
5469
				mouseFollower.stop(!dropLocation, function() {
5470
					if (isDragging) {
5471
						view.unrenderDrag();
5472
						_this.segDragStop(seg, ev);
5473
					}
5474

  
5475
					if (dropLocation) {
5476
						// no need to re-show original, will rerender all anyways. esp important if eventRenderWait
5477
						view.reportSegDrop(seg, dropLocation, _this.largeUnit, el, ev);
5478
					}
5479
					else {
5480
						view.showEvent(event);
5481
					}
5482
				});
5483
				_this.segDragListener = null;
5484
			}
5485
		});
5486

  
5487
		return dragListener;
5488
	},
5489

  
5490

  
5491
	// seg isn't draggable, but let's use a generic DragListener
5492
	// simply for the delay, so it can be selected.
5493
	// Has side effect of setting/unsetting `segDragListener`
5494
	buildSegSelectListener: function(seg) {
5495
		var _this = this;
5496
		var view = this.view;
5497
		var event = seg.event;
5498

  
5499
		if (this.segDragListener) {
5500
			return this.segDragListener;
5501
		}
5502

  
5503
		var dragListener = this.segDragListener = new DragListener({
5504
			dragStart: function(ev) {
5505
				if (dragListener.isTouch && !view.isEventSelected(event)) {
5506
					// if not previously selected, will fire after a delay. then, select the event
5507
					view.selectEvent(event);
5508
				}
5509
			},
5510
			interactionEnd: function(ev) {
5511
				_this.segDragListener = null;
5512
			}
5513
		});
5514

  
5515
		return dragListener;
5516
	},
5517

  
5518

  
5519
	// Called before event segment dragging starts
5520
	segDragStart: function(seg, ev) {
5521
		this.isDraggingSeg = true;
5522
		this.view.publiclyTrigger('eventDragStart', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
5523
	},
5524

  
5525

  
5526
	// Called after event segment dragging stops
5527
	segDragStop: function(seg, ev) {
5528
		this.isDraggingSeg = false;
5529
		this.view.publiclyTrigger('eventDragStop', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
5530
	},
5531

  
5532

  
5533
	// Given the spans an event drag began, and the span event was dropped, calculates the new zoned start/end/allDay
5534
	// values for the event. Subclasses may override and set additional properties to be used by renderDrag.
5535
	// A falsy returned value indicates an invalid drop.
5536
	// DOES NOT consider overlap/constraint.
5537
	computeEventDrop: function(startSpan, endSpan, event) {
5538
		var calendar = this.view.calendar;
5539
		var dragStart = startSpan.start;
5540
		var dragEnd = endSpan.start;
5541
		var delta;
5542
		var dropLocation; // zoned event date properties
5543

  
5544
		if (dragStart.hasTime() === dragEnd.hasTime()) {
5545
			delta = this.diffDates(dragEnd, dragStart);
5546

  
5547
			// if an all-day event was in a timed area and it was dragged to a different time,
5548
			// guarantee an end and adjust start/end to have times
5549
			if (event.allDay && durationHasTime(delta)) {
5550
				dropLocation = {
5551
					start: event.start.clone(),
5552
					end: calendar.getEventEnd(event), // will be an ambig day
5553
					allDay: false // for normalizeEventTimes
5554
				};
5555
				calendar.normalizeEventTimes(dropLocation);
5556
			}
5557
			// othewise, work off existing values
5558
			else {
5559
				dropLocation = pluckEventDateProps(event);
5560
			}
5561

  
5562
			dropLocation.start.add(delta);
5563
			if (dropLocation.end) {
5564
				dropLocation.end.add(delta);
5565
			}
5566
		}
5567
		else {
5568
			// if switching from day <-> timed, start should be reset to the dropped date, and the end cleared
5569
			dropLocation = {
5570
				start: dragEnd.clone(),
5571
				end: null, // end should be cleared
5572
				allDay: !dragEnd.hasTime()
5573
			};
5574
		}
5575

  
5576
		return dropLocation;
5577
	},
5578

  
5579

  
5580
	// Utility for apply dragOpacity to a jQuery set
5581
	applyDragOpacity: function(els) {
5582
		var opacity = this.view.opt('dragOpacity');
5583

  
5584
		if (opacity != null) {
5585
			els.css('opacity', opacity);
5586
		}
5587
	},
5588

  
5589

  
5590
	/* External Element Dragging
5591
	------------------------------------------------------------------------------------------------------------------*/
5592

  
5593

  
5594
	// Called when a jQuery UI drag is initiated anywhere in the DOM
5595
	externalDragStart: function(ev, ui) {
5596
		var view = this.view;
5597
		var el;
5598
		var accept;
5599

  
5600
		if (view.opt('droppable')) { // only listen if this setting is on
5601
			el = $((ui ? ui.item : null) || ev.target);
5602

  
5603
			// Test that the dragged element passes the dropAccept selector or filter function.
5604
			// FYI, the default is "*" (matches all)
5605
			accept = view.opt('dropAccept');
5606
			if ($.isFunction(accept) ? accept.call(el[0], el) : el.is(accept)) {
5607
				if (!this.isDraggingExternal) { // prevent double-listening if fired twice
5608
					this.listenToExternalDrag(el, ev, ui);
5609
				}
5610
			}
5611
		}
5612
	},
5613

  
5614

  
5615
	// Called when a jQuery UI drag starts and it needs to be monitored for dropping
5616
	listenToExternalDrag: function(el, ev, ui) {
5617
		var _this = this;
5618
		var view = this.view;
5619
		var meta = getDraggedElMeta(el); // extra data about event drop, including possible event to create
5620
		var dropLocation; // a null value signals an unsuccessful drag
5621

  
5622
		// listener that tracks mouse movement over date-associated pixel regions
5623
		var dragListener = _this.externalDragListener = new HitDragListener(this, {
5624
			interactionStart: function() {
5625
				_this.isDraggingExternal = true;
5626
			},
5627
			hitOver: function(hit) {
5628
				var isAllowed = true;
5629
				var hitSpan = hit.component.getSafeHitSpan(hit); // hit might not belong to this grid
5630

  
5631
				if (hitSpan) {
5632
					dropLocation = _this.computeExternalDrop(hitSpan, meta);
5633
					isAllowed = dropLocation && _this.isExternalLocationAllowed(dropLocation, meta.eventProps);
5634
				}
5635
				else {
5636
					isAllowed = false;
5637
				}
5638

  
5639
				if (!isAllowed) {
5640
					dropLocation = null;
5641
					disableCursor();
5642
				}
5643

  
5644
				if (dropLocation) {
5645
					_this.renderDrag(dropLocation); // called without a seg parameter
5646
				}
5647
			},
5648
			hitOut: function() {
5649
				dropLocation = null; // signal unsuccessful
5650
			},
5651
			hitDone: function() { // Called after a hitOut OR before a dragEnd
5652
				enableCursor();
5653
				_this.unrenderDrag();
5654
			},
5655
			interactionEnd: function(ev) {
5656
				if (dropLocation) { // element was dropped on a valid hit
5657
					view.reportExternalDrop(meta, dropLocation, el, ev, ui);
5658
				}
5659
				_this.isDraggingExternal = false;
5660
				_this.externalDragListener = null;
5661
			}
5662
		});
5663

  
5664
		dragListener.startDrag(ev); // start listening immediately
5665
	},
5666

  
5667

  
5668
	// Given a hit to be dropped upon, and misc data associated with the jqui drag (guaranteed to be a plain object),
5669
	// returns the zoned start/end dates for the event that would result from the hypothetical drop. end might be null.
5670
	// Returning a null value signals an invalid drop hit.
5671
	// DOES NOT consider overlap/constraint.
5672
	computeExternalDrop: function(span, meta) {
5673
		var calendar = this.view.calendar;
5674
		var dropLocation = {
5675
			start: calendar.applyTimezone(span.start), // simulate a zoned event start date
5676
			end: null
5677
		};
5678

  
5679
		// if dropped on an all-day span, and element's metadata specified a time, set it
5680
		if (meta.startTime && !dropLocation.start.hasTime()) {
5681
			dropLocation.start.time(meta.startTime);
5682
		}
5683

  
5684
		if (meta.duration) {
5685
			dropLocation.end = dropLocation.start.clone().add(meta.duration);
5686
		}
5687

  
5688
		return dropLocation;
5689
	},
5690

  
5691

  
5692

  
5693
	/* Drag Rendering (for both events and an external elements)
5694
	------------------------------------------------------------------------------------------------------------------*/
5695

  
5696

  
5697
	// Renders a visual indication of an event or external element being dragged.
5698
	// `dropLocation` contains hypothetical start/end/allDay values the event would have if dropped. end can be null.
5699
	// `seg` is the internal segment object that is being dragged. If dragging an external element, `seg` is null.
5700
	// A truthy returned value indicates this method has rendered a helper element.
5701
	// Must return elements used for any mock events.
5702
	renderDrag: function(dropLocation, seg) {
5703
		// subclasses must implement
5704
	},
5705

  
5706

  
5707
	// Unrenders a visual indication of an event or external element being dragged
5708
	unrenderDrag: function() {
5709
		// subclasses must implement
5710
	},
5711

  
5712

  
5713
	/* Resizing
5714
	------------------------------------------------------------------------------------------------------------------*/
5715

  
5716

  
5717
	// Creates a listener that tracks the user as they resize an event segment.
5718
	// Generic enough to work with any type of Grid.
5719
	buildSegResizeListener: function(seg, isStart) {
5720
		var _this = this;
5721
		var view = this.view;
5722
		var calendar = view.calendar;
5723
		var el = seg.el;
5724
		var event = seg.event;
5725
		var eventEnd = calendar.getEventEnd(event);
5726
		var isDragging;
5727
		var resizeLocation; // zoned event date properties. falsy if invalid resize
5728

  
5729
		// Tracks mouse movement over the *grid's* coordinate map
5730
		var dragListener = this.segResizeListener = new HitDragListener(this, {
5731
			scroll: view.opt('dragScroll'),
5732
			subjectEl: el,
5733
			interactionStart: function() {
5734
				isDragging = false;
5735
			},
5736
			dragStart: function(ev) {
5737
				isDragging = true;
5738
				_this.handleSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
5739
				_this.segResizeStart(seg, ev);
5740
			},
5741
			hitOver: function(hit, isOrig, origHit) {
5742
				var isAllowed = true;
5743
				var origHitSpan = _this.getSafeHitSpan(origHit);
5744
				var hitSpan = _this.getSafeHitSpan(hit);
5745

  
5746
				if (origHitSpan && hitSpan) {
5747
					resizeLocation = isStart ?
5748
						_this.computeEventStartResize(origHitSpan, hitSpan, event) :
5749
						_this.computeEventEndResize(origHitSpan, hitSpan, event);
5750

  
5751
					isAllowed = resizeLocation && _this.isEventLocationAllowed(resizeLocation, event);
5752
				}
5753
				else {
5754
					isAllowed = false;
5755
				}
5756

  
5757
				if (!isAllowed) {
5758
					resizeLocation = null;
5759
					disableCursor();
5760
				}
5761
				else {
5762
					if (
5763
						resizeLocation.start.isSame(event.start.clone().stripZone()) &&
5764
						resizeLocation.end.isSame(eventEnd.clone().stripZone())
5765
					) {
5766
						// no change. (FYI, event dates might have zones)
5767
						resizeLocation = null;
5768
					}
5769
				}
5770

  
5771
				if (resizeLocation) {
5772
					view.hideEvent(event);
5773
					_this.renderEventResize(resizeLocation, seg);
5774
				}
5775
			},
5776
			hitOut: function() { // called before mouse moves to a different hit OR moved out of all hits
5777
				resizeLocation = null;
5778
				view.showEvent(event); // for when out-of-bounds. show original
5779
			},
5780
			hitDone: function() { // resets the rendering to show the original event
5781
				_this.unrenderEventResize();
5782
				enableCursor();
5783
			},
5784
			interactionEnd: function(ev) {
5785
				if (isDragging) {
5786
					_this.segResizeStop(seg, ev);
5787
				}
5788

  
5789
				if (resizeLocation) { // valid date to resize to?
5790
					// no need to re-show original, will rerender all anyways. esp important if eventRenderWait
5791
					view.reportSegResize(seg, resizeLocation, _this.largeUnit, el, ev);
5792
				}
5793
				else {
5794
					view.showEvent(event);
5795
				}
5796
				_this.segResizeListener = null;
5797
			}
5798
		});
5799

  
5800
		return dragListener;
5801
	},
5802

  
5803

  
5804
	// Called before event segment resizing starts
5805
	segResizeStart: function(seg, ev) {
5806
		this.isResizingSeg = true;
5807
		this.view.publiclyTrigger('eventResizeStart', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
5808
	},
5809

  
5810

  
5811
	// Called after event segment resizing stops
5812
	segResizeStop: function(seg, ev) {
5813
		this.isResizingSeg = false;
5814
		this.view.publiclyTrigger('eventResizeStop', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
5815
	},
5816

  
5817

  
5818
	// Returns new date-information for an event segment being resized from its start
5819
	computeEventStartResize: function(startSpan, endSpan, event) {
5820
		return this.computeEventResize('start', startSpan, endSpan, event);
5821
	},
5822

  
5823

  
5824
	// Returns new date-information for an event segment being resized from its end
5825
	computeEventEndResize: function(startSpan, endSpan, event) {
5826
		return this.computeEventResize('end', startSpan, endSpan, event);
5827
	},
5828

  
5829

  
5830
	// Returns new zoned date information for an event segment being resized from its start OR end
5831
	// `type` is either 'start' or 'end'.
5832
	// DOES NOT consider overlap/constraint.
5833
	computeEventResize: function(type, startSpan, endSpan, event) {
5834
		var calendar = this.view.calendar;
5835
		var delta = this.diffDates(endSpan[type], startSpan[type]);
5836
		var resizeLocation; // zoned event date properties
5837
		var defaultDuration;
5838

  
5839
		// build original values to work from, guaranteeing a start and end
5840
		resizeLocation = {
5841
			start: event.start.clone(),
5842
			end: calendar.getEventEnd(event),
5843
			allDay: event.allDay
5844
		};
5845

  
5846
		// if an all-day event was in a timed area and was resized to a time, adjust start/end to have times
5847
		if (resizeLocation.allDay && durationHasTime(delta)) {
5848
			resizeLocation.allDay = false;
5849
			calendar.normalizeEventTimes(resizeLocation);
5850
		}
5851

  
5852
		resizeLocation[type].add(delta); // apply delta to start or end
5853

  
5854
		// if the event was compressed too small, find a new reasonable duration for it
5855
		if (!resizeLocation.start.isBefore(resizeLocation.end)) {
5856

  
5857
			defaultDuration =
5858
				this.minResizeDuration || // TODO: hack
5859
				(event.allDay ?
5860
					calendar.defaultAllDayEventDuration :
5861
					calendar.defaultTimedEventDuration);
5862

  
5863
			if (type == 'start') { // resizing the start?
5864
				resizeLocation.start = resizeLocation.end.clone().subtract(defaultDuration);
5865
			}
5866
			else { // resizing the end?
5867
				resizeLocation.end = resizeLocation.start.clone().add(defaultDuration);
5868
			}
5869
		}
5870

  
5871
		return resizeLocation;
5872
	},
5873

  
5874

  
5875
	// Renders a visual indication of an event being resized.
5876
	// `range` has the updated dates of the event. `seg` is the original segment object involved in the drag.
5877
	// Must return elements used for any mock events.
5878
	renderEventResize: function(range, seg) {
5879
		// subclasses must implement
5880
	},
5881

  
5882

  
5883
	// Unrenders a visual indication of an event being resized.
5884
	unrenderEventResize: function() {
5885
		// subclasses must implement
5886
	},
5887

  
5888

  
5889
	/* Rendering Utils
5890
	------------------------------------------------------------------------------------------------------------------*/
5891

  
5892

  
5893
	// Compute the text that should be displayed on an event's element.
5894
	// `range` can be the Event object itself, or something range-like, with at least a `start`.
5895
	// If event times are disabled, or the event has no time, will return a blank string.
5896
	// If not specified, formatStr will default to the eventTimeFormat setting,
5897
	// and displayEnd will default to the displayEventEnd setting.
5898
	getEventTimeText: function(range, formatStr, displayEnd) {
5899

  
5900
		if (formatStr == null) {
5901
			formatStr = this.eventTimeFormat;
5902
		}
5903

  
5904
		if (displayEnd == null) {
5905
			displayEnd = this.displayEventEnd;
5906
		}
5907

  
5908
		if (this.displayEventTime && range.start.hasTime()) {
5909
			if (displayEnd && range.end) {
5910
				return this.view.formatRange(range, formatStr);
5911
			}
5912
			else {
5913
				return range.start.format(formatStr);
5914
			}
5915
		}
5916

  
5917
		return '';
5918
	},
5919

  
5920

  
5921
	// Generic utility for generating the HTML classNames for an event segment's element
5922
	getSegClasses: function(seg, isDraggable, isResizable) {
5923
		var view = this.view;
5924
		var classes = [
5925
			'fc-event',
5926
			seg.isStart ? 'fc-start' : 'fc-not-start',
5927
			seg.isEnd ? 'fc-end' : 'fc-not-end'
5928
		].concat(this.getSegCustomClasses(seg));
5929

  
5930
		if (isDraggable) {
5931
			classes.push('fc-draggable');
5932
		}
5933
		if (isResizable) {
5934
			classes.push('fc-resizable');
5935
		}
5936

  
5937
		// event is currently selected? attach a className.
5938
		if (view.isEventSelected(seg.event)) {
5939
			classes.push('fc-selected');
5940
		}
5941

  
5942
		return classes;
5943
	},
5944

  
5945

  
5946
	// List of classes that were defined by the caller of the API in some way
5947
	getSegCustomClasses: function(seg) {
5948
		var event = seg.event;
5949

  
5950
		return [].concat(
5951
			event.className, // guaranteed to be an array
5952
			event.source ? event.source.className : []
5953
		);
5954
	},
5955

  
5956

  
5957
	// Utility for generating event skin-related CSS properties
5958
	getSegSkinCss: function(seg) {
5959
		return {
5960
			'background-color': this.getSegBackgroundColor(seg),
5961
			'border-color': this.getSegBorderColor(seg),
5962
			color: this.getSegTextColor(seg)
5963
		};
5964
	},
5965

  
5966

  
5967
	// Queries for caller-specified color, then falls back to default
5968
	getSegBackgroundColor: function(seg) {
5969
		return seg.event.backgroundColor ||
5970
			seg.event.color ||
5971
			this.getSegDefaultBackgroundColor(seg);
5972
	},
5973

  
5974

  
5975
	getSegDefaultBackgroundColor: function(seg) {
5976
		var source = seg.event.source || {};
5977

  
5978
		return source.backgroundColor ||
5979
			source.color ||
5980
			this.view.opt('eventBackgroundColor') ||
5981
			this.view.opt('eventColor');
5982
	},
5983

  
5984

  
5985
	// Queries for caller-specified color, then falls back to default
5986
	getSegBorderColor: function(seg) {
5987
		return seg.event.borderColor ||
5988
			seg.event.color ||
5989
			this.getSegDefaultBorderColor(seg);
5990
	},
5991

  
5992

  
5993
	getSegDefaultBorderColor: function(seg) {
5994
		var source = seg.event.source || {};
5995

  
5996
		return source.borderColor ||
5997
			source.color ||
5998
			this.view.opt('eventBorderColor') ||
5999
			this.view.opt('eventColor');
6000
	},
6001

  
6002

  
6003
	// Queries for caller-specified color, then falls back to default
6004
	getSegTextColor: function(seg) {
6005
		return seg.event.textColor ||
6006
			this.getSegDefaultTextColor(seg);
6007
	},
6008

  
6009

  
6010
	getSegDefaultTextColor: function(seg) {
6011
		var source = seg.event.source || {};
6012

  
6013
		return source.textColor ||
6014
			this.view.opt('eventTextColor');
6015
	},
6016

  
6017

  
6018
	/* Event Location Validation
6019
	------------------------------------------------------------------------------------------------------------------*/
6020

  
6021

  
6022
	isEventLocationAllowed: function(eventLocation, event) {
6023
		if (this.isEventLocationInRange(eventLocation)) {
6024
			var calendar = this.view.calendar;
6025
			var eventSpans = this.eventToSpans(eventLocation);
6026
			var i;
6027

  
6028
			if (eventSpans.length) {
6029
				for (i = 0; i < eventSpans.length; i++) {
6030
					if (!calendar.isEventSpanAllowed(eventSpans[i], event)) {
6031
						return false;
6032
					}
6033
				}
6034

  
6035
				return true;
6036
			}
6037
		}
6038

  
6039
		return false;
6040
	},
6041

  
6042

  
6043
	isExternalLocationAllowed: function(eventLocation, metaProps) { // FOR the external element
6044
		if (this.isEventLocationInRange(eventLocation)) {
6045
			var calendar = this.view.calendar;
6046
			var eventSpans = this.eventToSpans(eventLocation);
6047
			var i;
6048

  
6049
			if (eventSpans.length) {
6050
				for (i = 0; i < eventSpans.length; i++) {
6051
					if (!calendar.isExternalSpanAllowed(eventSpans[i], eventLocation, metaProps)) {
6052
						return false;
6053
					}
6054
				}
6055

  
6056
				return true;
6057
			}
6058
		}
6059

  
6060
		return false;
6061
	},
6062

  
6063

  
6064
	isEventLocationInRange: function(eventLocation) {
6065
		return isRangeWithinRange(
6066
			this.eventToRawRange(eventLocation),
6067
			this.view.validRange
6068
		);
6069
	},
6070

  
6071

  
6072
	/* Converting events -> eventRange -> eventSpan -> eventSegs
6073
	------------------------------------------------------------------------------------------------------------------*/
6074

  
6075

  
6076
	// Generates an array of segments for the given single event
6077
	// Can accept an event "location" as well (which only has start/end and no allDay)
6078
	eventToSegs: function(event) {
6079
		return this.eventsToSegs([ event ]);
6080
	},
6081

  
6082

  
6083
	// Generates spans (always unzoned) for the given event.
6084
	// Does not do any inverting for inverse-background events.
6085
	// Can accept an event "location" as well (which only has start/end and no allDay)
6086
	eventToSpans: function(event) {
6087
		var eventRange = this.eventToRange(event); // { start, end, isStart, isEnd }
6088

  
6089
		if (eventRange) {
6090
			return this.eventRangeToSpans(eventRange, event);
6091
		}
6092
		else { // out of view's valid range
6093
			return [];
6094
		}
6095
	},
6096

  
6097

  
6098

  
6099
	// Converts an array of event objects into an array of event segment objects.
6100
	// A custom `segSliceFunc` may be given for arbitrarily slicing up events.
6101
	// Doesn't guarantee an order for the resulting array.
6102
	eventsToSegs: function(allEvents, segSliceFunc) {
6103
		var _this = this;
6104
		var eventsById = groupEventsById(allEvents);
6105
		var segs = [];
6106

  
6107
		$.each(eventsById, function(id, events) {
6108
			var visibleEvents = [];
6109
			var eventRanges = [];
6110
			var eventRange; // { start, end, isStart, isEnd }
6111
			var i;
6112

  
6113
			for (i = 0; i < events.length; i++) {
6114
				eventRange = _this.eventToRange(events[i]); // might be null if completely out of range
6115

  
6116
				if (eventRange) {
6117
					eventRanges.push(eventRange);
6118
					visibleEvents.push(events[i]);
6119
				}
6120
			}
6121

  
6122
			// inverse-background events (utilize only the first event in calculations)
6123
			if (isInverseBgEvent(events[0])) {
6124
				eventRanges = _this.invertRanges(eventRanges); // will lose isStart/isEnd
6125

  
6126
				for (i = 0; i < eventRanges.length; i++) {
6127
					segs.push.apply(segs, // append to
6128
						_this.eventRangeToSegs(eventRanges[i], events[0], segSliceFunc)
6129
					);
6130
				}
6131
			}
6132
			// normal event ranges
6133
			else {
6134
				for (i = 0; i < eventRanges.length; i++) {
6135
					segs.push.apply(segs, // append to
6136
						_this.eventRangeToSegs(eventRanges[i], visibleEvents[i], segSliceFunc)
6137
					);
6138
				}
6139
			}
6140
		});
6141

  
6142
		return segs;
6143
	},
6144

  
6145

  
6146
	// Generates the unzoned start/end dates an event appears to occupy
6147
	// Can accept an event "location" as well (which only has start/end and no allDay)
6148
	// returns { start, end, isStart, isEnd }
6149
	// If the event is completely outside of the grid's valid range, will return undefined.
6150
	eventToRange: function(event) {
6151
		return this.refineRawEventRange(
6152
			this.eventToRawRange(event)
6153
		);
6154
	},
6155

  
6156

  
6157
	// Ensures the given range is within the view's activeRange and is correctly localized.
6158
	// Always returns a result
6159
	refineRawEventRange: function(rawRange) {
6160
		var view = this.view;
6161
		var calendar = view.calendar;
6162
		var range = intersectRanges(rawRange, view.activeRange);
6163

  
6164
		if (range) { // otherwise, event doesn't have valid range
6165

  
6166
			// hack: dynamic locale change forgets to upate stored event localed
6167
			calendar.localizeMoment(range.start);
6168
			calendar.localizeMoment(range.end);
6169

  
6170
			return range;
6171
		}
6172
	},
6173

  
6174

  
6175
	// not constrained to valid dates
6176
	// not given localizeMoment hack
6177
	eventToRawRange: function(event) {
6178
		var calendar = this.view.calendar;
6179
		var start = event.start.clone().stripZone();
6180
		var end = (
6181
				event.end ?
6182
					event.end.clone() :
6183
					// derive the end from the start and allDay. compute allDay if necessary
6184
					calendar.getDefaultEventEnd(
6185
						event.allDay != null ?
6186
							event.allDay :
6187
							!event.start.hasTime(),
6188
						event.start
6189
					)
6190
			).stripZone();
6191

  
6192
		return { start: start, end: end };
6193
	},
6194

  
6195

  
6196
	// Given an event's range (unzoned start/end), and the event itself,
6197
	// slice into segments (using the segSliceFunc function if specified)
6198
	// eventRange - { start, end, isStart, isEnd }
6199
	eventRangeToSegs: function(eventRange, event, segSliceFunc) {
6200
		var eventSpans = this.eventRangeToSpans(eventRange, event);
6201
		var segs = [];
6202
		var i;
6203

  
6204
		for (i = 0; i < eventSpans.length; i++) {
6205
			segs.push.apply(segs, // append to
6206
				this.eventSpanToSegs(eventSpans[i], event, segSliceFunc)
6207
			);
6208
		}
6209

  
6210
		return segs;
6211
	},
6212

  
6213

  
6214
	// Given an event's unzoned date range, return an array of eventSpan objects.
6215
	// eventSpan - { start, end, isStart, isEnd, otherthings... }
6216
	// Subclasses can override.
6217
	// Subclasses are obligated to forward eventRange.isStart/isEnd to the resulting spans.
6218
	eventRangeToSpans: function(eventRange, event) {
6219
		return [ $.extend({}, eventRange) ]; // copy into a single-item array
6220
	},
6221

  
6222

  
6223
	// Given an event's span (unzoned start/end and other misc data), and the event itself,
6224
	// slices into segments and attaches event-derived properties to them.
6225
	// eventSpan - { start, end, isStart, isEnd, otherthings... }
6226
	eventSpanToSegs: function(eventSpan, event, segSliceFunc) {
6227
		var segs = segSliceFunc ? segSliceFunc(eventSpan) : this.spanToSegs(eventSpan);
6228
		var i, seg;
6229

  
6230
		for (i = 0; i < segs.length; i++) {
6231
			seg = segs[i];
6232

  
6233
			// the eventSpan's isStart/isEnd takes precedence over the seg's
6234
			if (!eventSpan.isStart) {
6235
				seg.isStart = false;
6236
			}
6237
			if (!eventSpan.isEnd) {
6238
				seg.isEnd = false;
6239
			}
6240

  
6241
			seg.event = event;
6242
			seg.eventStartMS = +eventSpan.start; // TODO: not the best name after making spans unzoned
6243
			seg.eventDurationMS = eventSpan.end - eventSpan.start;
6244
		}
6245

  
6246
		return segs;
6247
	},
6248

  
6249

  
6250
	// Produces a new array of range objects that will cover all the time NOT covered by the given ranges.
6251
	// SIDE EFFECT: will mutate the given array and will use its date references.
6252
	invertRanges: function(ranges) {
6253
		var view = this.view;
6254
		var viewStart = view.activeRange.start.clone(); // need a copy
6255
		var viewEnd = view.activeRange.end.clone(); // need a copy
6256
		var inverseRanges = [];
6257
		var start = viewStart; // the end of the previous range. the start of the new range
6258
		var i, range;
6259

  
6260
		// ranges need to be in order. required for our date-walking algorithm
6261
		ranges.sort(compareRanges);
6262

  
6263
		for (i = 0; i < ranges.length; i++) {
6264
			range = ranges[i];
6265

  
6266
			// add the span of time before the event (if there is any)
6267
			if (range.start > start) { // compare millisecond time (skip any ambig logic)
6268
				inverseRanges.push({
6269
					start: start,
6270
					end: range.start
6271
				});
6272
			}
6273

  
6274
			if (range.end > start) {
6275
				start = range.end;
6276
			}
6277
		}
6278

  
6279
		// add the span of time after the last event (if there is any)
6280
		if (start < viewEnd) { // compare millisecond time (skip any ambig logic)
6281
			inverseRanges.push({
6282
				start: start,
6283
				end: viewEnd
6284
			});
6285
		}
6286

  
6287
		return inverseRanges;
6288
	},
6289

  
6290

  
6291
	sortEventSegs: function(segs) {
6292
		segs.sort(proxy(this, 'compareEventSegs'));
6293
	},
6294

  
6295

  
6296
	// A cmp function for determining which segments should take visual priority
6297
	compareEventSegs: function(seg1, seg2) {
6298
		return seg1.eventStartMS - seg2.eventStartMS || // earlier events go first
6299
			seg2.eventDurationMS - seg1.eventDurationMS || // tie? longer events go first
6300
			seg2.event.allDay - seg1.event.allDay || // tie? put all-day events first (booleans cast to 0/1)
6301
			compareByFieldSpecs(seg1.event, seg2.event, this.view.eventOrderSpecs);
6302
	}
6303

  
6304
});
6305

  
6306

  
6307
/* Utilities
6308
----------------------------------------------------------------------------------------------------------------------*/
6309

  
6310

  
6311
function pluckEventDateProps(event) {
6312
	return {
6313
		start: event.start.clone(),
6314
		end: event.end ? event.end.clone() : null,
6315
		allDay: event.allDay // keep it the same
6316
	};
6317
}
6318
FC.pluckEventDateProps = pluckEventDateProps;
6319

  
6320

  
6321
function isBgEvent(event) { // returns true if background OR inverse-background
6322
	var rendering = getEventRendering(event);
6323
	return rendering === 'background' || rendering === 'inverse-background';
6324
}
6325
FC.isBgEvent = isBgEvent; // export
6326

  
6327

  
6328
function isInverseBgEvent(event) {
6329
	return getEventRendering(event) === 'inverse-background';
6330
}
6331

  
6332

  
6333
function getEventRendering(event) {
6334
	return firstDefined((event.source || {}).rendering, event.rendering);
6335
}
6336

  
6337

  
6338
function groupEventsById(events) {
6339
	var eventsById = {};
6340
	var i, event;
6341

  
6342
	for (i = 0; i < events.length; i++) {
6343
		event = events[i];
6344
		(eventsById[event._id] || (eventsById[event._id] = [])).push(event);
6345
	}
6346

  
6347
	return eventsById;
6348
}
6349

  
6350

  
6351
// A cmp function for determining which non-inverted "ranges" (see above) happen earlier
6352
function compareRanges(range1, range2) {
6353
	return range1.start - range2.start; // earlier ranges go first
6354
}
6355

  
6356

  
6357
/* External-Dragging-Element Data
6358
----------------------------------------------------------------------------------------------------------------------*/
6359

  
6360
// Require all HTML5 data-* attributes used by FullCalendar to have this prefix.
6361
// A value of '' will query attributes like data-event. A value of 'fc' will query attributes like data-fc-event.
6362
FC.dataAttrPrefix = '';
6363

  
6364
// Given a jQuery element that might represent a dragged FullCalendar event, returns an intermediate data structure
6365
// to be used for Event Object creation.
6366
// A defined `.eventProps`, even when empty, indicates that an event should be created.
6367
function getDraggedElMeta(el) {
6368
	var prefix = FC.dataAttrPrefix;
6369
	var eventProps; // properties for creating the event, not related to date/time
6370
	var startTime; // a Duration
6371
	var duration;
6372
	var stick;
6373

  
6374
	if (prefix) { prefix += '-'; }
6375
	eventProps = el.data(prefix + 'event') || null;
6376

  
6377
	if (eventProps) {
6378
		if (typeof eventProps === 'object') {
6379
			eventProps = $.extend({}, eventProps); // make a copy
6380
		}
6381
		else { // something like 1 or true. still signal event creation
6382
			eventProps = {};
6383
		}
6384

  
6385
		// pluck special-cased date/time properties
6386
		startTime = eventProps.start;
6387
		if (startTime == null) { startTime = eventProps.time; } // accept 'time' as well
6388
		duration = eventProps.duration;
6389
		stick = eventProps.stick;
6390
		delete eventProps.start;
6391
		delete eventProps.time;
6392
		delete eventProps.duration;
6393
		delete eventProps.stick;
6394
	}
6395

  
6396
	// fallback to standalone attribute values for each of the date/time properties
6397
	if (startTime == null) { startTime = el.data(prefix + 'start'); }
6398
	if (startTime == null) { startTime = el.data(prefix + 'time'); } // accept 'time' as well
6399
	if (duration == null) { duration = el.data(prefix + 'duration'); }
6400
	if (stick == null) { stick = el.data(prefix + 'stick'); }
6401

  
6402
	// massage into correct data types
6403
	startTime = startTime != null ? moment.duration(startTime) : null;
6404
	duration = duration != null ? moment.duration(duration) : null;
6405
	stick = Boolean(stick);
6406

  
6407
	return { eventProps: eventProps, startTime: startTime, duration: duration, stick: stick };
6408
}
6409

  
6410

  
6411
;;
6412

  
6413
/*
6414
A set of rendering and date-related methods for a visual component comprised of one or more rows of day columns.
6415
Prerequisite: the object being mixed into needs to be a *Grid*
6416
*/
6417
var DayTableMixin = FC.DayTableMixin = {
6418

  
6419
	breakOnWeeks: false, // should create a new row for each week?
6420
	dayDates: null, // whole-day dates for each column. left to right
6421
	dayIndices: null, // for each day from start, the offset
6422
	daysPerRow: null,
6423
	rowCnt: null,
6424
	colCnt: null,
6425
	colHeadFormat: null,
6426

  
6427

  
6428
	// Populates internal variables used for date calculation and rendering
6429
	updateDayTable: function() {
6430
		var view = this.view;
6431
		var date = this.start.clone();
6432
		var dayIndex = -1;
6433
		var dayIndices = [];
6434
		var dayDates = [];
6435
		var daysPerRow;
6436
		var firstDay;
6437
		var rowCnt;
6438

  
6439
		while (date.isBefore(this.end)) { // loop each day from start to end
6440
			if (view.isHiddenDay(date)) {
6441
				dayIndices.push(dayIndex + 0.5); // mark that it's between indices
6442
			}
6443
			else {
6444
				dayIndex++;
6445
				dayIndices.push(dayIndex);
6446
				dayDates.push(date.clone());
6447
			}
6448
			date.add(1, 'days');
6449
		}
6450

  
6451
		if (this.breakOnWeeks) {
6452
			// count columns until the day-of-week repeats
6453
			firstDay = dayDates[0].day();
6454
			for (daysPerRow = 1; daysPerRow < dayDates.length; daysPerRow++) {
6455
				if (dayDates[daysPerRow].day() == firstDay) {
6456
					break;
6457
				}
6458
			}
6459
			rowCnt = Math.ceil(dayDates.length / daysPerRow);
6460
		}
6461
		else {
6462
			rowCnt = 1;
6463
			daysPerRow = dayDates.length;
6464
		}
6465

  
6466
		this.dayDates = dayDates;
6467
		this.dayIndices = dayIndices;
6468
		this.daysPerRow = daysPerRow;
6469
		this.rowCnt = rowCnt;
6470

  
6471
		this.updateDayTableCols();
6472
	},
6473

  
6474

  
6475
	// Computes and assigned the colCnt property and updates any options that may be computed from it
6476
	updateDayTableCols: function() {
6477
		this.colCnt = this.computeColCnt();
6478
		this.colHeadFormat = this.view.opt('columnFormat') || this.computeColHeadFormat();
6479
	},
6480

  
6481

  
6482
	// Determines how many columns there should be in the table
6483
	computeColCnt: function() {
6484
		return this.daysPerRow;
6485
	},
6486

  
6487

  
6488
	// Computes the ambiguously-timed moment for the given cell
6489
	getCellDate: function(row, col) {
6490
		return this.dayDates[
6491
				this.getCellDayIndex(row, col)
6492
			].clone();
6493
	},
6494

  
6495

  
6496
	// Computes the ambiguously-timed date range for the given cell
6497
	getCellRange: function(row, col) {
6498
		var start = this.getCellDate(row, col);
6499
		var end = start.clone().add(1, 'days');
6500

  
6501
		return { start: start, end: end };
6502
	},
6503

  
6504

  
6505
	// Returns the number of day cells, chronologically, from the first of the grid (0-based)
6506
	getCellDayIndex: function(row, col) {
6507
		return row * this.daysPerRow + this.getColDayIndex(col);
6508
	},
6509

  
6510

  
6511
	// Returns the numner of day cells, chronologically, from the first cell in *any given row*
6512
	getColDayIndex: function(col) {
6513
		if (this.isRTL) {
6514
			return this.colCnt - 1 - col;
6515
		}
6516
		else {
6517
			return col;
6518
		}
6519
	},
6520

  
6521

  
6522
	// Given a date, returns its chronolocial cell-index from the first cell of the grid.
6523
	// If the date lies between cells (because of hiddenDays), returns a floating-point value between offsets.
6524
	// If before the first offset, returns a negative number.
6525
	// If after the last offset, returns an offset past the last cell offset.
6526
	// Only works for *start* dates of cells. Will not work for exclusive end dates for cells.
6527
	getDateDayIndex: function(date) {
6528
		var dayIndices = this.dayIndices;
6529
		var dayOffset = date.diff(this.start, 'days');
6530

  
6531
		if (dayOffset < 0) {
6532
			return dayIndices[0] - 1;
6533
		}
6534
		else if (dayOffset >= dayIndices.length) {
6535
			return dayIndices[dayIndices.length - 1] + 1;
6536
		}
6537
		else {
6538
			return dayIndices[dayOffset];
6539
		}
6540
	},
6541

  
6542

  
6543
	/* Options
6544
	------------------------------------------------------------------------------------------------------------------*/
6545

  
6546

  
6547
	// Computes a default column header formatting string if `colFormat` is not explicitly defined
6548
	computeColHeadFormat: function() {
6549
		// if more than one week row, or if there are a lot of columns with not much space,
6550
		// put just the day numbers will be in each cell
6551
		if (this.rowCnt > 1 || this.colCnt > 10) {
6552
			return 'ddd'; // "Sat"
6553
		}
6554
		// multiple days, so full single date string WON'T be in title text
6555
		else if (this.colCnt > 1) {
6556
			return this.view.opt('dayOfMonthFormat'); // "Sat 12/10"
6557
		}
6558
		// single day, so full single date string will probably be in title text
6559
		else {
6560
			return 'dddd'; // "Saturday"
6561
		}
6562
	},
6563

  
6564

  
6565
	/* Slicing
6566
	------------------------------------------------------------------------------------------------------------------*/
6567

  
6568

  
6569
	// Slices up a date range into a segment for every week-row it intersects with
6570
	sliceRangeByRow: function(range) {
6571
		var daysPerRow = this.daysPerRow;
6572
		var normalRange = this.view.computeDayRange(range); // make whole-day range, considering nextDayThreshold
6573
		var rangeFirst = this.getDateDayIndex(normalRange.start); // inclusive first index
6574
		var rangeLast = this.getDateDayIndex(normalRange.end.clone().subtract(1, 'days')); // inclusive last index
6575
		var segs = [];
6576
		var row;
6577
		var rowFirst, rowLast; // inclusive day-index range for current row
6578
		var segFirst, segLast; // inclusive day-index range for segment
6579

  
6580
		for (row = 0; row < this.rowCnt; row++) {
6581
			rowFirst = row * daysPerRow;
6582
			rowLast = rowFirst + daysPerRow - 1;
6583

  
6584
			// intersect segment's offset range with the row's
6585
			segFirst = Math.max(rangeFirst, rowFirst);
6586
			segLast = Math.min(rangeLast, rowLast);
6587

  
6588
			// deal with in-between indices
6589
			segFirst = Math.ceil(segFirst); // in-between starts round to next cell
6590
			segLast = Math.floor(segLast); // in-between ends round to prev cell
6591

  
6592
			if (segFirst <= segLast) { // was there any intersection with the current row?
6593
				segs.push({
6594
					row: row,
6595

  
6596
					// normalize to start of row
6597
					firstRowDayIndex: segFirst - rowFirst,
6598
					lastRowDayIndex: segLast - rowFirst,
6599

  
6600
					// must be matching integers to be the segment's start/end
6601
					isStart: segFirst === rangeFirst,
6602
					isEnd: segLast === rangeLast
6603
				});
6604
			}
6605
		}
6606

  
6607
		return segs;
6608
	},
6609

  
6610

  
6611
	// Slices up a date range into a segment for every day-cell it intersects with.
6612
	// TODO: make more DRY with sliceRangeByRow somehow.
6613
	sliceRangeByDay: function(range) {
6614
		var daysPerRow = this.daysPerRow;
6615
		var normalRange = this.view.computeDayRange(range); // make whole-day range, considering nextDayThreshold
6616
		var rangeFirst = this.getDateDayIndex(normalRange.start); // inclusive first index
6617
		var rangeLast = this.getDateDayIndex(normalRange.end.clone().subtract(1, 'days')); // inclusive last index
6618
		var segs = [];
6619
		var row;
6620
		var rowFirst, rowLast; // inclusive day-index range for current row
6621
		var i;
6622
		var segFirst, segLast; // inclusive day-index range for segment
6623

  
6624
		for (row = 0; row < this.rowCnt; row++) {
6625
			rowFirst = row * daysPerRow;
6626
			rowLast = rowFirst + daysPerRow - 1;
6627

  
6628
			for (i = rowFirst; i <= rowLast; i++) {
6629

  
6630
				// intersect segment's offset range with the row's
6631
				segFirst = Math.max(rangeFirst, i);
6632
				segLast = Math.min(rangeLast, i);
6633

  
6634
				// deal with in-between indices
6635
				segFirst = Math.ceil(segFirst); // in-between starts round to next cell
6636
				segLast = Math.floor(segLast); // in-between ends round to prev cell
6637

  
6638
				if (segFirst <= segLast) { // was there any intersection with the current row?
6639
					segs.push({
6640
						row: row,
6641

  
6642
						// normalize to start of row
6643
						firstRowDayIndex: segFirst - rowFirst,
6644
						lastRowDayIndex: segLast - rowFirst,
6645

  
6646
						// must be matching integers to be the segment's start/end
6647
						isStart: segFirst === rangeFirst,
6648
						isEnd: segLast === rangeLast
6649
					});
6650
				}
6651
			}
6652
		}
6653

  
6654
		return segs;
6655
	},
6656

  
6657

  
6658
	/* Header Rendering
6659
	------------------------------------------------------------------------------------------------------------------*/
6660

  
6661

  
6662
	renderHeadHtml: function() {
6663
		var view = this.view;
6664

  
6665
		return '' +
6666
			'<div class="fc-row ' + view.widgetHeaderClass + '">' +
6667
				'<table>' +
6668
					'<thead>' +
6669
						this.renderHeadTrHtml() +
6670
					'</thead>' +
6671
				'</table>' +
6672
			'</div>';
6673
	},
6674

  
6675

  
6676
	renderHeadIntroHtml: function() {
6677
		return this.renderIntroHtml(); // fall back to generic
6678
	},
6679

  
6680

  
6681
	renderHeadTrHtml: function() {
6682
		return '' +
6683
			'<tr>' +
6684
				(this.isRTL ? '' : this.renderHeadIntroHtml()) +
6685
				this.renderHeadDateCellsHtml() +
6686
				(this.isRTL ? this.renderHeadIntroHtml() : '') +
6687
			'</tr>';
6688
	},
6689

  
6690

  
6691
	renderHeadDateCellsHtml: function() {
6692
		var htmls = [];
6693
		var col, date;
6694

  
6695
		for (col = 0; col < this.colCnt; col++) {
6696
			date = this.getCellDate(0, col);
6697
			htmls.push(this.renderHeadDateCellHtml(date));
6698
		}
6699

  
6700
		return htmls.join('');
6701
	},
6702

  
6703

  
6704
	// TODO: when internalApiVersion, accept an object for HTML attributes
6705
	// (colspan should be no different)
6706
	renderHeadDateCellHtml: function(date, colspan, otherAttrs) {
6707
		var view = this.view;
6708
		var isDateValid = isDateWithinRange(date, view.activeRange); // TODO: called too frequently. cache somehow.
6709
		var classNames = [
6710
			'fc-day-header',
6711
			view.widgetHeaderClass
6712
		];
6713
		var innerHtml = htmlEscape(date.format(this.colHeadFormat));
6714

  
6715
		// if only one row of days, the classNames on the header can represent the specific days beneath
6716
		if (this.rowCnt === 1) {
6717
			classNames = classNames.concat(
6718
				// includes the day-of-week class
6719
				// noThemeHighlight=true (don't highlight the header)
6720
				this.getDayClasses(date, true)
6721
			);
6722
		}
6723
		else {
6724
			classNames.push('fc-' + dayIDs[date.day()]); // only add the day-of-week class
6725
		}
6726

  
6727
		return '' +
6728
            '<th class="' + classNames.join(' ') + '"' +
6729
				((isDateValid && this.rowCnt) === 1 ?
6730
					' data-date="' + date.format('YYYY-MM-DD') + '"' :
6731
					'') +
6732
				(colspan > 1 ?
6733
					' colspan="' + colspan + '"' :
6734
					'') +
6735
				(otherAttrs ?
6736
					' ' + otherAttrs :
6737
					'') +
6738
				'>' +
6739
				(isDateValid ?
6740
					// don't make a link if the heading could represent multiple days, or if there's only one day (forceOff)
6741
					view.buildGotoAnchorHtml(
6742
						{ date: date, forceOff: this.rowCnt > 1 || this.colCnt === 1 },
6743
						innerHtml
6744
					) :
6745
					// if not valid, display text, but no link
6746
					innerHtml
6747
				) +
6748
			'</th>';
6749
	},
6750

  
6751

  
6752
	/* Background Rendering
6753
	------------------------------------------------------------------------------------------------------------------*/
6754

  
6755

  
6756
	renderBgTrHtml: function(row) {
6757
		return '' +
6758
			'<tr>' +
6759
				(this.isRTL ? '' : this.renderBgIntroHtml(row)) +
6760
				this.renderBgCellsHtml(row) +
6761
				(this.isRTL ? this.renderBgIntroHtml(row) : '') +
6762
			'</tr>';
6763
	},
6764

  
6765

  
6766
	renderBgIntroHtml: function(row) {
6767
		return this.renderIntroHtml(); // fall back to generic
6768
	},
6769

  
6770

  
6771
	renderBgCellsHtml: function(row) {
6772
		var htmls = [];
6773
		var col, date;
6774

  
6775
		for (col = 0; col < this.colCnt; col++) {
6776
			date = this.getCellDate(row, col);
6777
			htmls.push(this.renderBgCellHtml(date));
6778
		}
6779

  
6780
		return htmls.join('');
6781
	},
6782

  
6783

  
6784
	renderBgCellHtml: function(date, otherAttrs) {
6785
		var view = this.view;
6786
		var isDateValid = isDateWithinRange(date, view.activeRange); // TODO: called too frequently. cache somehow.
6787
		var classes = this.getDayClasses(date);
6788

  
6789
		classes.unshift('fc-day', view.widgetContentClass);
6790

  
6791
		return '<td class="' + classes.join(' ') + '"' +
6792
			(isDateValid ?
6793
				' data-date="' + date.format('YYYY-MM-DD') + '"' : // if date has a time, won't format it
6794
				'') +
6795
			(otherAttrs ?
6796
				' ' + otherAttrs :
6797
				'') +
6798
			'></td>';
6799
	},
6800

  
6801

  
6802
	/* Generic
6803
	------------------------------------------------------------------------------------------------------------------*/
6804

  
6805

  
6806
	// Generates the default HTML intro for any row. User classes should override
6807
	renderIntroHtml: function() {
6808
	},
6809

  
6810

  
6811
	// TODO: a generic method for dealing with <tr>, RTL, intro
6812
	// when increment internalApiVersion
6813
	// wrapTr (scheduler)
6814

  
6815

  
6816
	/* Utils
6817
	------------------------------------------------------------------------------------------------------------------*/
6818

  
6819

  
6820
	// Applies the generic "intro" and "outro" HTML to the given cells.
6821
	// Intro means the leftmost cell when the calendar is LTR and the rightmost cell when RTL. Vice-versa for outro.
6822
	bookendCells: function(trEl) {
6823
		var introHtml = this.renderIntroHtml();
6824

  
6825
		if (introHtml) {
6826
			if (this.isRTL) {
6827
				trEl.append(introHtml);
6828
			}
6829
			else {
6830
				trEl.prepend(introHtml);
6831
			}
6832
		}
6833
	}
6834

  
6835
};
6836

  
6837
;;
6838

  
6839
/* A component that renders a grid of whole-days that runs horizontally. There can be multiple rows, one per week.
6840
----------------------------------------------------------------------------------------------------------------------*/
6841

  
6842
var DayGrid = FC.DayGrid = Grid.extend(DayTableMixin, {
6843

  
6844
	numbersVisible: false, // should render a row for day/week numbers? set by outside view. TODO: make internal
6845
	bottomCoordPadding: 0, // hack for extending the hit area for the last row of the coordinate grid
6846

  
6847
	rowEls: null, // set of fake row elements
6848
	cellEls: null, // set of whole-day elements comprising the row's background
6849
	helperEls: null, // set of cell skeleton elements for rendering the mock event "helper"
6850

  
6851
	rowCoordCache: null,
6852
	colCoordCache: null,
6853

  
6854

  
6855
	// Renders the rows and columns into the component's `this.el`, which should already be assigned.
6856
	// isRigid determins whether the individual rows should ignore the contents and be a constant height.
6857
	// Relies on the view's colCnt and rowCnt. In the future, this component should probably be self-sufficient.
6858
	renderDates: function(isRigid) {
6859
		var view = this.view;
6860
		var rowCnt = this.rowCnt;
6861
		var colCnt = this.colCnt;
6862
		var html = '';
6863
		var row;
6864
		var col;
6865

  
6866
		for (row = 0; row < rowCnt; row++) {
6867
			html += this.renderDayRowHtml(row, isRigid);
6868
		}
6869
		this.el.html(html);
6870

  
6871
		this.rowEls = this.el.find('.fc-row');
6872
		this.cellEls = this.el.find('.fc-day, .fc-disabled-day');
6873

  
6874
		this.rowCoordCache = new CoordCache({
6875
			els: this.rowEls,
6876
			isVertical: true
6877
		});
6878
		this.colCoordCache = new CoordCache({
6879
			els: this.cellEls.slice(0, this.colCnt), // only the first row
6880
			isHorizontal: true
6881
		});
6882

  
6883
		// trigger dayRender with each cell's element
6884
		for (row = 0; row < rowCnt; row++) {
6885
			for (col = 0; col < colCnt; col++) {
6886
				view.publiclyTrigger(
6887
					'dayRender',
6888
					null,
6889
					this.getCellDate(row, col),
6890
					this.getCellEl(row, col)
6891
				);
6892
			}
6893
		}
6894
	},
6895

  
6896

  
6897
	unrenderDates: function() {
6898
		this.removeSegPopover();
6899
	},
6900

  
6901

  
6902
	renderBusinessHours: function() {
6903
		var segs = this.buildBusinessHourSegs(true); // wholeDay=true
6904
		this.renderFill('businessHours', segs, 'bgevent');
6905
	},
6906

  
6907

  
6908
	unrenderBusinessHours: function() {
6909
		this.unrenderFill('businessHours');
6910
	},
6911

  
6912

  
6913
	// Generates the HTML for a single row, which is a div that wraps a table.
6914
	// `row` is the row number.
6915
	renderDayRowHtml: function(row, isRigid) {
6916
		var view = this.view;
6917
		var classes = [ 'fc-row', 'fc-week', view.widgetContentClass ];
6918

  
6919
		if (isRigid) {
6920
			classes.push('fc-rigid');
6921
		}
6922

  
6923
		return '' +
6924
			'<div class="' + classes.join(' ') + '">' +
6925
				'<div class="fc-bg">' +
6926
					'<table>' +
6927
						this.renderBgTrHtml(row) +
6928
					'</table>' +
6929
				'</div>' +
6930
				'<div class="fc-content-skeleton">' +
6931
					'<table>' +
6932
						(this.numbersVisible ?
6933
							'<thead>' +
6934
								this.renderNumberTrHtml(row) +
6935
							'</thead>' :
6936
							''
6937
							) +
6938
					'</table>' +
6939
				'</div>' +
6940
			'</div>';
6941
	},
6942

  
6943

  
6944
	/* Grid Number Rendering
6945
	------------------------------------------------------------------------------------------------------------------*/
6946

  
6947

  
6948
	renderNumberTrHtml: function(row) {
6949
		return '' +
6950
			'<tr>' +
6951
				(this.isRTL ? '' : this.renderNumberIntroHtml(row)) +
6952
				this.renderNumberCellsHtml(row) +
6953
				(this.isRTL ? this.renderNumberIntroHtml(row) : '') +
6954
			'</tr>';
6955
	},
6956

  
6957

  
6958
	renderNumberIntroHtml: function(row) {
6959
		return this.renderIntroHtml();
6960
	},
6961

  
6962

  
6963
	renderNumberCellsHtml: function(row) {
6964
		var htmls = [];
6965
		var col, date;
6966

  
6967
		for (col = 0; col < this.colCnt; col++) {
6968
			date = this.getCellDate(row, col);
6969
			htmls.push(this.renderNumberCellHtml(date));
6970
		}
6971

  
6972
		return htmls.join('');
6973
	},
6974

  
6975

  
6976
	// Generates the HTML for the <td>s of the "number" row in the DayGrid's content skeleton.
6977
	// The number row will only exist if either day numbers or week numbers are turned on.
6978
	renderNumberCellHtml: function(date) {
6979
		var view = this.view;
6980
		var html = '';
6981
		var isDateValid = isDateWithinRange(date, view.activeRange); // TODO: called too frequently. cache somehow.
6982
		var isDayNumberVisible = view.dayNumbersVisible && isDateValid;
6983
		var classes;
6984
		var weekCalcFirstDoW;
6985

  
6986
		if (!isDayNumberVisible && !view.cellWeekNumbersVisible) {
6987
			// no numbers in day cell (week number must be along the side)
6988
			return '<td/>'; //  will create an empty space above events :(
6989
		}
6990

  
6991
		classes = this.getDayClasses(date);
6992
		classes.unshift('fc-day-top');
6993

  
6994
		if (view.cellWeekNumbersVisible) {
6995
			// To determine the day of week number change under ISO, we cannot
6996
			// rely on moment.js methods such as firstDayOfWeek() or weekday(),
6997
			// because they rely on the locale's dow (possibly overridden by
6998
			// our firstDay option), which may not be Monday. We cannot change
6999
			// dow, because that would affect the calendar start day as well.
7000
			if (date._locale._fullCalendar_weekCalc === 'ISO') {
7001
				weekCalcFirstDoW = 1;  // Monday by ISO 8601 definition
7002
			}
7003
			else {
7004
				weekCalcFirstDoW = date._locale.firstDayOfWeek();
7005
			}
7006
		}
7007

  
7008
		html += '<td class="' + classes.join(' ') + '"' +
7009
			(isDateValid ?
7010
				' data-date="' + date.format() + '"' :
7011
				''
7012
				) +
7013
			'>';
7014

  
7015
		if (view.cellWeekNumbersVisible && (date.day() == weekCalcFirstDoW)) {
7016
			html += view.buildGotoAnchorHtml(
7017
				{ date: date, type: 'week' },
7018
				{ 'class': 'fc-week-number' },
7019
				date.format('w') // inner HTML
7020
			);
7021
		}
7022

  
7023
		if (isDayNumberVisible) {
7024
			html += view.buildGotoAnchorHtml(
7025
				date,
7026
				{ 'class': 'fc-day-number' },
7027
				date.date() // inner HTML
7028
			);
7029
		}
7030

  
7031
		html += '</td>';
7032

  
7033
		return html;
7034
	},
7035

  
7036

  
7037
	/* Options
7038
	------------------------------------------------------------------------------------------------------------------*/
7039

  
7040

  
7041
	// Computes a default event time formatting string if `timeFormat` is not explicitly defined
7042
	computeEventTimeFormat: function() {
7043
		return this.view.opt('extraSmallTimeFormat'); // like "6p" or "6:30p"
7044
	},
7045

  
7046

  
7047
	// Computes a default `displayEventEnd` value if one is not expliclty defined
7048
	computeDisplayEventEnd: function() {
7049
		return this.colCnt == 1; // we'll likely have space if there's only one day
7050
	},
7051

  
7052

  
7053
	/* Dates
7054
	------------------------------------------------------------------------------------------------------------------*/
7055

  
7056

  
7057
	rangeUpdated: function() {
7058
		this.updateDayTable();
7059
	},
7060

  
7061

  
7062
	// Slices up the given span (unzoned start/end with other misc data) into an array of segments
7063
	spanToSegs: function(span) {
7064
		var segs = this.sliceRangeByRow(span);
7065
		var i, seg;
7066

  
7067
		for (i = 0; i < segs.length; i++) {
7068
			seg = segs[i];
7069
			if (this.isRTL) {
7070
				seg.leftCol = this.daysPerRow - 1 - seg.lastRowDayIndex;
7071
				seg.rightCol = this.daysPerRow - 1 - seg.firstRowDayIndex;
7072
			}
7073
			else {
7074
				seg.leftCol = seg.firstRowDayIndex;
7075
				seg.rightCol = seg.lastRowDayIndex;
7076
			}
7077
		}
7078

  
7079
		return segs;
7080
	},
7081

  
7082

  
7083
	/* Hit System
7084
	------------------------------------------------------------------------------------------------------------------*/
7085

  
7086

  
7087
	prepareHits: function() {
7088
		this.colCoordCache.build();
7089
		this.rowCoordCache.build();
7090
		this.rowCoordCache.bottoms[this.rowCnt - 1] += this.bottomCoordPadding; // hack
7091
	},
7092

  
7093

  
7094
	releaseHits: function() {
7095
		this.colCoordCache.clear();
7096
		this.rowCoordCache.clear();
7097
	},
7098

  
7099

  
7100
	queryHit: function(leftOffset, topOffset) {
7101
		if (this.colCoordCache.isLeftInBounds(leftOffset) && this.rowCoordCache.isTopInBounds(topOffset)) {
7102
			var col = this.colCoordCache.getHorizontalIndex(leftOffset);
7103
			var row = this.rowCoordCache.getVerticalIndex(topOffset);
7104

  
7105
			if (row != null && col != null) {
7106
				return this.getCellHit(row, col);
7107
			}
7108
		}
7109
	},
7110

  
7111

  
7112
	getHitSpan: function(hit) {
7113
		return this.getCellRange(hit.row, hit.col);
7114
	},
7115

  
7116

  
7117
	getHitEl: function(hit) {
7118
		return this.getCellEl(hit.row, hit.col);
7119
	},
7120

  
7121

  
7122
	/* Cell System
7123
	------------------------------------------------------------------------------------------------------------------*/
7124
	// FYI: the first column is the leftmost column, regardless of date
7125

  
7126

  
7127
	getCellHit: function(row, col) {
7128
		return {
7129
			row: row,
7130
			col: col,
7131
			component: this, // needed unfortunately :(
7132
			left: this.colCoordCache.getLeftOffset(col),
7133
			right: this.colCoordCache.getRightOffset(col),
7134
			top: this.rowCoordCache.getTopOffset(row),
7135
			bottom: this.rowCoordCache.getBottomOffset(row)
7136
		};
7137
	},
7138

  
7139

  
7140
	getCellEl: function(row, col) {
7141
		return this.cellEls.eq(row * this.colCnt + col);
7142
	},
7143

  
7144

  
7145
	/* Event Drag Visualization
7146
	------------------------------------------------------------------------------------------------------------------*/
7147
	// TODO: move to DayGrid.event, similar to what we did with Grid's drag methods
7148

  
7149

  
7150
	// Renders a visual indication of an event or external element being dragged.
7151
	// `eventLocation` has zoned start and end (optional)
7152
	renderDrag: function(eventLocation, seg) {
7153
		var eventSpans = this.eventToSpans(eventLocation);
7154
		var i;
7155

  
7156
		// always render a highlight underneath
7157
		for (i = 0; i < eventSpans.length; i++) {
7158
			this.renderHighlight(eventSpans[i]);
7159
		}
7160

  
7161
		// if a segment from the same calendar but another component is being dragged, render a helper event
7162
		if (seg && seg.component !== this) {
7163
			return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements
7164
		}
7165
	},
7166

  
7167

  
7168
	// Unrenders any visual indication of a hovering event
7169
	unrenderDrag: function() {
7170
		this.unrenderHighlight();
7171
		this.unrenderHelper();
7172
	},
7173

  
7174

  
7175
	/* Event Resize Visualization
7176
	------------------------------------------------------------------------------------------------------------------*/
7177

  
7178

  
7179
	// Renders a visual indication of an event being resized
7180
	renderEventResize: function(eventLocation, seg) {
7181
		var eventSpans = this.eventToSpans(eventLocation);
7182
		var i;
7183

  
7184
		for (i = 0; i < eventSpans.length; i++) {
7185
			this.renderHighlight(eventSpans[i]);
7186
		}
7187

  
7188
		return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements
7189
	},
7190

  
7191

  
7192
	// Unrenders a visual indication of an event being resized
7193
	unrenderEventResize: function() {
7194
		this.unrenderHighlight();
7195
		this.unrenderHelper();
7196
	},
7197

  
7198

  
7199
	/* Event Helper
7200
	------------------------------------------------------------------------------------------------------------------*/
7201

  
7202

  
7203
	// Renders a mock "helper" event. `sourceSeg` is the associated internal segment object. It can be null.
7204
	renderHelper: function(event, sourceSeg) {
7205
		var helperNodes = [];
7206
		var segs = this.eventToSegs(event);
7207
		var rowStructs;
7208

  
7209
		segs = this.renderFgSegEls(segs); // assigns each seg's el and returns a subset of segs that were rendered
7210
		rowStructs = this.renderSegRows(segs);
7211

  
7212
		// inject each new event skeleton into each associated row
7213
		this.rowEls.each(function(row, rowNode) {
7214
			var rowEl = $(rowNode); // the .fc-row
7215
			var skeletonEl = $('<div class="fc-helper-skeleton"><table/></div>'); // will be absolutely positioned
7216
			var skeletonTop;
7217

  
7218
			// If there is an original segment, match the top position. Otherwise, put it at the row's top level
7219
			if (sourceSeg && sourceSeg.row === row) {
7220
				skeletonTop = sourceSeg.el.position().top;
7221
			}
7222
			else {
7223
				skeletonTop = rowEl.find('.fc-content-skeleton tbody').position().top;
7224
			}
7225

  
7226
			skeletonEl.css('top', skeletonTop)
7227
				.find('table')
7228
					.append(rowStructs[row].tbodyEl);
7229

  
7230
			rowEl.append(skeletonEl);
7231
			helperNodes.push(skeletonEl[0]);
7232
		});
7233

  
7234
		return ( // must return the elements rendered
7235
			this.helperEls = $(helperNodes) // array -> jQuery set
7236
		);
7237
	},
7238

  
7239

  
7240
	// Unrenders any visual indication of a mock helper event
7241
	unrenderHelper: function() {
7242
		if (this.helperEls) {
7243
			this.helperEls.remove();
7244
			this.helperEls = null;
7245
		}
7246
	},
7247

  
7248

  
7249
	/* Fill System (highlight, background events, business hours)
7250
	------------------------------------------------------------------------------------------------------------------*/
7251

  
7252

  
7253
	fillSegTag: 'td', // override the default tag name
7254

  
7255

  
7256
	// Renders a set of rectangles over the given segments of days.
7257
	// Only returns segments that successfully rendered.
7258
	renderFill: function(type, segs, className) {
7259
		var nodes = [];
7260
		var i, seg;
7261
		var skeletonEl;
7262

  
7263
		segs = this.renderFillSegEls(type, segs); // assignes `.el` to each seg. returns successfully rendered segs
7264

  
7265
		for (i = 0; i < segs.length; i++) {
7266
			seg = segs[i];
7267
			skeletonEl = this.renderFillRow(type, seg, className);
7268
			this.rowEls.eq(seg.row).append(skeletonEl);
7269
			nodes.push(skeletonEl[0]);
7270
		}
7271

  
7272
		this.elsByFill[type] = $(nodes);
7273

  
7274
		return segs;
7275
	},
7276

  
7277

  
7278
	// Generates the HTML needed for one row of a fill. Requires the seg's el to be rendered.
7279
	renderFillRow: function(type, seg, className) {
7280
		var colCnt = this.colCnt;
7281
		var startCol = seg.leftCol;
7282
		var endCol = seg.rightCol + 1;
7283
		var skeletonEl;
7284
		var trEl;
7285

  
7286
		className = className || type.toLowerCase();
7287

  
7288
		skeletonEl = $(
7289
			'<div class="fc-' + className + '-skeleton">' +
7290
				'<table><tr/></table>' +
7291
			'</div>'
7292
		);
7293
		trEl = skeletonEl.find('tr');
7294

  
7295
		if (startCol > 0) {
7296
			trEl.append('<td colspan="' + startCol + '"/>');
7297
		}
7298

  
7299
		trEl.append(
7300
			seg.el.attr('colspan', endCol - startCol)
7301
		);
7302

  
7303
		if (endCol < colCnt) {
7304
			trEl.append('<td colspan="' + (colCnt - endCol) + '"/>');
7305
		}
7306

  
7307
		this.bookendCells(trEl);
7308

  
7309
		return skeletonEl;
7310
	}
7311

  
7312
});
7313

  
7314
;;
7315

  
7316
/* Event-rendering methods for the DayGrid class
7317
----------------------------------------------------------------------------------------------------------------------*/
7318

  
7319
DayGrid.mixin({
7320

  
7321
	rowStructs: null, // an array of objects, each holding information about a row's foreground event-rendering
7322

  
7323

  
7324
	// Unrenders all events currently rendered on the grid
7325
	unrenderEvents: function() {
7326
		this.removeSegPopover(); // removes the "more.." events popover
7327
		Grid.prototype.unrenderEvents.apply(this, arguments); // calls the super-method
7328
	},
7329

  
7330

  
7331
	// Retrieves all rendered segment objects currently rendered on the grid
7332
	getEventSegs: function() {
7333
		return Grid.prototype.getEventSegs.call(this) // get the segments from the super-method
7334
			.concat(this.popoverSegs || []); // append the segments from the "more..." popover
7335
	},
7336

  
7337

  
7338
	// Renders the given background event segments onto the grid
7339
	renderBgSegs: function(segs) {
7340

  
7341
		// don't render timed background events
7342
		var allDaySegs = $.grep(segs, function(seg) {
7343
			return seg.event.allDay;
7344
		});
7345

  
7346
		return Grid.prototype.renderBgSegs.call(this, allDaySegs); // call the super-method
7347
	},
7348

  
7349

  
7350
	// Renders the given foreground event segments onto the grid
7351
	renderFgSegs: function(segs) {
7352
		var rowStructs;
7353

  
7354
		// render an `.el` on each seg
7355
		// returns a subset of the segs. segs that were actually rendered
7356
		segs = this.renderFgSegEls(segs);
7357

  
7358
		rowStructs = this.rowStructs = this.renderSegRows(segs);
7359

  
7360
		// append to each row's content skeleton
7361
		this.rowEls.each(function(i, rowNode) {
7362
			$(rowNode).find('.fc-content-skeleton > table').append(
7363
				rowStructs[i].tbodyEl
7364
			);
7365
		});
7366

  
7367
		return segs; // return only the segs that were actually rendered
7368
	},
7369

  
7370

  
7371
	// Unrenders all currently rendered foreground event segments
7372
	unrenderFgSegs: function() {
7373
		var rowStructs = this.rowStructs || [];
7374
		var rowStruct;
7375

  
7376
		while ((rowStruct = rowStructs.pop())) {
7377
			rowStruct.tbodyEl.remove();
7378
		}
7379

  
7380
		this.rowStructs = null;
7381
	},
7382

  
7383

  
7384
	// Uses the given events array to generate <tbody> elements that should be appended to each row's content skeleton.
7385
	// Returns an array of rowStruct objects (see the bottom of `renderSegRow`).
7386
	// PRECONDITION: each segment shoud already have a rendered and assigned `.el`
7387
	renderSegRows: function(segs) {
7388
		var rowStructs = [];
7389
		var segRows;
7390
		var row;
7391

  
7392
		segRows = this.groupSegRows(segs); // group into nested arrays
7393

  
7394
		// iterate each row of segment groupings
7395
		for (row = 0; row < segRows.length; row++) {
7396
			rowStructs.push(
7397
				this.renderSegRow(row, segRows[row])
7398
			);
7399
		}
7400

  
7401
		return rowStructs;
7402
	},
7403

  
7404

  
7405
	// Builds the HTML to be used for the default element for an individual segment
7406
	fgSegHtml: function(seg, disableResizing) {
7407
		var view = this.view;
7408
		var event = seg.event;
7409
		var isDraggable = view.isEventDraggable(event);
7410
		var isResizableFromStart = !disableResizing && event.allDay &&
7411
			seg.isStart && view.isEventResizableFromStart(event);
7412
		var isResizableFromEnd = !disableResizing && event.allDay &&
7413
			seg.isEnd && view.isEventResizableFromEnd(event);
7414
		var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd);
7415
		var skinCss = cssToStr(this.getSegSkinCss(seg));
7416
		var timeHtml = '';
7417
		var timeText;
7418
		var titleHtml;
7419

  
7420
		classes.unshift('fc-day-grid-event', 'fc-h-event');
7421

  
7422
		// Only display a timed events time if it is the starting segment
7423
		if (seg.isStart) {
7424
			timeText = this.getEventTimeText(event);
7425
			if (timeText) {
7426
				timeHtml = '<span class="fc-time">' + htmlEscape(timeText) + '</span>';
7427
			}
7428
		}
7429

  
7430
		titleHtml =
7431
			'<span class="fc-title">' +
7432
				(htmlEscape(event.title || '') || '&nbsp;') + // we always want one line of height
7433
			'</span>';
7434
		
7435
		return '<a class="' + classes.join(' ') + '"' +
7436
				(event.url ?
7437
					' href="' + htmlEscape(event.url) + '"' :
7438
					''
7439
					) +
7440
				(skinCss ?
7441
					' style="' + skinCss + '"' :
7442
					''
7443
					) +
7444
			'>' +
7445
				'<div class="fc-content">' +
7446
					(this.isRTL ?
7447
						titleHtml + ' ' + timeHtml : // put a natural space in between
7448
						timeHtml + ' ' + titleHtml   //
7449
						) +
7450
				'</div>' +
7451
				(isResizableFromStart ?
7452
					'<div class="fc-resizer fc-start-resizer" />' :
7453
					''
7454
					) +
7455
				(isResizableFromEnd ?
7456
					'<div class="fc-resizer fc-end-resizer" />' :
7457
					''
7458
					) +
7459
			'</a>';
7460
	},
7461

  
7462

  
7463
	// Given a row # and an array of segments all in the same row, render a <tbody> element, a skeleton that contains
7464
	// the segments. Returns object with a bunch of internal data about how the render was calculated.
7465
	// NOTE: modifies rowSegs
7466
	renderSegRow: function(row, rowSegs) {
7467
		var colCnt = this.colCnt;
7468
		var segLevels = this.buildSegLevels(rowSegs); // group into sub-arrays of levels
7469
		var levelCnt = Math.max(1, segLevels.length); // ensure at least one level
7470
		var tbody = $('<tbody/>');
7471
		var segMatrix = []; // lookup for which segments are rendered into which level+col cells
7472
		var cellMatrix = []; // lookup for all <td> elements of the level+col matrix
7473
		var loneCellMatrix = []; // lookup for <td> elements that only take up a single column
7474
		var i, levelSegs;
7475
		var col;
7476
		var tr;
7477
		var j, seg;
7478
		var td;
7479

  
7480
		// populates empty cells from the current column (`col`) to `endCol`
7481
		function emptyCellsUntil(endCol) {
7482
			while (col < endCol) {
7483
				// try to grab a cell from the level above and extend its rowspan. otherwise, create a fresh cell
7484
				td = (loneCellMatrix[i - 1] || [])[col];
7485
				if (td) {
7486
					td.attr(
7487
						'rowspan',
7488
						parseInt(td.attr('rowspan') || 1, 10) + 1
7489
					);
7490
				}
7491
				else {
7492
					td = $('<td/>');
7493
					tr.append(td);
7494
				}
7495
				cellMatrix[i][col] = td;
7496
				loneCellMatrix[i][col] = td;
7497
				col++;
7498
			}
7499
		}
7500

  
7501
		for (i = 0; i < levelCnt; i++) { // iterate through all levels
7502
			levelSegs = segLevels[i];
7503
			col = 0;
7504
			tr = $('<tr/>');
7505

  
7506
			segMatrix.push([]);
7507
			cellMatrix.push([]);
7508
			loneCellMatrix.push([]);
7509

  
7510
			// levelCnt might be 1 even though there are no actual levels. protect against this.
7511
			// this single empty row is useful for styling.
7512
			if (levelSegs) {
7513
				for (j = 0; j < levelSegs.length; j++) { // iterate through segments in level
7514
					seg = levelSegs[j];
7515

  
7516
					emptyCellsUntil(seg.leftCol);
7517

  
7518
					// create a container that occupies or more columns. append the event element.
7519
					td = $('<td class="fc-event-container"/>').append(seg.el);
7520
					if (seg.leftCol != seg.rightCol) {
7521
						td.attr('colspan', seg.rightCol - seg.leftCol + 1);
7522
					}
7523
					else { // a single-column segment
7524
						loneCellMatrix[i][col] = td;
7525
					}
7526

  
7527
					while (col <= seg.rightCol) {
7528
						cellMatrix[i][col] = td;
7529
						segMatrix[i][col] = seg;
7530
						col++;
7531
					}
7532

  
7533
					tr.append(td);
7534
				}
7535
			}
7536

  
7537
			emptyCellsUntil(colCnt); // finish off the row
7538
			this.bookendCells(tr);
7539
			tbody.append(tr);
7540
		}
7541

  
7542
		return { // a "rowStruct"
7543
			row: row, // the row number
7544
			tbodyEl: tbody,
7545
			cellMatrix: cellMatrix,
7546
			segMatrix: segMatrix,
7547
			segLevels: segLevels,
7548
			segs: rowSegs
7549
		};
7550
	},
7551

  
7552

  
7553
	// Stacks a flat array of segments, which are all assumed to be in the same row, into subarrays of vertical levels.
7554
	// NOTE: modifies segs
7555
	buildSegLevels: function(segs) {
7556
		var levels = [];
7557
		var i, seg;
7558
		var j;
7559

  
7560
		// Give preference to elements with certain criteria, so they have
7561
		// a chance to be closer to the top.
7562
		this.sortEventSegs(segs);
7563
		
7564
		for (i = 0; i < segs.length; i++) {
7565
			seg = segs[i];
7566

  
7567
			// loop through levels, starting with the topmost, until the segment doesn't collide with other segments
7568
			for (j = 0; j < levels.length; j++) {
7569
				if (!isDaySegCollision(seg, levels[j])) {
7570
					break;
7571
				}
7572
			}
7573
			// `j` now holds the desired subrow index
7574
			seg.level = j;
7575

  
7576
			// create new level array if needed and append segment
7577
			(levels[j] || (levels[j] = [])).push(seg);
7578
		}
7579

  
7580
		// order segments left-to-right. very important if calendar is RTL
7581
		for (j = 0; j < levels.length; j++) {
7582
			levels[j].sort(compareDaySegCols);
7583
		}
7584

  
7585
		return levels;
7586
	},
7587

  
7588

  
7589
	// Given a flat array of segments, return an array of sub-arrays, grouped by each segment's row
7590
	groupSegRows: function(segs) {
7591
		var segRows = [];
7592
		var i;
7593

  
7594
		for (i = 0; i < this.rowCnt; i++) {
7595
			segRows.push([]);
7596
		}
7597

  
7598
		for (i = 0; i < segs.length; i++) {
7599
			segRows[segs[i].row].push(segs[i]);
7600
		}
7601

  
7602
		return segRows;
7603
	}
7604

  
7605
});
7606

  
7607

  
7608
// Computes whether two segments' columns collide. They are assumed to be in the same row.
7609
function isDaySegCollision(seg, otherSegs) {
7610
	var i, otherSeg;
7611

  
7612
	for (i = 0; i < otherSegs.length; i++) {
7613
		otherSeg = otherSegs[i];
7614

  
7615
		if (
7616
			otherSeg.leftCol <= seg.rightCol &&
7617
			otherSeg.rightCol >= seg.leftCol
7618
		) {
7619
			return true;
7620
		}
7621
	}
7622

  
7623
	return false;
7624
}
7625

  
7626

  
7627
// A cmp function for determining the leftmost event
7628
function compareDaySegCols(a, b) {
7629
	return a.leftCol - b.leftCol;
7630
}
7631

  
7632
;;
7633

  
7634
/* Methods relate to limiting the number events for a given day on a DayGrid
7635
----------------------------------------------------------------------------------------------------------------------*/
7636
// NOTE: all the segs being passed around in here are foreground segs
7637

  
7638
DayGrid.mixin({
7639

  
7640
	segPopover: null, // the Popover that holds events that can't fit in a cell. null when not visible
7641
	popoverSegs: null, // an array of segment objects that the segPopover holds. null when not visible
7642

  
7643

  
7644
	removeSegPopover: function() {
7645
		if (this.segPopover) {
7646
			this.segPopover.hide(); // in handler, will call segPopover's removeElement
7647
		}
7648
	},
7649

  
7650

  
7651
	// Limits the number of "levels" (vertically stacking layers of events) for each row of the grid.
7652
	// `levelLimit` can be false (don't limit), a number, or true (should be computed).
7653
	limitRows: function(levelLimit) {
7654
		var rowStructs = this.rowStructs || [];
7655
		var row; // row #
7656
		var rowLevelLimit;
7657

  
7658
		for (row = 0; row < rowStructs.length; row++) {
7659
			this.unlimitRow(row);
7660

  
7661
			if (!levelLimit) {
7662
				rowLevelLimit = false;
7663
			}
7664
			else if (typeof levelLimit === 'number') {
7665
				rowLevelLimit = levelLimit;
7666
			}
7667
			else {
7668
				rowLevelLimit = this.computeRowLevelLimit(row);
7669
			}
7670

  
7671
			if (rowLevelLimit !== false) {
7672
				this.limitRow(row, rowLevelLimit);
7673
			}
7674
		}
7675
	},
7676

  
7677

  
7678
	// Computes the number of levels a row will accomodate without going outside its bounds.
7679
	// Assumes the row is "rigid" (maintains a constant height regardless of what is inside).
7680
	// `row` is the row number.
7681
	computeRowLevelLimit: function(row) {
7682
		var rowEl = this.rowEls.eq(row); // the containing "fake" row div
7683
		var rowHeight = rowEl.height(); // TODO: cache somehow?
7684
		var trEls = this.rowStructs[row].tbodyEl.children();
7685
		var i, trEl;
7686
		var trHeight;
7687

  
7688
		function iterInnerHeights(i, childNode) {
7689
			trHeight = Math.max(trHeight, $(childNode).outerHeight());
7690
		}
7691

  
7692
		// Reveal one level <tr> at a time and stop when we find one out of bounds
7693
		for (i = 0; i < trEls.length; i++) {
7694
			trEl = trEls.eq(i).removeClass('fc-limited'); // reset to original state (reveal)
7695

  
7696
			// with rowspans>1 and IE8, trEl.outerHeight() would return the height of the largest cell,
7697
			// so instead, find the tallest inner content element.
7698
			trHeight = 0;
7699
			trEl.find('> td > :first-child').each(iterInnerHeights);
7700

  
7701
			if (trEl.position().top + trHeight > rowHeight) {
7702
				return i;
7703
			}
7704
		}
7705

  
7706
		return false; // should not limit at all
7707
	},
7708

  
7709

  
7710
	// Limits the given grid row to the maximum number of levels and injects "more" links if necessary.
7711
	// `row` is the row number.
7712
	// `levelLimit` is a number for the maximum (inclusive) number of levels allowed.
7713
	limitRow: function(row, levelLimit) {
7714
		var _this = this;
7715
		var rowStruct = this.rowStructs[row];
7716
		var moreNodes = []; // array of "more" <a> links and <td> DOM nodes
7717
		var col = 0; // col #, left-to-right (not chronologically)
7718
		var levelSegs; // array of segment objects in the last allowable level, ordered left-to-right
7719
		var cellMatrix; // a matrix (by level, then column) of all <td> jQuery elements in the row
7720
		var limitedNodes; // array of temporarily hidden level <tr> and segment <td> DOM nodes
7721
		var i, seg;
7722
		var segsBelow; // array of segment objects below `seg` in the current `col`
7723
		var totalSegsBelow; // total number of segments below `seg` in any of the columns `seg` occupies
7724
		var colSegsBelow; // array of segment arrays, below seg, one for each column (offset from segs's first column)
7725
		var td, rowspan;
7726
		var segMoreNodes; // array of "more" <td> cells that will stand-in for the current seg's cell
7727
		var j;
7728
		var moreTd, moreWrap, moreLink;
7729

  
7730
		// Iterates through empty level cells and places "more" links inside if need be
7731
		function emptyCellsUntil(endCol) { // goes from current `col` to `endCol`
7732
			while (col < endCol) {
7733
				segsBelow = _this.getCellSegs(row, col, levelLimit);
7734
				if (segsBelow.length) {
7735
					td = cellMatrix[levelLimit - 1][col];
7736
					moreLink = _this.renderMoreLink(row, col, segsBelow);
7737
					moreWrap = $('<div/>').append(moreLink);
7738
					td.append(moreWrap);
7739
					moreNodes.push(moreWrap[0]);
7740
				}
7741
				col++;
7742
			}
7743
		}
7744

  
7745
		if (levelLimit && levelLimit < rowStruct.segLevels.length) { // is it actually over the limit?
7746
			levelSegs = rowStruct.segLevels[levelLimit - 1];
7747
			cellMatrix = rowStruct.cellMatrix;
7748

  
7749
			limitedNodes = rowStruct.tbodyEl.children().slice(levelLimit) // get level <tr> elements past the limit
7750
				.addClass('fc-limited').get(); // hide elements and get a simple DOM-nodes array
7751

  
7752
			// iterate though segments in the last allowable level
7753
			for (i = 0; i < levelSegs.length; i++) {
7754
				seg = levelSegs[i];
7755
				emptyCellsUntil(seg.leftCol); // process empty cells before the segment
7756

  
7757
				// determine *all* segments below `seg` that occupy the same columns
7758
				colSegsBelow = [];
7759
				totalSegsBelow = 0;
7760
				while (col <= seg.rightCol) {
7761
					segsBelow = this.getCellSegs(row, col, levelLimit);
7762
					colSegsBelow.push(segsBelow);
7763
					totalSegsBelow += segsBelow.length;
7764
					col++;
7765
				}
7766

  
7767
				if (totalSegsBelow) { // do we need to replace this segment with one or many "more" links?
7768
					td = cellMatrix[levelLimit - 1][seg.leftCol]; // the segment's parent cell
7769
					rowspan = td.attr('rowspan') || 1;
7770
					segMoreNodes = [];
7771

  
7772
					// make a replacement <td> for each column the segment occupies. will be one for each colspan
7773
					for (j = 0; j < colSegsBelow.length; j++) {
7774
						moreTd = $('<td class="fc-more-cell"/>').attr('rowspan', rowspan);
7775
						segsBelow = colSegsBelow[j];
7776
						moreLink = this.renderMoreLink(
7777
							row,
7778
							seg.leftCol + j,
7779
							[ seg ].concat(segsBelow) // count seg as hidden too
7780
						);
7781
						moreWrap = $('<div/>').append(moreLink);
7782
						moreTd.append(moreWrap);
7783
						segMoreNodes.push(moreTd[0]);
7784
						moreNodes.push(moreTd[0]);
7785
					}
7786

  
7787
					td.addClass('fc-limited').after($(segMoreNodes)); // hide original <td> and inject replacements
7788
					limitedNodes.push(td[0]);
7789
				}
7790
			}
7791

  
7792
			emptyCellsUntil(this.colCnt); // finish off the level
7793
			rowStruct.moreEls = $(moreNodes); // for easy undoing later
7794
			rowStruct.limitedEls = $(limitedNodes); // for easy undoing later
7795
		}
7796
	},
7797

  
7798

  
7799
	// Reveals all levels and removes all "more"-related elements for a grid's row.
7800
	// `row` is a row number.
7801
	unlimitRow: function(row) {
7802
		var rowStruct = this.rowStructs[row];
7803

  
7804
		if (rowStruct.moreEls) {
7805
			rowStruct.moreEls.remove();
7806
			rowStruct.moreEls = null;
7807
		}
7808

  
7809
		if (rowStruct.limitedEls) {
7810
			rowStruct.limitedEls.removeClass('fc-limited');
7811
			rowStruct.limitedEls = null;
7812
		}
7813
	},
7814

  
7815

  
7816
	// Renders an <a> element that represents hidden event element for a cell.
7817
	// Responsible for attaching click handler as well.
7818
	renderMoreLink: function(row, col, hiddenSegs) {
7819
		var _this = this;
7820
		var view = this.view;
7821

  
7822
		return $('<a class="fc-more"/>')
7823
			.text(
7824
				this.getMoreLinkText(hiddenSegs.length)
7825
			)
7826
			.on('click', function(ev) {
7827
				var clickOption = view.opt('eventLimitClick');
7828
				var date = _this.getCellDate(row, col);
7829
				var moreEl = $(this);
7830
				var dayEl = _this.getCellEl(row, col);
7831
				var allSegs = _this.getCellSegs(row, col);
7832

  
7833
				// rescope the segments to be within the cell's date
7834
				var reslicedAllSegs = _this.resliceDaySegs(allSegs, date);
7835
				var reslicedHiddenSegs = _this.resliceDaySegs(hiddenSegs, date);
7836

  
7837
				if (typeof clickOption === 'function') {
7838
					// the returned value can be an atomic option
7839
					clickOption = view.publiclyTrigger('eventLimitClick', null, {
7840
						date: date,
7841
						dayEl: dayEl,
7842
						moreEl: moreEl,
7843
						segs: reslicedAllSegs,
7844
						hiddenSegs: reslicedHiddenSegs
7845
					}, ev);
7846
				}
7847

  
7848
				if (clickOption === 'popover') {
7849
					_this.showSegPopover(row, col, moreEl, reslicedAllSegs);
7850
				}
7851
				else if (typeof clickOption === 'string') { // a view name
7852
					view.calendar.zoomTo(date, clickOption);
7853
				}
7854
			});
7855
	},
7856

  
7857

  
7858
	// Reveals the popover that displays all events within a cell
7859
	showSegPopover: function(row, col, moreLink, segs) {
7860
		var _this = this;
7861
		var view = this.view;
7862
		var moreWrap = moreLink.parent(); // the <div> wrapper around the <a>
7863
		var topEl; // the element we want to match the top coordinate of
7864
		var options;
7865

  
7866
		if (this.rowCnt == 1) {
7867
			topEl = view.el; // will cause the popover to cover any sort of header
7868
		}
7869
		else {
7870
			topEl = this.rowEls.eq(row); // will align with top of row
7871
		}
7872

  
7873
		options = {
7874
			className: 'fc-more-popover',
7875
			content: this.renderSegPopoverContent(row, col, segs),
7876
			parentEl: this.view.el, // attach to root of view. guarantees outside of scrollbars.
7877
			top: topEl.offset().top,
7878
			autoHide: true, // when the user clicks elsewhere, hide the popover
7879
			viewportConstrain: view.opt('popoverViewportConstrain'),
7880
			hide: function() {
7881
				// kill everything when the popover is hidden
7882
				// notify events to be removed
7883
				if (_this.popoverSegs) {
7884
					var seg;
7885
					for (var i = 0; i < _this.popoverSegs.length; ++i) {
7886
						seg = _this.popoverSegs[i];
7887
						view.publiclyTrigger('eventDestroy', seg.event, seg.event, seg.el);
7888
					}
7889
				}
7890
				_this.segPopover.removeElement();
7891
				_this.segPopover = null;
7892
				_this.popoverSegs = null;
7893
			}
7894
		};
7895

  
7896
		// Determine horizontal coordinate.
7897
		// We use the moreWrap instead of the <td> to avoid border confusion.
7898
		if (this.isRTL) {
7899
			options.right = moreWrap.offset().left + moreWrap.outerWidth() + 1; // +1 to be over cell border
7900
		}
7901
		else {
7902
			options.left = moreWrap.offset().left - 1; // -1 to be over cell border
7903
		}
7904

  
7905
		this.segPopover = new Popover(options);
7906
		this.segPopover.show();
7907

  
7908
		// the popover doesn't live within the grid's container element, and thus won't get the event
7909
		// delegated-handlers for free. attach event-related handlers to the popover.
7910
		this.bindSegHandlersToEl(this.segPopover.el);
7911
	},
7912

  
7913

  
7914
	// Builds the inner DOM contents of the segment popover
7915
	renderSegPopoverContent: function(row, col, segs) {
7916
		var view = this.view;
7917
		var isTheme = view.opt('theme');
7918
		var title = this.getCellDate(row, col).format(view.opt('dayPopoverFormat'));
7919
		var content = $(
7920
			'<div class="fc-header ' + view.widgetHeaderClass + '">' +
7921
				'<span class="fc-close ' +
7922
					(isTheme ? 'ui-icon ui-icon-closethick' : 'fc-icon fc-icon-x') +
7923
				'"></span>' +
7924
				'<span class="fc-title">' +
7925
					htmlEscape(title) +
7926
				'</span>' +
7927
				'<div class="fc-clear"/>' +
7928
			'</div>' +
7929
			'<div class="fc-body ' + view.widgetContentClass + '">' +
7930
				'<div class="fc-event-container"></div>' +
7931
			'</div>'
7932
		);
7933
		var segContainer = content.find('.fc-event-container');
7934
		var i;
7935

  
7936
		// render each seg's `el` and only return the visible segs
7937
		segs = this.renderFgSegEls(segs, true); // disableResizing=true
7938
		this.popoverSegs = segs;
7939

  
7940
		for (i = 0; i < segs.length; i++) {
7941

  
7942
			// because segments in the popover are not part of a grid coordinate system, provide a hint to any
7943
			// grids that want to do drag-n-drop about which cell it came from
7944
			this.hitsNeeded();
7945
			segs[i].hit = this.getCellHit(row, col);
7946
			this.hitsNotNeeded();
7947

  
7948
			segContainer.append(segs[i].el);
7949
		}
7950

  
7951
		return content;
7952
	},
7953

  
7954

  
7955
	// Given the events within an array of segment objects, reslice them to be in a single day
7956
	resliceDaySegs: function(segs, dayDate) {
7957

  
7958
		// build an array of the original events
7959
		var events = $.map(segs, function(seg) {
7960
			return seg.event;
7961
		});
7962

  
7963
		var dayStart = dayDate.clone();
7964
		var dayEnd = dayStart.clone().add(1, 'days');
7965
		var dayRange = { start: dayStart, end: dayEnd };
7966

  
7967
		// slice the events with a custom slicing function
7968
		segs = this.eventsToSegs(
7969
			events,
7970
			function(range) {
7971
				var seg = intersectRanges(range, dayRange); // undefind if no intersection
7972
				return seg ? [ seg ] : []; // must return an array of segments
7973
			}
7974
		);
7975

  
7976
		// force an order because eventsToSegs doesn't guarantee one
7977
		this.sortEventSegs(segs);
7978

  
7979
		return segs;
7980
	},
7981

  
7982

  
7983
	// Generates the text that should be inside a "more" link, given the number of events it represents
7984
	getMoreLinkText: function(num) {
7985
		var opt = this.view.opt('eventLimitText');
7986

  
7987
		if (typeof opt === 'function') {
7988
			return opt(num);
7989
		}
7990
		else {
7991
			return '+' + num + ' ' + opt;
7992
		}
7993
	},
7994

  
7995

  
7996
	// Returns segments within a given cell.
7997
	// If `startLevel` is specified, returns only events including and below that level. Otherwise returns all segs.
7998
	getCellSegs: function(row, col, startLevel) {
7999
		var segMatrix = this.rowStructs[row].segMatrix;
8000
		var level = startLevel || 0;
8001
		var segs = [];
8002
		var seg;
8003

  
8004
		while (level < segMatrix.length) {
8005
			seg = segMatrix[level][col];
8006
			if (seg) {
8007
				segs.push(seg);
8008
			}
8009
			level++;
8010
		}
8011

  
8012
		return segs;
8013
	}
8014

  
8015
});
8016

  
8017
;;
8018

  
8019
/* A component that renders one or more columns of vertical time slots
8020
----------------------------------------------------------------------------------------------------------------------*/
8021
// We mixin DayTable, even though there is only a single row of days
8022

  
8023
var TimeGrid = FC.TimeGrid = Grid.extend(DayTableMixin, {
8024

  
8025
	slotDuration: null, // duration of a "slot", a distinct time segment on given day, visualized by lines
8026
	snapDuration: null, // granularity of time for dragging and selecting
8027
	snapsPerSlot: null,
8028
	labelFormat: null, // formatting string for times running along vertical axis
8029
	labelInterval: null, // duration of how often a label should be displayed for a slot
8030

  
8031
	colEls: null, // cells elements in the day-row background
8032
	slatContainerEl: null, // div that wraps all the slat rows
8033
	slatEls: null, // elements running horizontally across all columns
8034
	nowIndicatorEls: null,
8035

  
8036
	colCoordCache: null,
8037
	slatCoordCache: null,
8038

  
8039

  
8040
	constructor: function() {
8041
		Grid.apply(this, arguments); // call the super-constructor
8042

  
8043
		this.processOptions();
8044
	},
8045

  
8046

  
8047
	// Renders the time grid into `this.el`, which should already be assigned.
8048
	// Relies on the view's colCnt. In the future, this component should probably be self-sufficient.
8049
	renderDates: function() {
8050
		this.el.html(this.renderHtml());
8051
		this.colEls = this.el.find('.fc-day, .fc-disabled-day');
8052
		this.slatContainerEl = this.el.find('.fc-slats');
8053
		this.slatEls = this.slatContainerEl.find('tr');
8054

  
8055
		this.colCoordCache = new CoordCache({
8056
			els: this.colEls,
8057
			isHorizontal: true
8058
		});
8059
		this.slatCoordCache = new CoordCache({
8060
			els: this.slatEls,
8061
			isVertical: true
8062
		});
8063

  
8064
		this.renderContentSkeleton();
8065
	},
8066

  
8067

  
8068
	// Renders the basic HTML skeleton for the grid
8069
	renderHtml: function() {
8070
		return '' +
8071
			'<div class="fc-bg">' +
8072
				'<table>' +
8073
					this.renderBgTrHtml(0) + // row=0
8074
				'</table>' +
8075
			'</div>' +
8076
			'<div class="fc-slats">' +
8077
				'<table>' +
8078
					this.renderSlatRowHtml() +
8079
				'</table>' +
8080
			'</div>';
8081
	},
8082

  
8083

  
8084
	// Generates the HTML for the horizontal "slats" that run width-wise. Has a time axis on a side. Depends on RTL.
8085
	renderSlatRowHtml: function() {
8086
		var view = this.view;
8087
		var isRTL = this.isRTL;
8088
		var html = '';
8089
		var slotTime = moment.duration(+this.view.minTime); // wish there was .clone() for durations
8090
		var slotDate; // will be on the view's first day, but we only care about its time
8091
		var isLabeled;
8092
		var axisHtml;
8093

  
8094
		// Calculate the time for each slot
8095
		while (slotTime < this.view.maxTime) {
8096
			slotDate = this.start.clone().time(slotTime);
8097
			isLabeled = isInt(divideDurationByDuration(slotTime, this.labelInterval));
8098

  
8099
			axisHtml =
8100
				'<td class="fc-axis fc-time ' + view.widgetContentClass + '" ' + view.axisStyleAttr() + '>' +
8101
					(isLabeled ?
8102
						'<span>' + // for matchCellWidths
8103
							htmlEscape(slotDate.format(this.labelFormat)) +
8104
						'</span>' :
8105
						''
8106
						) +
8107
				'</td>';
8108

  
8109
			html +=
8110
				'<tr data-time="' + slotDate.format('HH:mm:ss') + '"' +
8111
					(isLabeled ? '' : ' class="fc-minor"') +
8112
					'>' +
8113
					(!isRTL ? axisHtml : '') +
8114
					'<td class="' + view.widgetContentClass + '"/>' +
8115
					(isRTL ? axisHtml : '') +
8116
				"</tr>";
8117

  
8118
			slotTime.add(this.slotDuration);
8119
		}
8120

  
8121
		return html;
8122
	},
8123

  
8124

  
8125
	/* Options
8126
	------------------------------------------------------------------------------------------------------------------*/
8127

  
8128

  
8129
	// Parses various options into properties of this object
8130
	processOptions: function() {
8131
		var view = this.view;
8132
		var slotDuration = view.opt('slotDuration');
8133
		var snapDuration = view.opt('snapDuration');
8134
		var input;
8135

  
8136
		slotDuration = moment.duration(slotDuration);
8137
		snapDuration = snapDuration ? moment.duration(snapDuration) : slotDuration;
8138

  
8139
		this.slotDuration = slotDuration;
8140
		this.snapDuration = snapDuration;
8141
		this.snapsPerSlot = slotDuration / snapDuration; // TODO: ensure an integer multiple?
8142

  
8143
		this.minResizeDuration = snapDuration; // hack
8144

  
8145
		// might be an array value (for TimelineView).
8146
		// if so, getting the most granular entry (the last one probably).
8147
		input = view.opt('slotLabelFormat');
8148
		if ($.isArray(input)) {
8149
			input = input[input.length - 1];
8150
		}
8151

  
8152
		this.labelFormat =
8153
			input ||
8154
			view.opt('smallTimeFormat'); // the computed default
8155

  
8156
		input = view.opt('slotLabelInterval');
8157
		this.labelInterval = input ?
8158
			moment.duration(input) :
8159
			this.computeLabelInterval(slotDuration);
8160
	},
8161

  
8162

  
8163
	// Computes an automatic value for slotLabelInterval
8164
	computeLabelInterval: function(slotDuration) {
8165
		var i;
8166
		var labelInterval;
8167
		var slotsPerLabel;
8168

  
8169
		// find the smallest stock label interval that results in more than one slots-per-label
8170
		for (i = AGENDA_STOCK_SUB_DURATIONS.length - 1; i >= 0; i--) {
8171
			labelInterval = moment.duration(AGENDA_STOCK_SUB_DURATIONS[i]);
8172
			slotsPerLabel = divideDurationByDuration(labelInterval, slotDuration);
8173
			if (isInt(slotsPerLabel) && slotsPerLabel > 1) {
8174
				return labelInterval;
8175
			}
8176
		}
8177

  
8178
		return moment.duration(slotDuration); // fall back. clone
8179
	},
8180

  
8181

  
8182
	// Computes a default event time formatting string if `timeFormat` is not explicitly defined
8183
	computeEventTimeFormat: function() {
8184
		return this.view.opt('noMeridiemTimeFormat'); // like "6:30" (no AM/PM)
8185
	},
8186

  
8187

  
8188
	// Computes a default `displayEventEnd` value if one is not expliclty defined
8189
	computeDisplayEventEnd: function() {
8190
		return true;
8191
	},
8192

  
8193

  
8194
	/* Hit System
8195
	------------------------------------------------------------------------------------------------------------------*/
8196

  
8197

  
8198
	prepareHits: function() {
8199
		this.colCoordCache.build();
8200
		this.slatCoordCache.build();
8201
	},
8202

  
8203

  
8204
	releaseHits: function() {
8205
		this.colCoordCache.clear();
8206
		// NOTE: don't clear slatCoordCache because we rely on it for computeTimeTop
8207
	},
8208

  
8209

  
8210
	queryHit: function(leftOffset, topOffset) {
8211
		var snapsPerSlot = this.snapsPerSlot;
8212
		var colCoordCache = this.colCoordCache;
8213
		var slatCoordCache = this.slatCoordCache;
8214

  
8215
		if (colCoordCache.isLeftInBounds(leftOffset) && slatCoordCache.isTopInBounds(topOffset)) {
8216
			var colIndex = colCoordCache.getHorizontalIndex(leftOffset);
8217
			var slatIndex = slatCoordCache.getVerticalIndex(topOffset);
8218

  
8219
			if (colIndex != null && slatIndex != null) {
8220
				var slatTop = slatCoordCache.getTopOffset(slatIndex);
8221
				var slatHeight = slatCoordCache.getHeight(slatIndex);
8222
				var partial = (topOffset - slatTop) / slatHeight; // floating point number between 0 and 1
8223
				var localSnapIndex = Math.floor(partial * snapsPerSlot); // the snap # relative to start of slat
8224
				var snapIndex = slatIndex * snapsPerSlot + localSnapIndex;
8225
				var snapTop = slatTop + (localSnapIndex / snapsPerSlot) * slatHeight;
8226
				var snapBottom = slatTop + ((localSnapIndex + 1) / snapsPerSlot) * slatHeight;
8227

  
8228
				return {
8229
					col: colIndex,
8230
					snap: snapIndex,
8231
					component: this, // needed unfortunately :(
8232
					left: colCoordCache.getLeftOffset(colIndex),
8233
					right: colCoordCache.getRightOffset(colIndex),
8234
					top: snapTop,
8235
					bottom: snapBottom
8236
				};
8237
			}
8238
		}
8239
	},
8240

  
8241

  
8242
	getHitSpan: function(hit) {
8243
		var start = this.getCellDate(0, hit.col); // row=0
8244
		var time = this.computeSnapTime(hit.snap); // pass in the snap-index
8245
		var end;
8246

  
8247
		start.time(time);
8248
		end = start.clone().add(this.snapDuration);
8249

  
8250
		return { start: start, end: end };
8251
	},
8252

  
8253

  
8254
	getHitEl: function(hit) {
8255
		return this.colEls.eq(hit.col);
8256
	},
8257

  
8258

  
8259
	/* Dates
8260
	------------------------------------------------------------------------------------------------------------------*/
8261

  
8262

  
8263
	rangeUpdated: function() {
8264
		this.updateDayTable();
8265
	},
8266

  
8267

  
8268
	// Given a row number of the grid, representing a "snap", returns a time (Duration) from its start-of-day
8269
	computeSnapTime: function(snapIndex) {
8270
		return moment.duration(this.view.minTime + this.snapDuration * snapIndex);
8271
	},
8272

  
8273

  
8274
	// Slices up the given span (unzoned start/end with other misc data) into an array of segments
8275
	spanToSegs: function(span) {
8276
		var segs = this.sliceRangeByTimes(span);
8277
		var i;
8278

  
8279
		for (i = 0; i < segs.length; i++) {
8280
			if (this.isRTL) {
8281
				segs[i].col = this.daysPerRow - 1 - segs[i].dayIndex;
8282
			}
8283
			else {
8284
				segs[i].col = segs[i].dayIndex;
8285
			}
8286
		}
8287

  
8288
		return segs;
8289
	},
8290

  
8291

  
8292
	sliceRangeByTimes: function(range) {
8293
		var segs = [];
8294
		var seg;
8295
		var dayIndex;
8296
		var dayDate;
8297
		var dayRange;
8298

  
8299
		for (dayIndex = 0; dayIndex < this.daysPerRow; dayIndex++) {
8300
			dayDate = this.dayDates[dayIndex].clone().time(0); // TODO: better API for this?
8301
			dayRange = {
8302
				start: dayDate.clone().add(this.view.minTime), // don't use .time() because it sux with negatives
8303
				end: dayDate.clone().add(this.view.maxTime)
8304
			};
8305
			seg = intersectRanges(range, dayRange); // both will be ambig timezone
8306
			if (seg) {
8307
				seg.dayIndex = dayIndex;
8308
				segs.push(seg);
8309
			}
8310
		}
8311

  
8312
		return segs;
8313
	},
8314

  
8315

  
8316
	/* Coordinates
8317
	------------------------------------------------------------------------------------------------------------------*/
8318

  
8319

  
8320
	updateSize: function(isResize) { // NOT a standard Grid method
8321
		this.slatCoordCache.build();
8322

  
8323
		if (isResize) {
8324
			this.updateSegVerticals(
8325
				[].concat(this.fgSegs || [], this.bgSegs || [], this.businessSegs || [])
8326
			);
8327
		}
8328
	},
8329

  
8330

  
8331
	getTotalSlatHeight: function() {
8332
		return this.slatContainerEl.outerHeight();
8333
	},
8334

  
8335

  
8336
	// Computes the top coordinate, relative to the bounds of the grid, of the given date.
8337
	// A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight.
8338
	computeDateTop: function(date, startOfDayDate) {
8339
		return this.computeTimeTop(
8340
			moment.duration(
8341
				date - startOfDayDate.clone().stripTime()
8342
			)
8343
		);
8344
	},
8345

  
8346

  
8347
	// Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration).
8348
	computeTimeTop: function(time) {
8349
		var len = this.slatEls.length;
8350
		var slatCoverage = (time - this.view.minTime) / this.slotDuration; // floating-point value of # of slots covered
8351
		var slatIndex;
8352
		var slatRemainder;
8353

  
8354
		// compute a floating-point number for how many slats should be progressed through.
8355
		// from 0 to number of slats (inclusive)
8356
		// constrained because minTime/maxTime might be customized.
8357
		slatCoverage = Math.max(0, slatCoverage);
8358
		slatCoverage = Math.min(len, slatCoverage);
8359

  
8360
		// an integer index of the furthest whole slat
8361
		// from 0 to number slats (*exclusive*, so len-1)
8362
		slatIndex = Math.floor(slatCoverage);
8363
		slatIndex = Math.min(slatIndex, len - 1);
8364

  
8365
		// how much further through the slatIndex slat (from 0.0-1.0) must be covered in addition.
8366
		// could be 1.0 if slatCoverage is covering *all* the slots
8367
		slatRemainder = slatCoverage - slatIndex;
8368

  
8369
		return this.slatCoordCache.getTopPosition(slatIndex) +
8370
			this.slatCoordCache.getHeight(slatIndex) * slatRemainder;
8371
	},
8372

  
8373

  
8374

  
8375
	/* Event Drag Visualization
8376
	------------------------------------------------------------------------------------------------------------------*/
8377

  
8378

  
8379
	// Renders a visual indication of an event being dragged over the specified date(s).
8380
	// A returned value of `true` signals that a mock "helper" event has been rendered.
8381
	renderDrag: function(eventLocation, seg) {
8382
		var eventSpans;
8383
		var i;
8384

  
8385
		if (seg) { // if there is event information for this drag, render a helper event
8386

  
8387
			// returns mock event elements
8388
			// signal that a helper has been rendered
8389
			return this.renderEventLocationHelper(eventLocation, seg);
8390
		}
8391
		else { // otherwise, just render a highlight
8392
			eventSpans = this.eventToSpans(eventLocation);
8393

  
8394
			for (i = 0; i < eventSpans.length; i++) {
8395
				this.renderHighlight(eventSpans[i]);
8396
			}
8397
		}
8398
	},
8399

  
8400

  
8401
	// Unrenders any visual indication of an event being dragged
8402
	unrenderDrag: function() {
8403
		this.unrenderHelper();
8404
		this.unrenderHighlight();
8405
	},
8406

  
8407

  
8408
	/* Event Resize Visualization
8409
	------------------------------------------------------------------------------------------------------------------*/
8410

  
8411

  
8412
	// Renders a visual indication of an event being resized
8413
	renderEventResize: function(eventLocation, seg) {
8414
		return this.renderEventLocationHelper(eventLocation, seg); // returns mock event elements
8415
	},
8416

  
8417

  
8418
	// Unrenders any visual indication of an event being resized
8419
	unrenderEventResize: function() {
8420
		this.unrenderHelper();
8421
	},
8422

  
8423

  
8424
	/* Event Helper
8425
	------------------------------------------------------------------------------------------------------------------*/
8426

  
8427

  
8428
	// Renders a mock "helper" event. `sourceSeg` is the original segment object and might be null (an external drag)
8429
	renderHelper: function(event, sourceSeg) {
8430
		return this.renderHelperSegs(this.eventToSegs(event), sourceSeg); // returns mock event elements
8431
	},
8432

  
8433

  
8434
	// Unrenders any mock helper event
8435
	unrenderHelper: function() {
8436
		this.unrenderHelperSegs();
8437
	},
8438

  
8439

  
8440
	/* Business Hours
8441
	------------------------------------------------------------------------------------------------------------------*/
8442

  
8443

  
8444
	renderBusinessHours: function() {
8445
		this.renderBusinessSegs(
8446
			this.buildBusinessHourSegs()
8447
		);
8448
	},
8449

  
8450

  
8451
	unrenderBusinessHours: function() {
8452
		this.unrenderBusinessSegs();
8453
	},
8454

  
8455

  
8456
	/* Now Indicator
8457
	------------------------------------------------------------------------------------------------------------------*/
8458

  
8459

  
8460
	getNowIndicatorUnit: function() {
8461
		return 'minute'; // will refresh on the minute
8462
	},
8463

  
8464

  
8465
	renderNowIndicator: function(date) {
8466
		// seg system might be overkill, but it handles scenario where line needs to be rendered
8467
		//  more than once because of columns with the same date (resources columns for example)
8468
		var segs = this.spanToSegs({ start: date, end: date });
8469
		var top = this.computeDateTop(date, date);
8470
		var nodes = [];
8471
		var i;
8472

  
8473
		// render lines within the columns
8474
		for (i = 0; i < segs.length; i++) {
8475
			nodes.push($('<div class="fc-now-indicator fc-now-indicator-line"></div>')
8476
				.css('top', top)
8477
				.appendTo(this.colContainerEls.eq(segs[i].col))[0]);
8478
		}
8479

  
8480
		// render an arrow over the axis
8481
		if (segs.length > 0) { // is the current time in view?
8482
			nodes.push($('<div class="fc-now-indicator fc-now-indicator-arrow"></div>')
8483
				.css('top', top)
8484
				.appendTo(this.el.find('.fc-content-skeleton'))[0]);
8485
		}
8486

  
8487
		this.nowIndicatorEls = $(nodes);
8488
	},
8489

  
8490

  
8491
	unrenderNowIndicator: function() {
8492
		if (this.nowIndicatorEls) {
8493
			this.nowIndicatorEls.remove();
8494
			this.nowIndicatorEls = null;
8495
		}
8496
	},
8497

  
8498

  
8499
	/* Selection
8500
	------------------------------------------------------------------------------------------------------------------*/
8501

  
8502

  
8503
	// Renders a visual indication of a selection. Overrides the default, which was to simply render a highlight.
8504
	renderSelection: function(span) {
8505
		if (this.view.opt('selectHelper')) { // this setting signals that a mock helper event should be rendered
8506

  
8507
			// normally acceps an eventLocation, span has a start/end, which is good enough
8508
			this.renderEventLocationHelper(span);
8509
		}
8510
		else {
8511
			this.renderHighlight(span);
8512
		}
8513
	},
8514

  
8515

  
8516
	// Unrenders any visual indication of a selection
8517
	unrenderSelection: function() {
8518
		this.unrenderHelper();
8519
		this.unrenderHighlight();
8520
	},
8521

  
8522

  
8523
	/* Highlight
8524
	------------------------------------------------------------------------------------------------------------------*/
8525

  
8526

  
8527
	renderHighlight: function(span) {
8528
		this.renderHighlightSegs(this.spanToSegs(span));
8529
	},
8530

  
8531

  
8532
	unrenderHighlight: function() {
8533
		this.unrenderHighlightSegs();
8534
	}
8535

  
8536
});
8537

  
8538
;;
8539

  
8540
/* Methods for rendering SEGMENTS, pieces of content that live on the view
8541
 ( this file is no longer just for events )
8542
----------------------------------------------------------------------------------------------------------------------*/
8543

  
8544
TimeGrid.mixin({
8545

  
8546
	colContainerEls: null, // containers for each column
8547

  
8548
	// inner-containers for each column where different types of segs live
8549
	fgContainerEls: null,
8550
	bgContainerEls: null,
8551
	helperContainerEls: null,
8552
	highlightContainerEls: null,
8553
	businessContainerEls: null,
8554

  
8555
	// arrays of different types of displayed segments
8556
	fgSegs: null,
8557
	bgSegs: null,
8558
	helperSegs: null,
8559
	highlightSegs: null,
8560
	businessSegs: null,
8561

  
8562

  
8563
	// Renders the DOM that the view's content will live in
8564
	renderContentSkeleton: function() {
8565
		var cellHtml = '';
8566
		var i;
8567
		var skeletonEl;
8568

  
8569
		for (i = 0; i < this.colCnt; i++) {
8570
			cellHtml +=
8571
				'<td>' +
8572
					'<div class="fc-content-col">' +
8573
						'<div class="fc-event-container fc-helper-container"></div>' +
8574
						'<div class="fc-event-container"></div>' +
8575
						'<div class="fc-highlight-container"></div>' +
8576
						'<div class="fc-bgevent-container"></div>' +
8577
						'<div class="fc-business-container"></div>' +
8578
					'</div>' +
8579
				'</td>';
8580
		}
8581

  
8582
		skeletonEl = $(
8583
			'<div class="fc-content-skeleton">' +
8584
				'<table>' +
8585
					'<tr>' + cellHtml + '</tr>' +
8586
				'</table>' +
8587
			'</div>'
8588
		);
8589

  
8590
		this.colContainerEls = skeletonEl.find('.fc-content-col');
8591
		this.helperContainerEls = skeletonEl.find('.fc-helper-container');
8592
		this.fgContainerEls = skeletonEl.find('.fc-event-container:not(.fc-helper-container)');
8593
		this.bgContainerEls = skeletonEl.find('.fc-bgevent-container');
8594
		this.highlightContainerEls = skeletonEl.find('.fc-highlight-container');
8595
		this.businessContainerEls = skeletonEl.find('.fc-business-container');
8596

  
8597
		this.bookendCells(skeletonEl.find('tr')); // TODO: do this on string level
8598
		this.el.append(skeletonEl);
8599
	},
8600

  
8601

  
8602
	/* Foreground Events
8603
	------------------------------------------------------------------------------------------------------------------*/
8604

  
8605

  
8606
	renderFgSegs: function(segs) {
8607
		segs = this.renderFgSegsIntoContainers(segs, this.fgContainerEls);
8608
		this.fgSegs = segs;
8609
		return segs; // needed for Grid::renderEvents
8610
	},
8611

  
8612

  
8613
	unrenderFgSegs: function() {
8614
		this.unrenderNamedSegs('fgSegs');
8615
	},
8616

  
8617

  
8618
	/* Foreground Helper Events
8619
	------------------------------------------------------------------------------------------------------------------*/
8620

  
8621

  
8622
	renderHelperSegs: function(segs, sourceSeg) {
8623
		var helperEls = [];
8624
		var i, seg;
8625
		var sourceEl;
8626

  
8627
		segs = this.renderFgSegsIntoContainers(segs, this.helperContainerEls);
8628

  
8629
		// Try to make the segment that is in the same row as sourceSeg look the same
8630
		for (i = 0; i < segs.length; i++) {
8631
			seg = segs[i];
8632
			if (sourceSeg && sourceSeg.col === seg.col) {
8633
				sourceEl = sourceSeg.el;
8634
				seg.el.css({
8635
					left: sourceEl.css('left'),
8636
					right: sourceEl.css('right'),
8637
					'margin-left': sourceEl.css('margin-left'),
8638
					'margin-right': sourceEl.css('margin-right')
8639
				});
8640
			}
8641
			helperEls.push(seg.el[0]);
8642
		}
8643

  
8644
		this.helperSegs = segs;
8645

  
8646
		return $(helperEls); // must return rendered helpers
8647
	},
8648

  
8649

  
8650
	unrenderHelperSegs: function() {
8651
		this.unrenderNamedSegs('helperSegs');
8652
	},
8653

  
8654

  
8655
	/* Background Events
8656
	------------------------------------------------------------------------------------------------------------------*/
8657

  
8658

  
8659
	renderBgSegs: function(segs) {
8660
		segs = this.renderFillSegEls('bgEvent', segs); // TODO: old fill system
8661
		this.updateSegVerticals(segs);
8662
		this.attachSegsByCol(this.groupSegsByCol(segs), this.bgContainerEls);
8663
		this.bgSegs = segs;
8664
		return segs; // needed for Grid::renderEvents
8665
	},
8666

  
8667

  
8668
	unrenderBgSegs: function() {
8669
		this.unrenderNamedSegs('bgSegs');
8670
	},
8671

  
8672

  
8673
	/* Highlight
8674
	------------------------------------------------------------------------------------------------------------------*/
8675

  
8676

  
8677
	renderHighlightSegs: function(segs) {
8678
		segs = this.renderFillSegEls('highlight', segs); // TODO: old fill system
8679
		this.updateSegVerticals(segs);
8680
		this.attachSegsByCol(this.groupSegsByCol(segs), this.highlightContainerEls);
8681
		this.highlightSegs = segs;
8682
	},
8683

  
8684

  
8685
	unrenderHighlightSegs: function() {
8686
		this.unrenderNamedSegs('highlightSegs');
8687
	},
8688

  
8689

  
8690
	/* Business Hours
8691
	------------------------------------------------------------------------------------------------------------------*/
8692

  
8693

  
8694
	renderBusinessSegs: function(segs) {
8695
		segs = this.renderFillSegEls('businessHours', segs); // TODO: old fill system
8696
		this.updateSegVerticals(segs);
8697
		this.attachSegsByCol(this.groupSegsByCol(segs), this.businessContainerEls);
8698
		this.businessSegs = segs;
8699
	},
8700

  
8701

  
8702
	unrenderBusinessSegs: function() {
8703
		this.unrenderNamedSegs('businessSegs');
8704
	},
8705

  
8706

  
8707
	/* Seg Rendering Utils
8708
	------------------------------------------------------------------------------------------------------------------*/
8709

  
8710

  
8711
	// Given a flat array of segments, return an array of sub-arrays, grouped by each segment's col
8712
	groupSegsByCol: function(segs) {
8713
		var segsByCol = [];
8714
		var i;
8715

  
8716
		for (i = 0; i < this.colCnt; i++) {
8717
			segsByCol.push([]);
8718
		}
8719

  
8720
		for (i = 0; i < segs.length; i++) {
8721
			segsByCol[segs[i].col].push(segs[i]);
8722
		}
8723

  
8724
		return segsByCol;
8725
	},
8726

  
8727

  
8728
	// Given segments grouped by column, insert the segments' elements into a parallel array of container
8729
	// elements, each living within a column.
8730
	attachSegsByCol: function(segsByCol, containerEls) {
8731
		var col;
8732
		var segs;
8733
		var i;
8734

  
8735
		for (col = 0; col < this.colCnt; col++) { // iterate each column grouping
8736
			segs = segsByCol[col];
8737

  
8738
			for (i = 0; i < segs.length; i++) {
8739
				containerEls.eq(col).append(segs[i].el);
8740
			}
8741
		}
8742
	},
8743

  
8744

  
8745
	// Given the name of a property of `this` object, assumed to be an array of segments,
8746
	// loops through each segment and removes from DOM. Will null-out the property afterwards.
8747
	unrenderNamedSegs: function(propName) {
8748
		var segs = this[propName];
8749
		var i;
8750

  
8751
		if (segs) {
8752
			for (i = 0; i < segs.length; i++) {
8753
				segs[i].el.remove();
8754
			}
8755
			this[propName] = null;
8756
		}
8757
	},
8758

  
8759

  
8760

  
8761
	/* Foreground Event Rendering Utils
8762
	------------------------------------------------------------------------------------------------------------------*/
8763

  
8764

  
8765
	// Given an array of foreground segments, render a DOM element for each, computes position,
8766
	// and attaches to the column inner-container elements.
8767
	renderFgSegsIntoContainers: function(segs, containerEls) {
8768
		var segsByCol;
8769
		var col;
8770

  
8771
		segs = this.renderFgSegEls(segs); // will call fgSegHtml
8772
		segsByCol = this.groupSegsByCol(segs);
8773

  
8774
		for (col = 0; col < this.colCnt; col++) {
8775
			this.updateFgSegCoords(segsByCol[col]);
8776
		}
8777

  
8778
		this.attachSegsByCol(segsByCol, containerEls);
8779

  
8780
		return segs;
8781
	},
8782

  
8783

  
8784
	// Renders the HTML for a single event segment's default rendering
8785
	fgSegHtml: function(seg, disableResizing) {
8786
		var view = this.view;
8787
		var event = seg.event;
8788
		var isDraggable = view.isEventDraggable(event);
8789
		var isResizableFromStart = !disableResizing && seg.isStart && view.isEventResizableFromStart(event);
8790
		var isResizableFromEnd = !disableResizing && seg.isEnd && view.isEventResizableFromEnd(event);
8791
		var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd);
8792
		var skinCss = cssToStr(this.getSegSkinCss(seg));
8793
		var timeText;
8794
		var fullTimeText; // more verbose time text. for the print stylesheet
8795
		var startTimeText; // just the start time text
8796

  
8797
		classes.unshift('fc-time-grid-event', 'fc-v-event');
8798

  
8799
		if (view.isMultiDayEvent(event)) { // if the event appears to span more than one day...
8800
			// Don't display time text on segments that run entirely through a day.
8801
			// That would appear as midnight-midnight and would look dumb.
8802
			// Otherwise, display the time text for the *segment's* times (like 6pm-midnight or midnight-10am)
8803
			if (seg.isStart || seg.isEnd) {
8804
				timeText = this.getEventTimeText(seg);
8805
				fullTimeText = this.getEventTimeText(seg, 'LT');
8806
				startTimeText = this.getEventTimeText(seg, null, false); // displayEnd=false
8807
			}
8808
		} else {
8809
			// Display the normal time text for the *event's* times
8810
			timeText = this.getEventTimeText(event);
8811
			fullTimeText = this.getEventTimeText(event, 'LT');
8812
			startTimeText = this.getEventTimeText(event, null, false); // displayEnd=false
8813
		}
8814

  
8815
		return '<a class="' + classes.join(' ') + '"' +
8816
			(event.url ?
8817
				' href="' + htmlEscape(event.url) + '"' :
8818
				''
8819
				) +
8820
			(skinCss ?
8821
				' style="' + skinCss + '"' :
8822
				''
8823
				) +
8824
			'>' +
8825
				'<div class="fc-content">' +
8826
					(timeText ?
8827
						'<div class="fc-time"' +
8828
						' data-start="' + htmlEscape(startTimeText) + '"' +
8829
						' data-full="' + htmlEscape(fullTimeText) + '"' +
8830
						'>' +
8831
							'<span>' + htmlEscape(timeText) + '</span>' +
8832
						'</div>' :
8833
						''
8834
						) +
8835
					(event.title ?
8836
						'<div class="fc-title">' +
8837
							htmlEscape(event.title) +
8838
						'</div>' :
8839
						''
8840
						) +
8841
				'</div>' +
8842
				'<div class="fc-bg"/>' +
8843
				/* TODO: write CSS for this
8844
				(isResizableFromStart ?
8845
					'<div class="fc-resizer fc-start-resizer" />' :
8846
					''
8847
					) +
8848
				*/
8849
				(isResizableFromEnd ?
8850
					'<div class="fc-resizer fc-end-resizer" />' :
8851
					''
8852
					) +
8853
			'</a>';
8854
	},
8855

  
8856

  
8857
	/* Seg Position Utils
8858
	------------------------------------------------------------------------------------------------------------------*/
8859

  
8860

  
8861
	// Refreshes the CSS top/bottom coordinates for each segment element.
8862
	// Works when called after initial render, after a window resize/zoom for example.
8863
	updateSegVerticals: function(segs) {
8864
		this.computeSegVerticals(segs);
8865
		this.assignSegVerticals(segs);
8866
	},
8867

  
8868

  
8869
	// For each segment in an array, computes and assigns its top and bottom properties
8870
	computeSegVerticals: function(segs) {
8871
		var i, seg;
8872
		var dayDate;
8873

  
8874
		for (i = 0; i < segs.length; i++) {
8875
			seg = segs[i];
8876
			dayDate = this.dayDates[seg.dayIndex];
8877

  
8878
			seg.top = this.computeDateTop(seg.start, dayDate);
8879
			seg.bottom = this.computeDateTop(seg.end, dayDate);
8880
		}
8881
	},
8882

  
8883

  
8884
	// Given segments that already have their top/bottom properties computed, applies those values to
8885
	// the segments' elements.
8886
	assignSegVerticals: function(segs) {
8887
		var i, seg;
8888

  
8889
		for (i = 0; i < segs.length; i++) {
8890
			seg = segs[i];
8891
			seg.el.css(this.generateSegVerticalCss(seg));
8892
		}
8893
	},
8894

  
8895

  
8896
	// Generates an object with CSS properties for the top/bottom coordinates of a segment element
8897
	generateSegVerticalCss: function(seg) {
8898
		return {
8899
			top: seg.top,
8900
			bottom: -seg.bottom // flipped because needs to be space beyond bottom edge of event container
8901
		};
8902
	},
8903

  
8904

  
8905
	/* Foreground Event Positioning Utils
8906
	------------------------------------------------------------------------------------------------------------------*/
8907

  
8908

  
8909
	// Given segments that are assumed to all live in the *same column*,
8910
	// compute their verical/horizontal coordinates and assign to their elements.
8911
	updateFgSegCoords: function(segs) {
8912
		this.computeSegVerticals(segs); // horizontals relies on this
8913
		this.computeFgSegHorizontals(segs); // compute horizontal coordinates, z-index's, and reorder the array
8914
		this.assignSegVerticals(segs);
8915
		this.assignFgSegHorizontals(segs);
8916
	},
8917

  
8918

  
8919
	// Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each.
8920
	// NOTE: Also reorders the given array by date!
8921
	computeFgSegHorizontals: function(segs) {
8922
		var levels;
8923
		var level0;
8924
		var i;
8925

  
8926
		this.sortEventSegs(segs); // order by certain criteria
8927
		levels = buildSlotSegLevels(segs);
8928
		computeForwardSlotSegs(levels);
8929

  
8930
		if ((level0 = levels[0])) {
8931

  
8932
			for (i = 0; i < level0.length; i++) {
8933
				computeSlotSegPressures(level0[i]);
8934
			}
8935

  
8936
			for (i = 0; i < level0.length; i++) {
8937
				this.computeFgSegForwardBack(level0[i], 0, 0);
8938
			}
8939
		}
8940
	},
8941

  
8942

  
8943
	// Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range
8944
	// from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and
8945
	// seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left.
8946
	//
8947
	// The segment might be part of a "series", which means consecutive segments with the same pressure
8948
	// who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of
8949
	// segments behind this one in the current series, and `seriesBackwardCoord` is the starting
8950
	// coordinate of the first segment in the series.
8951
	computeFgSegForwardBack: function(seg, seriesBackwardPressure, seriesBackwardCoord) {
8952
		var forwardSegs = seg.forwardSegs;
8953
		var i;
8954

  
8955
		if (seg.forwardCoord === undefined) { // not already computed
8956

  
8957
			if (!forwardSegs.length) {
8958

  
8959
				// if there are no forward segments, this segment should butt up against the edge
8960
				seg.forwardCoord = 1;
8961
			}
8962
			else {
8963

  
8964
				// sort highest pressure first
8965
				this.sortForwardSegs(forwardSegs);
8966

  
8967
				// this segment's forwardCoord will be calculated from the backwardCoord of the
8968
				// highest-pressure forward segment.
8969
				this.computeFgSegForwardBack(forwardSegs[0], seriesBackwardPressure + 1, seriesBackwardCoord);
8970
				seg.forwardCoord = forwardSegs[0].backwardCoord;
8971
			}
8972

  
8973
			// calculate the backwardCoord from the forwardCoord. consider the series
8974
			seg.backwardCoord = seg.forwardCoord -
8975
				(seg.forwardCoord - seriesBackwardCoord) / // available width for series
8976
				(seriesBackwardPressure + 1); // # of segments in the series
8977

  
8978
			// use this segment's coordinates to computed the coordinates of the less-pressurized
8979
			// forward segments
8980
			for (i=0; i<forwardSegs.length; i++) {
8981
				this.computeFgSegForwardBack(forwardSegs[i], 0, seg.forwardCoord);
8982
			}
8983
		}
8984
	},
8985

  
8986

  
8987
	sortForwardSegs: function(forwardSegs) {
8988
		forwardSegs.sort(proxy(this, 'compareForwardSegs'));
8989
	},
8990

  
8991

  
8992
	// A cmp function for determining which forward segment to rely on more when computing coordinates.
8993
	compareForwardSegs: function(seg1, seg2) {
8994
		// put higher-pressure first
8995
		return seg2.forwardPressure - seg1.forwardPressure ||
8996
			// put segments that are closer to initial edge first (and favor ones with no coords yet)
8997
			(seg1.backwardCoord || 0) - (seg2.backwardCoord || 0) ||
8998
			// do normal sorting...
8999
			this.compareEventSegs(seg1, seg2);
9000
	},
9001

  
9002

  
9003
	// Given foreground event segments that have already had their position coordinates computed,
9004
	// assigns position-related CSS values to their elements.
9005
	assignFgSegHorizontals: function(segs) {
9006
		var i, seg;
9007

  
9008
		for (i = 0; i < segs.length; i++) {
9009
			seg = segs[i];
9010
			seg.el.css(this.generateFgSegHorizontalCss(seg));
9011

  
9012
			// if the height is short, add a className for alternate styling
9013
			if (seg.bottom - seg.top < 30) {
9014
				seg.el.addClass('fc-short');
9015
			}
9016
		}
9017
	},
9018

  
9019

  
9020
	// Generates an object with CSS properties/values that should be applied to an event segment element.
9021
	// Contains important positioning-related properties that should be applied to any event element, customized or not.
9022
	generateFgSegHorizontalCss: function(seg) {
9023
		var shouldOverlap = this.view.opt('slotEventOverlap');
9024
		var backwardCoord = seg.backwardCoord; // the left side if LTR. the right side if RTL. floating-point
9025
		var forwardCoord = seg.forwardCoord; // the right side if LTR. the left side if RTL. floating-point
9026
		var props = this.generateSegVerticalCss(seg); // get top/bottom first
9027
		var left; // amount of space from left edge, a fraction of the total width
9028
		var right; // amount of space from right edge, a fraction of the total width
9029

  
9030
		if (shouldOverlap) {
9031
			// double the width, but don't go beyond the maximum forward coordinate (1.0)
9032
			forwardCoord = Math.min(1, backwardCoord + (forwardCoord - backwardCoord) * 2);
9033
		}
9034

  
9035
		if (this.isRTL) {
9036
			left = 1 - forwardCoord;
9037
			right = backwardCoord;
9038
		}
9039
		else {
9040
			left = backwardCoord;
9041
			right = 1 - forwardCoord;
9042
		}
9043

  
9044
		props.zIndex = seg.level + 1; // convert from 0-base to 1-based
9045
		props.left = left * 100 + '%';
9046
		props.right = right * 100 + '%';
9047

  
9048
		if (shouldOverlap && seg.forwardPressure) {
9049
			// add padding to the edge so that forward stacked events don't cover the resizer's icon
9050
			props[this.isRTL ? 'marginLeft' : 'marginRight'] = 10 * 2; // 10 is a guesstimate of the icon's width
9051
		}
9052

  
9053
		return props;
9054
	}
9055

  
9056
});
9057

  
9058

  
9059
// Builds an array of segments "levels". The first level will be the leftmost tier of segments if the calendar is
9060
// left-to-right, or the rightmost if the calendar is right-to-left. Assumes the segments are already ordered by date.
9061
function buildSlotSegLevels(segs) {
9062
	var levels = [];
9063
	var i, seg;
9064
	var j;
9065

  
9066
	for (i=0; i<segs.length; i++) {
9067
		seg = segs[i];
9068

  
9069
		// go through all the levels and stop on the first level where there are no collisions
9070
		for (j=0; j<levels.length; j++) {
9071
			if (!computeSlotSegCollisions(seg, levels[j]).length) {
9072
				break;
9073
			}
9074
		}
9075

  
9076
		seg.level = j;
9077

  
9078
		(levels[j] || (levels[j] = [])).push(seg);
9079
	}
9080

  
9081
	return levels;
9082
}
9083

  
9084

  
9085
// For every segment, figure out the other segments that are in subsequent
9086
// levels that also occupy the same vertical space. Accumulate in seg.forwardSegs
9087
function computeForwardSlotSegs(levels) {
9088
	var i, level;
9089
	var j, seg;
9090
	var k;
9091

  
9092
	for (i=0; i<levels.length; i++) {
9093
		level = levels[i];
9094

  
9095
		for (j=0; j<level.length; j++) {
9096
			seg = level[j];
9097

  
9098
			seg.forwardSegs = [];
9099
			for (k=i+1; k<levels.length; k++) {
9100
				computeSlotSegCollisions(seg, levels[k], seg.forwardSegs);
9101
			}
9102
		}
9103
	}
9104
}
9105

  
9106

  
9107
// Figure out which path forward (via seg.forwardSegs) results in the longest path until
9108
// the furthest edge is reached. The number of segments in this path will be seg.forwardPressure
9109
function computeSlotSegPressures(seg) {
9110
	var forwardSegs = seg.forwardSegs;
9111
	var forwardPressure = 0;
9112
	var i, forwardSeg;
9113

  
9114
	if (seg.forwardPressure === undefined) { // not already computed
9115

  
9116
		for (i=0; i<forwardSegs.length; i++) {
9117
			forwardSeg = forwardSegs[i];
9118

  
9119
			// figure out the child's maximum forward path
9120
			computeSlotSegPressures(forwardSeg);
9121

  
9122
			// either use the existing maximum, or use the child's forward pressure
9123
			// plus one (for the forwardSeg itself)
9124
			forwardPressure = Math.max(
9125
				forwardPressure,
9126
				1 + forwardSeg.forwardPressure
9127
			);
9128
		}
9129

  
9130
		seg.forwardPressure = forwardPressure;
9131
	}
9132
}
9133

  
9134

  
9135
// Find all the segments in `otherSegs` that vertically collide with `seg`.
9136
// Append into an optionally-supplied `results` array and return.
9137
function computeSlotSegCollisions(seg, otherSegs, results) {
9138
	results = results || [];
9139

  
9140
	for (var i=0; i<otherSegs.length; i++) {
9141
		if (isSlotSegCollision(seg, otherSegs[i])) {
9142
			results.push(otherSegs[i]);
9143
		}
9144
	}
9145

  
9146
	return results;
9147
}
9148

  
9149

  
9150
// Do these segments occupy the same vertical space?
9151
function isSlotSegCollision(seg1, seg2) {
9152
	return seg1.bottom > seg2.top && seg1.top < seg2.bottom;
9153
}
9154

  
9155
;;
9156

  
9157
/* An abstract class from which other views inherit from
9158
----------------------------------------------------------------------------------------------------------------------*/
9159

  
9160
var View = FC.View = Model.extend({
9161

  
9162
	type: null, // subclass' view name (string)
9163
	name: null, // deprecated. use `type` instead
9164
	title: null, // the text that will be displayed in the header's title
9165

  
9166
	calendar: null, // owner Calendar object
9167
	viewSpec: null,
9168
	options: null, // hash containing all options. already merged with view-specific-options
9169
	el: null, // the view's containing element. set by Calendar
9170

  
9171
	renderQueue: null,
9172
	batchRenderDepth: 0,
9173
	isDatesRendered: false,
9174
	isEventsRendered: false,
9175
	isBaseRendered: false, // related to viewRender/viewDestroy triggers
9176

  
9177
	queuedScroll: null,
9178

  
9179
	isRTL: false,
9180
	isSelected: false, // boolean whether a range of time is user-selected or not
9181
	selectedEvent: null,
9182

  
9183
	eventOrderSpecs: null, // criteria for ordering events when they have same date/time
9184

  
9185
	// classNames styled by jqui themes
9186
	widgetHeaderClass: null,
9187
	widgetContentClass: null,
9188
	highlightStateClass: null,
9189

  
9190
	// for date utils, computed from options
9191
	nextDayThreshold: null,
9192
	isHiddenDayHash: null,
9193

  
9194
	// now indicator
9195
	isNowIndicatorRendered: null,
9196
	initialNowDate: null, // result first getNow call
9197
	initialNowQueriedMs: null, // ms time the getNow was called
9198
	nowIndicatorTimeoutID: null, // for refresh timing of now indicator
9199
	nowIndicatorIntervalID: null, // "
9200

  
9201

  
9202
	constructor: function(calendar, viewSpec) {
9203
		Model.prototype.constructor.call(this);
9204

  
9205
		this.calendar = calendar;
9206
		this.viewSpec = viewSpec;
9207

  
9208
		// shortcuts
9209
		this.type = viewSpec.type;
9210
		this.options = viewSpec.options;
9211

  
9212
		// .name is deprecated
9213
		this.name = this.type;
9214

  
9215
		this.nextDayThreshold = moment.duration(this.opt('nextDayThreshold'));
9216
		this.initThemingProps();
9217
		this.initHiddenDays();
9218
		this.isRTL = this.opt('isRTL');
9219

  
9220
		this.eventOrderSpecs = parseFieldSpecs(this.opt('eventOrder'));
9221

  
9222
		this.renderQueue = this.buildRenderQueue();
9223
		this.initAutoBatchRender();
9224

  
9225
		this.initialize();
9226
	},
9227

  
9228

  
9229
	buildRenderQueue: function() {
9230
		var _this = this;
9231
		var renderQueue = new RenderQueue({
9232
			event: this.opt('eventRenderWait')
9233
		});
9234

  
9235
		renderQueue.on('start', function() {
9236
			_this.freezeHeight();
9237
			_this.addScroll(_this.queryScroll());
9238
		});
9239

  
9240
		renderQueue.on('stop', function() {
9241
			_this.thawHeight();
9242
			_this.popScroll();
9243
		});
9244

  
9245
		return renderQueue;
9246
	},
9247

  
9248

  
9249
	initAutoBatchRender: function() {
9250
		var _this = this;
9251

  
9252
		this.on('before:change', function() {
9253
			_this.startBatchRender();
9254
		});
9255

  
9256
		this.on('change', function() {
9257
			_this.stopBatchRender();
9258
		});
9259
	},
9260

  
9261

  
9262
	startBatchRender: function() {
9263
		if (!(this.batchRenderDepth++)) {
9264
			this.renderQueue.pause();
9265
		}
9266
	},
9267

  
9268

  
9269
	stopBatchRender: function() {
9270
		if (!(--this.batchRenderDepth)) {
9271
			this.renderQueue.resume();
9272
		}
9273
	},
9274

  
9275

  
9276
	// A good place for subclasses to initialize member variables
9277
	initialize: function() {
9278
		// subclasses can implement
9279
	},
9280

  
9281

  
9282
	// Retrieves an option with the given name
9283
	opt: function(name) {
9284
		return this.options[name];
9285
	},
9286

  
9287

  
9288
	// Triggers handlers that are view-related. Modifies args before passing to calendar.
9289
	publiclyTrigger: function(name, thisObj) { // arguments beyond thisObj are passed along
9290
		var calendar = this.calendar;
9291

  
9292
		return calendar.publiclyTrigger.apply(
9293
			calendar,
9294
			[name, thisObj || this].concat(
9295
				Array.prototype.slice.call(arguments, 2), // arguments beyond thisObj
9296
				[ this ] // always make the last argument a reference to the view. TODO: deprecate
9297
			)
9298
		);
9299
	},
9300

  
9301

  
9302
	/* Title and Date Formatting
9303
	------------------------------------------------------------------------------------------------------------------*/
9304

  
9305

  
9306
	// Sets the view's title property to the most updated computed value
9307
	updateTitle: function() {
9308
		this.title = this.computeTitle();
9309
		this.calendar.setToolbarsTitle(this.title);
9310
	},
9311

  
9312

  
9313
	// Computes what the title at the top of the calendar should be for this view
9314
	computeTitle: function() {
9315
		var range;
9316

  
9317
		// for views that span a large unit of time, show the proper interval, ignoring stray days before and after
9318
		if (/^(year|month)$/.test(this.currentRangeUnit)) {
9319
			range = this.currentRange;
9320
		}
9321
		else { // for day units or smaller, use the actual day range
9322
			range = this.activeRange;
9323
		}
9324

  
9325
		return this.formatRange(
9326
			{
9327
				// in case currentRange has a time, make sure timezone is correct
9328
				start: this.calendar.applyTimezone(range.start),
9329
				end: this.calendar.applyTimezone(range.end)
9330
			},
9331
			this.opt('titleFormat') || this.computeTitleFormat(),
9332
			this.opt('titleRangeSeparator')
9333
		);
9334
	},
9335

  
9336

  
9337
	// Generates the format string that should be used to generate the title for the current date range.
9338
	// Attempts to compute the most appropriate format if not explicitly specified with `titleFormat`.
9339
	computeTitleFormat: function() {
9340
		if (this.currentRangeUnit == 'year') {
9341
			return 'YYYY';
9342
		}
9343
		else if (this.currentRangeUnit == 'month') {
9344
			return this.opt('monthYearFormat'); // like "September 2014"
9345
		}
9346
		else if (this.currentRangeAs('days') > 1) {
9347
			return 'll'; // multi-day range. shorter, like "Sep 9 - 10 2014"
9348
		}
9349
		else {
9350
			return 'LL'; // one day. longer, like "September 9 2014"
9351
		}
9352
	},
9353

  
9354

  
9355
	// Utility for formatting a range. Accepts a range object, formatting string, and optional separator.
9356
	// Displays all-day ranges naturally, with an inclusive end. Takes the current isRTL into account.
9357
	// The timezones of the dates within `range` will be respected.
9358
	formatRange: function(range, formatStr, separator) {
9359
		var end = range.end;
9360

  
9361
		if (!end.hasTime()) { // all-day?
9362
			end = end.clone().subtract(1); // convert to inclusive. last ms of previous day
9363
		}
9364

  
9365
		return formatRange(range.start, end, formatStr, separator, this.opt('isRTL'));
9366
	},
9367

  
9368

  
9369
	getAllDayHtml: function() {
9370
		return this.opt('allDayHtml') || htmlEscape(this.opt('allDayText'));
9371
	},
9372

  
9373

  
9374
	/* Navigation
9375
	------------------------------------------------------------------------------------------------------------------*/
9376

  
9377

  
9378
	// Generates HTML for an anchor to another view into the calendar.
9379
	// Will either generate an <a> tag or a non-clickable <span> tag, depending on enabled settings.
9380
	// `gotoOptions` can either be a moment input, or an object with the form:
9381
	// { date, type, forceOff }
9382
	// `type` is a view-type like "day" or "week". default value is "day".
9383
	// `attrs` and `innerHtml` are use to generate the rest of the HTML tag.
9384
	buildGotoAnchorHtml: function(gotoOptions, attrs, innerHtml) {
9385
		var date, type, forceOff;
9386
		var finalOptions;
9387

  
9388
		if ($.isPlainObject(gotoOptions)) {
9389
			date = gotoOptions.date;
9390
			type = gotoOptions.type;
9391
			forceOff = gotoOptions.forceOff;
9392
		}
9393
		else {
9394
			date = gotoOptions; // a single moment input
9395
		}
9396
		date = FC.moment(date); // if a string, parse it
9397

  
9398
		finalOptions = { // for serialization into the link
9399
			date: date.format('YYYY-MM-DD'),
9400
			type: type || 'day'
9401
		};
9402

  
9403
		if (typeof attrs === 'string') {
9404
			innerHtml = attrs;
9405
			attrs = null;
9406
		}
9407

  
9408
		attrs = attrs ? ' ' + attrsToStr(attrs) : ''; // will have a leading space
9409
		innerHtml = innerHtml || '';
9410

  
9411
		if (!forceOff && this.opt('navLinks')) {
9412
			return '<a' + attrs +
9413
				' data-goto="' + htmlEscape(JSON.stringify(finalOptions)) + '">' +
9414
				innerHtml +
9415
				'</a>';
9416
		}
9417
		else {
9418
			return '<span' + attrs + '>' +
9419
				innerHtml +
9420
				'</span>';
9421
		}
9422
	},
9423

  
9424

  
9425
	// Rendering Non-date-related Content
9426
	// -----------------------------------------------------------------------------------------------------------------
9427

  
9428

  
9429
	// Sets the container element that the view should render inside of, does global DOM-related initializations,
9430
	// and renders all the non-date-related content inside.
9431
	setElement: function(el) {
9432
		this.el = el;
9433
		this.bindGlobalHandlers();
9434
		this.bindBaseRenderHandlers();
9435
		this.renderSkeleton();
9436
	},
9437

  
9438

  
9439
	// Removes the view's container element from the DOM, clearing any content beforehand.
9440
	// Undoes any other DOM-related attachments.
9441
	removeElement: function() {
9442
		this.unsetDate();
9443
		this.unrenderSkeleton();
9444

  
9445
		this.unbindGlobalHandlers();
9446
		this.unbindBaseRenderHandlers();
9447

  
9448
		this.el.remove();
9449
		// NOTE: don't null-out this.el in case the View was destroyed within an API callback.
9450
		// We don't null-out the View's other jQuery element references upon destroy,
9451
		//  so we shouldn't kill this.el either.
9452
	},
9453

  
9454

  
9455
	// Renders the basic structure of the view before any content is rendered
9456
	renderSkeleton: function() {
9457
		// subclasses should implement
9458
	},
9459

  
9460

  
9461
	// Unrenders the basic structure of the view
9462
	unrenderSkeleton: function() {
9463
		// subclasses should implement
9464
	},
9465

  
9466

  
9467
	// Date Setting/Unsetting
9468
	// -----------------------------------------------------------------------------------------------------------------
9469

  
9470

  
9471
	setDate: function(date) {
9472
		var currentDateProfile = this.get('dateProfile');
9473
		var newDateProfile = this.buildDateProfile(date, null, true); // forceToValid=true
9474

  
9475
		if (
9476
			!currentDateProfile ||
9477
			!isRangesEqual(currentDateProfile.activeRange, newDateProfile.activeRange)
9478
		) {
9479
			this.set('dateProfile', newDateProfile);
9480
		}
9481

  
9482
		return newDateProfile.date;
9483
	},
9484

  
9485

  
9486
	unsetDate: function() {
9487
		this.unset('dateProfile');
9488
	},
9489

  
9490

  
9491
	// Date Rendering
9492
	// -----------------------------------------------------------------------------------------------------------------
9493

  
9494

  
9495
	requestDateRender: function(dateProfile) {
9496
		var _this = this;
9497

  
9498
		this.renderQueue.queue(function() {
9499
			_this.executeDateRender(dateProfile);
9500
		}, 'date', 'init');
9501
	},
9502

  
9503

  
9504
	requestDateUnrender: function() {
9505
		var _this = this;
9506

  
9507
		this.renderQueue.queue(function() {
9508
			_this.executeDateUnrender();
9509
		}, 'date', 'destroy');
9510
	},
9511

  
9512

  
9513
	// Event Data
9514
	// -----------------------------------------------------------------------------------------------------------------
9515

  
9516

  
9517
	fetchInitialEvents: function(dateProfile) {
9518
		return this.calendar.requestEvents(
9519
			dateProfile.activeRange.start,
9520
			dateProfile.activeRange.end
9521
		);
9522
	},
9523

  
9524

  
9525
	bindEventChanges: function() {
9526
		this.listenTo(this.calendar, 'eventsReset', this.resetEvents);
9527
	},
9528

  
9529

  
9530
	unbindEventChanges: function() {
9531
		this.stopListeningTo(this.calendar, 'eventsReset');
9532
	},
9533

  
9534

  
9535
	setEvents: function(events) {
9536
		this.set('currentEvents', events);
9537
		this.set('hasEvents', true);
9538
	},
9539

  
9540

  
9541
	unsetEvents: function() {
9542
		this.unset('currentEvents');
9543
		this.unset('hasEvents');
9544
	},
9545

  
9546

  
9547
	resetEvents: function(events) {
9548
		this.startBatchRender();
9549
		this.unsetEvents();
9550
		this.setEvents(events);
9551
		this.stopBatchRender();
9552
	},
9553

  
9554

  
9555
	// Event Rendering
9556
	// -----------------------------------------------------------------------------------------------------------------
9557

  
9558

  
9559
	requestEventsRender: function(events) {
9560
		var _this = this;
9561

  
9562
		this.renderQueue.queue(function() {
9563
			_this.executeEventsRender(events);
9564
		}, 'event', 'init');
9565
	},
9566

  
9567

  
9568
	requestEventsUnrender: function() {
9569
		var _this = this;
9570

  
9571
		this.renderQueue.queue(function() {
9572
			_this.executeEventsUnrender();
9573
		}, 'event', 'destroy');
9574
	},
9575

  
9576

  
9577
	// Date High-level Rendering
9578
	// -----------------------------------------------------------------------------------------------------------------
9579

  
9580

  
9581
	// if dateProfile not specified, uses current
9582
	executeDateRender: function(dateProfile, skipScroll) {
9583

  
9584
		this.setDateProfileForRendering(dateProfile);
9585
		this.updateTitle();
9586
		this.calendar.updateToolbarButtons();
9587

  
9588
		if (this.render) {
9589
			this.render(); // TODO: deprecate
9590
		}
9591

  
9592
		this.renderDates();
9593
		this.updateSize();
9594
		this.renderBusinessHours(); // might need coordinates, so should go after updateSize()
9595
		this.startNowIndicator();
9596

  
9597
		if (!skipScroll) {
9598
			this.addScroll(this.computeInitialDateScroll());
9599
		}
9600

  
9601
		this.isDatesRendered = true;
9602
		this.trigger('datesRendered');
9603
	},
9604

  
9605

  
9606
	executeDateUnrender: function() {
9607

  
9608
		this.unselect();
9609
		this.stopNowIndicator();
9610

  
9611
		this.trigger('before:datesUnrendered');
9612

  
9613
		this.unrenderBusinessHours();
9614
		this.unrenderDates();
9615

  
9616
		if (this.destroy) {
9617
			this.destroy(); // TODO: deprecate
9618
		}
9619

  
9620
		this.isDatesRendered = false;
9621
	},
9622

  
9623

  
9624
	// Date Low-level Rendering
9625
	// -----------------------------------------------------------------------------------------------------------------
9626

  
9627

  
9628
	// date-cell content only
9629
	renderDates: function() {
9630
		// subclasses should implement
9631
	},
9632

  
9633

  
9634
	// date-cell content only
9635
	unrenderDates: function() {
9636
		// subclasses should override
9637
	},
9638

  
9639

  
9640
	// Determing when the "meat" of the view is rendered (aka the base)
9641
	// -----------------------------------------------------------------------------------------------------------------
9642

  
9643

  
9644
	bindBaseRenderHandlers: function() {
9645
		var _this = this;
9646

  
9647
		this.on('datesRendered.baseHandler', function() {
9648
			_this.onBaseRender();
9649
		});
9650

  
9651
		this.on('before:datesUnrendered.baseHandler', function() {
9652
			_this.onBeforeBaseUnrender();
9653
		});
9654
	},
9655

  
9656

  
9657
	unbindBaseRenderHandlers: function() {
9658
		this.off('.baseHandler');
9659
	},
9660

  
9661

  
9662
	onBaseRender: function() {
9663
		this.applyScreenState();
9664
		this.publiclyTrigger('viewRender', this, this, this.el);
9665
	},
9666

  
9667

  
9668
	onBeforeBaseUnrender: function() {
9669
		this.applyScreenState();
9670
		this.publiclyTrigger('viewDestroy', this, this, this.el);
9671
	},
9672

  
9673

  
9674
	// Misc view rendering utils
9675
	// -----------------------------------------------------------------------------------------------------------------
9676

  
9677

  
9678
	// Binds DOM handlers to elements that reside outside the view container, such as the document
9679
	bindGlobalHandlers: function() {
9680
		this.listenTo(GlobalEmitter.get(), {
9681
			touchstart: this.processUnselect,
9682
			mousedown: this.handleDocumentMousedown
9683
		});
9684
	},
9685

  
9686

  
9687
	// Unbinds DOM handlers from elements that reside outside the view container
9688
	unbindGlobalHandlers: function() {
9689
		this.stopListeningTo(GlobalEmitter.get());
9690
	},
9691

  
9692

  
9693
	// Initializes internal variables related to theming
9694
	initThemingProps: function() {
9695
		var tm = this.opt('theme') ? 'ui' : 'fc';
9696

  
9697
		this.widgetHeaderClass = tm + '-widget-header';
9698
		this.widgetContentClass = tm + '-widget-content';
9699
		this.highlightStateClass = tm + '-state-highlight';
9700
	},
9701

  
9702

  
9703
	/* Business Hours
9704
	------------------------------------------------------------------------------------------------------------------*/
9705

  
9706

  
9707
	// Renders business-hours onto the view. Assumes updateSize has already been called.
9708
	renderBusinessHours: function() {
9709
		// subclasses should implement
9710
	},
9711

  
9712

  
9713
	// Unrenders previously-rendered business-hours
9714
	unrenderBusinessHours: function() {
9715
		// subclasses should implement
9716
	},
9717

  
9718

  
9719
	/* Now Indicator
9720
	------------------------------------------------------------------------------------------------------------------*/
9721

  
9722

  
9723
	// Immediately render the current time indicator and begins re-rendering it at an interval,
9724
	// which is defined by this.getNowIndicatorUnit().
9725
	// TODO: somehow do this for the current whole day's background too
9726
	startNowIndicator: function() {
9727
		var _this = this;
9728
		var unit;
9729
		var update;
9730
		var delay; // ms wait value
9731

  
9732
		if (this.opt('nowIndicator')) {
9733
			unit = this.getNowIndicatorUnit();
9734
			if (unit) {
9735
				update = proxy(this, 'updateNowIndicator'); // bind to `this`
9736

  
9737
				this.initialNowDate = this.calendar.getNow();
9738
				this.initialNowQueriedMs = +new Date();
9739
				this.renderNowIndicator(this.initialNowDate);
9740
				this.isNowIndicatorRendered = true;
9741

  
9742
				// wait until the beginning of the next interval
9743
				delay = this.initialNowDate.clone().startOf(unit).add(1, unit) - this.initialNowDate;
9744
				this.nowIndicatorTimeoutID = setTimeout(function() {
9745
					_this.nowIndicatorTimeoutID = null;
9746
					update();
9747
					delay = +moment.duration(1, unit);
9748
					delay = Math.max(100, delay); // prevent too frequent
9749
					_this.nowIndicatorIntervalID = setInterval(update, delay); // update every interval
9750
				}, delay);
9751
			}
9752
		}
9753
	},
9754

  
9755

  
9756
	// rerenders the now indicator, computing the new current time from the amount of time that has passed
9757
	// since the initial getNow call.
9758
	updateNowIndicator: function() {
9759
		if (this.isNowIndicatorRendered) {
9760
			this.unrenderNowIndicator();
9761
			this.renderNowIndicator(
9762
				this.initialNowDate.clone().add(new Date() - this.initialNowQueriedMs) // add ms
9763
			);
9764
		}
9765
	},
9766

  
9767

  
9768
	// Immediately unrenders the view's current time indicator and stops any re-rendering timers.
9769
	// Won't cause side effects if indicator isn't rendered.
9770
	stopNowIndicator: function() {
9771
		if (this.isNowIndicatorRendered) {
9772

  
9773
			if (this.nowIndicatorTimeoutID) {
9774
				clearTimeout(this.nowIndicatorTimeoutID);
9775
				this.nowIndicatorTimeoutID = null;
9776
			}
9777
			if (this.nowIndicatorIntervalID) {
9778
				clearTimeout(this.nowIndicatorIntervalID);
9779
				this.nowIndicatorIntervalID = null;
9780
			}
9781

  
9782
			this.unrenderNowIndicator();
9783
			this.isNowIndicatorRendered = false;
9784
		}
9785
	},
9786

  
9787

  
9788
	// Returns a string unit, like 'second' or 'minute' that defined how often the current time indicator
9789
	// should be refreshed. If something falsy is returned, no time indicator is rendered at all.
9790
	getNowIndicatorUnit: function() {
9791
		// subclasses should implement
9792
	},
9793

  
9794

  
9795
	// Renders a current time indicator at the given datetime
9796
	renderNowIndicator: function(date) {
9797
		// subclasses should implement
9798
	},
9799

  
9800

  
9801
	// Undoes the rendering actions from renderNowIndicator
9802
	unrenderNowIndicator: function() {
9803
		// subclasses should implement
9804
	},
9805

  
9806

  
9807
	/* Dimensions
9808
	------------------------------------------------------------------------------------------------------------------*/
9809

  
9810

  
9811
	// Refreshes anything dependant upon sizing of the container element of the grid
9812
	updateSize: function(isResize) {
9813
		var scroll;
9814

  
9815
		if (isResize) {
9816
			scroll = this.queryScroll();
9817
		}
9818

  
9819
		this.updateHeight(isResize);
9820
		this.updateWidth(isResize);
9821
		this.updateNowIndicator();
9822

  
9823
		if (isResize) {
9824
			this.applyScroll(scroll);
9825
		}
9826
	},
9827

  
9828

  
9829
	// Refreshes the horizontal dimensions of the calendar
9830
	updateWidth: function(isResize) {
9831
		// subclasses should implement
9832
	},
9833

  
9834

  
9835
	// Refreshes the vertical dimensions of the calendar
9836
	updateHeight: function(isResize) {
9837
		var calendar = this.calendar; // we poll the calendar for height information
9838

  
9839
		this.setHeight(
9840
			calendar.getSuggestedViewHeight(),
9841
			calendar.isHeightAuto()
9842
		);
9843
	},
9844

  
9845

  
9846
	// Updates the vertical dimensions of the calendar to the specified height.
9847
	// if `isAuto` is set to true, height becomes merely a suggestion and the view should use its "natural" height.
9848
	setHeight: function(height, isAuto) {
9849
		// subclasses should implement
9850
	},
9851

  
9852

  
9853
	/* Scroller
9854
	------------------------------------------------------------------------------------------------------------------*/
9855

  
9856

  
9857
	addForcedScroll: function(scroll) {
9858
		this.addScroll(
9859
			$.extend(scroll, { isForced: true })
9860
		);
9861
	},
9862

  
9863

  
9864
	addScroll: function(scroll) {
9865
		var queuedScroll = this.queuedScroll || (this.queuedScroll = {});
9866

  
9867
		if (!queuedScroll.isForced) {
9868
			$.extend(queuedScroll, scroll);
9869
		}
9870
	},
9871

  
9872

  
9873
	popScroll: function() {
9874
		this.applyQueuedScroll();
9875
		this.queuedScroll = null;
9876
	},
9877

  
9878

  
9879
	applyQueuedScroll: function() {
9880
		if (this.queuedScroll) {
9881
			this.applyScroll(this.queuedScroll);
9882
		}
9883
	},
9884

  
9885

  
9886
	queryScroll: function() {
9887
		var scroll = {};
9888

  
9889
		if (this.isDatesRendered) {
9890
			$.extend(scroll, this.queryDateScroll());
9891
		}
9892

  
9893
		return scroll;
9894
	},
9895

  
9896

  
9897
	applyScroll: function(scroll) {
9898
		if (this.isDatesRendered) {
9899
			this.applyDateScroll(scroll);
9900
		}
9901
	},
9902

  
9903

  
9904
	computeInitialDateScroll: function() {
9905
		return {}; // subclasses must implement
9906
	},
9907

  
9908

  
9909
	queryDateScroll: function() {
9910
		return {}; // subclasses must implement
9911
	},
9912

  
9913

  
9914
	applyDateScroll: function(scroll) {
9915
		; // subclasses must implement
9916
	},
9917

  
9918

  
9919
	/* Height Freezing
9920
	------------------------------------------------------------------------------------------------------------------*/
9921

  
9922

  
9923
	freezeHeight: function() {
9924
		this.calendar.freezeContentHeight();
9925
	},
9926

  
9927

  
9928
	thawHeight: function() {
9929
		this.calendar.thawContentHeight();
9930
	},
9931

  
9932

  
9933
	// Event High-level Rendering
9934
	// -----------------------------------------------------------------------------------------------------------------
9935

  
9936

  
9937
	executeEventsRender: function(events) {
9938
		this.renderEvents(events);
9939
		this.isEventsRendered = true;
9940

  
9941
		this.onEventsRender();
9942
	},
9943

  
9944

  
9945
	executeEventsUnrender: function() {
9946
		this.onBeforeEventsUnrender();
9947

  
9948
		if (this.destroyEvents) {
9949
			this.destroyEvents(); // TODO: deprecate
9950
		}
9951

  
9952
		this.unrenderEvents();
9953
		this.isEventsRendered = false;
9954
	},
9955

  
9956

  
9957
	// Event Rendering Triggers
9958
	// -----------------------------------------------------------------------------------------------------------------
9959

  
9960

  
9961
	// Signals that all events have been rendered
9962
	onEventsRender: function() {
9963
		this.applyScreenState();
9964

  
9965
		this.renderedEventSegEach(function(seg) {
9966
			this.publiclyTrigger('eventAfterRender', seg.event, seg.event, seg.el);
9967
		});
9968
		this.publiclyTrigger('eventAfterAllRender');
9969
	},
9970

  
9971

  
9972
	// Signals that all event elements are about to be removed
9973
	onBeforeEventsUnrender: function() {
9974
		this.applyScreenState();
9975

  
9976
		this.renderedEventSegEach(function(seg) {
9977
			this.publiclyTrigger('eventDestroy', seg.event, seg.event, seg.el);
9978
		});
9979
	},
9980

  
9981

  
9982
	applyScreenState: function() {
9983
		this.thawHeight();
9984
		this.freezeHeight();
9985
		this.applyQueuedScroll();
9986
	},
9987

  
9988

  
9989
	// Event Low-level Rendering
9990
	// -----------------------------------------------------------------------------------------------------------------
9991

  
9992

  
9993
	// Renders the events onto the view.
9994
	renderEvents: function(events) {
9995
		// subclasses should implement
9996
	},
9997

  
9998

  
9999
	// Removes event elements from the view.
10000
	unrenderEvents: function() {
10001
		// subclasses should implement
10002
	},
10003

  
10004

  
10005
	// Event Rendering Utils
10006
	// -----------------------------------------------------------------------------------------------------------------
10007

  
10008

  
10009
	// Given an event and the default element used for rendering, returns the element that should actually be used.
10010
	// Basically runs events and elements through the eventRender hook.
10011
	resolveEventEl: function(event, el) {
10012
		var custom = this.publiclyTrigger('eventRender', event, event, el);
10013

  
10014
		if (custom === false) { // means don't render at all
10015
			el = null;
10016
		}
10017
		else if (custom && custom !== true) {
10018
			el = $(custom);
10019
		}
10020

  
10021
		return el;
10022
	},
10023

  
10024

  
10025
	// Hides all rendered event segments linked to the given event
10026
	showEvent: function(event) {
10027
		this.renderedEventSegEach(function(seg) {
10028
			seg.el.css('visibility', '');
10029
		}, event);
10030
	},
10031

  
10032

  
10033
	// Shows all rendered event segments linked to the given event
10034
	hideEvent: function(event) {
10035
		this.renderedEventSegEach(function(seg) {
10036
			seg.el.css('visibility', 'hidden');
10037
		}, event);
10038
	},
10039

  
10040

  
10041
	// Iterates through event segments that have been rendered (have an el). Goes through all by default.
10042
	// If the optional `event` argument is specified, only iterates through segments linked to that event.
10043
	// The `this` value of the callback function will be the view.
10044
	renderedEventSegEach: function(func, event) {
10045
		var segs = this.getEventSegs();
10046
		var i;
10047

  
10048
		for (i = 0; i < segs.length; i++) {
10049
			if (!event || segs[i].event._id === event._id) {
10050
				if (segs[i].el) {
10051
					func.call(this, segs[i]);
10052
				}
10053
			}
10054
		}
10055
	},
10056

  
10057

  
10058
	// Retrieves all the rendered segment objects for the view
10059
	getEventSegs: function() {
10060
		// subclasses must implement
10061
		return [];
10062
	},
10063

  
10064

  
10065
	/* Event Drag-n-Drop
10066
	------------------------------------------------------------------------------------------------------------------*/
10067

  
10068

  
10069
	// Computes if the given event is allowed to be dragged by the user
10070
	isEventDraggable: function(event) {
10071
		return this.isEventStartEditable(event);
10072
	},
10073

  
10074

  
10075
	isEventStartEditable: function(event) {
10076
		return firstDefined(
10077
			event.startEditable,
... Ce différentiel a été tronqué car il excède la taille maximale pouvant être affichée.