3
|
1 /*!
|
|
2 * FullCalendar v1.6.4-rcube-1.1.3
|
|
3 * Docs & License: http://arshaw.com/fullcalendar/
|
|
4 * (c) 2013 Adam Shaw, 2014 Kolab Systems AG
|
|
5 */
|
|
6
|
|
7 /*
|
|
8 * Use fullcalendar.css for basic styling.
|
|
9 * For event drag & drop, requires jQuery UI draggable.
|
|
10 * For event resizing, requires jQuery UI resizable.
|
|
11 */
|
|
12
|
|
13 (function($, undefined) {
|
|
14
|
|
15
|
|
16 ;;
|
|
17
|
|
18 var defaults = {
|
|
19
|
|
20 // display
|
|
21 defaultView: 'month',
|
|
22 aspectRatio: 1.35,
|
|
23 header: {
|
|
24 left: 'title',
|
|
25 center: '',
|
|
26 right: 'today prev,next'
|
|
27 },
|
|
28 weekends: true,
|
|
29 weekNumbers: false,
|
|
30 weekNumberCalculation: 'iso',
|
|
31 weekNumberTitle: 'W',
|
|
32 currentTimeIndicator: false,
|
|
33
|
|
34 // editing
|
|
35 //editable: false,
|
|
36 //disableDragging: false,
|
|
37 //disableResizing: false,
|
|
38
|
|
39 allDayDefault: true,
|
|
40 ignoreTimezone: true,
|
|
41
|
|
42 // event ajax
|
|
43 lazyFetching: true,
|
|
44 startParam: 'start',
|
|
45 endParam: 'end',
|
|
46
|
|
47 // time formats
|
|
48 titleFormat: {
|
|
49 month: 'MMMM yyyy',
|
|
50 week: "MMM d[ yyyy]{ '—'[ MMM] d yyyy}",
|
|
51 day: 'dddd, MMM d, yyyy',
|
|
52 list: 'MMM d, yyyy',
|
|
53 table: 'MMM d, yyyy'
|
|
54 },
|
|
55 columnFormat: {
|
|
56 month: 'ddd',
|
|
57 week: 'ddd M/d',
|
|
58 day: 'dddd M/d',
|
|
59 list: 'dddd, MMM d, yyyy',
|
|
60 table: 'MMM d, yyyy'
|
|
61 },
|
|
62 timeFormat: { // for event elements
|
|
63 '': 'h(:mm)t' // default
|
|
64 },
|
|
65
|
|
66 // locale
|
|
67 isRTL: false,
|
|
68 firstDay: 0,
|
|
69 monthNames: ['January','February','March','April','May','June','July','August','September','October','November','December'],
|
|
70 monthNamesShort: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'],
|
|
71 dayNames: ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'],
|
|
72 dayNamesShort: ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'],
|
|
73 buttonText: {
|
|
74 prev: "<span class='fc-text-arrow'>‹</span>",
|
|
75 next: "<span class='fc-text-arrow'>›</span>",
|
|
76 prevYear: "<span class='fc-text-arrow'>«</span>",
|
|
77 nextYear: "<span class='fc-text-arrow'>»</span>",
|
|
78 today: 'today',
|
|
79 month: 'month',
|
|
80 week: 'week',
|
|
81 day: 'day',
|
|
82 list: 'list',
|
|
83 table: 'table'
|
|
84 },
|
|
85 listTexts: {
|
|
86 until: 'until',
|
|
87 past: 'Past events',
|
|
88 today: 'Today',
|
|
89 tomorrow: 'Tomorrow',
|
|
90 thisWeek: 'This week',
|
|
91 nextWeek: 'Next week',
|
|
92 thisMonth: 'This month',
|
|
93 nextMonth: 'Next month',
|
|
94 future: 'Future events',
|
|
95 week: 'W'
|
|
96 },
|
|
97
|
|
98 // list/table options
|
|
99 listSections: 'month', // false|'day'|'week'|'month'|'smart'
|
|
100 listRange: 30, // number of days to be displayed
|
|
101 listPage: 7, // number of days to jump when paging
|
|
102 tableCols: ['handle', 'date', 'time', 'title'],
|
|
103
|
|
104 // jquery-ui theming
|
|
105 theme: false,
|
|
106 buttonIcons: {
|
|
107 prev: 'circle-triangle-w',
|
|
108 next: 'circle-triangle-e'
|
|
109 },
|
|
110
|
|
111 //selectable: false,
|
|
112 unselectAuto: true,
|
|
113
|
|
114 dropAccept: '*',
|
|
115
|
|
116 handleWindowResize: true
|
|
117
|
|
118 };
|
|
119
|
|
120 // right-to-left defaults
|
|
121 var rtlDefaults = {
|
|
122 header: {
|
|
123 left: 'next,prev today',
|
|
124 center: '',
|
|
125 right: 'title'
|
|
126 },
|
|
127 buttonText: {
|
|
128 prev: "<span class='fc-text-arrow'>›</span>",
|
|
129 next: "<span class='fc-text-arrow'>‹</span>",
|
|
130 prevYear: "<span class='fc-text-arrow'>»</span>",
|
|
131 nextYear: "<span class='fc-text-arrow'>«</span>"
|
|
132 },
|
|
133 buttonIcons: {
|
|
134 prev: 'circle-triangle-e',
|
|
135 next: 'circle-triangle-w'
|
|
136 }
|
|
137 };
|
|
138
|
|
139
|
|
140
|
|
141 ;;
|
|
142
|
|
143 var fc = $.fullCalendar = { version: "1.6.4-rcube-1.1.3" };
|
|
144 var fcViews = fc.views = {};
|
|
145
|
|
146
|
|
147 $.fn.fullCalendar = function(options) {
|
|
148
|
|
149
|
|
150 // method calling
|
|
151 if (typeof options == 'string') {
|
|
152 var args = Array.prototype.slice.call(arguments, 1);
|
|
153 var res;
|
|
154 this.each(function() {
|
|
155 var calendar = $.data(this, 'fullCalendar');
|
|
156 if (calendar && $.isFunction(calendar[options])) {
|
|
157 var r = calendar[options].apply(calendar, args);
|
|
158 if (res === undefined) {
|
|
159 res = r;
|
|
160 }
|
|
161 if (options == 'destroy') {
|
|
162 $.removeData(this, 'fullCalendar');
|
|
163 }
|
|
164 }
|
|
165 });
|
|
166 if (res !== undefined) {
|
|
167 return res;
|
|
168 }
|
|
169 return this;
|
|
170 }
|
|
171
|
|
172 options = options || {};
|
|
173
|
|
174 // would like to have this logic in EventManager, but needs to happen before options are recursively extended
|
|
175 var eventSources = options.eventSources || [];
|
|
176 delete options.eventSources;
|
|
177 if (options.events) {
|
|
178 eventSources.push(options.events);
|
|
179 delete options.events;
|
|
180 }
|
|
181
|
|
182
|
|
183 options = $.extend(true, {},
|
|
184 defaults,
|
|
185 (options.isRTL || options.isRTL===undefined && defaults.isRTL) ? rtlDefaults : {},
|
|
186 options
|
|
187 );
|
|
188
|
|
189
|
|
190 this.each(function(i, _element) {
|
|
191 var element = $(_element);
|
|
192 var calendar = new Calendar(element, options, eventSources);
|
|
193 element.data('fullCalendar', calendar); // TODO: look into memory leak implications
|
|
194 calendar.render();
|
|
195 });
|
|
196
|
|
197
|
|
198 return this;
|
|
199
|
|
200 };
|
|
201
|
|
202
|
|
203 // function for adding/overriding defaults
|
|
204 function setDefaults(d) {
|
|
205 $.extend(true, defaults, d);
|
|
206 }
|
|
207
|
|
208
|
|
209
|
|
210 ;;
|
|
211
|
|
212
|
|
213 function Calendar(element, options, eventSources) {
|
|
214 var t = this;
|
|
215
|
|
216
|
|
217 // exports
|
|
218 t.options = options;
|
|
219 t.render = render;
|
|
220 t.destroy = destroy;
|
|
221 t.refetchEvents = refetchEvents;
|
|
222 t.reportEvents = reportEvents;
|
|
223 t.reportEventChange = reportEventChange;
|
|
224 t.rerenderEvents = rerenderEvents;
|
|
225 t.changeView = changeView;
|
|
226 t.select = select;
|
|
227 t.unselect = unselect;
|
|
228 t.prev = prev;
|
|
229 t.next = next;
|
|
230 t.prevYear = prevYear;
|
|
231 t.nextYear = nextYear;
|
|
232 t.today = today;
|
|
233 t.gotoDate = gotoDate;
|
|
234 t.incrementDate = incrementDate;
|
|
235 t.formatDate = function(format, date) { return formatDate(format, date, options) };
|
|
236 t.formatDates = function(format, date1, date2) { return formatDates(format, date1, date2, options) };
|
|
237 t.getDate = getDate;
|
|
238 t.getView = getView;
|
|
239 t.option = option;
|
|
240 t.trigger = trigger;
|
|
241
|
|
242
|
|
243 // imports
|
|
244 EventManager.call(t, options, eventSources);
|
|
245 var isFetchNeeded = t.isFetchNeeded;
|
|
246 var fetchEvents = t.fetchEvents;
|
|
247
|
|
248
|
|
249 // locals
|
|
250 var _element = element[0];
|
|
251 var header;
|
|
252 var headerElement;
|
|
253 var content;
|
|
254 var tm; // for making theme classes
|
|
255 var currentView;
|
|
256 var elementOuterWidth;
|
|
257 var suggestedViewHeight;
|
|
258 var resizeUID = 0;
|
|
259 var ignoreWindowResize = 0;
|
|
260 var lazyRendering = false;
|
|
261 var date = new Date();
|
|
262 var events = [];
|
|
263 var _dragElement;
|
|
264
|
|
265
|
|
266
|
|
267 /* Main Rendering
|
|
268 -----------------------------------------------------------------------------*/
|
|
269
|
|
270
|
|
271 setYMD(date, options.year, options.month, options.date);
|
|
272
|
|
273
|
|
274 function render(inc) {
|
|
275 if (!content) {
|
|
276 initialRender();
|
|
277 }
|
|
278 else if (elementVisible()) {
|
|
279 // mainly for the public API
|
|
280 calcSize();
|
|
281 _renderView(inc);
|
|
282 }
|
|
283 }
|
|
284
|
|
285
|
|
286 function initialRender() {
|
|
287 tm = options.theme ? 'ui' : 'fc';
|
|
288 element.addClass('fc');
|
|
289 if (options.isRTL) {
|
|
290 element.addClass('fc-rtl');
|
|
291 }
|
|
292 else {
|
|
293 element.addClass('fc-ltr');
|
|
294 }
|
|
295 if (options.theme) {
|
|
296 element.addClass('ui-widget');
|
|
297 }
|
|
298
|
|
299 content = $("<div class='fc-content' style='position:relative'/>")
|
|
300 .prependTo(element);
|
|
301
|
|
302 header = new Header(t, options);
|
|
303 headerElement = header.render();
|
|
304 if (headerElement) {
|
|
305 element.prepend(headerElement);
|
|
306 }
|
|
307
|
|
308 changeView(options.defaultView);
|
|
309
|
|
310 if (options.handleWindowResize) {
|
|
311 $(window).resize(windowResize);
|
|
312 }
|
|
313
|
|
314 // needed for IE in a 0x0 iframe, b/c when it is resized, never triggers a windowResize
|
|
315 if (!bodyVisible()) {
|
|
316 lateRender();
|
|
317 }
|
|
318 }
|
|
319
|
|
320
|
|
321 // called when we know the calendar couldn't be rendered when it was initialized,
|
|
322 // but we think it's ready now
|
|
323 function lateRender() {
|
|
324 setTimeout(function() { // IE7 needs this so dimensions are calculated correctly
|
|
325 if (!currentView.start && bodyVisible()) { // !currentView.start makes sure this never happens more than once
|
|
326 renderView();
|
|
327 }
|
|
328 },0);
|
|
329 }
|
|
330
|
|
331
|
|
332 function destroy() {
|
|
333
|
|
334 if (currentView) {
|
|
335 trigger('viewDestroy', currentView, currentView, currentView.element);
|
|
336 currentView.triggerEventDestroy();
|
|
337 }
|
|
338
|
|
339 $(window).unbind('resize', windowResize);
|
|
340
|
|
341 header.destroy();
|
|
342 content.remove();
|
|
343 element.removeClass('fc fc-rtl ui-widget');
|
|
344 }
|
|
345
|
|
346
|
|
347 function elementVisible() {
|
|
348 return element.is(':visible');
|
|
349 }
|
|
350
|
|
351
|
|
352 function bodyVisible() {
|
|
353 return $('body').is(':visible');
|
|
354 }
|
|
355
|
|
356
|
|
357
|
|
358 /* View Rendering
|
|
359 -----------------------------------------------------------------------------*/
|
|
360
|
|
361
|
|
362 function changeView(newViewName) {
|
|
363 if (!currentView || newViewName != currentView.name) {
|
|
364 _changeView(newViewName);
|
|
365 }
|
|
366 }
|
|
367
|
|
368
|
|
369 function _changeView(newViewName) {
|
|
370 ignoreWindowResize++;
|
|
371
|
|
372 if (currentView) {
|
|
373 trigger('viewDestroy', currentView, currentView, currentView.element);
|
|
374 unselect();
|
|
375 currentView.triggerEventDestroy(); // trigger 'eventDestroy' for each event
|
|
376 freezeContentHeight();
|
|
377 currentView.element.remove();
|
|
378 header.deactivateButton(currentView.name);
|
|
379 }
|
|
380
|
|
381 header.activateButton(newViewName);
|
|
382
|
|
383 currentView = new fcViews[newViewName](
|
|
384 $("<div class='fc-view fc-view-" + newViewName + "' style='position:relative'/>")
|
|
385 .appendTo(content),
|
|
386 t // the calendar object
|
|
387 );
|
|
388
|
|
389 renderView();
|
|
390 unfreezeContentHeight();
|
|
391
|
|
392 ignoreWindowResize--;
|
|
393 }
|
|
394
|
|
395
|
|
396 function renderView(inc) {
|
|
397 if (
|
|
398 !currentView.start || // never rendered before
|
|
399 inc || date < currentView.start || date >= currentView.end // or new date range
|
|
400 ) {
|
|
401 if (elementVisible()) {
|
|
402 _renderView(inc);
|
|
403 }
|
|
404 }
|
|
405 }
|
|
406
|
|
407
|
|
408 function _renderView(inc) { // assumes elementVisible
|
|
409 ignoreWindowResize++;
|
|
410
|
|
411 if (currentView.start) { // already been rendered?
|
|
412 trigger('viewDestroy', currentView, currentView, currentView.element);
|
|
413 unselect();
|
|
414 clearEvents();
|
|
415 }
|
|
416
|
|
417 freezeContentHeight();
|
|
418 currentView.render(date, inc || 0); // the view's render method ONLY renders the skeleton, nothing else
|
|
419 setSize();
|
|
420 unfreezeContentHeight();
|
|
421 (currentView.afterRender || noop)();
|
|
422
|
|
423 updateTitle();
|
|
424 updateTodayButton();
|
|
425
|
|
426 trigger('viewRender', currentView, currentView, currentView.element);
|
|
427 currentView.trigger('viewDisplay', _element); // deprecated
|
|
428
|
|
429 ignoreWindowResize--;
|
|
430
|
|
431 getAndRenderEvents();
|
|
432 }
|
|
433
|
|
434
|
|
435
|
|
436 /* Resizing
|
|
437 -----------------------------------------------------------------------------*/
|
|
438
|
|
439
|
|
440 function updateSize() {
|
|
441 if (elementVisible()) {
|
|
442 unselect();
|
|
443 clearEvents();
|
|
444 calcSize();
|
|
445 setSize();
|
|
446 unselect();
|
|
447 currentView.clearEvents();
|
|
448 currentView.trigger('viewRender', currentView);
|
|
449 currentView.renderEvents(events);
|
|
450 currentView.sizeDirty = false;
|
|
451 }
|
|
452 }
|
|
453
|
|
454
|
|
455 function calcSize() { // assumes elementVisible
|
|
456 if (options.contentHeight) {
|
|
457 suggestedViewHeight = options.contentHeight;
|
|
458 }
|
|
459 else if (options.height) {
|
|
460 suggestedViewHeight = options.height - (headerElement ? headerElement.height() : 0) - vsides(content);
|
|
461 }
|
|
462 else {
|
|
463 suggestedViewHeight = Math.round(content.width() / Math.max(options.aspectRatio, .5));
|
|
464 }
|
|
465 }
|
|
466
|
|
467
|
|
468 function setSize() { // assumes elementVisible
|
|
469
|
|
470 if (suggestedViewHeight === undefined) {
|
|
471 calcSize(); // for first time
|
|
472 // NOTE: we don't want to recalculate on every renderView because
|
|
473 // it could result in oscillating heights due to scrollbars.
|
|
474 }
|
|
475
|
|
476 ignoreWindowResize++;
|
|
477 currentView.setHeight(suggestedViewHeight);
|
|
478 currentView.setWidth(content.width());
|
|
479 ignoreWindowResize--;
|
|
480
|
|
481 elementOuterWidth = element.outerWidth();
|
|
482 }
|
|
483
|
|
484
|
|
485 function windowResize() {
|
|
486 if (!ignoreWindowResize) {
|
|
487 if (currentView.start) { // view has already been rendered
|
|
488 var uid = ++resizeUID;
|
|
489 setTimeout(function() { // add a delay
|
|
490 if (uid == resizeUID && !ignoreWindowResize && elementVisible()) {
|
|
491 if (elementOuterWidth != (elementOuterWidth = element.outerWidth())) {
|
|
492 ignoreWindowResize++; // in case the windowResize callback changes the height
|
|
493 updateSize();
|
|
494 currentView.trigger('windowResize', _element);
|
|
495 ignoreWindowResize--;
|
|
496 }
|
|
497 }
|
|
498 }, 200);
|
|
499 }else{
|
|
500 // calendar must have been initialized in a 0x0 iframe that has just been resized
|
|
501 lateRender();
|
|
502 }
|
|
503 }
|
|
504 }
|
|
505
|
|
506
|
|
507
|
|
508 /* Event Fetching/Rendering
|
|
509 -----------------------------------------------------------------------------*/
|
|
510 // TODO: going forward, most of this stuff should be directly handled by the view
|
|
511
|
|
512
|
|
513 function refetchEvents(source, lazy) { // can be called as an API method
|
|
514 lazyRendering = lazy || false;
|
|
515 if (!lazyRendering) {
|
|
516 clearEvents();
|
|
517 }
|
|
518 fetchAndRenderEvents(source);
|
|
519 }
|
|
520
|
|
521
|
|
522 function rerenderEvents(modifiedEventID) { // can be called as an API method
|
|
523 clearEvents();
|
|
524 renderEvents(modifiedEventID);
|
|
525 }
|
|
526
|
|
527
|
|
528 function renderEvents(modifiedEventID) { // TODO: remove modifiedEventID hack
|
|
529 if (elementVisible()) {
|
|
530 currentView.setEventData(events); // for View.js, TODO: unify with renderEvents
|
|
531 currentView.renderEvents(events, modifiedEventID); // actually render the DOM elements
|
|
532 currentView.trigger('eventAfterAllRender');
|
|
533 }
|
|
534 }
|
|
535
|
|
536
|
|
537 function clearEvents() {
|
|
538 currentView.triggerEventDestroy(); // trigger 'eventDestroy' for each event
|
|
539 currentView.clearEvents(); // actually remove the DOM elements
|
|
540 currentView.clearEventData(); // for View.js, TODO: unify with clearEvents
|
|
541 }
|
|
542
|
|
543
|
|
544 function getAndRenderEvents() {
|
|
545 if (!options.lazyFetching || isFetchNeeded(currentView.visStart, currentView.visEnd)) {
|
|
546 fetchAndRenderEvents();
|
|
547 }
|
|
548 else {
|
|
549 renderEvents();
|
|
550 }
|
|
551 }
|
|
552
|
|
553
|
|
554 function fetchAndRenderEvents(source) {
|
|
555 fetchEvents(currentView.visStart, currentView.visEnd, source);
|
|
556 // ... will call reportEvents
|
|
557 // ... which will call renderEvents
|
|
558 }
|
|
559
|
|
560
|
|
561 // called when event data arrives
|
|
562 function reportEvents(_events) {
|
|
563 if (lazyRendering) {
|
|
564 clearEvents();
|
|
565 lazyRendering = false;
|
|
566 }
|
|
567 events = _events;
|
|
568 renderEvents();
|
|
569 }
|
|
570
|
|
571
|
|
572 // called when a single event's data has been changed
|
|
573 function reportEventChange(eventID) {
|
|
574 rerenderEvents(eventID);
|
|
575 }
|
|
576
|
|
577
|
|
578
|
|
579 /* Header Updating
|
|
580 -----------------------------------------------------------------------------*/
|
|
581
|
|
582
|
|
583 function updateTitle() {
|
|
584 header.updateTitle(currentView.title);
|
|
585 }
|
|
586
|
|
587
|
|
588 function updateTodayButton() {
|
|
589 var today = new Date();
|
|
590 if (today >= currentView.start && today < currentView.end) {
|
|
591 header.disableButton('today');
|
|
592 }
|
|
593 else {
|
|
594 header.enableButton('today');
|
|
595 }
|
|
596 }
|
|
597
|
|
598
|
|
599
|
|
600 /* Selection
|
|
601 -----------------------------------------------------------------------------*/
|
|
602
|
|
603
|
|
604 function select(start, end, allDay) {
|
|
605 currentView.select(start, end, allDay===undefined ? true : allDay);
|
|
606 }
|
|
607
|
|
608
|
|
609 function unselect() { // safe to be called before renderView
|
|
610 if (currentView) {
|
|
611 currentView.unselect();
|
|
612 }
|
|
613 }
|
|
614
|
|
615
|
|
616
|
|
617 /* Date
|
|
618 -----------------------------------------------------------------------------*/
|
|
619
|
|
620
|
|
621 function prev() {
|
|
622 renderView(-1);
|
|
623 }
|
|
624
|
|
625
|
|
626 function next() {
|
|
627 renderView(1);
|
|
628 }
|
|
629
|
|
630
|
|
631 function prevYear() {
|
|
632 addYears(date, -1);
|
|
633 renderView();
|
|
634 }
|
|
635
|
|
636
|
|
637 function nextYear() {
|
|
638 addYears(date, 1);
|
|
639 renderView();
|
|
640 }
|
|
641
|
|
642
|
|
643 function today() {
|
|
644 date = new Date();
|
|
645 renderView();
|
|
646 }
|
|
647
|
|
648
|
|
649 function gotoDate(year, month, dateOfMonth) {
|
|
650 if (year instanceof Date) {
|
|
651 date = cloneDate(year); // provided 1 argument, a Date
|
|
652 }else{
|
|
653 setYMD(date, year, month, dateOfMonth);
|
|
654 }
|
|
655 renderView();
|
|
656 }
|
|
657
|
|
658
|
|
659 function incrementDate(years, months, days) {
|
|
660 if (years !== undefined) {
|
|
661 addYears(date, years);
|
|
662 }
|
|
663 if (months !== undefined) {
|
|
664 addMonths(date, months);
|
|
665 }
|
|
666 if (days !== undefined) {
|
|
667 addDays(date, days);
|
|
668 }
|
|
669 renderView();
|
|
670 }
|
|
671
|
|
672
|
|
673 function getDate() {
|
|
674 return cloneDate(date);
|
|
675 }
|
|
676
|
|
677
|
|
678
|
|
679 /* Height "Freezing"
|
|
680 -----------------------------------------------------------------------------*/
|
|
681
|
|
682
|
|
683 function freezeContentHeight() {
|
|
684 content.css({
|
|
685 width: '100%',
|
|
686 height: content.height(),
|
|
687 overflow: 'hidden'
|
|
688 });
|
|
689 }
|
|
690
|
|
691
|
|
692 function unfreezeContentHeight() {
|
|
693 content.css({
|
|
694 width: '',
|
|
695 height: '',
|
|
696 overflow: ''
|
|
697 });
|
|
698 }
|
|
699
|
|
700
|
|
701
|
|
702 /* Misc
|
|
703 -----------------------------------------------------------------------------*/
|
|
704
|
|
705
|
|
706 function getView() {
|
|
707 return currentView;
|
|
708 }
|
|
709
|
|
710
|
|
711 function option(name, value) {
|
|
712 if (value === undefined) {
|
|
713 return options[name];
|
|
714 }
|
|
715 if (name == 'height' || name == 'contentHeight' || name == 'aspectRatio') {
|
|
716 options[name] = value;
|
|
717 updateSize();
|
|
718 } else if (name.indexOf('list') == 0 || name == 'tableCols') {
|
|
719 options[name] = value;
|
|
720 currentView.start = null; // force re-render
|
|
721 } else if (name == 'maxHeight') {
|
|
722 options[name] = value;
|
|
723 }
|
|
724 }
|
|
725
|
|
726
|
|
727 function trigger(name, thisObj) {
|
|
728 if (options[name]) {
|
|
729 return options[name].apply(
|
|
730 thisObj || _element,
|
|
731 Array.prototype.slice.call(arguments, 2)
|
|
732 );
|
|
733 }
|
|
734 }
|
|
735
|
|
736
|
|
737
|
|
738 /* External Dragging
|
|
739 ------------------------------------------------------------------------*/
|
|
740
|
|
741 if (options.droppable) {
|
|
742 $(document)
|
|
743 .bind('dragstart', function(ev, ui) {
|
|
744 var _e = ev.target;
|
|
745 var e = $(_e);
|
|
746 if (!e.parents('.fc').length) { // not already inside a calendar
|
|
747 var accept = options.dropAccept;
|
|
748 if ($.isFunction(accept) ? accept.call(_e, e) : e.is(accept)) {
|
|
749 _dragElement = _e;
|
|
750 currentView.dragStart(_dragElement, ev, ui);
|
|
751 }
|
|
752 }
|
|
753 })
|
|
754 .bind('dragstop', function(ev, ui) {
|
|
755 if (_dragElement) {
|
|
756 currentView.dragStop(_dragElement, ev, ui);
|
|
757 _dragElement = null;
|
|
758 }
|
|
759 });
|
|
760 }
|
|
761
|
|
762
|
|
763 }
|
|
764
|
|
765 ;;
|
|
766
|
|
767 function Header(calendar, options) {
|
|
768 var t = this;
|
|
769
|
|
770
|
|
771 // exports
|
|
772 t.render = render;
|
|
773 t.destroy = destroy;
|
|
774 t.updateTitle = updateTitle;
|
|
775 t.activateButton = activateButton;
|
|
776 t.deactivateButton = deactivateButton;
|
|
777 t.disableButton = disableButton;
|
|
778 t.enableButton = enableButton;
|
|
779
|
|
780
|
|
781 // locals
|
|
782 var element = $([]);
|
|
783 var tm;
|
|
784
|
|
785
|
|
786
|
|
787 function render() {
|
|
788 tm = options.theme ? 'ui' : 'fc';
|
|
789 var sections = options.header;
|
|
790 if (sections) {
|
|
791 element = $("<table class='fc-header' style='width:100%'/>")
|
|
792 .append(
|
|
793 $("<tr/>")
|
|
794 .append(renderSection('left'))
|
|
795 .append(renderSection('center'))
|
|
796 .append(renderSection('right'))
|
|
797 );
|
|
798 return element;
|
|
799 }
|
|
800 }
|
|
801
|
|
802
|
|
803 function destroy() {
|
|
804 element.remove();
|
|
805 }
|
|
806
|
|
807
|
|
808 function renderSection(position) {
|
|
809 var e = $("<td class='fc-header-" + position + "'/>");
|
|
810 var buttonStr = options.header[position];
|
|
811 if (buttonStr) {
|
|
812 $.each(buttonStr.split(' '), function(i) {
|
|
813 if (i > 0) {
|
|
814 e.append("<span class='fc-header-space'/>");
|
|
815 }
|
|
816 var prevButton;
|
|
817 $.each(this.split(','), function(j, buttonName) {
|
|
818 if (buttonName == 'title') {
|
|
819 e.append("<span class='fc-header-title'><h2 aria-live='polite' aria-relevant='text' aria-atomic='true'> </h2></span>");
|
|
820 if (prevButton) {
|
|
821 prevButton.addClass(tm + '-corner-right');
|
|
822 }
|
|
823 prevButton = null;
|
|
824 }else{
|
|
825 var buttonClick;
|
|
826 if (calendar[buttonName]) {
|
|
827 buttonClick = calendar[buttonName]; // calendar method
|
|
828 }
|
|
829 else if (fcViews[buttonName]) {
|
|
830 buttonClick = function() {
|
|
831 button.removeClass(tm + '-state-hover'); // forget why
|
|
832 calendar.changeView(buttonName);
|
|
833 };
|
|
834 }
|
|
835 if (buttonClick) {
|
|
836 var icon = options.theme ? smartProperty(options.buttonIcons, buttonName) : null; // why are we using smartProperty here?
|
|
837 var text = smartProperty(options.buttonText, buttonName); // why are we using smartProperty here?
|
|
838 var button = $(
|
|
839 "<span class='fc-button fc-button-" + buttonName + " " + tm + "-state-default' role='button' tabindex='0'>" +
|
|
840 (icon ?
|
|
841 "<span class='fc-icon-wrap'>" +
|
|
842 "<span class='ui-icon ui-icon-" + icon + "'/>" +
|
|
843 "</span>" :
|
|
844 text
|
|
845 ) +
|
|
846 "</span>"
|
|
847 )
|
|
848 .click(function() {
|
|
849 if (!button.hasClass(tm + '-state-disabled')) {
|
|
850 buttonClick();
|
|
851 }
|
|
852 })
|
|
853 .mousedown(function() {
|
|
854 button
|
|
855 .not('.' + tm + '-state-active')
|
|
856 .not('.' + tm + '-state-disabled')
|
|
857 .addClass(tm + '-state-down');
|
|
858 })
|
|
859 .mouseup(function() {
|
|
860 button.removeClass(tm + '-state-down');
|
|
861 })
|
|
862 .hover(
|
|
863 function() {
|
|
864 button
|
|
865 .not('.' + tm + '-state-active')
|
|
866 .not('.' + tm + '-state-disabled')
|
|
867 .addClass(tm + '-state-hover');
|
|
868 },
|
|
869 function() {
|
|
870 button
|
|
871 .removeClass(tm + '-state-hover')
|
|
872 .removeClass(tm + '-state-down');
|
|
873 }
|
|
874 )
|
|
875 .keypress(function(ev) {
|
|
876 if (ev.keyCode == 13)
|
|
877 $(ev.target).trigger('click');
|
|
878 })
|
|
879 .appendTo(e);
|
|
880 disableTextSelection(button);
|
|
881 if (!prevButton) {
|
|
882 button.addClass(tm + '-corner-left');
|
|
883 }
|
|
884 prevButton = button;
|
|
885 }
|
|
886 }
|
|
887 });
|
|
888 if (prevButton) {
|
|
889 prevButton.addClass(tm + '-corner-right');
|
|
890 }
|
|
891 });
|
|
892 }
|
|
893 return e;
|
|
894 }
|
|
895
|
|
896
|
|
897 function updateTitle(html) {
|
|
898 element.find('h2')
|
|
899 .html(html);
|
|
900 }
|
|
901
|
|
902
|
|
903 function activateButton(buttonName) {
|
|
904 element.find('span.fc-button-' + buttonName)
|
|
905 .addClass(tm + '-state-active').attr('tabindex', '-1');
|
|
906 }
|
|
907
|
|
908
|
|
909 function deactivateButton(buttonName) {
|
|
910 element.find('span.fc-button-' + buttonName)
|
|
911 .removeClass(tm + '-state-active').attr('tabindex', '0');
|
|
912 }
|
|
913
|
|
914
|
|
915 function disableButton(buttonName) {
|
|
916 element.find('span.fc-button-' + buttonName)
|
|
917 .addClass(tm + '-state-disabled').attr('tabindex', '-1');
|
|
918 }
|
|
919
|
|
920
|
|
921 function enableButton(buttonName) {
|
|
922 element.find('span.fc-button-' + buttonName)
|
|
923 .removeClass(tm + '-state-disabled').attr('tabindex', '0');
|
|
924 }
|
|
925
|
|
926
|
|
927 }
|
|
928
|
|
929 ;;
|
|
930
|
|
931 fc.sourceNormalizers = [];
|
|
932 fc.sourceFetchers = [];
|
|
933
|
|
934 var ajaxDefaults = {
|
|
935 dataType: 'json',
|
|
936 cache: false
|
|
937 };
|
|
938
|
|
939 var eventGUID = 1;
|
|
940
|
|
941
|
|
942 function EventManager(options, _sources) {
|
|
943 var t = this;
|
|
944
|
|
945
|
|
946 // exports
|
|
947 t.isFetchNeeded = isFetchNeeded;
|
|
948 t.fetchEvents = fetchEvents;
|
|
949 t.addEventSource = addEventSource;
|
|
950 t.removeEventSource = removeEventSource;
|
|
951 t.removeEventSources = removeEventSources;
|
|
952 t.updateEvent = updateEvent;
|
|
953 t.renderEvent = renderEvent;
|
|
954 t.removeEvents = removeEvents;
|
|
955 t.clientEvents = clientEvents;
|
|
956 t.normalizeEvent = normalizeEvent;
|
|
957
|
|
958
|
|
959 // imports
|
|
960 var trigger = t.trigger;
|
|
961 var getView = t.getView;
|
|
962 var reportEvents = t.reportEvents;
|
|
963
|
|
964
|
|
965 // locals
|
|
966 var stickySource = { events: [] };
|
|
967 var sources = [ stickySource ];
|
|
968 var rangeStart, rangeEnd;
|
|
969 var currentFetchID = 0;
|
|
970 var pendingSourceCnt = 0;
|
|
971 var loadingLevel = 0;
|
|
972 var cache = [];
|
|
973
|
|
974
|
|
975 for (var i=0; i<_sources.length; i++) {
|
|
976 _addEventSource(_sources[i]);
|
|
977 }
|
|
978
|
|
979
|
|
980
|
|
981 /* Fetching
|
|
982 -----------------------------------------------------------------------------*/
|
|
983
|
|
984
|
|
985 function isFetchNeeded(start, end) {
|
|
986 return !rangeStart || start < rangeStart || end > rangeEnd;
|
|
987 }
|
|
988
|
|
989
|
|
990 function fetchEvents(start, end, src) {
|
|
991 rangeStart = start;
|
|
992 rangeEnd = end;
|
|
993 // partially clear cache if refreshing one source only (issue #1061)
|
|
994 cache = typeof src != 'undefined' ? $.grep(cache, function(e) { return !isSourcesEqual(e.source, src); }) : [];
|
|
995 var fetchID = ++currentFetchID;
|
|
996 var len = sources.length;
|
|
997 pendingSourceCnt = typeof src == 'undefined' ? len : 1;
|
|
998 for (var i=0; i<len; i++) {
|
|
999 if (typeof src == 'undefined' || isSourcesEqual(sources[i], src))
|
|
1000 fetchEventSource(sources[i], fetchID);
|
|
1001 }
|
|
1002 }
|
|
1003
|
|
1004
|
|
1005
|
|
1006 function fetchEventSource(source, fetchID) {
|
|
1007 _fetchEventSource(source, function(events) {
|
|
1008 if (fetchID == currentFetchID) {
|
|
1009 if (events) {
|
|
1010
|
|
1011 if (options.eventDataTransform) {
|
|
1012 events = $.map(events, options.eventDataTransform);
|
|
1013 }
|
|
1014 if (source.eventDataTransform) {
|
|
1015 events = $.map(events, source.eventDataTransform);
|
|
1016 }
|
|
1017 // TODO: this technique is not ideal for static array event sources.
|
|
1018 // For arrays, we'll want to process all events right in the beginning, then never again.
|
|
1019
|
|
1020 for (var i=0; i<events.length; i++) {
|
|
1021 events[i].source = source;
|
|
1022 normalizeEvent(events[i]);
|
|
1023 }
|
|
1024 cache = cache.concat(events);
|
|
1025 }
|
|
1026 pendingSourceCnt--;
|
|
1027 if (!pendingSourceCnt) {
|
|
1028 reportEvents(cache);
|
|
1029 }
|
|
1030 }
|
|
1031 });
|
|
1032 }
|
|
1033
|
|
1034
|
|
1035 function _fetchEventSource(source, callback) {
|
|
1036 var i;
|
|
1037 var fetchers = fc.sourceFetchers;
|
|
1038 var res;
|
|
1039 for (i=0; i<fetchers.length; i++) {
|
|
1040 res = fetchers[i](source, rangeStart, rangeEnd, callback);
|
|
1041 if (res === true) {
|
|
1042 // the fetcher is in charge. made its own async request
|
|
1043 return;
|
|
1044 }
|
|
1045 else if (typeof res == 'object') {
|
|
1046 // the fetcher returned a new source. process it
|
|
1047 _fetchEventSource(res, callback);
|
|
1048 return;
|
|
1049 }
|
|
1050 }
|
|
1051 var events = source.events;
|
|
1052 if (events) {
|
|
1053 if ($.isFunction(events)) {
|
|
1054 pushLoading();
|
|
1055 events(cloneDate(rangeStart), cloneDate(rangeEnd), function(events) {
|
|
1056 callback(events);
|
|
1057 popLoading();
|
|
1058 });
|
|
1059 }
|
|
1060 else if ($.isArray(events)) {
|
|
1061 callback(events);
|
|
1062 }
|
|
1063 else {
|
|
1064 callback();
|
|
1065 }
|
|
1066 }else{
|
|
1067 var url = source.url;
|
|
1068 if (url) {
|
|
1069 var success = source.success;
|
|
1070 var error = source.error;
|
|
1071 var complete = source.complete;
|
|
1072
|
|
1073 // retrieve any outbound GET/POST $.ajax data from the options
|
|
1074 var customData;
|
|
1075 if ($.isFunction(source.data)) {
|
|
1076 // supplied as a function that returns a key/value object
|
|
1077 customData = source.data();
|
|
1078 }
|
|
1079 else {
|
|
1080 // supplied as a straight key/value object
|
|
1081 customData = source.data;
|
|
1082 }
|
|
1083
|
|
1084 // use a copy of the custom data so we can modify the parameters
|
|
1085 // and not affect the passed-in object.
|
|
1086 var data = $.extend({}, customData || {});
|
|
1087
|
|
1088 var startParam = firstDefined(source.startParam, options.startParam);
|
|
1089 var endParam = firstDefined(source.endParam, options.endParam);
|
|
1090 if (startParam) {
|
|
1091 data[startParam] = Math.round(+rangeStart / 1000);
|
|
1092 }
|
|
1093 if (endParam) {
|
|
1094 data[endParam] = Math.round(+rangeEnd / 1000);
|
|
1095 }
|
|
1096
|
|
1097 pushLoading();
|
|
1098 $.ajax($.extend({}, ajaxDefaults, source, {
|
|
1099 data: data,
|
|
1100 success: function(events) {
|
|
1101 events = events || [];
|
|
1102 var res = applyAll(success, this, arguments);
|
|
1103 if ($.isArray(res)) {
|
|
1104 events = res;
|
|
1105 }
|
|
1106 callback(events);
|
|
1107 },
|
|
1108 error: function() {
|
|
1109 applyAll(error, this, arguments);
|
|
1110 callback();
|
|
1111 },
|
|
1112 complete: function() {
|
|
1113 applyAll(complete, this, arguments);
|
|
1114 popLoading();
|
|
1115 }
|
|
1116 }));
|
|
1117 }else{
|
|
1118 callback();
|
|
1119 }
|
|
1120 }
|
|
1121 }
|
|
1122
|
|
1123
|
|
1124
|
|
1125 /* Sources
|
|
1126 -----------------------------------------------------------------------------*/
|
|
1127
|
|
1128
|
|
1129 function addEventSource(source) {
|
|
1130 source = _addEventSource(source);
|
|
1131 if (source) {
|
|
1132 pendingSourceCnt++;
|
|
1133 fetchEventSource(source, currentFetchID); // will eventually call reportEvents
|
|
1134 }
|
|
1135 }
|
|
1136
|
|
1137
|
|
1138 function _addEventSource(source) {
|
|
1139 if ($.isFunction(source) || $.isArray(source)) {
|
|
1140 source = { events: source };
|
|
1141 }
|
|
1142 else if (typeof source == 'string') {
|
|
1143 source = { url: source };
|
|
1144 }
|
|
1145 if (typeof source == 'object') {
|
|
1146 normalizeSource(source);
|
|
1147 sources.push(source);
|
|
1148 return source;
|
|
1149 }
|
|
1150 }
|
|
1151
|
|
1152
|
|
1153 function removeEventSource(source) {
|
|
1154 sources = $.grep(sources, function(src) {
|
|
1155 return !isSourcesEqual(src, source);
|
|
1156 });
|
|
1157 // remove all client events from that source
|
|
1158 cache = $.grep(cache, function(e) {
|
|
1159 return !isSourcesEqual(e.source, source);
|
|
1160 });
|
|
1161 reportEvents(cache);
|
|
1162 }
|
|
1163
|
|
1164
|
|
1165 function removeEventSources() {
|
|
1166 sources = [];
|
|
1167 removeEvents();
|
|
1168 }
|
|
1169
|
|
1170
|
|
1171
|
|
1172 /* Manipulation
|
|
1173 -----------------------------------------------------------------------------*/
|
|
1174
|
|
1175
|
|
1176 function updateEvent(event) { // update an existing event
|
|
1177 var i, len = cache.length, e,
|
|
1178 defaultEventEnd = getView().defaultEventEnd, // getView???
|
|
1179 startDelta = event.start - event._start,
|
|
1180 endDelta = event.end ?
|
|
1181 (event.end - (event._end || defaultEventEnd(event))) // event._end would be null if event.end
|
|
1182 : 0; // was null and event was just resized
|
|
1183 for (i=0; i<len; i++) {
|
|
1184 e = cache[i];
|
|
1185 if (e._id == event._id && e != event) {
|
|
1186 e.start = new Date(+e.start + startDelta);
|
|
1187 if (event.end) {
|
|
1188 if (e.end) {
|
|
1189 e.end = new Date(+e.end + endDelta);
|
|
1190 }else{
|
|
1191 e.end = new Date(+defaultEventEnd(e) + endDelta);
|
|
1192 }
|
|
1193 }else{
|
|
1194 e.end = null;
|
|
1195 }
|
|
1196 e.title = event.title;
|
|
1197 e.url = event.url;
|
|
1198 e.allDay = event.allDay;
|
|
1199 e.className = event.className;
|
|
1200 e.editable = event.editable;
|
|
1201 e.color = event.color;
|
|
1202 e.backgroundColor = event.backgroundColor;
|
|
1203 e.borderColor = event.borderColor;
|
|
1204 e.textColor = event.textColor;
|
|
1205 normalizeEvent(e);
|
|
1206 }
|
|
1207 }
|
|
1208 normalizeEvent(event);
|
|
1209 reportEvents(cache);
|
|
1210 }
|
|
1211
|
|
1212
|
|
1213 function renderEvent(event, stick) {
|
|
1214 normalizeEvent(event);
|
|
1215 if (!event.source) {
|
|
1216 if (stick) {
|
|
1217 stickySource.events.push(event);
|
|
1218 event.source = stickySource;
|
|
1219 }
|
|
1220 }
|
|
1221 // always push event to cache (issue #1112:)
|
|
1222 cache.push(event);
|
|
1223 reportEvents(cache);
|
|
1224 }
|
|
1225
|
|
1226
|
|
1227 function removeEvents(filter) {
|
|
1228 if (!filter) { // remove all
|
|
1229 cache = [];
|
|
1230 // clear all array sources
|
|
1231 for (var i=0; i<sources.length; i++) {
|
|
1232 if ($.isArray(sources[i].events)) {
|
|
1233 sources[i].events = [];
|
|
1234 }
|
|
1235 }
|
|
1236 }else{
|
|
1237 if (!$.isFunction(filter)) { // an event ID
|
|
1238 var id = filter + '';
|
|
1239 filter = function(e) {
|
|
1240 return e._id == id;
|
|
1241 };
|
|
1242 }
|
|
1243 cache = $.grep(cache, filter, true);
|
|
1244 // remove events from array sources
|
|
1245 for (var i=0; i<sources.length; i++) {
|
|
1246 if ($.isArray(sources[i].events)) {
|
|
1247 sources[i].events = $.grep(sources[i].events, filter, true);
|
|
1248 }
|
|
1249 }
|
|
1250 }
|
|
1251 reportEvents(cache);
|
|
1252 }
|
|
1253
|
|
1254
|
|
1255 function clientEvents(filter) {
|
|
1256 if ($.isFunction(filter)) {
|
|
1257 return $.grep(cache, filter);
|
|
1258 }
|
|
1259 else if (filter) { // an event ID
|
|
1260 filter += '';
|
|
1261 return $.grep(cache, function(e) {
|
|
1262 return e._id == filter;
|
|
1263 });
|
|
1264 }
|
|
1265 return cache; // else, return all
|
|
1266 }
|
|
1267
|
|
1268
|
|
1269
|
|
1270 /* Loading State
|
|
1271 -----------------------------------------------------------------------------*/
|
|
1272
|
|
1273
|
|
1274 function pushLoading() {
|
|
1275 if (!loadingLevel++) {
|
|
1276 trigger('loading', null, true, getView());
|
|
1277 }
|
|
1278 }
|
|
1279
|
|
1280
|
|
1281 function popLoading() {
|
|
1282 if (!--loadingLevel) {
|
|
1283 trigger('loading', null, false, getView());
|
|
1284 }
|
|
1285 }
|
|
1286
|
|
1287
|
|
1288
|
|
1289 /* Event Normalization
|
|
1290 -----------------------------------------------------------------------------*/
|
|
1291
|
|
1292
|
|
1293 function normalizeEvent(event) {
|
|
1294 var source = event.source || {};
|
|
1295 var ignoreTimezone = firstDefined(source.ignoreTimezone, options.ignoreTimezone);
|
|
1296 event._id = event._id || (event.id === undefined ? '_fc' + eventGUID++ : event.id + '');
|
|
1297 if (event.date) {
|
|
1298 if (!event.start) {
|
|
1299 event.start = event.date;
|
|
1300 }
|
|
1301 delete event.date;
|
|
1302 }
|
|
1303 event._start = cloneDate(event.start = parseDate(event.start, ignoreTimezone));
|
|
1304 event.end = parseDate(event.end, ignoreTimezone);
|
|
1305 if (event.end && event.end <= event.start) {
|
|
1306 event.end = null;
|
|
1307 }
|
|
1308 event._end = event.end ? cloneDate(event.end) : null;
|
|
1309 if (event.allDay === undefined) {
|
|
1310 event.allDay = firstDefined(source.allDayDefault, options.allDayDefault);
|
|
1311 }
|
|
1312 if (event.className) {
|
|
1313 if (typeof event.className == 'string') {
|
|
1314 event.className = event.className.split(/\s+/);
|
|
1315 }
|
|
1316 }else{
|
|
1317 event.className = [];
|
|
1318 }
|
|
1319 // TODO: if there is no start date, return false to indicate an invalid event
|
|
1320 }
|
|
1321
|
|
1322
|
|
1323
|
|
1324 /* Utils
|
|
1325 ------------------------------------------------------------------------------*/
|
|
1326
|
|
1327
|
|
1328 function normalizeSource(source) {
|
|
1329 if (source.className) {
|
|
1330 // TODO: repeat code, same code for event classNames
|
|
1331 if (typeof source.className == 'string') {
|
|
1332 source.className = source.className.split(/\s+/);
|
|
1333 }
|
|
1334 }else{
|
|
1335 source.className = [];
|
|
1336 }
|
|
1337 var normalizers = fc.sourceNormalizers;
|
|
1338 for (var i=0; i<normalizers.length; i++) {
|
|
1339 normalizers[i](source);
|
|
1340 }
|
|
1341 }
|
|
1342
|
|
1343
|
|
1344 function isSourcesEqual(source1, source2) {
|
|
1345 return source1 && source2 && getSourcePrimitive(source1) == getSourcePrimitive(source2);
|
|
1346 }
|
|
1347
|
|
1348
|
|
1349 function getSourcePrimitive(source) {
|
|
1350 return ((typeof source == 'object') ? (source.events || source.url) : '') || source;
|
|
1351 }
|
|
1352
|
|
1353
|
|
1354 }
|
|
1355
|
|
1356 ;;
|
|
1357
|
|
1358
|
|
1359 fc.addDays = addDays;
|
|
1360 fc.cloneDate = cloneDate;
|
|
1361 fc.parseDate = parseDate;
|
|
1362 fc.parseISO8601 = parseISO8601;
|
|
1363 fc.parseTime = parseTime;
|
|
1364 fc.formatDate = formatDate;
|
|
1365 fc.formatDates = formatDates;
|
|
1366
|
|
1367
|
|
1368
|
|
1369 /* Date Math
|
|
1370 -----------------------------------------------------------------------------*/
|
|
1371
|
|
1372 var dayIDs = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'],
|
|
1373 DAY_MS = 86400000,
|
|
1374 HOUR_MS = 3600000,
|
|
1375 MINUTE_MS = 60000;
|
|
1376
|
|
1377
|
|
1378 function addYears(d, n, keepTime) {
|
|
1379 d.setFullYear(d.getFullYear() + n);
|
|
1380 if (!keepTime) {
|
|
1381 clearTime(d);
|
|
1382 }
|
|
1383 return d;
|
|
1384 }
|
|
1385
|
|
1386
|
|
1387 function addMonths(d, n, keepTime) { // prevents day overflow/underflow
|
|
1388 if (+d) { // prevent infinite looping on invalid dates
|
|
1389 var m = d.getMonth() + n,
|
|
1390 check = cloneDate(d);
|
|
1391 check.setDate(1);
|
|
1392 check.setMonth(m);
|
|
1393 d.setMonth(m);
|
|
1394 if (!keepTime) {
|
|
1395 clearTime(d);
|
|
1396 }
|
|
1397 while (d.getMonth() != check.getMonth()) {
|
|
1398 d.setDate(d.getDate() + (d < check ? 1 : -1));
|
|
1399 }
|
|
1400 }
|
|
1401 return d;
|
|
1402 }
|
|
1403
|
|
1404
|
|
1405 function addDays(d, n, keepTime) { // deals with daylight savings
|
|
1406 if (+d) {
|
|
1407 var dd = d.getDate() + n,
|
|
1408 check = cloneDate(d);
|
|
1409 check.setHours(9); // set to middle of day
|
|
1410 check.setDate(dd);
|
|
1411 d.setDate(dd);
|
|
1412 if (!keepTime) {
|
|
1413 clearTime(d);
|
|
1414 }
|
|
1415 fixDate(d, check);
|
|
1416 }
|
|
1417 return d;
|
|
1418 }
|
|
1419
|
|
1420
|
|
1421 function fixDate(d, check) { // force d to be on check's YMD, for daylight savings purposes
|
|
1422 if (+d) { // prevent infinite looping on invalid dates
|
|
1423 while (d.getDate() != check.getDate()) {
|
|
1424 d.setTime(+d + (d < check ? 1 : -1) * HOUR_MS);
|
|
1425 }
|
|
1426 }
|
|
1427 }
|
|
1428
|
|
1429
|
|
1430 function addMinutes(d, n) {
|
|
1431 d.setMinutes(d.getMinutes() + n);
|
|
1432 return d;
|
|
1433 }
|
|
1434
|
|
1435
|
|
1436 function clearTime(d) {
|
|
1437 d.setHours(0);
|
|
1438 d.setMinutes(0);
|
|
1439 d.setSeconds(0);
|
|
1440 d.setMilliseconds(0);
|
|
1441 return d;
|
|
1442 }
|
|
1443
|
|
1444
|
|
1445 function cloneDate(d, dontKeepTime) {
|
|
1446 if (dontKeepTime) {
|
|
1447 return clearTime(new Date(+d));
|
|
1448 }
|
|
1449 return new Date(+d);
|
|
1450 }
|
|
1451
|
|
1452
|
|
1453 function zeroDate() { // returns a Date with time 00:00:00 and dateOfMonth=1
|
|
1454 var i=0, d;
|
|
1455 do {
|
|
1456 d = new Date(1970, i++, 1);
|
|
1457 } while (d.getHours()); // != 0
|
|
1458 return d;
|
|
1459 }
|
|
1460
|
|
1461
|
|
1462 function dayDiff(d1, d2) { // d1 - d2
|
|
1463 return Math.round((cloneDate(d1, true) - cloneDate(d2, true)) / DAY_MS);
|
|
1464 }
|
|
1465
|
|
1466
|
|
1467 function setYMD(date, y, m, d) {
|
|
1468 if (y !== undefined && y != date.getFullYear()) {
|
|
1469 date.setDate(1);
|
|
1470 date.setMonth(0);
|
|
1471 date.setFullYear(y);
|
|
1472 }
|
|
1473 if (m !== undefined && m != date.getMonth()) {
|
|
1474 date.setDate(1);
|
|
1475 date.setMonth(m);
|
|
1476 }
|
|
1477 if (d !== undefined) {
|
|
1478 date.setDate(d);
|
|
1479 }
|
|
1480 }
|
|
1481
|
|
1482
|
|
1483
|
|
1484 /* Date Parsing
|
|
1485 -----------------------------------------------------------------------------*/
|
|
1486
|
|
1487
|
|
1488 function parseDate(s, ignoreTimezone) { // ignoreTimezone defaults to true
|
|
1489 if (typeof s == 'object') { // already a Date object
|
|
1490 return s;
|
|
1491 }
|
|
1492 if (typeof s == 'number') { // a UNIX timestamp
|
|
1493 return new Date(s * 1000);
|
|
1494 }
|
|
1495 if (typeof s == 'string') {
|
|
1496 if (s.match(/^\d+(\.\d+)?$/)) { // a UNIX timestamp
|
|
1497 return new Date(parseFloat(s) * 1000);
|
|
1498 }
|
|
1499 if (ignoreTimezone === undefined) {
|
|
1500 ignoreTimezone = true;
|
|
1501 }
|
|
1502 return parseISO8601(s, ignoreTimezone) || (s ? new Date(s) : null);
|
|
1503 }
|
|
1504 // TODO: never return invalid dates (like from new Date(<string>)), return null instead
|
|
1505 return null;
|
|
1506 }
|
|
1507
|
|
1508
|
|
1509 function parseISO8601(s, ignoreTimezone) { // ignoreTimezone defaults to false
|
|
1510 // derived from http://delete.me.uk/2005/03/iso8601.html
|
|
1511 // TODO: for a know glitch/feature, read tests/issue_206_parseDate_dst.html
|
|
1512 var m = s.match(/^([0-9]{4})(-([0-9]{2})(-([0-9]{2})([T ]([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?(Z|(([-+])([0-9]{2})(:?([0-9]{2}))?))?)?)?)?$/);
|
|
1513 if (!m) {
|
|
1514 return null;
|
|
1515 }
|
|
1516 var date = new Date(m[1], 0, 2);
|
|
1517 if (ignoreTimezone || !m[13]) {
|
|
1518 var check = new Date(m[1], 0, 2, 9, 0);
|
|
1519 if (m[3]) {
|
|
1520 date.setMonth(m[3] - 1);
|
|
1521 check.setMonth(m[3] - 1);
|
|
1522 }
|
|
1523 if (m[5]) {
|
|
1524 date.setDate(m[5]);
|
|
1525 check.setDate(m[5]);
|
|
1526 }
|
|
1527 fixDate(date, check);
|
|
1528 if (m[7]) {
|
|
1529 date.setHours(m[7]);
|
|
1530 }
|
|
1531 if (m[8]) {
|
|
1532 date.setMinutes(m[8]);
|
|
1533 }
|
|
1534 if (m[10]) {
|
|
1535 date.setSeconds(m[10]);
|
|
1536 }
|
|
1537 if (m[12]) {
|
|
1538 date.setMilliseconds(Number("0." + m[12]) * 1000);
|
|
1539 }
|
|
1540 fixDate(date, check);
|
|
1541 }else{
|
|
1542 date.setUTCFullYear(
|
|
1543 m[1],
|
|
1544 m[3] ? m[3] - 1 : 0,
|
|
1545 m[5] || 1
|
|
1546 );
|
|
1547 date.setUTCHours(
|
|
1548 m[7] || 0,
|
|
1549 m[8] || 0,
|
|
1550 m[10] || 0,
|
|
1551 m[12] ? Number("0." + m[12]) * 1000 : 0
|
|
1552 );
|
|
1553 if (m[14]) {
|
|
1554 var offset = Number(m[16]) * 60 + (m[18] ? Number(m[18]) : 0);
|
|
1555 offset *= m[15] == '-' ? 1 : -1;
|
|
1556 date = new Date(+date + (offset * 60 * 1000));
|
|
1557 }
|
|
1558 }
|
|
1559 return date;
|
|
1560 }
|
|
1561
|
|
1562
|
|
1563 function parseTime(s) { // returns minutes since start of day
|
|
1564 if (typeof s == 'number') { // an hour
|
|
1565 return s * 60;
|
|
1566 }
|
|
1567 if (typeof s == 'object') { // a Date object
|
|
1568 return s.getHours() * 60 + s.getMinutes();
|
|
1569 }
|
|
1570 var m = s.match(/(\d+)(?::(\d+))?\s*(\w+)?/);
|
|
1571 if (m) {
|
|
1572 var h = parseInt(m[1], 10);
|
|
1573 if (m[3]) {
|
|
1574 h %= 12;
|
|
1575 if (m[3].toLowerCase().charAt(0) == 'p') {
|
|
1576 h += 12;
|
|
1577 }
|
|
1578 }
|
|
1579 return h * 60 + (m[2] ? parseInt(m[2], 10) : 0);
|
|
1580 }
|
|
1581 }
|
|
1582
|
|
1583
|
|
1584
|
|
1585 /* Date Formatting
|
|
1586 -----------------------------------------------------------------------------*/
|
|
1587 // TODO: use same function formatDate(date, [date2], format, [options])
|
|
1588
|
|
1589
|
|
1590 function formatDate(date, format, options) {
|
|
1591 return formatDates(date, null, format, options);
|
|
1592 }
|
|
1593
|
|
1594
|
|
1595 function formatDates(date1, date2, format, options) {
|
|
1596 options = options || defaults;
|
|
1597 var date = date1,
|
|
1598 otherDate = date2,
|
|
1599 i, len = format.length, c,
|
|
1600 i2, formatter,
|
|
1601 res = '';
|
|
1602 for (i=0; i<len; i++) {
|
|
1603 c = format.charAt(i);
|
|
1604 if (c == "'") {
|
|
1605 for (i2=i+1; i2<len; i2++) {
|
|
1606 if (format.charAt(i2) == "'") {
|
|
1607 if (date) {
|
|
1608 if (i2 == i+1) {
|
|
1609 res += "'";
|
|
1610 }else{
|
|
1611 res += format.substring(i+1, i2);
|
|
1612 }
|
|
1613 i = i2;
|
|
1614 }
|
|
1615 break;
|
|
1616 }
|
|
1617 }
|
|
1618 }
|
|
1619 else if (c == '(') {
|
|
1620 for (i2=i+1; i2<len; i2++) {
|
|
1621 if (format.charAt(i2) == ')') {
|
|
1622 var subres = formatDate(date, format.substring(i+1, i2), options);
|
|
1623 if (parseInt(subres.replace(/\D/, ''), 10)) {
|
|
1624 res += subres;
|
|
1625 }
|
|
1626 i = i2;
|
|
1627 break;
|
|
1628 }
|
|
1629 }
|
|
1630 }
|
|
1631 else if (c == '[') {
|
|
1632 for (i2=i+1; i2<len; i2++) {
|
|
1633 if (format.charAt(i2) == ']') {
|
|
1634 var subformat = format.substring(i+1, i2);
|
|
1635 var subres = formatDate(date, subformat, options);
|
|
1636 if (subres != formatDate(otherDate, subformat, options)) {
|
|
1637 res += subres;
|
|
1638 }
|
|
1639 i = i2;
|
|
1640 break;
|
|
1641 }
|
|
1642 }
|
|
1643 }
|
|
1644 else if (c == '{') {
|
|
1645 date = date2;
|
|
1646 otherDate = date1;
|
|
1647 }
|
|
1648 else if (c == '}') {
|
|
1649 date = date1;
|
|
1650 otherDate = date2;
|
|
1651 }
|
|
1652 else {
|
|
1653 for (i2=len; i2>i; i2--) {
|
|
1654 if (formatter = dateFormatters[format.substring(i, i2)]) {
|
|
1655 if (date) {
|
|
1656 res += formatter(date, options);
|
|
1657 }
|
|
1658 i = i2 - 1;
|
|
1659 break;
|
|
1660 }
|
|
1661 }
|
|
1662 if (i2 == i) {
|
|
1663 if (date) {
|
|
1664 res += c;
|
|
1665 }
|
|
1666 }
|
|
1667 }
|
|
1668 }
|
|
1669 return res;
|
|
1670 };
|
|
1671
|
|
1672
|
|
1673 var dateFormatters = {
|
|
1674 s : function(d) { return d.getSeconds() },
|
|
1675 ss : function(d) { return zeroPad(d.getSeconds()) },
|
|
1676 m : function(d) { return d.getMinutes() },
|
|
1677 mm : function(d) { return zeroPad(d.getMinutes()) },
|
|
1678 h : function(d) { return d.getHours() % 12 || 12 },
|
|
1679 hh : function(d) { return zeroPad(d.getHours() % 12 || 12) },
|
|
1680 H : function(d) { return d.getHours() },
|
|
1681 HH : function(d) { return zeroPad(d.getHours()) },
|
|
1682 d : function(d) { return d.getDate() },
|
|
1683 dd : function(d) { return zeroPad(d.getDate()) },
|
|
1684 ddd : function(d,o) { return o.dayNamesShort[d.getDay()] },
|
|
1685 dddd: function(d,o) { return o.dayNames[d.getDay()] },
|
|
1686 M : function(d) { return d.getMonth() + 1 },
|
|
1687 MM : function(d) { return zeroPad(d.getMonth() + 1) },
|
|
1688 MMM : function(d,o) { return o.monthNamesShort[d.getMonth()] },
|
|
1689 MMMM: function(d,o) { return o.monthNames[d.getMonth()] },
|
|
1690 yy : function(d) { return (d.getFullYear()+'').substring(2) },
|
|
1691 yyyy: function(d) { return d.getFullYear() },
|
|
1692 t : function(d) { return d.getHours() < 12 ? 'a' : 'p' },
|
|
1693 tt : function(d) { return d.getHours() < 12 ? 'am' : 'pm' },
|
|
1694 T : function(d) { return d.getHours() < 12 ? 'A' : 'P' },
|
|
1695 TT : function(d) { return d.getHours() < 12 ? 'AM' : 'PM' },
|
|
1696 u : function(d) { return formatDate(d, "yyyy-MM-dd'T'HH:mm:ss'Z'") },
|
|
1697 S : function(d) {
|
|
1698 var date = d.getDate();
|
|
1699 if (date > 10 && date < 20) {
|
|
1700 return 'th';
|
|
1701 }
|
|
1702 return ['st', 'nd', 'rd'][date%10-1] || 'th';
|
|
1703 },
|
|
1704 w : function(d, o) { // local
|
|
1705 return o.weekNumberCalculation(d);
|
|
1706 },
|
|
1707 W : function(d) { // ISO
|
|
1708 return iso8601Week(d);
|
|
1709 }
|
|
1710 };
|
|
1711 fc.dateFormatters = dateFormatters;
|
|
1712
|
|
1713
|
|
1714 /* thanks jQuery UI (https://github.com/jquery/jquery-ui/blob/master/ui/jquery.ui.datepicker.js)
|
|
1715 *
|
|
1716 * Set as calculateWeek to determine the week of the year based on the ISO 8601 definition.
|
|
1717 * `date` - the date to get the week for
|
|
1718 * `number` - the number of the week within the year that contains this date
|
|
1719 */
|
|
1720 function iso8601Week(date) {
|
|
1721 var time;
|
|
1722 var checkDate = new Date(date.getTime());
|
|
1723
|
|
1724 // Find Thursday of this week starting on Monday
|
|
1725 checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7));
|
|
1726
|
|
1727 time = checkDate.getTime();
|
|
1728 checkDate.setMonth(0); // Compare with Jan 1
|
|
1729 checkDate.setDate(1);
|
|
1730 return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1;
|
|
1731 }
|
|
1732
|
|
1733 // Determine the week of the year based on the ISO 8601 definition.
|
|
1734 // copied from jquery UI Datepicker
|
|
1735 var iso8601Week = function(date) {
|
|
1736 var checkDate = cloneDate(date);
|
|
1737 // Find Thursday of this week starting on Monday
|
|
1738 checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7));
|
|
1739 var time = checkDate.getTime();
|
|
1740 checkDate.setMonth(0); // Compare with Jan 1
|
|
1741 checkDate.setDate(1);
|
|
1742 return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1;
|
|
1743 };
|
|
1744
|
|
1745
|
|
1746 ;;
|
|
1747
|
|
1748 fc.applyAll = applyAll;
|
|
1749
|
|
1750
|
|
1751 /* Event Date Math
|
|
1752 -----------------------------------------------------------------------------*/
|
|
1753
|
|
1754
|
|
1755 function exclEndDay(event) {
|
|
1756 if (event.end) {
|
|
1757 return _exclEndDay(event.end, event.allDay);
|
|
1758 }else{
|
|
1759 return addDays(cloneDate(event.start), 1);
|
|
1760 }
|
|
1761 }
|
|
1762
|
|
1763
|
|
1764 function _exclEndDay(end, allDay) {
|
|
1765 end = cloneDate(end);
|
|
1766 return allDay || end.getHours() || end.getMinutes() ? addDays(end, 1) : clearTime(end);
|
|
1767 // why don't we check for seconds/ms too?
|
|
1768 }
|
|
1769
|
|
1770
|
|
1771
|
|
1772 /* Event Element Binding
|
|
1773 -----------------------------------------------------------------------------*/
|
|
1774
|
|
1775
|
|
1776 function lazySegBind(container, segs, bindHandlers) {
|
|
1777 container.unbind('mouseover focusin').bind('mouseover focusin', function(ev) {
|
|
1778 var parent=ev.target, e,
|
|
1779 i, seg;
|
|
1780 while (parent != this) {
|
|
1781 e = parent;
|
|
1782 parent = parent.parentNode;
|
|
1783 }
|
|
1784 if ((i = e._fci) !== undefined) {
|
|
1785 e._fci = undefined;
|
|
1786 seg = segs[i];
|
|
1787 bindHandlers(seg.event, seg.element, seg);
|
|
1788 $(ev.target).trigger(ev);
|
|
1789 }
|
|
1790 ev.stopPropagation();
|
|
1791 });
|
|
1792 }
|
|
1793
|
|
1794
|
|
1795
|
|
1796 /* Element Dimensions
|
|
1797 -----------------------------------------------------------------------------*/
|
|
1798
|
|
1799
|
|
1800 function setOuterWidth(element, width, includeMargins) {
|
|
1801 for (var i=0, e; i<element.length; i++) {
|
|
1802 e = $(element[i]);
|
|
1803 e.width(Math.max(0, width - hsides(e, includeMargins)));
|
|
1804 }
|
|
1805 }
|
|
1806
|
|
1807
|
|
1808 function setOuterHeight(element, height, includeMargins) {
|
|
1809 for (var i=0, e; i<element.length; i++) {
|
|
1810 e = $(element[i]);
|
|
1811 e.height(Math.max(0, height - vsides(e, includeMargins)));
|
|
1812 }
|
|
1813 }
|
|
1814
|
|
1815
|
|
1816 function hsides(element, includeMargins) {
|
|
1817 return hpadding(element) + hborders(element) + (includeMargins ? hmargins(element) : 0);
|
|
1818 }
|
|
1819
|
|
1820
|
|
1821 function hpadding(element) {
|
|
1822 return (parseFloat($.css(element[0], 'paddingLeft', true)) || 0) +
|
|
1823 (parseFloat($.css(element[0], 'paddingRight', true)) || 0);
|
|
1824 }
|
|
1825
|
|
1826
|
|
1827 function hmargins(element) {
|
|
1828 return (parseFloat($.css(element[0], 'marginLeft', true)) || 0) +
|
|
1829 (parseFloat($.css(element[0], 'marginRight', true)) || 0);
|
|
1830 }
|
|
1831
|
|
1832
|
|
1833 function hborders(element) {
|
|
1834 return (parseFloat($.css(element[0], 'borderLeftWidth', true)) || 0) +
|
|
1835 (parseFloat($.css(element[0], 'borderRightWidth', true)) || 0);
|
|
1836 }
|
|
1837
|
|
1838
|
|
1839 function vsides(element, includeMargins) {
|
|
1840 return vpadding(element) + vborders(element) + (includeMargins ? vmargins(element) : 0);
|
|
1841 }
|
|
1842
|
|
1843
|
|
1844 function vpadding(element) {
|
|
1845 return (parseFloat($.css(element[0], 'paddingTop', true)) || 0) +
|
|
1846 (parseFloat($.css(element[0], 'paddingBottom', true)) || 0);
|
|
1847 }
|
|
1848
|
|
1849
|
|
1850 function vmargins(element) {
|
|
1851 return (parseFloat($.css(element[0], 'marginTop', true)) || 0) +
|
|
1852 (parseFloat($.css(element[0], 'marginBottom', true)) || 0);
|
|
1853 }
|
|
1854
|
|
1855
|
|
1856 function vborders(element) {
|
|
1857 return (parseFloat($.css(element[0], 'borderTopWidth', true)) || 0) +
|
|
1858 (parseFloat($.css(element[0], 'borderBottomWidth', true)) || 0);
|
|
1859 }
|
|
1860
|
|
1861
|
|
1862
|
|
1863 /* Misc Utils
|
|
1864 -----------------------------------------------------------------------------*/
|
|
1865
|
|
1866
|
|
1867 //TODO: arraySlice
|
|
1868 //TODO: isFunction, grep ?
|
|
1869
|
|
1870
|
|
1871 function noop() { }
|
|
1872
|
|
1873
|
|
1874 function dateCompare(a, b) {
|
|
1875 return a - b;
|
|
1876 }
|
|
1877
|
|
1878
|
|
1879 function arrayMax(a) {
|
|
1880 return Math.max.apply(Math, a);
|
|
1881 }
|
|
1882
|
|
1883
|
|
1884 function zeroPad(n) {
|
|
1885 return (n < 10 ? '0' : '') + n;
|
|
1886 }
|
|
1887
|
|
1888
|
|
1889 function smartProperty(obj, name) { // get a camel-cased/namespaced property of an object
|
|
1890 if (obj[name] !== undefined) {
|
|
1891 return obj[name];
|
|
1892 }
|
|
1893 var parts = name.split(/(?=[A-Z])/),
|
|
1894 i=parts.length-1, res;
|
|
1895 for (; i>=0; i--) {
|
|
1896 res = obj[parts[i].toLowerCase()];
|
|
1897 if (res !== undefined) {
|
|
1898 return res;
|
|
1899 }
|
|
1900 }
|
|
1901 return obj[''];
|
|
1902 }
|
|
1903
|
|
1904
|
|
1905 function htmlEscape(s) {
|
|
1906 return s.replace(/&/g, '&')
|
|
1907 .replace(/</g, '<')
|
|
1908 .replace(/>/g, '>')
|
|
1909 .replace(/'/g, ''')
|
|
1910 .replace(/"/g, '"')
|
|
1911 .replace(/\n/g, '<br />');
|
|
1912 }
|
|
1913
|
|
1914
|
|
1915 function disableTextSelection(element) {
|
|
1916 element
|
|
1917 .attr('unselectable', 'on')
|
|
1918 .css('MozUserSelect', 'none')
|
|
1919 .bind('selectstart.ui', function() { return false; });
|
|
1920 }
|
|
1921
|
|
1922
|
|
1923 /*
|
|
1924 function enableTextSelection(element) {
|
|
1925 element
|
|
1926 .attr('unselectable', 'off')
|
|
1927 .css('MozUserSelect', '')
|
|
1928 .unbind('selectstart.ui');
|
|
1929 }
|
|
1930 */
|
|
1931
|
|
1932
|
|
1933 function markFirstLast(e) {
|
|
1934 e.children()
|
|
1935 .removeClass('fc-first fc-last')
|
|
1936 .filter(':first-child')
|
|
1937 .addClass('fc-first')
|
|
1938 .end()
|
|
1939 .filter(':last-child')
|
|
1940 .addClass('fc-last');
|
|
1941 }
|
|
1942
|
|
1943
|
|
1944 function setDayID(cell, date) {
|
|
1945 cell.each(function(i, _cell) {
|
|
1946 _cell.className = _cell.className.replace(/^fc-\w*/, 'fc-' + dayIDs[date.getDay()]);
|
|
1947 // TODO: make a way that doesn't rely on order of classes
|
|
1948 });
|
|
1949 }
|
|
1950
|
|
1951
|
|
1952 function getSkinCss(event, opt) {
|
|
1953 var source = event.source || {};
|
|
1954 var eventColor = event.color;
|
|
1955 var sourceColor = source.color;
|
|
1956 var optionColor = opt('eventColor');
|
|
1957 var backgroundColor =
|
|
1958 event.backgroundColor ||
|
|
1959 eventColor ||
|
|
1960 source.backgroundColor ||
|
|
1961 sourceColor ||
|
|
1962 opt('eventBackgroundColor') ||
|
|
1963 optionColor;
|
|
1964 var borderColor =
|
|
1965 event.borderColor ||
|
|
1966 eventColor ||
|
|
1967 source.borderColor ||
|
|
1968 sourceColor ||
|
|
1969 opt('eventBorderColor') ||
|
|
1970 optionColor;
|
|
1971 var textColor =
|
|
1972 event.textColor ||
|
|
1973 source.textColor ||
|
|
1974 opt('eventTextColor');
|
|
1975 var statements = [];
|
|
1976 if (backgroundColor) {
|
|
1977 statements.push('background-color:' + backgroundColor);
|
|
1978 }
|
|
1979 if (borderColor) {
|
|
1980 statements.push('border-color:' + borderColor);
|
|
1981 }
|
|
1982 if (textColor) {
|
|
1983 statements.push('color:' + textColor);
|
|
1984 }
|
|
1985 return statements.join(';');
|
|
1986 }
|
|
1987
|
|
1988
|
|
1989 function applyAll(functions, thisObj, args) {
|
|
1990 if ($.isFunction(functions)) {
|
|
1991 functions = [ functions ];
|
|
1992 }
|
|
1993 if (functions) {
|
|
1994 var i;
|
|
1995 var ret;
|
|
1996 for (i=0; i<functions.length; i++) {
|
|
1997 ret = functions[i].apply(thisObj, args) || ret;
|
|
1998 }
|
|
1999 return ret;
|
|
2000 }
|
|
2001 }
|
|
2002
|
|
2003
|
|
2004 function firstDefined() {
|
|
2005 for (var i=0; i<arguments.length; i++) {
|
|
2006 if (arguments[i] !== undefined) {
|
|
2007 return arguments[i];
|
|
2008 }
|
|
2009 }
|
|
2010 }
|
|
2011
|
|
2012
|
|
2013 ;;
|
|
2014
|
|
2015 fcViews.month = MonthView;
|
|
2016
|
|
2017 function MonthView(element, calendar) {
|
|
2018 var t = this;
|
|
2019
|
|
2020
|
|
2021 // exports
|
|
2022 t.render = render;
|
|
2023
|
|
2024
|
|
2025 // imports
|
|
2026 BasicView.call(t, element, calendar, 'month');
|
|
2027 var opt = t.opt;
|
|
2028 var renderBasic = t.renderBasic;
|
|
2029 var skipHiddenDays = t.skipHiddenDays;
|
|
2030 var getCellsPerWeek = t.getCellsPerWeek;
|
|
2031 var formatDate = calendar.formatDate;
|
|
2032
|
|
2033
|
|
2034 function render(date, delta) {
|
|
2035
|
|
2036 if (delta) {
|
|
2037 addMonths(date, delta);
|
|
2038 date.setDate(1);
|
|
2039 }
|
|
2040
|
|
2041 var firstDay = opt('firstDay');
|
|
2042
|
|
2043 var start = cloneDate(date, true);
|
|
2044 start.setDate(1);
|
|
2045
|
|
2046 var end = addMonths(cloneDate(start), 1);
|
|
2047
|
|
2048 var visStart = cloneDate(start);
|
|
2049 addDays(visStart, -((visStart.getDay() - firstDay + 7) % 7));
|
|
2050 skipHiddenDays(visStart);
|
|
2051
|
|
2052 var visEnd = cloneDate(end);
|
|
2053 addDays(visEnd, (7 - visEnd.getDay() + firstDay) % 7);
|
|
2054 skipHiddenDays(visEnd, -1, true);
|
|
2055
|
|
2056 var colCnt = getCellsPerWeek();
|
|
2057 var rowCnt = Math.round(dayDiff(visEnd, visStart) / 7); // should be no need for Math.round
|
|
2058
|
|
2059 if (opt('weekMode') == 'fixed') {
|
|
2060 addDays(visEnd, (6 - rowCnt) * 7); // add weeks to make up for it
|
|
2061 rowCnt = 6;
|
|
2062 }
|
|
2063
|
|
2064 t.title = formatDate(start, opt('titleFormat'));
|
|
2065
|
|
2066 t.start = start;
|
|
2067 t.end = end;
|
|
2068 t.visStart = visStart;
|
|
2069 t.visEnd = visEnd;
|
|
2070
|
|
2071 renderBasic(rowCnt, colCnt, true);
|
|
2072 }
|
|
2073
|
|
2074
|
|
2075 }
|
|
2076
|
|
2077 ;;
|
|
2078
|
|
2079 fcViews.basicWeek = BasicWeekView;
|
|
2080
|
|
2081 function BasicWeekView(element, calendar) {
|
|
2082 var t = this;
|
|
2083
|
|
2084
|
|
2085 // exports
|
|
2086 t.render = render;
|
|
2087
|
|
2088
|
|
2089 // imports
|
|
2090 BasicView.call(t, element, calendar, 'basicWeek');
|
|
2091 var opt = t.opt;
|
|
2092 var renderBasic = t.renderBasic;
|
|
2093 var skipHiddenDays = t.skipHiddenDays;
|
|
2094 var getCellsPerWeek = t.getCellsPerWeek;
|
|
2095 var formatDates = calendar.formatDates;
|
|
2096
|
|
2097
|
|
2098 function render(date, delta) {
|
|
2099
|
|
2100 if (delta) {
|
|
2101 addDays(date, delta * 7);
|
|
2102 }
|
|
2103
|
|
2104 var start = addDays(cloneDate(date), -((date.getDay() - opt('firstDay') + 7) % 7));
|
|
2105 var end = addDays(cloneDate(start), 7);
|
|
2106
|
|
2107 var visStart = cloneDate(start);
|
|
2108 skipHiddenDays(visStart);
|
|
2109
|
|
2110 var visEnd = cloneDate(end);
|
|
2111 skipHiddenDays(visEnd, -1, true);
|
|
2112
|
|
2113 var colCnt = getCellsPerWeek();
|
|
2114
|
|
2115 t.start = start;
|
|
2116 t.end = end;
|
|
2117 t.visStart = visStart;
|
|
2118 t.visEnd = visEnd;
|
|
2119
|
|
2120 t.title = formatDates(
|
|
2121 visStart,
|
|
2122 addDays(cloneDate(visEnd), -1),
|
|
2123 opt('titleFormat')
|
|
2124 );
|
|
2125
|
|
2126 renderBasic(1, colCnt, false);
|
|
2127 }
|
|
2128
|
|
2129
|
|
2130 }
|
|
2131
|
|
2132 ;;
|
|
2133
|
|
2134 fcViews.basicDay = BasicDayView;
|
|
2135
|
|
2136
|
|
2137 function BasicDayView(element, calendar) {
|
|
2138 var t = this;
|
|
2139
|
|
2140
|
|
2141 // exports
|
|
2142 t.render = render;
|
|
2143
|
|
2144
|
|
2145 // imports
|
|
2146 BasicView.call(t, element, calendar, 'basicDay');
|
|
2147 var opt = t.opt;
|
|
2148 var renderBasic = t.renderBasic;
|
|
2149 var skipHiddenDays = t.skipHiddenDays;
|
|
2150 var formatDate = calendar.formatDate;
|
|
2151
|
|
2152
|
|
2153 function render(date, delta) {
|
|
2154
|
|
2155 if (delta) {
|
|
2156 addDays(date, delta);
|
|
2157 }
|
|
2158 skipHiddenDays(date, delta < 0 ? -1 : 1);
|
|
2159
|
|
2160 var start = cloneDate(date, true);
|
|
2161 var end = addDays(cloneDate(start), 1);
|
|
2162
|
|
2163 t.title = formatDate(date, opt('titleFormat'));
|
|
2164
|
|
2165 t.start = t.visStart = start;
|
|
2166 t.end = t.visEnd = end;
|
|
2167
|
|
2168 renderBasic(1, 1, false);
|
|
2169 }
|
|
2170
|
|
2171
|
|
2172 }
|
|
2173
|
|
2174 ;;
|
|
2175
|
|
2176 setDefaults({
|
|
2177 weekMode: 'fixed'
|
|
2178 });
|
|
2179
|
|
2180
|
|
2181 function BasicView(element, calendar, viewName) {
|
|
2182 var t = this;
|
|
2183
|
|
2184
|
|
2185 // exports
|
|
2186 t.renderBasic = renderBasic;
|
|
2187 t.setHeight = setHeight;
|
|
2188 t.setWidth = setWidth;
|
|
2189 t.renderDayOverlay = renderDayOverlay;
|
|
2190 t.defaultSelectionEnd = defaultSelectionEnd;
|
|
2191 t.renderSelection = renderSelection;
|
|
2192 t.clearSelection = clearSelection;
|
|
2193 t.reportDayClick = reportDayClick; // for selection (kinda hacky)
|
|
2194 t.dragStart = dragStart;
|
|
2195 t.dragStop = dragStop;
|
|
2196 t.defaultEventEnd = defaultEventEnd;
|
|
2197 t.getHoverListener = function() { return hoverListener };
|
|
2198 t.colLeft = colLeft;
|
|
2199 t.colRight = colRight;
|
|
2200 t.colContentLeft = colContentLeft;
|
|
2201 t.colContentRight = colContentRight;
|
|
2202 t.getIsCellAllDay = function() { return true };
|
|
2203 t.allDayRow = allDayRow;
|
|
2204 t.getRowCnt = function() { return rowCnt };
|
|
2205 t.getColCnt = function() { return colCnt };
|
|
2206 t.getColWidth = function() { return colWidth };
|
|
2207 t.getDaySegmentContainer = function() { return daySegmentContainer };
|
|
2208
|
|
2209
|
|
2210 // imports
|
|
2211 View.call(t, element, calendar, viewName);
|
|
2212 OverlayManager.call(t);
|
|
2213 SelectionManager.call(t);
|
|
2214 BasicEventRenderer.call(t);
|
|
2215 var opt = t.opt;
|
|
2216 var trigger = t.trigger;
|
|
2217 var renderOverlay = t.renderOverlay;
|
|
2218 var clearOverlays = t.clearOverlays;
|
|
2219 var daySelectionMousedown = t.daySelectionMousedown;
|
|
2220 var cellToDate = t.cellToDate;
|
|
2221 var dateToCell = t.dateToCell;
|
|
2222 var rangeToSegments = t.rangeToSegments;
|
|
2223 var formatDate = calendar.formatDate;
|
|
2224
|
|
2225
|
|
2226 // locals
|
|
2227
|
|
2228 var table;
|
|
2229 var head;
|
|
2230 var headCells;
|
|
2231 var body;
|
|
2232 var bodyRows;
|
|
2233 var bodyCells;
|
|
2234 var bodyFirstCells;
|
|
2235 var firstRowCellInners;
|
|
2236 var firstRowCellContentInners;
|
|
2237 var daySegmentContainer;
|
|
2238
|
|
2239 var viewWidth;
|
|
2240 var viewHeight;
|
|
2241 var colWidth;
|
|
2242 var weekNumberWidth;
|
|
2243
|
|
2244 var rowCnt, colCnt;
|
|
2245 var showNumbers;
|
|
2246 var coordinateGrid;
|
|
2247 var hoverListener;
|
|
2248 var colPositions;
|
|
2249 var colContentPositions;
|
|
2250
|
|
2251 var tm;
|
|
2252 var colFormat;
|
|
2253 var showWeekNumbers;
|
|
2254 var weekNumberTitle;
|
|
2255 var weekNumberFormat;
|
|
2256
|
|
2257
|
|
2258
|
|
2259 /* Rendering
|
|
2260 ------------------------------------------------------------*/
|
|
2261
|
|
2262
|
|
2263 disableTextSelection(element.addClass('fc-grid'));
|
|
2264
|
|
2265
|
|
2266 function renderBasic(_rowCnt, _colCnt, _showNumbers) {
|
|
2267 rowCnt = _rowCnt;
|
|
2268 colCnt = _colCnt;
|
|
2269 showNumbers = _showNumbers;
|
|
2270 updateOptions();
|
|
2271
|
|
2272 if (!body) {
|
|
2273 buildEventContainer();
|
|
2274 }
|
|
2275
|
|
2276 buildTable();
|
|
2277 }
|
|
2278
|
|
2279
|
|
2280 function updateOptions() {
|
|
2281 tm = opt('theme') ? 'ui' : 'fc';
|
|
2282 colFormat = opt('columnFormat');
|
|
2283
|
|
2284 // week # options. (TODO: bad, logic also in other views)
|
|
2285 showWeekNumbers = opt('weekNumbers');
|
|
2286 weekNumberTitle = opt('weekNumberTitle');
|
|
2287 if (opt('weekNumberCalculation') != 'iso') {
|
|
2288 weekNumberFormat = "w";
|
|
2289 }
|
|
2290 else {
|
|
2291 weekNumberFormat = "W";
|
|
2292 }
|
|
2293 }
|
|
2294
|
|
2295
|
|
2296 function buildEventContainer() {
|
|
2297 daySegmentContainer =
|
|
2298 $("<div class='fc-event-container' style='position:absolute;z-index:8;top:0;left:0'/>")
|
|
2299 .appendTo(element);
|
|
2300 }
|
|
2301
|
|
2302
|
|
2303 function buildTable() {
|
|
2304 var html = buildTableHTML();
|
|
2305
|
|
2306 if (table) {
|
|
2307 table.remove();
|
|
2308 }
|
|
2309 table = $(html).appendTo(element);
|
|
2310
|
|
2311 head = table.find('thead');
|
|
2312 headCells = head.find('.fc-day-header');
|
|
2313 body = table.find('tbody');
|
|
2314 bodyRows = body.find('tr');
|
|
2315 bodyCells = body.find('.fc-day');
|
|
2316 bodyFirstCells = bodyRows.find('td:first-child');
|
|
2317
|
|
2318 firstRowCellInners = bodyRows.eq(0).find('.fc-day > div');
|
|
2319 firstRowCellContentInners = bodyRows.eq(0).find('.fc-day-content > div');
|
|
2320
|
|
2321 markFirstLast(head.add(head.find('tr'))); // marks first+last tr/th's
|
|
2322 markFirstLast(bodyRows); // marks first+last td's
|
|
2323 bodyRows.eq(0).addClass('fc-first');
|
|
2324 bodyRows.filter(':last').addClass('fc-last');
|
|
2325
|
|
2326 bodyCells.each(function(i, _cell) {
|
|
2327 var date = cellToDate(
|
|
2328 Math.floor(i / colCnt),
|
|
2329 i % colCnt
|
|
2330 );
|
|
2331 trigger('dayRender', t, date, $(_cell));
|
|
2332 });
|
|
2333
|
|
2334 dayBind(bodyCells);
|
|
2335 }
|
|
2336
|
|
2337
|
|
2338
|
|
2339 /* HTML Building
|
|
2340 -----------------------------------------------------------*/
|
|
2341
|
|
2342
|
|
2343 function buildTableHTML() {
|
|
2344 var html =
|
|
2345 "<table class='fc-border-separate' style='width:100%' cellspacing='0'>" +
|
|
2346 buildHeadHTML() +
|
|
2347 buildBodyHTML() +
|
|
2348 "</table>";
|
|
2349
|
|
2350 return html;
|
|
2351 }
|
|
2352
|
|
2353
|
|
2354 function buildHeadHTML() {
|
|
2355 var headerClass = tm + "-widget-header";
|
|
2356 var html = '';
|
|
2357 var col;
|
|
2358 var date;
|
|
2359
|
|
2360 html += "<thead><tr>";
|
|
2361
|
|
2362 if (showWeekNumbers) {
|
|
2363 html +=
|
|
2364 "<th class='fc-week-number " + headerClass + "'>" +
|
|
2365 htmlEscape(weekNumberTitle) +
|
|
2366 "</th>";
|
|
2367 }
|
|
2368
|
|
2369 for (col=0; col<colCnt; col++) {
|
|
2370 date = cellToDate(0, col);
|
|
2371 html +=
|
|
2372 "<th class='fc-day-header fc-" + dayIDs[date.getDay()] + " " + headerClass + "'>" +
|
|
2373 htmlEscape(formatDate(date, colFormat)) +
|
|
2374 "</th>";
|
|
2375 }
|
|
2376
|
|
2377 html += "</tr></thead>";
|
|
2378
|
|
2379 return html;
|
|
2380 }
|
|
2381
|
|
2382
|
|
2383 function buildBodyHTML() {
|
|
2384 var contentClass = tm + "-widget-content";
|
|
2385 var html = '';
|
|
2386 var row;
|
|
2387 var col;
|
|
2388 var date;
|
|
2389
|
|
2390 html += "<tbody>";
|
|
2391
|
|
2392 for (row=0; row<rowCnt; row++) {
|
|
2393
|
|
2394 html += "<tr class='fc-week'>";
|
|
2395
|
|
2396 if (showWeekNumbers) {
|
|
2397 date = cellToDate(row, 0);
|
|
2398 html +=
|
|
2399 "<td class='fc-week-number " + contentClass + "'>" +
|
|
2400 "<div>" +
|
|
2401 htmlEscape(formatDate(date, weekNumberFormat)) +
|
|
2402 "</div>" +
|
|
2403 "</td>";
|
|
2404 }
|
|
2405
|
|
2406 for (col=0; col<colCnt; col++) {
|
|
2407 date = cellToDate(row, col);
|
|
2408 html += buildCellHTML(date);
|
|
2409 }
|
|
2410
|
|
2411 html += "</tr>";
|
|
2412 }
|
|
2413
|
|
2414 html += "</tbody>";
|
|
2415
|
|
2416 return html;
|
|
2417 }
|
|
2418
|
|
2419
|
|
2420 function buildCellHTML(date) {
|
|
2421 var contentClass = tm + "-widget-content";
|
|
2422 var month = t.start.getMonth();
|
|
2423 var today = clearTime(new Date());
|
|
2424 var html = '';
|
|
2425 var classNames = [
|
|
2426 'fc-day',
|
|
2427 'fc-' + dayIDs[date.getDay()],
|
|
2428 contentClass
|
|
2429 ];
|
|
2430
|
|
2431 if (date.getMonth() != month) {
|
|
2432 classNames.push('fc-other-month');
|
|
2433 }
|
|
2434 if (+date == +today) {
|
|
2435 classNames.push(
|
|
2436 'fc-today',
|
|
2437 tm + '-state-highlight'
|
|
2438 );
|
|
2439 }
|
|
2440 else if (date < today) {
|
|
2441 classNames.push('fc-past');
|
|
2442 }
|
|
2443 else {
|
|
2444 classNames.push('fc-future');
|
|
2445 }
|
|
2446
|
|
2447 html +=
|
|
2448 "<td" +
|
|
2449 " class='" + classNames.join(' ') + "'" +
|
|
2450 " data-date='" + formatDate(date, 'yyyy-MM-dd') + "'" +
|
|
2451 ">" +
|
|
2452 "<div>";
|
|
2453
|
|
2454 if (showNumbers) {
|
|
2455 html += "<div class='fc-day-number'>" + date.getDate() + "</div>";
|
|
2456 }
|
|
2457
|
|
2458 html +=
|
|
2459 "<div class='fc-day-content'>" +
|
|
2460 "<div style='position:relative'> </div>" +
|
|
2461 "</div>" +
|
|
2462 "</div>" +
|
|
2463 "</td>";
|
|
2464
|
|
2465 return html;
|
|
2466 }
|
|
2467
|
|
2468
|
|
2469
|
|
2470 /* Dimensions
|
|
2471 -----------------------------------------------------------*/
|
|
2472
|
|
2473
|
|
2474 function setHeight(height) {
|
|
2475 viewHeight = height;
|
|
2476
|
|
2477 var bodyHeight = viewHeight - head.height();
|
|
2478 var rowHeight;
|
|
2479 var rowHeightLast;
|
|
2480 var cell;
|
|
2481
|
|
2482 if (opt('weekMode') == 'variable') {
|
|
2483 rowHeight = rowHeightLast = Math.floor(bodyHeight / (rowCnt==1 ? 2 : 6));
|
|
2484 }else{
|
|
2485 rowHeight = Math.floor(bodyHeight / rowCnt);
|
|
2486 rowHeightLast = bodyHeight - rowHeight * (rowCnt-1);
|
|
2487 }
|
|
2488
|
|
2489 bodyFirstCells.each(function(i, _cell) {
|
|
2490 if (i < rowCnt) {
|
|
2491 cell = $(_cell);
|
|
2492 cell.find('> div').css(
|
|
2493 'min-height',
|
|
2494 (i==rowCnt-1 ? rowHeightLast : rowHeight) - vsides(cell)
|
|
2495 );
|
|
2496 }
|
|
2497 });
|
|
2498
|
|
2499 }
|
|
2500
|
|
2501
|
|
2502 function setWidth(width) {
|
|
2503 viewWidth = width;
|
|
2504 colPositions.clear();
|
|
2505 colContentPositions.clear();
|
|
2506
|
|
2507 weekNumberWidth = 0;
|
|
2508 if (showWeekNumbers) {
|
|
2509 weekNumberWidth = head.find('th.fc-week-number').outerWidth();
|
|
2510 }
|
|
2511
|
|
2512 colWidth = Math.floor((viewWidth - weekNumberWidth) / colCnt);
|
|
2513 setOuterWidth(headCells.slice(0, -1), colWidth);
|
|
2514 }
|
|
2515
|
|
2516
|
|
2517
|
|
2518 /* Day clicking and binding
|
|
2519 -----------------------------------------------------------*/
|
|
2520
|
|
2521
|
|
2522 function dayBind(days) {
|
|
2523 days.click(dayClick)
|
|
2524 .mousedown(daySelectionMousedown);
|
|
2525 }
|
|
2526
|
|
2527
|
|
2528 function dayClick(ev) {
|
|
2529 if (!opt('selectable')) { // if selectable, SelectionManager will worry about dayClick
|
|
2530 var date = parseISO8601($(this).data('date'));
|
|
2531 trigger('dayClick', this, date, true, ev);
|
|
2532 }
|
|
2533 }
|
|
2534
|
|
2535
|
|
2536
|
|
2537 /* Semi-transparent Overlay Helpers
|
|
2538 ------------------------------------------------------*/
|
|
2539 // TODO: should be consolidated with AgendaView's methods
|
|
2540
|
|
2541
|
|
2542 function renderDayOverlay(overlayStart, overlayEnd, refreshCoordinateGrid) { // overlayEnd is exclusive
|
|
2543
|
|
2544 if (refreshCoordinateGrid) {
|
|
2545 coordinateGrid.build();
|
|
2546 }
|
|
2547
|
|
2548 var segments = rangeToSegments(overlayStart, overlayEnd);
|
|
2549
|
|
2550 for (var i=0; i<segments.length; i++) {
|
|
2551 var segment = segments[i];
|
|
2552 dayBind(
|
|
2553 renderCellOverlay(
|
|
2554 segment.row,
|
|
2555 segment.leftCol,
|
|
2556 segment.row,
|
|
2557 segment.rightCol
|
|
2558 )
|
|
2559 );
|
|
2560 }
|
|
2561 }
|
|
2562
|
|
2563
|
|
2564 function renderCellOverlay(row0, col0, row1, col1) { // row1,col1 is inclusive
|
|
2565 var rect = coordinateGrid.rect(row0, col0, row1, col1, element);
|
|
2566 return renderOverlay(rect, element);
|
|
2567 }
|
|
2568
|
|
2569
|
|
2570
|
|
2571 /* Selection
|
|
2572 -----------------------------------------------------------------------*/
|
|
2573
|
|
2574
|
|
2575 function defaultSelectionEnd(startDate, allDay) {
|
|
2576 return cloneDate(startDate);
|
|
2577 }
|
|
2578
|
|
2579
|
|
2580 function renderSelection(startDate, endDate, allDay) {
|
|
2581 renderDayOverlay(startDate, addDays(cloneDate(endDate), 1), true); // rebuild every time???
|
|
2582 }
|
|
2583
|
|
2584
|
|
2585 function clearSelection() {
|
|
2586 clearOverlays();
|
|
2587 }
|
|
2588
|
|
2589
|
|
2590 function reportDayClick(date, allDay, ev) {
|
|
2591 var cell = dateToCell(date);
|
|
2592 var _element = bodyCells[cell.row*colCnt + cell.col];
|
|
2593 trigger('dayClick', _element, date, allDay, ev);
|
|
2594 }
|
|
2595
|
|
2596
|
|
2597
|
|
2598 /* External Dragging
|
|
2599 -----------------------------------------------------------------------*/
|
|
2600
|
|
2601
|
|
2602 function dragStart(_dragElement, ev, ui) {
|
|
2603 hoverListener.start(function(cell) {
|
|
2604 clearOverlays();
|
|
2605 if (cell) {
|
|
2606 renderCellOverlay(cell.row, cell.col, cell.row, cell.col);
|
|
2607 }
|
|
2608 }, ev);
|
|
2609 }
|
|
2610
|
|
2611
|
|
2612 function dragStop(_dragElement, ev, ui) {
|
|
2613 var cell = hoverListener.stop();
|
|
2614 clearOverlays();
|
|
2615 if (cell) {
|
|
2616 var d = cellToDate(cell);
|
|
2617 trigger('drop', _dragElement, d, true, ev, ui);
|
|
2618 }
|
|
2619 }
|
|
2620
|
|
2621
|
|
2622
|
|
2623 /* Utilities
|
|
2624 --------------------------------------------------------*/
|
|
2625
|
|
2626
|
|
2627 function defaultEventEnd(event) {
|
|
2628 return cloneDate(event.start);
|
|
2629 }
|
|
2630
|
|
2631
|
|
2632 coordinateGrid = new CoordinateGrid(function(rows, cols) {
|
|
2633 var e, n, p;
|
|
2634 headCells.each(function(i, _e) {
|
|
2635 e = $(_e);
|
|
2636 n = e.offset().left;
|
|
2637 if (i) {
|
|
2638 p[1] = n;
|
|
2639 }
|
|
2640 p = [n];
|
|
2641 cols[i] = p;
|
|
2642 });
|
|
2643 p[1] = n + e.outerWidth();
|
|
2644 bodyRows.each(function(i, _e) {
|
|
2645 if (i < rowCnt) {
|
|
2646 e = $(_e);
|
|
2647 n = e.offset().top;
|
|
2648 if (i) {
|
|
2649 p[1] = n;
|
|
2650 }
|
|
2651 p = [n];
|
|
2652 rows[i] = p;
|
|
2653 }
|
|
2654 });
|
|
2655 p[1] = n + e.outerHeight();
|
|
2656 });
|
|
2657
|
|
2658
|
|
2659 hoverListener = new HoverListener(coordinateGrid);
|
|
2660
|
|
2661 colPositions = new HorizontalPositionCache(function(col) {
|
|
2662 return firstRowCellInners.eq(col);
|
|
2663 });
|
|
2664
|
|
2665 colContentPositions = new HorizontalPositionCache(function(col) {
|
|
2666 return firstRowCellContentInners.eq(col);
|
|
2667 });
|
|
2668
|
|
2669
|
|
2670 function colLeft(col) {
|
|
2671 return colPositions.left(col);
|
|
2672 }
|
|
2673
|
|
2674
|
|
2675 function colRight(col) {
|
|
2676 return colPositions.right(col);
|
|
2677 }
|
|
2678
|
|
2679
|
|
2680 function colContentLeft(col) {
|
|
2681 return colContentPositions.left(col);
|
|
2682 }
|
|
2683
|
|
2684
|
|
2685 function colContentRight(col) {
|
|
2686 return colContentPositions.right(col);
|
|
2687 }
|
|
2688
|
|
2689
|
|
2690 function allDayRow(i) {
|
|
2691 return bodyRows.eq(i);
|
|
2692 }
|
|
2693
|
|
2694 }
|
|
2695
|
|
2696 ;;
|
|
2697
|
|
2698 function BasicEventRenderer() {
|
|
2699 var t = this;
|
|
2700
|
|
2701
|
|
2702 // exports
|
|
2703 t.renderEvents = renderEvents;
|
|
2704 t.clearEvents = clearEvents;
|
|
2705
|
|
2706
|
|
2707 // imports
|
|
2708 DayEventRenderer.call(t);
|
|
2709
|
|
2710
|
|
2711 function renderEvents(events, modifiedEventId) {
|
|
2712 t.renderDayEvents(events, modifiedEventId);
|
|
2713 }
|
|
2714
|
|
2715
|
|
2716 function clearEvents() {
|
|
2717 t.getDaySegmentContainer().empty();
|
|
2718 }
|
|
2719
|
|
2720
|
|
2721 // TODO: have this class (and AgendaEventRenderer) be responsible for creating the event container div
|
|
2722
|
|
2723 }
|
|
2724
|
|
2725 ;;
|
|
2726
|
|
2727 fcViews.agendaWeek = AgendaWeekView;
|
|
2728
|
|
2729 function AgendaWeekView(element, calendar) {
|
|
2730 var t = this;
|
|
2731
|
|
2732
|
|
2733 // exports
|
|
2734 t.render = render;
|
|
2735
|
|
2736
|
|
2737 // imports
|
|
2738 AgendaView.call(t, element, calendar, 'agendaWeek');
|
|
2739 var opt = t.opt;
|
|
2740 var renderAgenda = t.renderAgenda;
|
|
2741 var skipHiddenDays = t.skipHiddenDays;
|
|
2742 var getCellsPerWeek = t.getCellsPerWeek;
|
|
2743 var formatDates = calendar.formatDates;
|
|
2744
|
|
2745
|
|
2746 function render(date, delta) {
|
|
2747
|
|
2748 if (delta) {
|
|
2749 addDays(date, delta * 7);
|
|
2750 }
|
|
2751
|
|
2752 var start = addDays(cloneDate(date), -((date.getDay() - opt('firstDay') + 7) % 7));
|
|
2753 var end = addDays(cloneDate(start), 7);
|
|
2754
|
|
2755 var visStart = cloneDate(start);
|
|
2756 skipHiddenDays(visStart);
|
|
2757
|
|
2758 var visEnd = cloneDate(end);
|
|
2759 skipHiddenDays(visEnd, -1, true);
|
|
2760
|
|
2761 var colCnt = getCellsPerWeek();
|
|
2762
|
|
2763 t.title = formatDates(
|
|
2764 visStart,
|
|
2765 addDays(cloneDate(visEnd), -1),
|
|
2766 opt('titleFormat')
|
|
2767 );
|
|
2768
|
|
2769 t.start = start;
|
|
2770 t.end = end;
|
|
2771 t.visStart = visStart;
|
|
2772 t.visEnd = visEnd;
|
|
2773
|
|
2774 renderAgenda(colCnt);
|
|
2775 }
|
|
2776
|
|
2777 }
|
|
2778
|
|
2779 ;;
|
|
2780
|
|
2781 fcViews.agendaDay = AgendaDayView;
|
|
2782
|
|
2783
|
|
2784 function AgendaDayView(element, calendar) {
|
|
2785 var t = this;
|
|
2786
|
|
2787
|
|
2788 // exports
|
|
2789 t.render = render;
|
|
2790
|
|
2791
|
|
2792 // imports
|
|
2793 AgendaView.call(t, element, calendar, 'agendaDay');
|
|
2794 var opt = t.opt;
|
|
2795 var renderAgenda = t.renderAgenda;
|
|
2796 var skipHiddenDays = t.skipHiddenDays;
|
|
2797 var formatDate = calendar.formatDate;
|
|
2798
|
|
2799
|
|
2800 function render(date, delta) {
|
|
2801
|
|
2802 if (delta) {
|
|
2803 addDays(date, delta);
|
|
2804 }
|
|
2805 skipHiddenDays(date, delta < 0 ? -1 : 1);
|
|
2806
|
|
2807 var start = cloneDate(date, true);
|
|
2808 var end = addDays(cloneDate(start), 1);
|
|
2809
|
|
2810 t.title = formatDate(date, opt('titleFormat'));
|
|
2811
|
|
2812 t.start = t.visStart = start;
|
|
2813 t.end = t.visEnd = end;
|
|
2814
|
|
2815 renderAgenda(1);
|
|
2816 }
|
|
2817
|
|
2818
|
|
2819 }
|
|
2820
|
|
2821 ;;
|
|
2822
|
|
2823 setDefaults({
|
|
2824 allDaySlot: true,
|
|
2825 allDayText: 'all-day',
|
|
2826 firstHour: 6,
|
|
2827 slotMinutes: 30,
|
|
2828 defaultEventMinutes: 120,
|
|
2829 axisFormat: 'h(:mm)tt',
|
|
2830 timeFormat: {
|
|
2831 agenda: 'h:mm{ - h:mm}'
|
|
2832 },
|
|
2833 dragOpacity: {
|
|
2834 agenda: .5
|
|
2835 },
|
|
2836 minTime: 0,
|
|
2837 maxTime: 24,
|
|
2838 slotEventOverlap: true
|
|
2839 });
|
|
2840
|
|
2841
|
|
2842 // TODO: make it work in quirks mode (event corners, all-day height)
|
|
2843 // TODO: test liquid width, especially in IE6
|
|
2844
|
|
2845
|
|
2846 function AgendaView(element, calendar, viewName) {
|
|
2847 var t = this;
|
|
2848
|
|
2849
|
|
2850 // exports
|
|
2851 t.renderAgenda = renderAgenda;
|
|
2852 t.setWidth = setWidth;
|
|
2853 t.setHeight = setHeight;
|
|
2854 t.afterRender = afterRender;
|
|
2855 t.defaultEventEnd = defaultEventEnd;
|
|
2856 t.timePosition = timePosition;
|
|
2857 t.getIsCellAllDay = getIsCellAllDay;
|
|
2858 t.allDayRow = getAllDayRow;
|
|
2859 t.getCoordinateGrid = function() { return coordinateGrid }; // specifically for AgendaEventRenderer
|
|
2860 t.getHoverListener = function() { return hoverListener };
|
|
2861 t.colLeft = colLeft;
|
|
2862 t.colRight = colRight;
|
|
2863 t.colContentLeft = colContentLeft;
|
|
2864 t.colContentRight = colContentRight;
|
|
2865 t.getDaySegmentContainer = function() { return daySegmentContainer };
|
|
2866 t.getSlotSegmentContainer = function() { return slotSegmentContainer };
|
|
2867 t.getMinMinute = function() { return minMinute };
|
|
2868 t.getMaxMinute = function() { return maxMinute };
|
|
2869 t.getSlotContainer = function() { return slotContainer };
|
|
2870 t.getRowCnt = function() { return 1 };
|
|
2871 t.getColCnt = function() { return colCnt };
|
|
2872 t.getColWidth = function() { return colWidth };
|
|
2873 t.getSnapHeight = function() { return snapHeight };
|
|
2874 t.getSnapMinutes = function() { return snapMinutes };
|
|
2875 t.defaultSelectionEnd = defaultSelectionEnd;
|
|
2876 t.renderDayOverlay = renderDayOverlay;
|
|
2877 t.renderSelection = renderSelection;
|
|
2878 t.clearSelection = clearSelection;
|
|
2879 t.reportDayClick = reportDayClick; // selection mousedown hack
|
|
2880 t.dragStart = dragStart;
|
|
2881 t.dragStop = dragStop;
|
|
2882
|
|
2883
|
|
2884 // imports
|
|
2885 View.call(t, element, calendar, viewName);
|
|
2886 OverlayManager.call(t);
|
|
2887 SelectionManager.call(t);
|
|
2888 AgendaEventRenderer.call(t);
|
|
2889 var opt = t.opt;
|
|
2890 var trigger = t.trigger;
|
|
2891 var renderOverlay = t.renderOverlay;
|
|
2892 var clearOverlays = t.clearOverlays;
|
|
2893 var reportSelection = t.reportSelection;
|
|
2894 var unselect = t.unselect;
|
|
2895 var daySelectionMousedown = t.daySelectionMousedown;
|
|
2896 var slotSegHtml = t.slotSegHtml;
|
|
2897 var cellToDate = t.cellToDate;
|
|
2898 var dateToCell = t.dateToCell;
|
|
2899 var rangeToSegments = t.rangeToSegments;
|
|
2900 var formatDate = calendar.formatDate;
|
|
2901
|
|
2902
|
|
2903 // locals
|
|
2904
|
|
2905 var dayTable;
|
|
2906 var dayHead;
|
|
2907 var dayHeadCells;
|
|
2908 var dayBody;
|
|
2909 var dayBodyCells;
|
|
2910 var dayBodyCellInners;
|
|
2911 var dayBodyCellContentInners;
|
|
2912 var dayBodyFirstCell;
|
|
2913 var dayBodyFirstCellStretcher;
|
|
2914 var slotLayer;
|
|
2915 var daySegmentContainer;
|
|
2916 var allDayTable;
|
|
2917 var allDayRow;
|
|
2918 var slotScroller;
|
|
2919 var slotContainer;
|
|
2920 var slotSegmentContainer;
|
|
2921 var slotTable;
|
|
2922 var selectionHelper;
|
|
2923
|
|
2924 var viewWidth;
|
|
2925 var viewHeight;
|
|
2926 var axisWidth;
|
|
2927 var colWidth;
|
|
2928 var gutterWidth;
|
|
2929 var slotHeight; // TODO: what if slotHeight changes? (see issue 650)
|
|
2930
|
|
2931 var snapMinutes;
|
|
2932 var snapRatio; // ratio of number of "selection" slots to normal slots. (ex: 1, 2, 4)
|
|
2933 var snapHeight; // holds the pixel hight of a "selection" slot
|
|
2934
|
|
2935 var colCnt;
|
|
2936 var slotCnt;
|
|
2937 var coordinateGrid;
|
|
2938 var hoverListener;
|
|
2939 var colPositions;
|
|
2940 var colContentPositions;
|
|
2941 var slotTopCache = {};
|
|
2942
|
|
2943 var tm;
|
|
2944 var rtl;
|
|
2945 var minMinute, maxMinute;
|
|
2946 var colFormat;
|
|
2947 var showWeekNumbers;
|
|
2948 var weekNumberTitle;
|
|
2949 var weekNumberFormat;
|
|
2950
|
|
2951
|
|
2952
|
|
2953 /* Rendering
|
|
2954 -----------------------------------------------------------------------------*/
|
|
2955
|
|
2956
|
|
2957 disableTextSelection(element.addClass('fc-agenda'));
|
|
2958
|
|
2959
|
|
2960 function renderAgenda(c) {
|
|
2961 colCnt = c;
|
|
2962 updateOptions();
|
|
2963
|
|
2964 if (!dayTable) { // first time rendering?
|
|
2965 buildSkeleton(); // builds day table, slot area, events containers
|
|
2966 }
|
|
2967 else {
|
|
2968 buildDayTable(); // rebuilds day table
|
|
2969 }
|
|
2970 }
|
|
2971
|
|
2972
|
|
2973 function updateOptions() {
|
|
2974
|
|
2975 tm = opt('theme') ? 'ui' : 'fc';
|
|
2976 rtl = opt('isRTL')
|
|
2977 minMinute = parseTime(opt('minTime'));
|
|
2978 maxMinute = parseTime(opt('maxTime'));
|
|
2979 colFormat = opt('columnFormat');
|
|
2980
|
|
2981 // week # options. (TODO: bad, logic also in other views)
|
|
2982 showWeekNumbers = opt('weekNumbers');
|
|
2983 weekNumberTitle = opt('weekNumberTitle');
|
|
2984 if (opt('weekNumberCalculation') != 'iso') {
|
|
2985 weekNumberFormat = "w";
|
|
2986 }
|
|
2987 else {
|
|
2988 weekNumberFormat = "W";
|
|
2989 }
|
|
2990
|
|
2991 snapMinutes = opt('snapMinutes') || opt('slotMinutes');
|
|
2992 }
|
|
2993
|
|
2994
|
|
2995
|
|
2996 /* Build DOM
|
|
2997 -----------------------------------------------------------------------*/
|
|
2998
|
|
2999
|
|
3000 function buildSkeleton() {
|
|
3001 var headerClass = tm + "-widget-header";
|
|
3002 var contentClass = tm + "-widget-content";
|
|
3003 var s;
|
|
3004 var d;
|
|
3005 var i;
|
|
3006 var maxd;
|
|
3007 var minutes;
|
|
3008 var slotNormal = opt('slotMinutes') % 15 == 0;
|
|
3009
|
|
3010 buildDayTable();
|
|
3011
|
|
3012 slotLayer =
|
|
3013 $("<div style='position:absolute;z-index:2;left:0;width:100%'/>")
|
|
3014 .appendTo(element);
|
|
3015
|
|
3016 if (opt('allDaySlot')) {
|
|
3017
|
|
3018 daySegmentContainer =
|
|
3019 $("<div class='fc-event-container' style='position:absolute;z-index:8;top:0;left:0'/>")
|
|
3020 .appendTo(slotLayer);
|
|
3021
|
|
3022 s =
|
|
3023 "<table style='width:100%' class='fc-agenda-allday' cellspacing='0'>" +
|
|
3024 "<tr>" +
|
|
3025 "<th class='" + headerClass + " fc-agenda-axis'>" + opt('allDayText') + "</th>" +
|
|
3026 "<td>" +
|
|
3027 "<div class='fc-day-content'><div style='position:relative'/></div>" +
|
|
3028 "</td>" +
|
|
3029 "<th class='" + headerClass + " fc-agenda-gutter'> </th>" +
|
|
3030 "</tr>" +
|
|
3031 "</table>";
|
|
3032 allDayTable = $(s).appendTo(slotLayer);
|
|
3033 allDayRow = allDayTable.find('tr');
|
|
3034
|
|
3035 dayBind(allDayRow.find('td'));
|
|
3036
|
|
3037 slotLayer.append(
|
|
3038 "<div class='fc-agenda-divider " + headerClass + "'>" +
|
|
3039 "<div class='fc-agenda-divider-inner'/>" +
|
|
3040 "</div>"
|
|
3041 );
|
|
3042
|
|
3043 }else{
|
|
3044
|
|
3045 daySegmentContainer = $([]); // in jQuery 1.4, we can just do $()
|
|
3046
|
|
3047 }
|
|
3048
|
|
3049 slotScroller =
|
|
3050 $("<div style='position:absolute;width:100%;overflow-x:hidden;overflow-y:auto'/>")
|
|
3051 .appendTo(slotLayer);
|
|
3052
|
|
3053 slotContainer =
|
|
3054 $("<div style='position:relative;width:100%;overflow:hidden'/>")
|
|
3055 .appendTo(slotScroller);
|
|
3056
|
|
3057 slotSegmentContainer =
|
|
3058 $("<div class='fc-event-container' style='position:absolute;z-index:8;top:0;left:0'/>")
|
|
3059 .appendTo(slotContainer);
|
|
3060
|
|
3061 s =
|
|
3062 "<table class='fc-agenda-slots' style='width:100%' cellspacing='0'>" +
|
|
3063 "<tbody>";
|
|
3064 d = zeroDate();
|
|
3065 maxd = addMinutes(cloneDate(d), maxMinute);
|
|
3066 addMinutes(d, minMinute);
|
|
3067 slotCnt = 0;
|
|
3068 for (i=0; d < maxd; i++) {
|
|
3069 minutes = d.getMinutes();
|
|
3070 s +=
|
|
3071 "<tr class='fc-slot" + i + ' ' + (!minutes ? '' : 'fc-minor') + "'>" +
|
|
3072 "<th class='fc-agenda-axis " + headerClass + "'>" +
|
|
3073 ((!slotNormal || !minutes) ? formatDate(d, opt('axisFormat')) : ' ') +
|
|
3074 "</th>" +
|
|
3075 "<td class='" + contentClass + "'>" +
|
|
3076 "<div style='position:relative'> </div>" +
|
|
3077 "</td>" +
|
|
3078 "</tr>";
|
|
3079 addMinutes(d, opt('slotMinutes'));
|
|
3080 slotCnt++;
|
|
3081 }
|
|
3082 s +=
|
|
3083 "</tbody>" +
|
|
3084 "</table>";
|
|
3085 slotTable = $(s).appendTo(slotContainer);
|
|
3086
|
|
3087 slotBind(slotTable.find('td'));
|
|
3088 }
|
|
3089
|
|
3090
|
|
3091
|
|
3092 /* Build Day Table
|
|
3093 -----------------------------------------------------------------------*/
|
|
3094
|
|
3095
|
|
3096 function buildDayTable() {
|
|
3097 var html = buildDayTableHTML();
|
|
3098
|
|
3099 if (dayTable) {
|
|
3100 dayTable.remove();
|
|
3101 }
|
|
3102 dayTable = $(html).appendTo(element);
|
|
3103
|
|
3104 dayHead = dayTable.find('thead');
|
|
3105 dayHeadCells = dayHead.find('th').slice(1, -1); // exclude gutter
|
|
3106 dayBody = dayTable.find('tbody');
|
|
3107 dayBodyCells = dayBody.find('td').slice(0, -1); // exclude gutter
|
|
3108 dayBodyCellInners = dayBodyCells.find('> div');
|
|
3109 dayBodyCellContentInners = dayBodyCells.find('.fc-day-content > div');
|
|
3110
|
|
3111 dayBodyFirstCell = dayBodyCells.eq(0);
|
|
3112 dayBodyFirstCellStretcher = dayBodyCellInners.eq(0);
|
|
3113
|
|
3114 markFirstLast(dayHead.add(dayHead.find('tr')));
|
|
3115 markFirstLast(dayBody.add(dayBody.find('tr')));
|
|
3116
|
|
3117 // TODO: now that we rebuild the cells every time, we should call dayRender
|
|
3118 }
|
|
3119
|
|
3120
|
|
3121 function buildDayTableHTML() {
|
|
3122 var html =
|
|
3123 "<table style='width:100%' class='fc-agenda-days fc-border-separate' cellspacing='0'>" +
|
|
3124 buildDayTableHeadHTML() +
|
|
3125 buildDayTableBodyHTML() +
|
|
3126 "</table>";
|
|
3127
|
|
3128 return html;
|
|
3129 }
|
|
3130
|
|
3131
|
|
3132 function buildDayTableHeadHTML() {
|
|
3133 var headerClass = tm + "-widget-header";
|
|
3134 var date;
|
|
3135 var html = '';
|
|
3136 var weekText;
|
|
3137 var col;
|
|
3138
|
|
3139 html +=
|
|
3140 "<thead>" +
|
|
3141 "<tr>";
|
|
3142
|
|
3143 if (showWeekNumbers) {
|
|
3144 date = cellToDate(0, 0);
|
|
3145 weekText = formatDate(date, weekNumberFormat);
|
|
3146 if (rtl) {
|
|
3147 weekText += weekNumberTitle;
|
|
3148 }
|
|
3149 else {
|
|
3150 weekText = weekNumberTitle + weekText;
|
|
3151 }
|
|
3152 html +=
|
|
3153 "<th class='fc-agenda-axis fc-week-number " + headerClass + "'>" +
|
|
3154 htmlEscape(weekText) +
|
|
3155 "</th>";
|
|
3156 }
|
|
3157 else {
|
|
3158 html += "<th class='fc-agenda-axis " + headerClass + "'> </th>";
|
|
3159 }
|
|
3160
|
|
3161 for (col=0; col<colCnt; col++) {
|
|
3162 date = cellToDate(0, col);
|
|
3163 html +=
|
|
3164 "<th class='fc-" + dayIDs[date.getDay()] + " fc-col" + col + ' ' + headerClass + "'>" +
|
|
3165 htmlEscape(formatDate(date, colFormat)) +
|
|
3166 "</th>";
|
|
3167 }
|
|
3168
|
|
3169 html +=
|
|
3170 "<th class='fc-agenda-gutter " + headerClass + "'> </th>" +
|
|
3171 "</tr>" +
|
|
3172 "</thead>";
|
|
3173
|
|
3174 return html;
|
|
3175 }
|
|
3176
|
|
3177
|
|
3178 function buildDayTableBodyHTML() {
|
|
3179 var headerClass = tm + "-widget-header"; // TODO: make these when updateOptions() called
|
|
3180 var contentClass = tm + "-widget-content";
|
|
3181 var date;
|
|
3182 var today = clearTime(new Date());
|
|
3183 var col;
|
|
3184 var cellsHTML;
|
|
3185 var cellHTML;
|
|
3186 var classNames;
|
|
3187 var html = '';
|
|
3188
|
|
3189 html +=
|
|
3190 "<tbody>" +
|
|
3191 "<tr>" +
|
|
3192 "<th class='fc-agenda-axis " + headerClass + "'> </th>";
|
|
3193
|
|
3194 cellsHTML = '';
|
|
3195
|
|
3196 for (col=0; col<colCnt; col++) {
|
|
3197
|
|
3198 date = cellToDate(0, col);
|
|
3199
|
|
3200 classNames = [
|
|
3201 'fc-col' + col,
|
|
3202 'fc-' + dayIDs[date.getDay()],
|
|
3203 contentClass
|
|
3204 ];
|
|
3205 if (+date == +today) {
|
|
3206 classNames.push(
|
|
3207 tm + '-state-highlight',
|
|
3208 'fc-today'
|
|
3209 );
|
|
3210 }
|
|
3211 else if (date < today) {
|
|
3212 classNames.push('fc-past');
|
|
3213 }
|
|
3214 else {
|
|
3215 classNames.push('fc-future');
|
|
3216 }
|
|
3217
|
|
3218 cellHTML =
|
|
3219 "<td class='" + classNames.join(' ') + "'>" +
|
|
3220 "<div>" +
|
|
3221 "<div class='fc-day-content'>" +
|
|
3222 "<div style='position:relative'> </div>" +
|
|
3223 "</div>" +
|
|
3224 "</div>" +
|
|
3225 "</td>";
|
|
3226
|
|
3227 cellsHTML += cellHTML;
|
|
3228 }
|
|
3229
|
|
3230 html += cellsHTML;
|
|
3231 html +=
|
|
3232 "<td class='fc-agenda-gutter " + contentClass + "'> </td>" +
|
|
3233 "</tr>" +
|
|
3234 "</tbody>";
|
|
3235
|
|
3236 return html;
|
|
3237 }
|
|
3238
|
|
3239
|
|
3240 // TODO: data-date on the cells
|
|
3241
|
|
3242
|
|
3243
|
|
3244 /* Dimensions
|
|
3245 -----------------------------------------------------------------------*/
|
|
3246
|
|
3247
|
|
3248 function setHeight(height) {
|
|
3249 if (height === undefined) {
|
|
3250 height = viewHeight;
|
|
3251 }
|
|
3252 viewHeight = height;
|
|
3253 slotTopCache = {};
|
|
3254
|
|
3255 var headHeight = dayBody.position().top;
|
|
3256 var allDayHeight = slotScroller.position().top; // including divider
|
|
3257 var bodyHeight = Math.min( // total body height, including borders
|
|
3258 height - headHeight, // when scrollbars
|
|
3259 slotTable.height() + allDayHeight + 1 // when no scrollbars. +1 for bottom border
|
|
3260 );
|
|
3261
|
|
3262 dayBodyFirstCellStretcher
|
|
3263 .height(bodyHeight - vsides(dayBodyFirstCell));
|
|
3264
|
|
3265 slotLayer.css('top', headHeight);
|
|
3266
|
|
3267 slotScroller.height(bodyHeight - allDayHeight - 1);
|
|
3268
|
|
3269 // the stylesheet guarantees that the first row has no border.
|
|
3270 // this allows .height() to work well cross-browser.
|
|
3271 slotHeight = slotTable.find('tr:first').height() + 1; // +1 for bottom border
|
|
3272
|
|
3273 snapRatio = opt('slotMinutes') / snapMinutes;
|
|
3274 snapHeight = slotHeight / snapRatio;
|
|
3275 }
|
|
3276
|
|
3277
|
|
3278 function setWidth(width) {
|
|
3279 viewWidth = width;
|
|
3280 colPositions.clear();
|
|
3281 colContentPositions.clear();
|
|
3282
|
|
3283 var axisFirstCells = dayHead.find('th:first');
|
|
3284 if (allDayTable) {
|
|
3285 axisFirstCells = axisFirstCells.add(allDayTable.find('th:first'));
|
|
3286 }
|
|
3287 axisFirstCells = axisFirstCells.add(slotTable.find('th:first'));
|
|
3288
|
|
3289 axisWidth = 0;
|
|
3290 setOuterWidth(
|
|
3291 axisFirstCells
|
|
3292 .width('')
|
|
3293 .each(function(i, _cell) {
|
|
3294 axisWidth = Math.max(axisWidth, $(_cell).outerWidth());
|
|
3295 }),
|
|
3296 axisWidth
|
|
3297 );
|
|
3298
|
|
3299 var gutterCells = dayTable.find('.fc-agenda-gutter');
|
|
3300 if (allDayTable) {
|
|
3301 gutterCells = gutterCells.add(allDayTable.find('th.fc-agenda-gutter'));
|
|
3302 }
|
|
3303
|
|
3304 var slotTableWidth = slotScroller[0].clientWidth; // needs to be done after axisWidth (for IE7)
|
|
3305
|
|
3306 gutterWidth = slotScroller.width() - slotTableWidth;
|
|
3307 if (gutterWidth) {
|
|
3308 setOuterWidth(gutterCells, gutterWidth);
|
|
3309 gutterCells
|
|
3310 .show()
|
|
3311 .prev()
|
|
3312 .removeClass('fc-last');
|
|
3313 }else{
|
|
3314 gutterCells
|
|
3315 .hide()
|
|
3316 .prev()
|
|
3317 .addClass('fc-last');
|
|
3318 }
|
|
3319
|
|
3320 colWidth = Math.floor((slotTableWidth - axisWidth) / colCnt);
|
|
3321 setOuterWidth(dayHeadCells.slice(0, -1), colWidth);
|
|
3322 }
|
|
3323
|
|
3324
|
|
3325
|
|
3326 /* Scrolling
|
|
3327 -----------------------------------------------------------------------*/
|
|
3328
|
|
3329
|
|
3330 function resetScroll() {
|
|
3331 var d0 = zeroDate();
|
|
3332 var scrollDate = cloneDate(d0);
|
|
3333 scrollDate.setHours(opt('firstHour'));
|
|
3334 var top = timePosition(d0, scrollDate) + 1; // +1 for the border
|
|
3335 function scroll() {
|
|
3336 slotScroller.scrollTop(top);
|
|
3337 }
|
|
3338 scroll();
|
|
3339 setTimeout(scroll, 0); // overrides any previous scroll state made by the browser
|
|
3340 }
|
|
3341
|
|
3342
|
|
3343 function afterRender() { // after the view has been freshly rendered and sized
|
|
3344 resetScroll();
|
|
3345 }
|
|
3346
|
|
3347
|
|
3348
|
|
3349 /* Slot/Day clicking and binding
|
|
3350 -----------------------------------------------------------------------*/
|
|
3351
|
|
3352
|
|
3353 function dayBind(cells) {
|
|
3354 cells.click(slotClick)
|
|
3355 .mousedown(daySelectionMousedown);
|
|
3356 }
|
|
3357
|
|
3358
|
|
3359 function slotBind(cells) {
|
|
3360 cells.click(slotClick)
|
|
3361 .mousedown(slotSelectionMousedown);
|
|
3362 }
|
|
3363
|
|
3364
|
|
3365 function slotClick(ev) {
|
|
3366 if (!opt('selectable')) { // if selectable, SelectionManager will worry about dayClick
|
|
3367 var col = Math.min(colCnt-1, Math.floor((ev.pageX - dayTable.offset().left - axisWidth) / colWidth));
|
|
3368 var date = cellToDate(0, col);
|
|
3369 var rowMatch = this.parentNode.className.match(/fc-slot(\d+)/); // TODO: maybe use data
|
|
3370 if (rowMatch) {
|
|
3371 var mins = parseInt(rowMatch[1]) * opt('slotMinutes');
|
|
3372 var hours = Math.floor(mins/60);
|
|
3373 date.setHours(hours);
|
|
3374 date.setMinutes(mins%60 + minMinute);
|
|
3375 trigger('dayClick', dayBodyCells[col], date, false, ev);
|
|
3376 }else{
|
|
3377 trigger('dayClick', dayBodyCells[col], date, true, ev);
|
|
3378 }
|
|
3379 }
|
|
3380 }
|
|
3381
|
|
3382
|
|
3383
|
|
3384 /* Semi-transparent Overlay Helpers
|
|
3385 -----------------------------------------------------*/
|
|
3386 // TODO: should be consolidated with BasicView's methods
|
|
3387
|
|
3388
|
|
3389 function renderDayOverlay(overlayStart, overlayEnd, refreshCoordinateGrid) { // overlayEnd is exclusive
|
|
3390
|
|
3391 if (refreshCoordinateGrid) {
|
|
3392 coordinateGrid.build();
|
|
3393 }
|
|
3394
|
|
3395 var segments = rangeToSegments(overlayStart, overlayEnd);
|
|
3396
|
|
3397 for (var i=0; i<segments.length; i++) {
|
|
3398 var segment = segments[i];
|
|
3399 dayBind(
|
|
3400 renderCellOverlay(
|
|
3401 segment.row,
|
|
3402 segment.leftCol,
|
|
3403 segment.row,
|
|
3404 segment.rightCol
|
|
3405 )
|
|
3406 );
|
|
3407 }
|
|
3408 }
|
|
3409
|
|
3410
|
|
3411 function renderCellOverlay(row0, col0, row1, col1) { // only for all-day?
|
|
3412 var rect = coordinateGrid.rect(row0, col0, row1, col1, slotLayer);
|
|
3413 return renderOverlay(rect, slotLayer);
|
|
3414 }
|
|
3415
|
|
3416
|
|
3417 function renderSlotOverlay(overlayStart, overlayEnd) {
|
|
3418 for (var i=0; i<colCnt; i++) {
|
|
3419 var dayStart = cellToDate(0, i);
|
|
3420 var dayEnd = addDays(cloneDate(dayStart), 1);
|
|
3421 var stretchStart = new Date(Math.max(dayStart, overlayStart));
|
|
3422 var stretchEnd = new Date(Math.min(dayEnd, overlayEnd));
|
|
3423 if (stretchStart < stretchEnd) {
|
|
3424 var rect = coordinateGrid.rect(0, i, 0, i, slotContainer); // only use it for horizontal coords
|
|
3425 var top = timePosition(dayStart, stretchStart);
|
|
3426 var bottom = timePosition(dayStart, stretchEnd);
|
|
3427 rect.top = top;
|
|
3428 rect.height = bottom - top;
|
|
3429 slotBind(
|
|
3430 renderOverlay(rect, slotContainer)
|
|
3431 );
|
|
3432 }
|
|
3433 }
|
|
3434 }
|
|
3435
|
|
3436
|
|
3437
|
|
3438 /* Coordinate Utilities
|
|
3439 -----------------------------------------------------------------------------*/
|
|
3440
|
|
3441
|
|
3442 coordinateGrid = new CoordinateGrid(function(rows, cols) {
|
|
3443 var e, n, p;
|
|
3444 dayHeadCells.each(function(i, _e) {
|
|
3445 e = $(_e);
|
|
3446 n = e.offset().left;
|
|
3447 if (i) {
|
|
3448 p[1] = n;
|
|
3449 }
|
|
3450 p = [n];
|
|
3451 cols[i] = p;
|
|
3452 });
|
|
3453 p[1] = n + e.outerWidth();
|
|
3454 if (opt('allDaySlot')) {
|
|
3455 e = allDayRow;
|
|
3456 n = e.offset().top;
|
|
3457 rows[0] = [n, n+e.outerHeight()];
|
|
3458 }
|
|
3459 var slotTableTop = slotContainer.offset().top;
|
|
3460 var slotScrollerTop = slotScroller.offset().top;
|
|
3461 var slotScrollerBottom = slotScrollerTop + slotScroller.outerHeight();
|
|
3462 function constrain(n) {
|
|
3463 return Math.max(slotScrollerTop, Math.min(slotScrollerBottom, n));
|
|
3464 }
|
|
3465 for (var i=0; i<slotCnt*snapRatio; i++) { // adapt slot count to increased/decreased selection slot count
|
|
3466 rows.push([
|
|
3467 constrain(slotTableTop + snapHeight*i),
|
|
3468 constrain(slotTableTop + snapHeight*(i+1))
|
|
3469 ]);
|
|
3470 }
|
|
3471 });
|
|
3472
|
|
3473
|
|
3474 hoverListener = new HoverListener(coordinateGrid);
|
|
3475
|
|
3476 colPositions = new HorizontalPositionCache(function(col) {
|
|
3477 return dayBodyCellInners.eq(col);
|
|
3478 });
|
|
3479
|
|
3480 colContentPositions = new HorizontalPositionCache(function(col) {
|
|
3481 return dayBodyCellContentInners.eq(col);
|
|
3482 });
|
|
3483
|
|
3484
|
|
3485 function colLeft(col) {
|
|
3486 return colPositions.left(col);
|
|
3487 }
|
|
3488
|
|
3489
|
|
3490 function colContentLeft(col) {
|
|
3491 return colContentPositions.left(col);
|
|
3492 }
|
|
3493
|
|
3494
|
|
3495 function colRight(col) {
|
|
3496 return colPositions.right(col);
|
|
3497 }
|
|
3498
|
|
3499
|
|
3500 function colContentRight(col) {
|
|
3501 return colContentPositions.right(col);
|
|
3502 }
|
|
3503
|
|
3504
|
|
3505 function getIsCellAllDay(cell) {
|
|
3506 return opt('allDaySlot') && !cell.row;
|
|
3507 }
|
|
3508
|
|
3509
|
|
3510 function realCellToDate(cell) { // ugh "real" ... but blame it on our abuse of the "cell" system
|
|
3511 var d = cellToDate(0, cell.col);
|
|
3512 var slotIndex = cell.row;
|
|
3513 if (opt('allDaySlot')) {
|
|
3514 slotIndex--;
|
|
3515 }
|
|
3516 if (slotIndex >= 0) {
|
|
3517 addMinutes(d, minMinute + slotIndex * snapMinutes);
|
|
3518 }
|
|
3519 return d;
|
|
3520 }
|
|
3521
|
|
3522
|
|
3523 // get the Y coordinate of the given time on the given day (both Date objects)
|
|
3524 function timePosition(day, time) { // both date objects. day holds 00:00 of current day
|
|
3525 day = cloneDate(day, true);
|
|
3526 if (time < addMinutes(cloneDate(day), minMinute)) {
|
|
3527 return 0;
|
|
3528 }
|
|
3529 if (time >= addMinutes(cloneDate(day), maxMinute)) {
|
|
3530 return slotTable.height();
|
|
3531 }
|
|
3532 var slotMinutes = opt('slotMinutes'),
|
|
3533 minutes = time.getHours()*60 + time.getMinutes() - minMinute,
|
|
3534 slotI = Math.floor(minutes / slotMinutes),
|
|
3535 slotTop = slotTopCache[slotI];
|
|
3536 if (slotTop === undefined) {
|
|
3537 slotTop = slotTopCache[slotI] =
|
|
3538 slotTable.find('tr').eq(slotI).find('td div')[0].offsetTop;
|
|
3539 // .eq() is faster than ":eq()" selector
|
|
3540 // [0].offsetTop is faster than .position().top (do we really need this optimization?)
|
|
3541 // a better optimization would be to cache all these divs
|
|
3542 }
|
|
3543 return Math.max(0, Math.round(
|
|
3544 slotTop - 1 + slotHeight * ((minutes % slotMinutes) / slotMinutes)
|
|
3545 ));
|
|
3546 }
|
|
3547
|
|
3548
|
|
3549 function getAllDayRow(index) {
|
|
3550 return allDayRow;
|
|
3551 }
|
|
3552
|
|
3553
|
|
3554 function defaultEventEnd(event) {
|
|
3555 var start = cloneDate(event.start);
|
|
3556 if (event.allDay) {
|
|
3557 return start;
|
|
3558 }
|
|
3559 return addMinutes(start, opt('defaultEventMinutes'));
|
|
3560 }
|
|
3561
|
|
3562
|
|
3563
|
|
3564 /* Selection
|
|
3565 ---------------------------------------------------------------------------------*/
|
|
3566
|
|
3567
|
|
3568 function defaultSelectionEnd(startDate, allDay) {
|
|
3569 if (allDay) {
|
|
3570 return cloneDate(startDate);
|
|
3571 }
|
|
3572 return addMinutes(cloneDate(startDate), opt('slotMinutes'));
|
|
3573 }
|
|
3574
|
|
3575
|
|
3576 function renderSelection(startDate, endDate, allDay) { // only for all-day
|
|
3577 if (allDay) {
|
|
3578 if (opt('allDaySlot')) {
|
|
3579 renderDayOverlay(startDate, addDays(cloneDate(endDate), 1), true);
|
|
3580 }
|
|
3581 }else{
|
|
3582 renderSlotSelection(startDate, endDate);
|
|
3583 }
|
|
3584 }
|
|
3585
|
|
3586
|
|
3587 function renderSlotSelection(startDate, endDate) {
|
|
3588 var helperOption = opt('selectHelper');
|
|
3589 coordinateGrid.build();
|
|
3590 if (helperOption) {
|
|
3591 var col = dateToCell(startDate).col;
|
|
3592 if (col >= 0 && col < colCnt) { // only works when times are on same day
|
|
3593 var rect = coordinateGrid.rect(0, col, 0, col, slotContainer); // only for horizontal coords
|
|
3594 var top = timePosition(startDate, startDate);
|
|
3595 var bottom = timePosition(startDate, endDate);
|
|
3596 if (bottom > top) { // protect against selections that are entirely before or after visible range
|
|
3597 rect.top = top;
|
|
3598 rect.height = bottom - top;
|
|
3599 rect.left += 2;
|
|
3600 rect.width -= 5;
|
|
3601 if ($.isFunction(helperOption)) {
|
|
3602 var helperRes = helperOption(startDate, endDate);
|
|
3603 if (helperRes) {
|
|
3604 rect.position = 'absolute';
|
|
3605 selectionHelper = $(helperRes)
|
|
3606 .css(rect)
|
|
3607 .appendTo(slotContainer);
|
|
3608 }
|
|
3609 }else{
|
|
3610 rect.isStart = true; // conside rect a "seg" now
|
|
3611 rect.isEnd = true; //
|
|
3612 selectionHelper = $(slotSegHtml(
|
|
3613 {
|
|
3614 title: '',
|
|
3615 start: startDate,
|
|
3616 end: endDate,
|
|
3617 className: ['fc-select-helper'],
|
|
3618 editable: false
|
|
3619 },
|
|
3620 rect
|
|
3621 ));
|
|
3622 selectionHelper.css('opacity', opt('dragOpacity'));
|
|
3623 }
|
|
3624 if (selectionHelper) {
|
|
3625 slotBind(selectionHelper);
|
|
3626 slotContainer.append(selectionHelper);
|
|
3627 setOuterWidth(selectionHelper, rect.width, true); // needs to be after appended
|
|
3628 setOuterHeight(selectionHelper, rect.height, true);
|
|
3629 }
|
|
3630 }
|
|
3631 }
|
|
3632 }else{
|
|
3633 renderSlotOverlay(startDate, endDate);
|
|
3634 }
|
|
3635 }
|
|
3636
|
|
3637
|
|
3638 function clearSelection() {
|
|
3639 clearOverlays();
|
|
3640 if (selectionHelper) {
|
|
3641 selectionHelper.remove();
|
|
3642 selectionHelper = null;
|
|
3643 }
|
|
3644 }
|
|
3645
|
|
3646
|
|
3647 function slotSelectionMousedown(ev) {
|
|
3648 if (ev.which == 1 && opt('selectable')) { // ev.which==1 means left mouse button
|
|
3649 unselect(ev);
|
|
3650 var dates, helperOption = opt('selectHelper');
|
|
3651 hoverListener.start(function(cell, origCell) {
|
|
3652 clearSelection();
|
|
3653 if (cell && (cell.col == origCell.col || !helperOption) && !getIsCellAllDay(cell)) {
|
|
3654 var d1 = realCellToDate(origCell);
|
|
3655 var d2 = realCellToDate(cell);
|
|
3656 dates = [
|
|
3657 d1,
|
|
3658 addMinutes(cloneDate(d1), snapMinutes), // calculate minutes depending on selection slot minutes
|
|
3659 d2,
|
|
3660 addMinutes(cloneDate(d2), snapMinutes)
|
|
3661 ].sort(dateCompare);
|
|
3662 renderSlotSelection(dates[0], dates[3]);
|
|
3663 }else{
|
|
3664 dates = null;
|
|
3665 }
|
|
3666 }, ev);
|
|
3667 $(document).one('mouseup', function(ev) {
|
|
3668 hoverListener.stop();
|
|
3669 if (dates) {
|
|
3670 if (+dates[0] == +dates[1]) {
|
|
3671 reportDayClick(dates[0], false, ev);
|
|
3672 }
|
|
3673 reportSelection(dates[0], dates[3], false, ev);
|
|
3674 }
|
|
3675 });
|
|
3676 }
|
|
3677 }
|
|
3678
|
|
3679
|
|
3680 function reportDayClick(date, allDay, ev) {
|
|
3681 trigger('dayClick', dayBodyCells[dateToCell(date).col], date, allDay, ev);
|
|
3682 }
|
|
3683
|
|
3684
|
|
3685
|
|
3686 /* External Dragging
|
|
3687 --------------------------------------------------------------------------------*/
|
|
3688
|
|
3689
|
|
3690 function dragStart(_dragElement, ev, ui) {
|
|
3691 hoverListener.start(function(cell) {
|
|
3692 clearOverlays();
|
|
3693 if (cell) {
|
|
3694 if (getIsCellAllDay(cell)) {
|
|
3695 renderCellOverlay(cell.row, cell.col, cell.row, cell.col);
|
|
3696 }else{
|
|
3697 var d1 = realCellToDate(cell);
|
|
3698 var d2 = addMinutes(cloneDate(d1), opt('defaultEventMinutes'));
|
|
3699 renderSlotOverlay(d1, d2);
|
|
3700 }
|
|
3701 }
|
|
3702 }, ev);
|
|
3703 }
|
|
3704
|
|
3705
|
|
3706 function dragStop(_dragElement, ev, ui) {
|
|
3707 var cell = hoverListener.stop();
|
|
3708 clearOverlays();
|
|
3709 if (cell) {
|
|
3710 trigger('drop', _dragElement, realCellToDate(cell), getIsCellAllDay(cell), ev, ui);
|
|
3711 }
|
|
3712 }
|
|
3713
|
|
3714
|
|
3715 }
|
|
3716
|
|
3717 ;;
|
|
3718
|
|
3719 function AgendaEventRenderer() {
|
|
3720 var t = this;
|
|
3721
|
|
3722
|
|
3723 // exports
|
|
3724 t.renderEvents = renderEvents;
|
|
3725 t.clearEvents = clearEvents;
|
|
3726 t.slotSegHtml = slotSegHtml;
|
|
3727
|
|
3728
|
|
3729 // imports
|
|
3730 DayEventRenderer.call(t);
|
|
3731 var opt = t.opt;
|
|
3732 var trigger = t.trigger;
|
|
3733 var isEventDraggable = t.isEventDraggable;
|
|
3734 var isEventResizable = t.isEventResizable;
|
|
3735 var eventEnd = t.eventEnd;
|
|
3736 var eventElementHandlers = t.eventElementHandlers;
|
|
3737 var setHeight = t.setHeight;
|
|
3738 var getDaySegmentContainer = t.getDaySegmentContainer;
|
|
3739 var getSlotSegmentContainer = t.getSlotSegmentContainer;
|
|
3740 var getHoverListener = t.getHoverListener;
|
|
3741 var getMaxMinute = t.getMaxMinute;
|
|
3742 var getMinMinute = t.getMinMinute;
|
|
3743 var timePosition = t.timePosition;
|
|
3744 var getIsCellAllDay = t.getIsCellAllDay;
|
|
3745 var colContentLeft = t.colContentLeft;
|
|
3746 var colContentRight = t.colContentRight;
|
|
3747 var cellToDate = t.cellToDate;
|
|
3748 var getColCnt = t.getColCnt;
|
|
3749 var getColWidth = t.getColWidth;
|
|
3750 var getSnapHeight = t.getSnapHeight;
|
|
3751 var getSnapMinutes = t.getSnapMinutes;
|
|
3752 var getSlotContainer = t.getSlotContainer;
|
|
3753 var reportEventElement = t.reportEventElement;
|
|
3754 var showEvents = t.showEvents;
|
|
3755 var hideEvents = t.hideEvents;
|
|
3756 var eventDrop = t.eventDrop;
|
|
3757 var eventResize = t.eventResize;
|
|
3758 var renderDayOverlay = t.renderDayOverlay;
|
|
3759 var clearOverlays = t.clearOverlays;
|
|
3760 var renderDayEvents = t.renderDayEvents;
|
|
3761 var calendar = t.calendar;
|
|
3762 var formatDate = calendar.formatDate;
|
|
3763 var formatDates = calendar.formatDates;
|
|
3764 var timeLineInterval;
|
|
3765
|
|
3766
|
|
3767 // overrides
|
|
3768 t.draggableDayEvent = draggableDayEvent;
|
|
3769
|
|
3770
|
|
3771 /* Rendering
|
|
3772 ----------------------------------------------------------------------------*/
|
|
3773
|
|
3774
|
|
3775 function renderEvents(events, modifiedEventId) {
|
|
3776 var i, len=events.length,
|
|
3777 dayEvents=[],
|
|
3778 slotEvents=[];
|
|
3779 for (i=0; i<len; i++) {
|
|
3780 if (events[i].allDay) {
|
|
3781 dayEvents.push(events[i]);
|
|
3782 }else{
|
|
3783 slotEvents.push(events[i]);
|
|
3784 }
|
|
3785 }
|
|
3786
|
|
3787 if (opt('allDaySlot')) {
|
|
3788 renderDayEvents(dayEvents, modifiedEventId);
|
|
3789 setHeight(); // no params means set to viewHeight
|
|
3790 }
|
|
3791
|
|
3792 renderSlotSegs(compileSlotSegs(slotEvents), modifiedEventId);
|
|
3793
|
|
3794 if (opt('currentTimeIndicator')) {
|
|
3795 window.clearInterval(timeLineInterval);
|
|
3796 timeLineInterval = window.setInterval(setTimeIndicator, 30000);
|
|
3797 setTimeIndicator();
|
|
3798 }
|
|
3799 }
|
|
3800
|
|
3801
|
|
3802 function clearEvents() {
|
|
3803 getDaySegmentContainer().empty();
|
|
3804 getSlotSegmentContainer().empty();
|
|
3805 }
|
|
3806
|
|
3807
|
|
3808 function compileSlotSegs(events) {
|
|
3809 var colCnt = getColCnt(),
|
|
3810 minMinute = getMinMinute(),
|
|
3811 maxMinute = getMaxMinute(),
|
|
3812 d,
|
|
3813 visEventEnds = $.map(events, slotEventEnd),
|
|
3814 i,
|
|
3815 j, seg,
|
|
3816 colSegs,
|
|
3817 segs = [];
|
|
3818
|
|
3819 for (i=0; i<colCnt; i++) {
|
|
3820
|
|
3821 d = cellToDate(0, i);
|
|
3822 addMinutes(d, minMinute);
|
|
3823
|
|
3824 colSegs = sliceSegs(
|
|
3825 events,
|
|
3826 visEventEnds,
|
|
3827 d,
|
|
3828 addMinutes(cloneDate(d), maxMinute-minMinute)
|
|
3829 );
|
|
3830
|
|
3831 colSegs = placeSlotSegs(colSegs); // returns a new order
|
|
3832
|
|
3833 for (j=0; j<colSegs.length; j++) {
|
|
3834 seg = colSegs[j];
|
|
3835 seg.col = i;
|
|
3836 segs.push(seg);
|
|
3837 }
|
|
3838 }
|
|
3839
|
|
3840 return segs;
|
|
3841 }
|
|
3842
|
|
3843
|
|
3844 function sliceSegs(events, visEventEnds, start, end) {
|
|
3845 var segs = [],
|
|
3846 i, len=events.length, event,
|
|
3847 eventStart, eventEnd,
|
|
3848 segStart, segEnd,
|
|
3849 isStart, isEnd;
|
|
3850 for (i=0; i<len; i++) {
|
|
3851 event = events[i];
|
|
3852 eventStart = event.start;
|
|
3853 eventEnd = visEventEnds[i];
|
|
3854 if (eventEnd > start && eventStart < end) {
|
|
3855 if (eventStart < start) {
|
|
3856 segStart = cloneDate(start);
|
|
3857 isStart = false;
|
|
3858 }else{
|
|
3859 segStart = eventStart;
|
|
3860 isStart = true;
|
|
3861 }
|
|
3862 if (eventEnd > end) {
|
|
3863 segEnd = cloneDate(end);
|
|
3864 isEnd = false;
|
|
3865 }else{
|
|
3866 segEnd = eventEnd;
|
|
3867 isEnd = true;
|
|
3868 }
|
|
3869 segs.push({
|
|
3870 event: event,
|
|
3871 start: segStart,
|
|
3872 end: segEnd,
|
|
3873 isStart: isStart,
|
|
3874 isEnd: isEnd
|
|
3875 });
|
|
3876 }
|
|
3877 }
|
|
3878 return segs.sort(compareSlotSegs);
|
|
3879 }
|
|
3880
|
|
3881
|
|
3882 function slotEventEnd(event) {
|
|
3883 if (event.end) {
|
|
3884 return cloneDate(event.end);
|
|
3885 }else{
|
|
3886 return addMinutes(cloneDate(event.start), opt('defaultEventMinutes'));
|
|
3887 }
|
|
3888 }
|
|
3889
|
|
3890
|
|
3891 // renders events in the 'time slots' at the bottom
|
|
3892 // TODO: when we refactor this, when user returns `false` eventRender, don't have empty space
|
|
3893 // TODO: refactor will include using pixels to detect collisions instead of dates (handy for seg cmp)
|
|
3894
|
|
3895 function renderSlotSegs(segs, modifiedEventId) {
|
|
3896
|
|
3897 var i, segCnt=segs.length, seg,
|
|
3898 event,
|
|
3899 top,
|
|
3900 bottom,
|
|
3901 columnLeft,
|
|
3902 columnRight,
|
|
3903 columnWidth,
|
|
3904 width,
|
|
3905 left,
|
|
3906 right,
|
|
3907 html = '',
|
|
3908 eventElements,
|
|
3909 eventElement,
|
|
3910 triggerRes,
|
|
3911 contentElement,
|
|
3912 height,
|
|
3913 slotSegmentContainer = getSlotSegmentContainer(),
|
|
3914 isRTL = opt('isRTL'),
|
|
3915 colCnt = getColCnt();
|
|
3916
|
|
3917 // calculate position/dimensions, create html
|
|
3918 for (i=0; i<segCnt; i++) {
|
|
3919 seg = segs[i];
|
|
3920 event = seg.event;
|
|
3921 top = timePosition(seg.start, seg.start);
|
|
3922 bottom = timePosition(seg.start, seg.end);
|
|
3923 columnLeft = colContentLeft(seg.col);
|
|
3924 columnRight = colContentRight(seg.col);
|
|
3925 columnWidth = columnRight - columnLeft;
|
|
3926
|
|
3927 // shave off space on right near scrollbars (2.5%)
|
|
3928 // TODO: move this to CSS somehow
|
|
3929 columnRight -= columnWidth * .025;
|
|
3930 columnWidth = columnRight - columnLeft;
|
|
3931
|
|
3932 width = columnWidth * (seg.forwardCoord - seg.backwardCoord);
|
|
3933
|
|
3934 // bruederli@kolabsys.com: always disable slotEventOverlap in single day view
|
|
3935 if (opt('slotEventOverlap') && colCnt > 1) {
|
|
3936 // double the width while making sure resize handle is visible
|
|
3937 // (assumed to be 20px wide)
|
|
3938 width = Math.max(
|
|
3939 (width - (20/2)) * 2,
|
|
3940 width // narrow columns will want to make the segment smaller than
|
|
3941 // the natural width. don't allow it
|
|
3942 );
|
|
3943 }
|
|
3944
|
|
3945 if (isRTL) {
|
|
3946 right = columnRight - seg.backwardCoord * columnWidth;
|
|
3947 left = right - width;
|
|
3948 }
|
|
3949 else {
|
|
3950 left = columnLeft + seg.backwardCoord * columnWidth;
|
|
3951 right = left + width;
|
|
3952 }
|
|
3953
|
|
3954 // make sure horizontal coordinates are in bounds
|
|
3955 left = Math.max(left, columnLeft);
|
|
3956 right = Math.min(right, columnRight);
|
|
3957 width = right - left;
|
|
3958
|
|
3959 seg.top = top;
|
|
3960 seg.left = left;
|
|
3961 seg.outerWidth = width;
|
|
3962 seg.outerHeight = bottom - top;
|
|
3963 html += slotSegHtml(event, seg);
|
|
3964 }
|
|
3965
|
|
3966 slotSegmentContainer[0].innerHTML = html; // faster than html()
|
|
3967 eventElements = slotSegmentContainer.children();
|
|
3968
|
|
3969 // retrieve elements, run through eventRender callback, bind event handlers
|
|
3970 for (i=0; i<segCnt; i++) {
|
|
3971 seg = segs[i];
|
|
3972 event = seg.event;
|
|
3973 eventElement = $(eventElements[i]); // faster than eq()
|
|
3974 triggerRes = trigger('eventRender', event, event, eventElement);
|
|
3975 if (triggerRes === false) {
|
|
3976 eventElement.remove();
|
|
3977 }else{
|
|
3978 if (triggerRes && triggerRes !== true) {
|
|
3979 eventElement.remove();
|
|
3980 eventElement = $(triggerRes)
|
|
3981 .css({
|
|
3982 position: 'absolute',
|
|
3983 top: seg.top,
|
|
3984 left: seg.left
|
|
3985 })
|
|
3986 .appendTo(slotSegmentContainer);
|
|
3987 }
|
|
3988 seg.element = eventElement;
|
|
3989 if (event._id === modifiedEventId) {
|
|
3990 bindSlotSeg(event, eventElement, seg);
|
|
3991 }else{
|
|
3992 eventElement[0]._fci = i; // for lazySegBind
|
|
3993 }
|
|
3994 reportEventElement(event, eventElement);
|
|
3995 }
|
|
3996 }
|
|
3997
|
|
3998 lazySegBind(slotSegmentContainer, segs, bindSlotSeg);
|
|
3999
|
|
4000 // record event sides and title positions
|
|
4001 for (i=0; i<segCnt; i++) {
|
|
4002 seg = segs[i];
|
|
4003 if (eventElement = seg.element) {
|
|
4004 seg.vsides = vsides(eventElement, true);
|
|
4005 seg.hsides = hsides(eventElement, true);
|
|
4006 contentElement = eventElement.find('.fc-event-content');
|
|
4007 if (contentElement.length) {
|
|
4008 seg.contentTop = contentElement[0].offsetTop;
|
|
4009 }
|
|
4010 }
|
|
4011 }
|
|
4012
|
|
4013 // set all positions/dimensions at once
|
|
4014 for (i=0; i<segCnt; i++) {
|
|
4015 seg = segs[i];
|
|
4016 if (eventElement = seg.element) {
|
|
4017 eventElement[0].style.width = Math.max(0, seg.outerWidth - seg.hsides) + 'px';
|
|
4018 height = Math.max(0, seg.outerHeight - seg.vsides);
|
|
4019 eventElement[0].style.height = height + 'px';
|
|
4020 event = seg.event;
|
|
4021 if (seg.contentTop !== undefined && height - seg.contentTop < 10) {
|
|
4022 // not enough room for title, put it in the time (TODO: maybe make both display:inline instead)
|
|
4023 eventElement.find('div.fc-event-time')
|
|
4024 .text(formatDate(event.start, opt('timeFormat')) + ' - ' + event.title);
|
|
4025 eventElement.find('div.fc-event-title')
|
|
4026 .remove();
|
|
4027 }
|
|
4028 trigger('eventAfterRender', event, event, eventElement);
|
|
4029 }
|
|
4030 }
|
|
4031
|
|
4032 }
|
|
4033
|
|
4034
|
|
4035 function slotSegHtml(event, seg) {
|
|
4036 var html = "<";
|
|
4037 var url = event.url;
|
|
4038 var skinCss = getSkinCss(event, opt);
|
|
4039 var skinCssAttr = (skinCss ? " style='" + skinCss + "'" : '');
|
|
4040 var classes = ['fc-event', 'fc-event-skin', 'fc-event-vert'];
|
|
4041 if (isEventDraggable(event)) {
|
|
4042 classes.push('fc-event-draggable');
|
|
4043 }
|
|
4044 if (seg.isStart) {
|
|
4045 classes.push('fc-event-start');
|
|
4046 }
|
|
4047 if (seg.isEnd) {
|
|
4048 classes.push('fc-event-end');
|
|
4049 }
|
|
4050 classes = classes.concat(event.className);
|
|
4051 if (event.source) {
|
|
4052 classes = classes.concat(event.source.className || []);
|
|
4053 }
|
|
4054 if (url) {
|
|
4055 html += "a href='" + htmlEscape(event.url) + "'";
|
|
4056 }else{
|
|
4057 html += "div";
|
|
4058 }
|
|
4059 html +=
|
|
4060 " class='" + classes.join(' ') + "'" +
|
|
4061 " style=" +
|
|
4062 "'" +
|
|
4063 "position:absolute;" +
|
|
4064 "top:" + seg.top + "px;" +
|
|
4065 "left:" + seg.left + "px;" +
|
|
4066 skinCss +
|
|
4067 "'" +
|
|
4068 " tabindex='0'>" +
|
|
4069 "<div class='fc-event-inner fc-event-skin'" + skinCssAttr + ">" +
|
|
4070 "<div class='fc-event-head fc-event-skin'" + skinCssAttr + ">" +
|
|
4071 "<div class='fc-event-time'>" +
|
|
4072 htmlEscape(formatDates(event.start, event.end, opt('timeFormat'))) +
|
|
4073 "</div>" +
|
|
4074 "</div>" +
|
|
4075 "<div class='fc-event-content'>" +
|
|
4076 "<div class='fc-event-title'>" +
|
|
4077 htmlEscape(event.title || '') +
|
|
4078 "</div>" +
|
|
4079 "</div>" +
|
|
4080 "<div class='fc-event-bg'></div>" +
|
|
4081 "</div>"; // close inner
|
|
4082 if (seg.isEnd && isEventResizable(event)) {
|
|
4083 html +=
|
|
4084 "<div class='ui-resizable-handle ui-resizable-s' role='presentation'>=</div>";
|
|
4085 }
|
|
4086 html +=
|
|
4087 "</" + (url ? "a" : "div") + ">";
|
|
4088 return html;
|
|
4089 }
|
|
4090
|
|
4091
|
|
4092 function bindSlotSeg(event, eventElement, seg) {
|
|
4093 var timeElement = eventElement.find('div.fc-event-time');
|
|
4094 if (isEventDraggable(event)) {
|
|
4095 draggableSlotEvent(event, eventElement, timeElement);
|
|
4096 }
|
|
4097 if (seg.isEnd && isEventResizable(event)) {
|
|
4098 resizableSlotEvent(event, eventElement, timeElement);
|
|
4099 }
|
|
4100 eventElementHandlers(event, eventElement);
|
|
4101 }
|
|
4102
|
|
4103
|
|
4104 // draw a horizontal line indicating the current time (#143)
|
|
4105 function setTimeIndicator()
|
|
4106 {
|
|
4107 var container = getSlotContainer();
|
|
4108 var timeline = container.children('.fc-timeline');
|
|
4109 if (timeline.length == 0) { // if timeline isn't there, add it
|
|
4110 timeline = $('<hr>').addClass('fc-timeline').appendTo(container);
|
|
4111 }
|
|
4112
|
|
4113 var cur_time = new Date();
|
|
4114 if (t.visStart < cur_time && t.visEnd > cur_time) {
|
|
4115 timeline.show();
|
|
4116 }
|
|
4117 else {
|
|
4118 timeline.hide();
|
|
4119 return;
|
|
4120 }
|
|
4121
|
|
4122 var secs = (cur_time.getHours() * 60 * 60) + (cur_time.getMinutes() * 60) + cur_time.getSeconds();
|
|
4123 var percents = secs / 86400; // 24 * 60 * 60 = 86400, # of seconds in a day
|
|
4124
|
|
4125 timeline.css('top', Math.floor(container.height() * percents - 1) + 'px');
|
|
4126
|
|
4127 if (t.name == 'agendaWeek') { // week view, don't want the timeline to go the whole way across
|
|
4128 var daycol = $('.fc-today', t.element);
|
|
4129 var left = daycol.position().left + 1;
|
|
4130 var width = daycol.width();
|
|
4131 timeline.css({ left: left + 'px', width: width + 'px' });
|
|
4132 }
|
|
4133 }
|
|
4134
|
|
4135
|
|
4136 /* Dragging
|
|
4137 -----------------------------------------------------------------------------------*/
|
|
4138
|
|
4139
|
|
4140 // when event starts out FULL-DAY
|
|
4141 // overrides DayEventRenderer's version because it needs to account for dragging elements
|
|
4142 // to and from the slot area.
|
|
4143
|
|
4144 function draggableDayEvent(event, eventElement, seg) {
|
|
4145 var isStart = seg.isStart;
|
|
4146 var origWidth;
|
|
4147 var revert;
|
|
4148 var allDay = true;
|
|
4149 var dayDelta;
|
|
4150 var hoverListener = getHoverListener();
|
|
4151 var colWidth = getColWidth();
|
|
4152 var snapHeight = getSnapHeight();
|
|
4153 var snapMinutes = getSnapMinutes();
|
|
4154 var minMinute = getMinMinute();
|
|
4155 eventElement.draggable({
|
|
4156 opacity: opt('dragOpacity', 'month'), // use whatever the month view was using
|
|
4157 revertDuration: opt('dragRevertDuration'),
|
|
4158 start: function(ev, ui) {
|
|
4159 trigger('eventDragStart', eventElement, event, ev, ui);
|
|
4160 hideEvents(event, eventElement);
|
|
4161 origWidth = eventElement.width();
|
|
4162 hoverListener.start(function(cell, origCell) {
|
|
4163 clearOverlays();
|
|
4164 if (cell) {
|
|
4165 revert = false;
|
|
4166 var origDate = cellToDate(0, origCell.col);
|
|
4167 var date = cellToDate(0, cell.col);
|
|
4168 dayDelta = dayDiff(date, origDate);
|
|
4169 if (!cell.row) {
|
|
4170 // on full-days
|
|
4171 renderDayOverlay(
|
|
4172 addDays(cloneDate(event.start), dayDelta),
|
|
4173 addDays(exclEndDay(event), dayDelta)
|
|
4174 );
|
|
4175 resetElement();
|
|
4176 }else{
|
|
4177 // mouse is over bottom slots
|
|
4178 if (isStart) {
|
|
4179 if (allDay) {
|
|
4180 // convert event to temporary slot-event
|
|
4181 eventElement.width(colWidth - 10); // don't use entire width
|
|
4182 setOuterHeight(
|
|
4183 eventElement,
|
|
4184 snapHeight * Math.round(
|
|
4185 (event.end ? ((event.end - event.start) / MINUTE_MS) : opt('defaultEventMinutes')) /
|
|
4186 snapMinutes
|
|
4187 )
|
|
4188 );
|
|
4189 eventElement.draggable('option', 'grid', [colWidth, 1]);
|
|
4190 allDay = false;
|
|
4191 }
|
|
4192 }else{
|
|
4193 revert = true;
|
|
4194 }
|
|
4195 }
|
|
4196 revert = revert || (allDay && !dayDelta);
|
|
4197 }else{
|
|
4198 resetElement();
|
|
4199 revert = true;
|
|
4200 }
|
|
4201 eventElement.draggable('option', 'revert', revert);
|
|
4202 }, ev, 'drag');
|
|
4203 },
|
|
4204 stop: function(ev, ui) {
|
|
4205 hoverListener.stop();
|
|
4206 clearOverlays();
|
|
4207 trigger('eventDragStop', eventElement, event, ev, ui);
|
|
4208 if (revert) {
|
|
4209 // hasn't moved or is out of bounds (draggable has already reverted)
|
|
4210 resetElement();
|
|
4211 eventElement.css('filter', ''); // clear IE opacity side-effects
|
|
4212 showEvents(event, eventElement);
|
|
4213 }else{
|
|
4214 // changed!
|
|
4215 var minuteDelta = 0;
|
|
4216 if (!allDay) {
|
|
4217 minuteDelta = Math.round((eventElement.offset().top - getSlotContainer().offset().top) / snapHeight)
|
|
4218 * snapMinutes
|
|
4219 + minMinute
|
|
4220 - (event.start.getHours() * 60 + event.start.getMinutes());
|
|
4221 }
|
|
4222 eventDrop(this, event, dayDelta, minuteDelta, allDay, ev, ui);
|
|
4223 }
|
|
4224 }
|
|
4225 });
|
|
4226 function resetElement() {
|
|
4227 if (!allDay) {
|
|
4228 eventElement
|
|
4229 .width(origWidth)
|
|
4230 .height('')
|
|
4231 .draggable('option', 'grid', null);
|
|
4232 allDay = true;
|
|
4233 }
|
|
4234 }
|
|
4235 }
|
|
4236
|
|
4237
|
|
4238 // when event starts out IN TIMESLOTS
|
|
4239
|
|
4240 function draggableSlotEvent(event, eventElement, timeElement) {
|
|
4241 var coordinateGrid = t.getCoordinateGrid();
|
|
4242 var colCnt = getColCnt();
|
|
4243 var colWidth = getColWidth();
|
|
4244 var snapHeight = getSnapHeight();
|
|
4245 var snapMinutes = getSnapMinutes();
|
|
4246
|
|
4247 // states
|
|
4248 var origPosition; // original position of the element, not the mouse
|
|
4249 var origCell;
|
|
4250 var isInBounds, prevIsInBounds;
|
|
4251 var isAllDay, prevIsAllDay;
|
|
4252 var colDelta, prevColDelta;
|
|
4253 var dayDelta; // derived from colDelta
|
|
4254 var minuteDelta, prevMinuteDelta;
|
|
4255
|
|
4256 eventElement.draggable({
|
|
4257 scroll: false,
|
|
4258 grid: [ colWidth, snapHeight ],
|
|
4259 axis: colCnt==1 ? 'y' : false,
|
|
4260 opacity: opt('dragOpacity'),
|
|
4261 revertDuration: opt('dragRevertDuration'),
|
|
4262 start: function(ev, ui) {
|
|
4263
|
|
4264 trigger('eventDragStart', eventElement, event, ev, ui);
|
|
4265 hideEvents(event, eventElement);
|
|
4266
|
|
4267 coordinateGrid.build();
|
|
4268
|
|
4269 // initialize states
|
|
4270 origPosition = eventElement.position();
|
|
4271 origCell = coordinateGrid.cell(ev.pageX, ev.pageY);
|
|
4272 isInBounds = prevIsInBounds = true;
|
|
4273 isAllDay = prevIsAllDay = getIsCellAllDay(origCell);
|
|
4274 colDelta = prevColDelta = 0;
|
|
4275 dayDelta = 0;
|
|
4276 minuteDelta = prevMinuteDelta = 0;
|
|
4277
|
|
4278 },
|
|
4279 drag: function(ev, ui) {
|
|
4280
|
|
4281 // NOTE: this `cell` value is only useful for determining in-bounds and all-day.
|
|
4282 // Bad for anything else due to the discrepancy between the mouse position and the
|
|
4283 // element position while snapping. (problem revealed in PR #55)
|
|
4284 //
|
|
4285 // PS- the problem exists for draggableDayEvent() when dragging an all-day event to a slot event.
|
|
4286 // We should overhaul the dragging system and stop relying on jQuery UI.
|
|
4287 var cell = coordinateGrid.cell(ev.pageX, ev.pageY);
|
|
4288
|
|
4289 // update states
|
|
4290 isInBounds = !!cell;
|
|
4291 if (isInBounds) {
|
|
4292 isAllDay = getIsCellAllDay(cell);
|
|
4293
|
|
4294 // calculate column delta
|
|
4295 colDelta = Math.round((ui.position.left - origPosition.left) / colWidth);
|
|
4296 if (colDelta != prevColDelta) {
|
|
4297 // calculate the day delta based off of the original clicked column and the column delta
|
|
4298 var origDate = cellToDate(0, origCell.col);
|
|
4299 var col = origCell.col + colDelta;
|
|
4300 col = Math.max(0, col);
|
|
4301 col = Math.min(colCnt-1, col);
|
|
4302 var date = cellToDate(0, col);
|
|
4303 dayDelta = dayDiff(date, origDate);
|
|
4304 }
|
|
4305
|
|
4306 // calculate minute delta (only if over slots)
|
|
4307 if (!isAllDay) {
|
|
4308 minuteDelta = Math.round((ui.position.top - origPosition.top) / snapHeight) * snapMinutes;
|
|
4309 }
|
|
4310 }
|
|
4311
|
|
4312 // any state changes?
|
|
4313 if (
|
|
4314 isInBounds != prevIsInBounds ||
|
|
4315 isAllDay != prevIsAllDay ||
|
|
4316 colDelta != prevColDelta ||
|
|
4317 minuteDelta != prevMinuteDelta
|
|
4318 ) {
|
|
4319
|
|
4320 updateUI();
|
|
4321
|
|
4322 // update previous states for next time
|
|
4323 prevIsInBounds = isInBounds;
|
|
4324 prevIsAllDay = isAllDay;
|
|
4325 prevColDelta = colDelta;
|
|
4326 prevMinuteDelta = minuteDelta;
|
|
4327 }
|
|
4328
|
|
4329 // if out-of-bounds, revert when done, and vice versa.
|
|
4330 eventElement.draggable('option', 'revert', !isInBounds);
|
|
4331
|
|
4332 },
|
|
4333 stop: function(ev, ui) {
|
|
4334
|
|
4335 clearOverlays();
|
|
4336 trigger('eventDragStop', eventElement, event, ev, ui);
|
|
4337
|
|
4338 if (isInBounds && (isAllDay || dayDelta || minuteDelta)) { // changed!
|
|
4339 eventDrop(this, event, dayDelta, isAllDay ? 0 : minuteDelta, isAllDay, ev, ui);
|
|
4340 }
|
|
4341 else { // either no change or out-of-bounds (draggable has already reverted)
|
|
4342
|
|
4343 // reset states for next time, and for updateUI()
|
|
4344 isInBounds = true;
|
|
4345 isAllDay = false;
|
|
4346 colDelta = 0;
|
|
4347 dayDelta = 0;
|
|
4348 minuteDelta = 0;
|
|
4349
|
|
4350 updateUI();
|
|
4351 eventElement.css('filter', ''); // clear IE opacity side-effects
|
|
4352
|
|
4353 // sometimes fast drags make event revert to wrong position, so reset.
|
|
4354 // also, if we dragged the element out of the area because of snapping,
|
|
4355 // but the *mouse* is still in bounds, we need to reset the position.
|
|
4356 eventElement.css(origPosition);
|
|
4357
|
|
4358 showEvents(event, eventElement);
|
|
4359 }
|
|
4360 }
|
|
4361 });
|
|
4362
|
|
4363 function updateUI() {
|
|
4364 clearOverlays();
|
|
4365 if (isInBounds) {
|
|
4366 if (isAllDay) {
|
|
4367 timeElement.hide();
|
|
4368 eventElement.draggable('option', 'grid', null); // disable grid snapping
|
|
4369 renderDayOverlay(
|
|
4370 addDays(cloneDate(event.start), dayDelta),
|
|
4371 addDays(exclEndDay(event), dayDelta)
|
|
4372 );
|
|
4373 }
|
|
4374 else {
|
|
4375 updateTimeText(minuteDelta);
|
|
4376 timeElement.css('display', ''); // show() was causing display=inline
|
|
4377 eventElement.draggable('option', 'grid', [colWidth, snapHeight]); // re-enable grid snapping
|
|
4378 }
|
|
4379 }
|
|
4380 }
|
|
4381
|
|
4382 function updateTimeText(minuteDelta) {
|
|
4383 var newStart = addMinutes(cloneDate(event.start), minuteDelta);
|
|
4384 var newEnd;
|
|
4385 if (event.end) {
|
|
4386 newEnd = addMinutes(cloneDate(event.end), minuteDelta);
|
|
4387 }
|
|
4388 timeElement.text(formatDates(newStart, newEnd, opt('timeFormat')));
|
|
4389 }
|
|
4390
|
|
4391 }
|
|
4392
|
|
4393
|
|
4394
|
|
4395 /* Resizing
|
|
4396 --------------------------------------------------------------------------------------*/
|
|
4397
|
|
4398
|
|
4399 function resizableSlotEvent(event, eventElement, timeElement) {
|
|
4400 var snapDelta, prevSnapDelta;
|
|
4401 var snapHeight = getSnapHeight();
|
|
4402 var snapMinutes = getSnapMinutes();
|
|
4403 eventElement.resizable({
|
|
4404 handles: {
|
|
4405 s: '.ui-resizable-handle'
|
|
4406 },
|
|
4407 grid: snapHeight,
|
|
4408 start: function(ev, ui) {
|
|
4409 snapDelta = prevSnapDelta = 0;
|
|
4410 hideEvents(event, eventElement);
|
|
4411 trigger('eventResizeStart', this, event, ev, ui);
|
|
4412 },
|
|
4413 resize: function(ev, ui) {
|
|
4414 // don't rely on ui.size.height, doesn't take grid into account
|
|
4415 snapDelta = Math.round((Math.max(snapHeight, eventElement.height()) - ui.originalSize.height) / snapHeight);
|
|
4416 if (snapDelta != prevSnapDelta) {
|
|
4417 timeElement.text(
|
|
4418 formatDates(
|
|
4419 event.start,
|
|
4420 (!snapDelta && !event.end) ? null : // no change, so don't display time range
|
|
4421 addMinutes(eventEnd(event), snapMinutes*snapDelta),
|
|
4422 opt('timeFormat')
|
|
4423 )
|
|
4424 );
|
|
4425 prevSnapDelta = snapDelta;
|
|
4426 }
|
|
4427 },
|
|
4428 stop: function(ev, ui) {
|
|
4429 trigger('eventResizeStop', this, event, ev, ui);
|
|
4430 if (snapDelta) {
|
|
4431 eventResize(this, event, 0, snapMinutes*snapDelta, ev, ui);
|
|
4432 }else{
|
|
4433 showEvents(event, eventElement);
|
|
4434 // BUG: if event was really short, need to put title back in span
|
|
4435 }
|
|
4436 }
|
|
4437 });
|
|
4438 }
|
|
4439
|
|
4440
|
|
4441 }
|
|
4442
|
|
4443
|
|
4444
|
|
4445 /* Agenda Event Segment Utilities
|
|
4446 -----------------------------------------------------------------------------*/
|
|
4447
|
|
4448
|
|
4449 // Sets the seg.backwardCoord and seg.forwardCoord on each segment and returns a new
|
|
4450 // list in the order they should be placed into the DOM (an implicit z-index).
|
|
4451 function placeSlotSegs(segs) {
|
|
4452 var levels = buildSlotSegLevels(segs);
|
|
4453 var level0 = levels[0];
|
|
4454 var i;
|
|
4455
|
|
4456 computeForwardSlotSegs(levels);
|
|
4457
|
|
4458 if (level0) {
|
|
4459
|
|
4460 for (i=0; i<level0.length; i++) {
|
|
4461 computeSlotSegPressures(level0[i]);
|
|
4462 }
|
|
4463
|
|
4464 for (i=0; i<level0.length; i++) {
|
|
4465 computeSlotSegCoords(level0[i], 0, 0);
|
|
4466 }
|
|
4467 }
|
|
4468
|
|
4469 return flattenSlotSegLevels(levels);
|
|
4470 }
|
|
4471
|
|
4472
|
|
4473 // Builds an array of segments "levels". The first level will be the leftmost tier of segments
|
|
4474 // if the calendar is left-to-right, or the rightmost if the calendar is right-to-left.
|
|
4475 function buildSlotSegLevels(segs) {
|
|
4476 var levels = [];
|
|
4477 var i, seg;
|
|
4478 var j;
|
|
4479
|
|
4480 for (i=0; i<segs.length; i++) {
|
|
4481 seg = segs[i];
|
|
4482
|
|
4483 // go through all the levels and stop on the first level where there are no collisions
|
|
4484 for (j=0; j<levels.length; j++) {
|
|
4485 if (!computeSlotSegCollisions(seg, levels[j]).length) {
|
|
4486 break;
|
|
4487 }
|
|
4488 }
|
|
4489
|
|
4490 (levels[j] || (levels[j] = [])).push(seg);
|
|
4491 }
|
|
4492
|
|
4493 return levels;
|
|
4494 }
|
|
4495
|
|
4496
|
|
4497 // For every segment, figure out the other segments that are in subsequent
|
|
4498 // levels that also occupy the same vertical space. Accumulate in seg.forwardSegs
|
|
4499 function computeForwardSlotSegs(levels) {
|
|
4500 var i, level;
|
|
4501 var j, seg;
|
|
4502 var k;
|
|
4503
|
|
4504 for (i=0; i<levels.length; i++) {
|
|
4505 level = levels[i];
|
|
4506
|
|
4507 for (j=0; j<level.length; j++) {
|
|
4508 seg = level[j];
|
|
4509
|
|
4510 seg.forwardSegs = [];
|
|
4511 for (k=i+1; k<levels.length; k++) {
|
|
4512 computeSlotSegCollisions(seg, levels[k], seg.forwardSegs);
|
|
4513 }
|
|
4514 }
|
|
4515 }
|
|
4516 }
|
|
4517
|
|
4518
|
|
4519 // Figure out which path forward (via seg.forwardSegs) results in the longest path until
|
|
4520 // the furthest edge is reached. The number of segments in this path will be seg.forwardPressure
|
|
4521 function computeSlotSegPressures(seg) {
|
|
4522 var forwardSegs = seg.forwardSegs;
|
|
4523 var forwardPressure = 0;
|
|
4524 var i, forwardSeg;
|
|
4525
|
|
4526 if (seg.forwardPressure === undefined) { // not already computed
|
|
4527
|
|
4528 for (i=0; i<forwardSegs.length; i++) {
|
|
4529 forwardSeg = forwardSegs[i];
|
|
4530
|
|
4531 // figure out the child's maximum forward path
|
|
4532 computeSlotSegPressures(forwardSeg);
|
|
4533
|
|
4534 // either use the existing maximum, or use the child's forward pressure
|
|
4535 // plus one (for the forwardSeg itself)
|
|
4536 forwardPressure = Math.max(
|
|
4537 forwardPressure,
|
|
4538 1 + forwardSeg.forwardPressure
|
|
4539 );
|
|
4540 }
|
|
4541
|
|
4542 seg.forwardPressure = forwardPressure;
|
|
4543 }
|
|
4544 }
|
|
4545
|
|
4546
|
|
4547 // Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range
|
|
4548 // from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and
|
|
4549 // seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left.
|
|
4550 //
|
|
4551 // The segment might be part of a "series", which means consecutive segments with the same pressure
|
|
4552 // who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of
|
|
4553 // segments behind this one in the current series, and `seriesBackwardCoord` is the starting
|
|
4554 // coordinate of the first segment in the series.
|
|
4555 function computeSlotSegCoords(seg, seriesBackwardPressure, seriesBackwardCoord) {
|
|
4556 var forwardSegs = seg.forwardSegs;
|
|
4557 var i;
|
|
4558
|
|
4559 if (seg.forwardCoord === undefined) { // not already computed
|
|
4560
|
|
4561 if (!forwardSegs.length) {
|
|
4562
|
|
4563 // if there are no forward segments, this segment should butt up against the edge
|
|
4564 seg.forwardCoord = 1;
|
|
4565 }
|
|
4566 else {
|
|
4567
|
|
4568 // sort highest pressure first
|
|
4569 forwardSegs.sort(compareForwardSlotSegs);
|
|
4570
|
|
4571 // this segment's forwardCoord will be calculated from the backwardCoord of the
|
|
4572 // highest-pressure forward segment.
|
|
4573 computeSlotSegCoords(forwardSegs[0], seriesBackwardPressure + 1, seriesBackwardCoord);
|
|
4574 seg.forwardCoord = forwardSegs[0].backwardCoord;
|
|
4575 }
|
|
4576
|
|
4577 // calculate the backwardCoord from the forwardCoord. consider the series
|
|
4578 seg.backwardCoord = seg.forwardCoord -
|
|
4579 (seg.forwardCoord - seriesBackwardCoord) / // available width for series
|
|
4580 (seriesBackwardPressure + 1); // # of segments in the series
|
|
4581
|
|
4582 // use this segment's coordinates to computed the coordinates of the less-pressurized
|
|
4583 // forward segments
|
|
4584 for (i=0; i<forwardSegs.length; i++) {
|
|
4585 computeSlotSegCoords(forwardSegs[i], 0, seg.forwardCoord);
|
|
4586 }
|
|
4587 }
|
|
4588 }
|
|
4589
|
|
4590
|
|
4591 // Outputs a flat array of segments, from lowest to highest level
|
|
4592 function flattenSlotSegLevels(levels) {
|
|
4593 var segs = [];
|
|
4594 var i, level;
|
|
4595 var j;
|
|
4596
|
|
4597 for (i=0; i<levels.length; i++) {
|
|
4598 level = levels[i];
|
|
4599
|
|
4600 for (j=0; j<level.length; j++) {
|
|
4601 segs.push(level[j]);
|
|
4602 }
|
|
4603 }
|
|
4604
|
|
4605 return segs;
|
|
4606 }
|
|
4607
|
|
4608
|
|
4609 // Find all the segments in `otherSegs` that vertically collide with `seg`.
|
|
4610 // Append into an optionally-supplied `results` array and return.
|
|
4611 function computeSlotSegCollisions(seg, otherSegs, results) {
|
|
4612 results = results || [];
|
|
4613
|
|
4614 for (var i=0; i<otherSegs.length; i++) {
|
|
4615 if (isSlotSegCollision(seg, otherSegs[i])) {
|
|
4616 results.push(otherSegs[i]);
|
|
4617 }
|
|
4618 }
|
|
4619
|
|
4620 return results;
|
|
4621 }
|
|
4622
|
|
4623
|
|
4624 // Do these segments occupy the same vertical space?
|
|
4625 function isSlotSegCollision(seg1, seg2) {
|
|
4626 return seg1.end > seg2.start && seg1.start < seg2.end;
|
|
4627 }
|
|
4628
|
|
4629
|
|
4630 // A cmp function for determining which forward segment to rely on more when computing coordinates.
|
|
4631 function compareForwardSlotSegs(seg1, seg2) {
|
|
4632 // put higher-pressure first
|
|
4633 return seg2.forwardPressure - seg1.forwardPressure ||
|
|
4634 // put segments that are closer to initial edge first (and favor ones with no coords yet)
|
|
4635 (seg1.backwardCoord || 0) - (seg2.backwardCoord || 0) ||
|
|
4636 // do normal sorting...
|
|
4637 compareSlotSegs(seg1, seg2);
|
|
4638 }
|
|
4639
|
|
4640
|
|
4641 // A cmp function for determining which segment should be closer to the initial edge
|
|
4642 // (the left edge on a left-to-right calendar).
|
|
4643 function compareSlotSegs(seg1, seg2) {
|
|
4644 return seg1.start - seg2.start || // earlier start time goes first
|
|
4645 (seg2.end - seg2.start) - (seg1.end - seg1.start) || // tie? longer-duration goes first
|
|
4646 (seg1.event.title || '').localeCompare(seg2.event.title); // tie? alphabetically by title
|
|
4647 }
|
|
4648
|
|
4649
|
|
4650 ;;
|
|
4651
|
|
4652 /* Additional view: list (by bruederli@kolabsys.com)
|
|
4653 ---------------------------------------------------------------------------------*/
|
|
4654
|
|
4655 fcViews.list = ListView;
|
|
4656
|
|
4657
|
|
4658 function ListView(element, calendar) {
|
|
4659 var t = this;
|
|
4660
|
|
4661 // exports
|
|
4662 t.render = render;
|
|
4663 t.select = dummy;
|
|
4664 t.unselect = dummy;
|
|
4665 t.reportSelection = dummy;
|
|
4666 t.getDaySegmentContainer = function(){ return body; };
|
|
4667
|
|
4668 // imports
|
|
4669 View.call(t, element, calendar, 'list');
|
|
4670 ListEventRenderer.call(t);
|
|
4671 var opt = t.opt;
|
|
4672 var trigger = t.trigger;
|
|
4673 var clearEvents = t.clearEvents;
|
|
4674 var reportEventClear = t.reportEventClear;
|
|
4675 var formatDates = calendar.formatDates;
|
|
4676 var formatDate = calendar.formatDate;
|
|
4677
|
|
4678 // overrides
|
|
4679 t.setWidth = setWidth;
|
|
4680 t.setHeight = setHeight;
|
|
4681
|
|
4682 // locals
|
|
4683 var body;
|
|
4684 var firstDay;
|
|
4685 var nwe;
|
|
4686 var tm;
|
|
4687 var colFormat;
|
|
4688
|
|
4689
|
|
4690 function render(date, delta) {
|
|
4691 if (delta) {
|
|
4692 addDays(date, opt('listPage') * delta);
|
|
4693 }
|
|
4694 t.start = t.visStart = cloneDate(date, true);
|
|
4695 t.end = addDays(cloneDate(t.start), opt('listPage'));
|
|
4696 t.visEnd = addDays(cloneDate(t.start), opt('listRange'));
|
|
4697 addMinutes(t.visEnd, -1); // set end to 23:59
|
|
4698 t.title = formatDates(date, t.visEnd, opt('titleFormat'));
|
|
4699
|
|
4700 updateOptions();
|
|
4701
|
|
4702 if (!body) {
|
|
4703 buildSkeleton();
|
|
4704 } else {
|
|
4705 clearEvents();
|
|
4706 }
|
|
4707 }
|
|
4708
|
|
4709
|
|
4710 function updateOptions() {
|
|
4711 firstDay = opt('firstDay');
|
|
4712 nwe = opt('weekends') ? 0 : 1;
|
|
4713 tm = opt('theme') ? 'ui' : 'fc';
|
|
4714 colFormat = opt('columnFormat', 'day');
|
|
4715 }
|
|
4716
|
|
4717
|
|
4718 function buildSkeleton() {
|
|
4719 body = $('<div>').addClass('fc-list-content').appendTo(element);
|
|
4720 }
|
|
4721
|
|
4722 function setHeight(height, dateChanged) {
|
|
4723 if (!opt('listNoHeight'))
|
|
4724 body.css('height', (height-1)+'px').css('overflow', 'auto');
|
|
4725 }
|
|
4726
|
|
4727 function setWidth(width) {
|
|
4728 // nothing to be done here
|
|
4729 }
|
|
4730
|
|
4731 function dummy() {
|
|
4732 // Stub.
|
|
4733 }
|
|
4734
|
|
4735 }
|
|
4736
|
|
4737 ;;
|
|
4738
|
|
4739 /* Additional view renderer: list (by bruederli@kolabsys.com)
|
|
4740 ---------------------------------------------------------------------------------*/
|
|
4741
|
|
4742 function ListEventRenderer() {
|
|
4743 var t = this;
|
|
4744
|
|
4745 // exports
|
|
4746 t.renderEvents = renderEvents;
|
|
4747 t.renderEventTime = renderEventTime;
|
|
4748 t.compileDaySegs = compileSegs; // for DayEventRenderer
|
|
4749 t.clearEvents = clearEvents;
|
|
4750 t.lazySegBind = lazySegBind;
|
|
4751 t.sortCmp = sortCmp;
|
|
4752
|
|
4753 // imports
|
|
4754 DayEventRenderer.call(t);
|
|
4755 var opt = t.opt;
|
|
4756 var trigger = t.trigger;
|
|
4757 var reportEventElement = t.reportEventElement;
|
|
4758 var eventElementHandlers = t.eventElementHandlers;
|
|
4759 var showEvents = t.showEvents;
|
|
4760 var hideEvents = t.hideEvents;
|
|
4761 var getListContainer = t.getDaySegmentContainer;
|
|
4762 var calendar = t.calendar;
|
|
4763 var formatDate = calendar.formatDate;
|
|
4764 var formatDates = calendar.formatDates;
|
|
4765
|
|
4766
|
|
4767 /* Rendering
|
|
4768 --------------------------------------------------------------------*/
|
|
4769
|
|
4770 function clearEvents() {
|
|
4771 getListContainer().empty();
|
|
4772 }
|
|
4773
|
|
4774 function renderEvents(events, modifiedEventId) {
|
|
4775 events.sort(sortCmp);
|
|
4776 clearEvents();
|
|
4777 renderSegs(compileSegs(events), modifiedEventId);
|
|
4778 }
|
|
4779
|
|
4780 function compileSegs(events) {
|
|
4781 var segs = [];
|
|
4782 var colFormat = opt('titleFormat', 'day');
|
|
4783 var firstDay = opt('firstDay');
|
|
4784 var segmode = opt('listSections');
|
|
4785 var event, i, dd, wd, md, seg, segHash, curSegHash, segDate, curSeg = -1;
|
|
4786 var today = clearTime(new Date());
|
|
4787 var weekstart = addDays(cloneDate(today), -((today.getDay() - firstDay + 7) % 7));
|
|
4788
|
|
4789 for (i=0; i < events.length; i++) {
|
|
4790 event = events[i];
|
|
4791
|
|
4792 // skip events out of range
|
|
4793 if ((event.end || event.start) < t.start || event.start > t.visEnd)
|
|
4794 continue;
|
|
4795
|
|
4796 // define sections of this event
|
|
4797 // create smart sections such as today, tomorrow, this week, next week, next month, ect.
|
|
4798 segDate = cloneDate(event.start < t.start && event.end > t.start ? t.start : event.start, true);
|
|
4799 dd = dayDiff(segDate, today);
|
|
4800 wd = Math.floor(dayDiff(segDate, weekstart) / 7);
|
|
4801 md = segDate.getMonth() + ((segDate.getYear() - today.getYear()) * 12) - today.getMonth();
|
|
4802
|
|
4803 // build section title
|
|
4804 if (segmode == 'smart') {
|
|
4805 if (dd < 0) {
|
|
4806 segHash = opt('listTexts', 'past');
|
|
4807 } else if (dd == 0) {
|
|
4808 segHash = opt('listTexts', 'today');
|
|
4809 } else if (dd == 1) {
|
|
4810 segHash = opt('listTexts', 'tomorrow');
|
|
4811 } else if (wd == 0) {
|
|
4812 segHash = opt('listTexts', 'thisWeek');
|
|
4813 } else if (wd == 1) {
|
|
4814 segHash = opt('listTexts', 'nextWeek');
|
|
4815 } else if (md == 0) {
|
|
4816 segHash = opt('listTexts', 'thisMonth');
|
|
4817 } else if (md == 1) {
|
|
4818 segHash = opt('listTexts', 'nextMonth');
|
|
4819 } else if (md > 1) {
|
|
4820 segHash = opt('listTexts', 'future');
|
|
4821 }
|
|
4822 } else if (segmode == 'month') {
|
|
4823 segHash = formatDate(segDate, 'MMMM yyyy');
|
|
4824 } else if (segmode == 'week') {
|
|
4825 segHash = opt('listTexts', 'week') + formatDate(segDate, ' W');
|
|
4826 } else if (segmode == 'day') {
|
|
4827 segHash = formatDate(segDate, colFormat);
|
|
4828 } else {
|
|
4829 segHash = '';
|
|
4830 }
|
|
4831
|
|
4832 // start new segment
|
|
4833 if (segHash != curSegHash) {
|
|
4834 segs[++curSeg] = { events: [], start: segDate, title: segHash, daydiff: dd, weekdiff: wd, monthdiff: md };
|
|
4835 curSegHash = segHash;
|
|
4836 }
|
|
4837
|
|
4838 segs[curSeg].events.push(event);
|
|
4839 }
|
|
4840
|
|
4841 return segs;
|
|
4842 }
|
|
4843
|
|
4844 function sortCmp(a, b) {
|
|
4845 var sd = a.start.getTime() - b.start.getTime();
|
|
4846 return sd || (a.end ? a.end.getTime() : 0) - (b.end ? b.end.getTime() : 0);
|
|
4847 }
|
|
4848
|
|
4849 function renderSegs(segs, modifiedEventId) {
|
|
4850 var tm = opt('theme') ? 'ui' : 'fc';
|
|
4851 var headerClass = tm + "-widget-header";
|
|
4852 var contentClass = tm + "-widget-content";
|
|
4853 var i, j, seg, event, times, s, skinCss, skinCssAttr, classes, segContainer, eventElement, eventElements, triggerRes;
|
|
4854
|
|
4855 for (j=0; j < segs.length; j++) {
|
|
4856 seg = segs[j];
|
|
4857
|
|
4858 if (seg.title) {
|
|
4859 $('<div class="fc-list-header ' + headerClass + '">' + htmlEscape(seg.title) + '</div>').appendTo(getListContainer());
|
|
4860 }
|
|
4861 segContainer = $('<div>').addClass('fc-list-section ' + contentClass).appendTo(getListContainer());
|
|
4862 s = '';
|
|
4863
|
|
4864 for (i=0; i < seg.events.length; i++) {
|
|
4865 event = seg.events[i];
|
|
4866 times = renderEventTime(event, seg);
|
|
4867 skinCss = getSkinCss(event, opt);
|
|
4868 skinCssAttr = (skinCss ? " style='" + skinCss + "'" : '');
|
|
4869 classes = ['fc-event', 'fc-event-skin', 'fc-event-vert', 'fc-corner-top', 'fc-corner-bottom'].concat(event.className);
|
|
4870 if (event.source && event.source.className) {
|
|
4871 classes = classes.concat(event.source.className);
|
|
4872 }
|
|
4873
|
|
4874 s +=
|
|
4875 "<div class='" + classes.join(' ') + "'" + skinCssAttr + ">" +
|
|
4876 "<div class='fc-event-inner fc-event-skin'" + skinCssAttr + ">" +
|
|
4877 "<div class='fc-event-head fc-event-skin'" + skinCssAttr + ">" +
|
|
4878 "<div class='fc-event-time'>" +
|
|
4879 (times[0] ? '<span class="fc-col-date">' + times[0] + '</span> ' : '') +
|
|
4880 (times[1] ? '<span class="fc-col-time">' + times[1] + '</span>' : '') +
|
|
4881 "</div>" +
|
|
4882 "</div>" +
|
|
4883 "<div class='fc-event-content'>" +
|
|
4884 "<div class='fc-event-title'>" +
|
|
4885 htmlEscape(event.title) +
|
|
4886 "</div>" +
|
|
4887 "</div>" +
|
|
4888 "<div class='fc-event-bg'></div>" +
|
|
4889 "</div>" + // close inner
|
|
4890 "</div>"; // close outer
|
|
4891 }
|
|
4892
|
|
4893 segContainer[0].innerHTML = s;
|
|
4894 eventElements = segContainer.children();
|
|
4895
|
|
4896 // retrieve elements, run through eventRender callback, bind event handlers
|
|
4897 for (i=0; i < seg.events.length; i++) {
|
|
4898 event = seg.events[i];
|
|
4899 eventElement = $(eventElements[i]); // faster than eq()
|
|
4900 triggerRes = trigger('eventRender', event, event, eventElement);
|
|
4901 if (triggerRes === false) {
|
|
4902 eventElement.remove();
|
|
4903 } else {
|
|
4904 if (triggerRes && triggerRes !== true) {
|
|
4905 eventElement.remove();
|
|
4906 eventElement = $(triggerRes).appendTo(segContainer);
|
|
4907 }
|
|
4908 if (event._id === modifiedEventId) {
|
|
4909 eventElementHandlers(event, eventElement, seg);
|
|
4910 } else {
|
|
4911 eventElement[0]._fci = i; // for lazySegBind
|
|
4912 }
|
|
4913 reportEventElement(event, eventElement);
|
|
4914 }
|
|
4915 }
|
|
4916
|
|
4917 lazySegBind(segContainer, seg, eventElementHandlers);
|
|
4918 }
|
|
4919
|
|
4920 markFirstLast(getListContainer());
|
|
4921 }
|
|
4922
|
|
4923 // event time/date range to display
|
|
4924 function renderEventTime(event, seg) {
|
|
4925 var timeFormat = opt('timeFormat');
|
|
4926 var dateFormat = opt('columnFormat');
|
|
4927 var segmode = opt('listSections');
|
|
4928 var duration = event.end ? event.end.getTime() - event.start.getTime() : 0;
|
|
4929 var datestr = '', timestr = '';
|
|
4930
|
|
4931 if (segmode == 'smart') {
|
|
4932 if (event.start < seg.start) {
|
|
4933 datestr = opt('listTexts', 'until') + ' ' + formatDate(event.end, (event.allDay || event.end.getDate() != seg.start.getDate()) ? dateFormat : timeFormat);
|
|
4934 } else if (duration > DAY_MS) {
|
|
4935 datestr = formatDates(event.start, event.end, dateFormat + '{ - ' + dateFormat + '}');
|
|
4936 } else if (seg.daydiff == 0) {
|
|
4937 datestr = opt('listTexts', 'today');
|
|
4938 } else if (seg.daydiff == 1) {
|
|
4939 datestr = opt('listTexts', 'tomorrow');
|
|
4940 } else if (seg.weekdiff == 0 || seg.weekdiff == 1) {
|
|
4941 datestr = formatDate(event.start, 'dddd');
|
|
4942 } else if (seg.daydiff > 1 || seg.daydiff < 0) {
|
|
4943 datestr = formatDate(event.start, dateFormat);
|
|
4944 }
|
|
4945 } else if (segmode != 'day') {
|
|
4946 datestr = formatDates(event.start, event.end, dateFormat + (duration > DAY_MS ? '{ - ' + dateFormat + '}' : ''));
|
|
4947 }
|
|
4948
|
|
4949 if (!datestr && event.allDay) {
|
|
4950 timestr = opt('allDayText');
|
|
4951 } else if ((duration < DAY_MS || !datestr) && !event.allDay) {
|
|
4952 timestr = formatDates(event.start, event.end, timeFormat);
|
|
4953 }
|
|
4954
|
|
4955 return [datestr, timestr];
|
|
4956 }
|
|
4957
|
|
4958 function lazySegBind(container, seg, bindHandlers) {
|
|
4959 container.unbind('mouseover focusin').bind('mouseover focusin', function(ev) {
|
|
4960 var parent = ev.target, e = parent, i, event;
|
|
4961 while (parent != this) {
|
|
4962 e = parent;
|
|
4963 parent = parent.parentNode;
|
|
4964 }
|
|
4965 if ((i = e._fci) !== undefined) {
|
|
4966 e._fci = undefined;
|
|
4967 event = seg.events[i];
|
|
4968 bindHandlers(event, container.children().eq(i), seg);
|
|
4969 $(ev.target).trigger(ev);
|
|
4970 }
|
|
4971 ev.stopPropagation();
|
|
4972 });
|
|
4973 }
|
|
4974
|
|
4975 }
|
|
4976
|
|
4977
|
|
4978 ;;
|
|
4979
|
|
4980 /* Additional view: table (by bruederli@kolabsys.com)
|
|
4981 ---------------------------------------------------------------------------------*/
|
|
4982
|
|
4983 fcViews.table = TableView;
|
|
4984
|
|
4985
|
|
4986 function TableView(element, calendar) {
|
|
4987 var t = this;
|
|
4988
|
|
4989 // exports
|
|
4990 t.render = render;
|
|
4991 t.select = dummy;
|
|
4992 t.unselect = dummy;
|
|
4993 t.getDaySegmentContainer = function(){ return table; };
|
|
4994
|
|
4995 // imports
|
|
4996 View.call(t, element, calendar, 'table');
|
|
4997 TableEventRenderer.call(t);
|
|
4998 var opt = t.opt;
|
|
4999 var trigger = t.trigger;
|
|
5000 var clearEvents = t.clearEvents;
|
|
5001 var reportEventClear = t.reportEventClear;
|
|
5002 var formatDates = calendar.formatDates;
|
|
5003 var formatDate = calendar.formatDate;
|
|
5004
|
|
5005 // overrides
|
|
5006 t.setWidth = setWidth;
|
|
5007 t.setHeight = setHeight;
|
|
5008
|
|
5009 // locals
|
|
5010 var div;
|
|
5011 var table;
|
|
5012 var firstDay;
|
|
5013 var nwe;
|
|
5014 var tm;
|
|
5015 var colFormat;
|
|
5016
|
|
5017
|
|
5018 function render(date, delta) {
|
|
5019 if (delta) {
|
|
5020 addDays(date, opt('listPage') * delta);
|
|
5021 }
|
|
5022 t.start = t.visStart = cloneDate(date, true);
|
|
5023 t.end = addDays(cloneDate(t.start), opt('listPage'));
|
|
5024 t.visEnd = addDays(cloneDate(t.start), opt('listRange'));
|
|
5025 addMinutes(t.visEnd, -1); // set end to 23:59
|
|
5026 t.title = (t.visEnd.getTime() - t.visStart.getTime() < DAY_MS) ? formatDate(date, opt('titleFormat')) : formatDates(date, t.visEnd, opt('titleFormat'));
|
|
5027
|
|
5028 updateOptions();
|
|
5029
|
|
5030 if (!table) {
|
|
5031 buildSkeleton();
|
|
5032 } else {
|
|
5033 clearEvents();
|
|
5034 }
|
|
5035 }
|
|
5036
|
|
5037
|
|
5038 function updateOptions() {
|
|
5039 firstDay = opt('firstDay');
|
|
5040 nwe = opt('weekends') ? 0 : 1;
|
|
5041 tm = opt('theme') ? 'ui' : 'fc';
|
|
5042 colFormat = opt('columnFormat');
|
|
5043 }
|
|
5044
|
|
5045
|
|
5046 function buildSkeleton() {
|
|
5047 var tableCols = opt('tableCols');
|
|
5048 var s =
|
|
5049 "<table class='fc-border-separate' style='width:100%' cellspacing='0'>" +
|
|
5050 "<colgroup>";
|
|
5051 for (var c=0; c < tableCols.length; c++) {
|
|
5052 s += "<col class='fc-event-" + tableCols[c] + "' />";
|
|
5053 }
|
|
5054 s += "</colgroup>" +
|
|
5055 "</table>";
|
|
5056 div = $('<div>').addClass('fc-list-content').appendTo(element);
|
|
5057 table = $(s).appendTo(div);
|
|
5058 }
|
|
5059
|
|
5060 function setHeight(height, dateChanged) {
|
|
5061 if (!opt('listNoHeight'))
|
|
5062 div.css('height', (height-1)+'px').css('overflow', 'auto');
|
|
5063 }
|
|
5064
|
|
5065 function setWidth(width) {
|
|
5066 // nothing to be done here
|
|
5067 }
|
|
5068
|
|
5069 function dummy() {
|
|
5070 // Stub.
|
|
5071 }
|
|
5072
|
|
5073 }
|
|
5074
|
|
5075 ;;
|
|
5076
|
|
5077 /* Additional view renderer: table (by bruederli@kolabsys.com)
|
|
5078 ---------------------------------------------------------------------------------*/
|
|
5079
|
|
5080 function TableEventRenderer() {
|
|
5081 var t = this;
|
|
5082
|
|
5083 // imports
|
|
5084 ListEventRenderer.call(t);
|
|
5085 var opt = t.opt;
|
|
5086 var sortCmp = t.sortCmp;
|
|
5087 var trigger = t.trigger;
|
|
5088 var compileSegs = t.compileDaySegs;
|
|
5089 var reportEventElement = t.reportEventElement;
|
|
5090 var eventElementHandlers = t.eventElementHandlers;
|
|
5091 var renderEventTime = t.renderEventTime;
|
|
5092 var showEvents = t.showEvents;
|
|
5093 var hideEvents = t.hideEvents;
|
|
5094 var getListContainer = t.getDaySegmentContainer;
|
|
5095 var lazySegBind = t.lazySegBind;
|
|
5096 var calendar = t.calendar;
|
|
5097 var formatDate = calendar.formatDate;
|
|
5098 var formatDates = calendar.formatDates;
|
|
5099
|
|
5100 // exports
|
|
5101 t.renderEvents = renderEvents;
|
|
5102 t.clearEvents = clearEvents;
|
|
5103
|
|
5104
|
|
5105 /* Rendering
|
|
5106 --------------------------------------------------------------------*/
|
|
5107
|
|
5108 function clearEvents() {
|
|
5109 getListContainer().children('tbody').remove();
|
|
5110 }
|
|
5111
|
|
5112 function renderEvents(events, modifiedEventId) {
|
|
5113 events.sort(sortCmp);
|
|
5114 clearEvents();
|
|
5115 renderSegs(compileSegs(events), modifiedEventId);
|
|
5116 getListContainer().removeClass('fc-list-smart fc-list-day fc-list-month fc-list-week').addClass('fc-list-' + opt('listSections'));
|
|
5117 }
|
|
5118
|
|
5119 function renderSegs(segs, modifiedEventId) {
|
|
5120 var tm = opt('theme') ? 'ui' : 'fc';
|
|
5121 var table = getListContainer();
|
|
5122 var headerClass = tm + "-widget-header";
|
|
5123 var contentClass = tm + "-widget-content";
|
|
5124 var tableCols = opt('tableCols');
|
|
5125 var timecol = $.inArray('time', tableCols) >= 0;
|
|
5126 var i, j, seg, event, times, s, skinCss, skinCssAttr, skinClasses, rowClasses, segContainer, eventElements, eventElement, triggerRes;
|
|
5127
|
|
5128 for (j=0; j < segs.length; j++) {
|
|
5129 seg = segs[j];
|
|
5130
|
|
5131 if (seg.title) {
|
|
5132 $('<tbody class="fc-list-header"><tr><td class="fc-list-header ' + headerClass + '" colspan="' + tableCols.length + '">' + htmlEscape(seg.title) + '</td></tr></tbody>').appendTo(table);
|
|
5133 }
|
|
5134 segContainer = $('<tbody>').addClass('fc-list-section ' + contentClass).appendTo(table);
|
|
5135 s = '';
|
|
5136
|
|
5137 for (i=0; i < seg.events.length; i++) {
|
|
5138 event = seg.events[i];
|
|
5139 times = renderEventTime(event, seg);
|
|
5140 skinCss = getSkinCss(event, opt);
|
|
5141 skinCssAttr = (skinCss ? " style='" + skinCss + "'" : '');
|
|
5142 skinClasses = ['fc-event-skin', 'fc-corner-left', 'fc-corner-right', 'fc-corner-top', 'fc-corner-bottom'].concat(event.className);
|
|
5143 if (event.source && event.source.className) {
|
|
5144 skinClasses = skinClasses.concat(event.source.className);
|
|
5145 }
|
|
5146 rowClasses = ['fc-event', 'fc-event-row', 'fc-'+dayIDs[event.start.getDay()]].concat(event.className);
|
|
5147 if (seg.daydiff == 0) {
|
|
5148 rowClasses.push('fc-today');
|
|
5149 }
|
|
5150
|
|
5151 s += "<tr class='" + rowClasses.join(' ') + "' tabindex='0'>";
|
|
5152 for (var col, c=0; c < tableCols.length; c++) {
|
|
5153 col = tableCols[c];
|
|
5154 if (col == 'handle') {
|
|
5155 s += "<td class='fc-event-handle'>" +
|
|
5156 "<div class='" + skinClasses.join(' ') + "'" + skinCssAttr + ">" +
|
|
5157 "<span class='fc-event-inner'></span>" +
|
|
5158 "</div></td>";
|
|
5159 } else if (col == 'date') {
|
|
5160 s += "<td class='fc-event-date' colspan='" + (times[1] || !timecol ? 1 : 2) + "'>" + htmlEscape(times[0]) + "</td>";
|
|
5161 } else if (col == 'time') {
|
|
5162 if (times[1]) {
|
|
5163 s += "<td class='fc-event-time'>" + htmlEscape(times[1]) + "</td>";
|
|
5164 }
|
|
5165 } else {
|
|
5166 s += "<td class='fc-event-" + col + "'>" + (event[col] ? htmlEscape(event[col]) : ' ') + "</td>";
|
|
5167 }
|
|
5168 }
|
|
5169 s += "</tr>";
|
|
5170
|
|
5171 // IE doesn't like innerHTML on tbody elements so we insert every row individually
|
|
5172 if (document.all) {
|
|
5173 $(s).appendTo(segContainer);
|
|
5174 s = '';
|
|
5175 }
|
|
5176 }
|
|
5177
|
|
5178 if (!document.all)
|
|
5179 segContainer[0].innerHTML = s;
|
|
5180
|
|
5181 eventElements = segContainer.children();
|
|
5182
|
|
5183 // retrieve elements, run through eventRender callback, bind event handlers
|
|
5184 for (i=0; i < seg.events.length; i++) {
|
|
5185 event = seg.events[i];
|
|
5186 eventElement = $(eventElements[i]); // faster than eq()
|
|
5187 triggerRes = trigger('eventRender', event, event, eventElement);
|
|
5188 if (triggerRes === false) {
|
|
5189 eventElement.remove();
|
|
5190 } else {
|
|
5191 if (triggerRes && triggerRes !== true) {
|
|
5192 eventElement.remove();
|
|
5193 eventElement = $(triggerRes).appendTo(segContainer);
|
|
5194 }
|
|
5195 if (event._id === modifiedEventId) {
|
|
5196 eventElementHandlers(event, eventElement, seg);
|
|
5197 } else {
|
|
5198 eventElement[0]._fci = i; // for lazySegBind
|
|
5199 }
|
|
5200 reportEventElement(event, eventElement);
|
|
5201 }
|
|
5202 }
|
|
5203
|
|
5204 lazySegBind(segContainer, seg, eventElementHandlers);
|
|
5205 markFirstLast(segContainer);
|
|
5206 }
|
|
5207
|
|
5208 //markFirstLast(table);
|
|
5209 }
|
|
5210
|
|
5211 }
|
|
5212 ;;
|
|
5213
|
|
5214
|
|
5215 function View(element, calendar, viewName) {
|
|
5216 var t = this;
|
|
5217
|
|
5218
|
|
5219 // exports
|
|
5220 t.element = element;
|
|
5221 t.calendar = calendar;
|
|
5222 t.name = viewName;
|
|
5223 t.opt = opt;
|
|
5224 t.trigger = trigger;
|
|
5225 t.isEventDraggable = isEventDraggable;
|
|
5226 t.isEventResizable = isEventResizable;
|
|
5227 t.setEventData = setEventData;
|
|
5228 t.clearEventData = clearEventData;
|
|
5229 t.eventEnd = eventEnd;
|
|
5230 t.reportEventElement = reportEventElement;
|
|
5231 t.triggerEventDestroy = triggerEventDestroy;
|
|
5232 t.eventElementHandlers = eventElementHandlers;
|
|
5233 t.showEvents = showEvents;
|
|
5234 t.hideEvents = hideEvents;
|
|
5235 t.eventDrop = eventDrop;
|
|
5236 t.eventResize = eventResize;
|
|
5237 // t.title
|
|
5238 // t.start, t.end
|
|
5239 // t.visStart, t.visEnd
|
|
5240
|
|
5241
|
|
5242 // imports
|
|
5243 var defaultEventEnd = t.defaultEventEnd;
|
|
5244 var normalizeEvent = calendar.normalizeEvent; // in EventManager
|
|
5245 var reportEventChange = calendar.reportEventChange;
|
|
5246
|
|
5247
|
|
5248 // locals
|
|
5249 var eventsByID = {}; // eventID mapped to array of events (there can be multiple b/c of repeating events)
|
|
5250 var eventElementsByID = {}; // eventID mapped to array of jQuery elements
|
|
5251 var eventElementCouples = []; // array of objects, { event, element } // TODO: unify with segment system
|
|
5252 var options = calendar.options;
|
|
5253
|
|
5254
|
|
5255
|
|
5256 function opt(name, viewNameOverride) {
|
|
5257 var v = options[name];
|
|
5258 if ($.isPlainObject(v)) {
|
|
5259 return smartProperty(v, viewNameOverride || viewName);
|
|
5260 }
|
|
5261 return v;
|
|
5262 }
|
|
5263
|
|
5264
|
|
5265 function trigger(name, thisObj) {
|
|
5266 return calendar.trigger.apply(
|
|
5267 calendar,
|
|
5268 [name, thisObj || t].concat(Array.prototype.slice.call(arguments, 2), [t])
|
|
5269 );
|
|
5270 }
|
|
5271
|
|
5272
|
|
5273
|
|
5274 /* Event Editable Boolean Calculations
|
|
5275 ------------------------------------------------------------------------------*/
|
|
5276
|
|
5277
|
|
5278 function isEventDraggable(event) {
|
|
5279 var source = event.source || {};
|
|
5280 return firstDefined(
|
|
5281 event.startEditable,
|
|
5282 source.startEditable,
|
|
5283 opt('eventStartEditable'),
|
|
5284 event.editable,
|
|
5285 source.editable,
|
|
5286 opt('editable')
|
|
5287 )
|
|
5288 && !opt('disableDragging'); // deprecated
|
|
5289 }
|
|
5290
|
|
5291
|
|
5292 function isEventResizable(event) { // but also need to make sure the seg.isEnd == true
|
|
5293 var source = event.source || {};
|
|
5294 return firstDefined(
|
|
5295 event.durationEditable,
|
|
5296 source.durationEditable,
|
|
5297 opt('eventDurationEditable'),
|
|
5298 event.editable,
|
|
5299 source.editable,
|
|
5300 opt('editable')
|
|
5301 )
|
|
5302 && !opt('disableResizing'); // deprecated
|
|
5303 }
|
|
5304
|
|
5305
|
|
5306
|
|
5307 /* Event Data
|
|
5308 ------------------------------------------------------------------------------*/
|
|
5309
|
|
5310
|
|
5311 function setEventData(events) { // events are already normalized at this point
|
|
5312 eventsByID = {};
|
|
5313 var i, len=events.length, event;
|
|
5314 for (i=0; i<len; i++) {
|
|
5315 event = events[i];
|
|
5316 if (eventsByID[event._id]) {
|
|
5317 eventsByID[event._id].push(event);
|
|
5318 }else{
|
|
5319 eventsByID[event._id] = [event];
|
|
5320 }
|
|
5321 }
|
|
5322 }
|
|
5323
|
|
5324
|
|
5325 function clearEventData() {
|
|
5326 eventsByID = {};
|
|
5327 eventElementsByID = {};
|
|
5328 eventElementCouples = [];
|
|
5329 }
|
|
5330
|
|
5331
|
|
5332 // returns a Date object for an event's end
|
|
5333 function eventEnd(event) {
|
|
5334 return event.end ? cloneDate(event.end) : defaultEventEnd(event);
|
|
5335 }
|
|
5336
|
|
5337
|
|
5338
|
|
5339 /* Event Elements
|
|
5340 ------------------------------------------------------------------------------*/
|
|
5341
|
|
5342
|
|
5343 // report when view creates an element for an event
|
|
5344 function reportEventElement(event, element) {
|
|
5345 eventElementCouples.push({ event: event, element: element });
|
|
5346 if (eventElementsByID[event._id]) {
|
|
5347 eventElementsByID[event._id].push(element);
|
|
5348 }else{
|
|
5349 eventElementsByID[event._id] = [element];
|
|
5350 }
|
|
5351 }
|
|
5352
|
|
5353
|
|
5354 function triggerEventDestroy() {
|
|
5355 $.each(eventElementCouples, function(i, couple) {
|
|
5356 t.trigger('eventDestroy', couple.event, couple.event, couple.element);
|
|
5357 });
|
|
5358 }
|
|
5359
|
|
5360
|
|
5361 // attaches eventClick, eventMouseover, eventMouseout
|
|
5362 function eventElementHandlers(event, eventElement) {
|
|
5363 eventElement
|
|
5364 .click(function(ev) {
|
|
5365 if (!eventElement.hasClass('ui-draggable-dragging') &&
|
|
5366 !eventElement.hasClass('ui-resizable-resizing')) {
|
|
5367 return trigger('eventClick', this, event, ev);
|
|
5368 }
|
|
5369 })
|
|
5370 .hover(
|
|
5371 function(ev) {
|
|
5372 trigger('eventMouseover', this, event, ev);
|
|
5373 },
|
|
5374 function(ev) {
|
|
5375 trigger('eventMouseout', this, event, ev);
|
|
5376 }
|
|
5377 )
|
|
5378 .keypress(function(ev) {
|
|
5379 if (ev.keyCode == 13)
|
|
5380 $(this).trigger('click', { pointerType:'keyboard' });
|
|
5381 });
|
|
5382 // TODO: don't fire eventMouseover/eventMouseout *while* dragging is occuring (on subject element)
|
|
5383 // TODO: same for resizing
|
|
5384 }
|
|
5385
|
|
5386
|
|
5387 function showEvents(event, exceptElement) {
|
|
5388 eachEventElement(event, exceptElement, 'show');
|
|
5389 }
|
|
5390
|
|
5391
|
|
5392 function hideEvents(event, exceptElement) {
|
|
5393 eachEventElement(event, exceptElement, 'hide');
|
|
5394 }
|
|
5395
|
|
5396
|
|
5397 function eachEventElement(event, exceptElement, funcName) {
|
|
5398 // NOTE: there may be multiple events per ID (repeating events)
|
|
5399 // and multiple segments per event
|
|
5400 var elements = eventElementsByID[event._id],
|
|
5401 i, len = elements.length;
|
|
5402 for (i=0; i<len; i++) {
|
|
5403 if (!exceptElement || elements[i][0] != exceptElement[0]) {
|
|
5404 elements[i][funcName]();
|
|
5405 }
|
|
5406 }
|
|
5407 }
|
|
5408
|
|
5409
|
|
5410
|
|
5411 /* Event Modification Reporting
|
|
5412 ---------------------------------------------------------------------------------*/
|
|
5413
|
|
5414
|
|
5415 function eventDrop(e, event, dayDelta, minuteDelta, allDay, ev, ui) {
|
|
5416 var oldAllDay = event.allDay;
|
|
5417 var eventId = event._id;
|
|
5418 moveEvents(eventsByID[eventId], dayDelta, minuteDelta, allDay);
|
|
5419 trigger(
|
|
5420 'eventDrop',
|
|
5421 e,
|
|
5422 event,
|
|
5423 dayDelta,
|
|
5424 minuteDelta,
|
|
5425 allDay,
|
|
5426 function() {
|
|
5427 // TODO: investigate cases where this inverse technique might not work
|
|
5428 moveEvents(eventsByID[eventId], -dayDelta, -minuteDelta, oldAllDay);
|
|
5429 reportEventChange(eventId);
|
|
5430 },
|
|
5431 ev,
|
|
5432 ui
|
|
5433 );
|
|
5434 reportEventChange(eventId);
|
|
5435 }
|
|
5436
|
|
5437
|
|
5438 function eventResize(e, event, dayDelta, minuteDelta, ev, ui) {
|
|
5439 var eventId = event._id;
|
|
5440 elongateEvents(eventsByID[eventId], dayDelta, minuteDelta);
|
|
5441 trigger(
|
|
5442 'eventResize',
|
|
5443 e,
|
|
5444 event,
|
|
5445 dayDelta,
|
|
5446 minuteDelta,
|
|
5447 function() {
|
|
5448 // TODO: investigate cases where this inverse technique might not work
|
|
5449 elongateEvents(eventsByID[eventId], -dayDelta, -minuteDelta);
|
|
5450 reportEventChange(eventId);
|
|
5451 },
|
|
5452 ev,
|
|
5453 ui
|
|
5454 );
|
|
5455 reportEventChange(eventId);
|
|
5456 }
|
|
5457
|
|
5458
|
|
5459
|
|
5460 /* Event Modification Math
|
|
5461 ---------------------------------------------------------------------------------*/
|
|
5462
|
|
5463
|
|
5464 function moveEvents(events, dayDelta, minuteDelta, allDay) {
|
|
5465 minuteDelta = minuteDelta || 0;
|
|
5466 for (var e, len=events.length, i=0; i<len; i++) {
|
|
5467 e = events[i];
|
|
5468 if (allDay !== undefined) {
|
|
5469 e.allDay = allDay;
|
|
5470 }
|
|
5471 addMinutes(addDays(e.start, dayDelta, true), minuteDelta);
|
|
5472 if (e.end) {
|
|
5473 e.end = addMinutes(addDays(e.end, dayDelta, true), minuteDelta);
|
|
5474 }
|
|
5475 normalizeEvent(e, options);
|
|
5476 }
|
|
5477 }
|
|
5478
|
|
5479
|
|
5480 function elongateEvents(events, dayDelta, minuteDelta) {
|
|
5481 minuteDelta = minuteDelta || 0;
|
|
5482 for (var e, len=events.length, i=0; i<len; i++) {
|
|
5483 e = events[i];
|
|
5484 e.end = addMinutes(addDays(eventEnd(e), dayDelta, true), minuteDelta);
|
|
5485 normalizeEvent(e, options);
|
|
5486 }
|
|
5487 }
|
|
5488
|
|
5489
|
|
5490
|
|
5491 // ====================================================================================================
|
|
5492 // Utilities for day "cells"
|
|
5493 // ====================================================================================================
|
|
5494 // The "basic" views are completely made up of day cells.
|
|
5495 // The "agenda" views have day cells at the top "all day" slot.
|
|
5496 // This was the obvious common place to put these utilities, but they should be abstracted out into
|
|
5497 // a more meaningful class (like DayEventRenderer).
|
|
5498 // ====================================================================================================
|
|
5499
|
|
5500
|
|
5501 // For determining how a given "cell" translates into a "date":
|
|
5502 //
|
|
5503 // 1. Convert the "cell" (row and column) into a "cell offset" (the # of the cell, cronologically from the first).
|
|
5504 // Keep in mind that column indices are inverted with isRTL. This is taken into account.
|
|
5505 //
|
|
5506 // 2. Convert the "cell offset" to a "day offset" (the # of days since the first visible day in the view).
|
|
5507 //
|
|
5508 // 3. Convert the "day offset" into a "date" (a JavaScript Date object).
|
|
5509 //
|
|
5510 // The reverse transformation happens when transforming a date into a cell.
|
|
5511
|
|
5512
|
|
5513 // exports
|
|
5514 t.isHiddenDay = isHiddenDay;
|
|
5515 t.skipHiddenDays = skipHiddenDays;
|
|
5516 t.getCellsPerWeek = getCellsPerWeek;
|
|
5517 t.dateToCell = dateToCell;
|
|
5518 t.dateToDayOffset = dateToDayOffset;
|
|
5519 t.dayOffsetToCellOffset = dayOffsetToCellOffset;
|
|
5520 t.cellOffsetToCell = cellOffsetToCell;
|
|
5521 t.cellToDate = cellToDate;
|
|
5522 t.cellToCellOffset = cellToCellOffset;
|
|
5523 t.cellOffsetToDayOffset = cellOffsetToDayOffset;
|
|
5524 t.dayOffsetToDate = dayOffsetToDate;
|
|
5525 t.rangeToSegments = rangeToSegments;
|
|
5526
|
|
5527
|
|
5528 // internals
|
|
5529 var hiddenDays = opt('hiddenDays') || []; // array of day-of-week indices that are hidden
|
|
5530 var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool)
|
|
5531 var cellsPerWeek;
|
|
5532 var dayToCellMap = []; // hash from dayIndex -> cellIndex, for one week
|
|
5533 var cellToDayMap = []; // hash from cellIndex -> dayIndex, for one week
|
|
5534 var isRTL = opt('isRTL');
|
|
5535
|
|
5536
|
|
5537 // initialize important internal variables
|
|
5538 (function() {
|
|
5539
|
|
5540 if (opt('weekends') === false) {
|
|
5541 hiddenDays.push(0, 6); // 0=sunday, 6=saturday
|
|
5542 }
|
|
5543
|
|
5544 // Loop through a hypothetical week and determine which
|
|
5545 // days-of-week are hidden. Record in both hashes (one is the reverse of the other).
|
|
5546 for (var dayIndex=0, cellIndex=0; dayIndex<7; dayIndex++) {
|
|
5547 dayToCellMap[dayIndex] = cellIndex;
|
|
5548 isHiddenDayHash[dayIndex] = $.inArray(dayIndex, hiddenDays) != -1;
|
|
5549 if (!isHiddenDayHash[dayIndex]) {
|
|
5550 cellToDayMap[cellIndex] = dayIndex;
|
|
5551 cellIndex++;
|
|
5552 }
|
|
5553 }
|
|
5554
|
|
5555 cellsPerWeek = cellIndex;
|
|
5556 if (!cellsPerWeek) {
|
|
5557 throw 'invalid hiddenDays'; // all days were hidden? bad.
|
|
5558 }
|
|
5559
|
|
5560 })();
|
|
5561
|
|
5562
|
|
5563 // Is the current day hidden?
|
|
5564 // `day` is a day-of-week index (0-6), or a Date object
|
|
5565 function isHiddenDay(day) {
|
|
5566 if (typeof day == 'object') {
|
|
5567 day = day.getDay();
|
|
5568 }
|
|
5569 return isHiddenDayHash[day];
|
|
5570 }
|
|
5571
|
|
5572
|
|
5573 function getCellsPerWeek() {
|
|
5574 return cellsPerWeek;
|
|
5575 }
|
|
5576
|
|
5577
|
|
5578 // Keep incrementing the current day until it is no longer a hidden day.
|
|
5579 // If the initial value of `date` is not a hidden day, don't do anything.
|
|
5580 // Pass `isExclusive` as `true` if you are dealing with an end date.
|
|
5581 // `inc` defaults to `1` (increment one day forward each time)
|
|
5582 function skipHiddenDays(date, inc, isExclusive) {
|
|
5583 inc = inc || 1;
|
|
5584 while (
|
|
5585 isHiddenDayHash[ ( date.getDay() + (isExclusive ? inc : 0) + 7 ) % 7 ]
|
|
5586 ) {
|
|
5587 addDays(date, inc);
|
|
5588 }
|
|
5589 }
|
|
5590
|
|
5591
|
|
5592 //
|
|
5593 // TRANSFORMATIONS: cell -> cell offset -> day offset -> date
|
|
5594 //
|
|
5595
|
|
5596 // cell -> date (combines all transformations)
|
|
5597 // Possible arguments:
|
|
5598 // - row, col
|
|
5599 // - { row:#, col: # }
|
|
5600 function cellToDate() {
|
|
5601 var cellOffset = cellToCellOffset.apply(null, arguments);
|
|
5602 var dayOffset = cellOffsetToDayOffset(cellOffset);
|
|
5603 var date = dayOffsetToDate(dayOffset);
|
|
5604 return date;
|
|
5605 }
|
|
5606
|
|
5607 // cell -> cell offset
|
|
5608 // Possible arguments:
|
|
5609 // - row, col
|
|
5610 // - { row:#, col:# }
|
|
5611 function cellToCellOffset(row, col) {
|
|
5612 var colCnt = t.getColCnt();
|
|
5613
|
|
5614 // rtl variables. wish we could pre-populate these. but where?
|
|
5615 var dis = isRTL ? -1 : 1;
|
|
5616 var dit = isRTL ? colCnt - 1 : 0;
|
|
5617
|
|
5618 if (typeof row == 'object') {
|
|
5619 col = row.col;
|
|
5620 row = row.row;
|
|
5621 }
|
|
5622 var cellOffset = row * colCnt + (col * dis + dit); // column, adjusted for RTL (dis & dit)
|
|
5623
|
|
5624 return cellOffset;
|
|
5625 }
|
|
5626
|
|
5627 // cell offset -> day offset
|
|
5628 function cellOffsetToDayOffset(cellOffset) {
|
|
5629 var day0 = t.visStart.getDay(); // first date's day of week
|
|
5630 cellOffset += dayToCellMap[day0]; // normlize cellOffset to beginning-of-week
|
|
5631 return Math.floor(cellOffset / cellsPerWeek) * 7 // # of days from full weeks
|
|
5632 + cellToDayMap[ // # of days from partial last week
|
|
5633 (cellOffset % cellsPerWeek + cellsPerWeek) % cellsPerWeek // crazy math to handle negative cellOffsets
|
|
5634 ]
|
|
5635 - day0; // adjustment for beginning-of-week normalization
|
|
5636 }
|
|
5637
|
|
5638 // day offset -> date (JavaScript Date object)
|
|
5639 function dayOffsetToDate(dayOffset) {
|
|
5640 var date = cloneDate(t.visStart);
|
|
5641 addDays(date, dayOffset);
|
|
5642 return date;
|
|
5643 }
|
|
5644
|
|
5645
|
|
5646 //
|
|
5647 // TRANSFORMATIONS: date -> day offset -> cell offset -> cell
|
|
5648 //
|
|
5649
|
|
5650 // date -> cell (combines all transformations)
|
|
5651 function dateToCell(date) {
|
|
5652 var dayOffset = dateToDayOffset(date);
|
|
5653 var cellOffset = dayOffsetToCellOffset(dayOffset);
|
|
5654 var cell = cellOffsetToCell(cellOffset);
|
|
5655 return cell;
|
|
5656 }
|
|
5657
|
|
5658 // date -> day offset
|
|
5659 function dateToDayOffset(date) {
|
|
5660 return dayDiff(date, t.visStart);
|
|
5661 }
|
|
5662
|
|
5663 // day offset -> cell offset
|
|
5664 function dayOffsetToCellOffset(dayOffset) {
|
|
5665 var day0 = t.visStart.getDay(); // first date's day of week
|
|
5666 dayOffset += day0; // normalize dayOffset to beginning-of-week
|
|
5667 return Math.floor(dayOffset / 7) * cellsPerWeek // # of cells from full weeks
|
|
5668 + dayToCellMap[ // # of cells from partial last week
|
|
5669 (dayOffset % 7 + 7) % 7 // crazy math to handle negative dayOffsets
|
|
5670 ]
|
|
5671 - dayToCellMap[day0]; // adjustment for beginning-of-week normalization
|
|
5672 }
|
|
5673
|
|
5674 // cell offset -> cell (object with row & col keys)
|
|
5675 function cellOffsetToCell(cellOffset) {
|
|
5676 var colCnt = t.getColCnt();
|
|
5677
|
|
5678 // rtl variables. wish we could pre-populate these. but where?
|
|
5679 var dis = isRTL ? -1 : 1;
|
|
5680 var dit = isRTL ? colCnt - 1 : 0;
|
|
5681
|
|
5682 var row = Math.floor(cellOffset / colCnt);
|
|
5683 var col = ((cellOffset % colCnt + colCnt) % colCnt) * dis + dit; // column, adjusted for RTL (dis & dit)
|
|
5684 return {
|
|
5685 row: row,
|
|
5686 col: col
|
|
5687 };
|
|
5688 }
|
|
5689
|
|
5690
|
|
5691 //
|
|
5692 // Converts a date range into an array of segment objects.
|
|
5693 // "Segments" are horizontal stretches of time, sliced up by row.
|
|
5694 // A segment object has the following properties:
|
|
5695 // - row
|
|
5696 // - cols
|
|
5697 // - isStart
|
|
5698 // - isEnd
|
|
5699 //
|
|
5700 function rangeToSegments(startDate, endDate) {
|
|
5701 var rowCnt = t.getRowCnt();
|
|
5702 var colCnt = t.getColCnt();
|
|
5703 var segments = []; // array of segments to return
|
|
5704
|
|
5705 // day offset for given date range
|
|
5706 var rangeDayOffsetStart = dateToDayOffset(startDate);
|
|
5707 var rangeDayOffsetEnd = dateToDayOffset(endDate); // exclusive
|
|
5708
|
|
5709 // first and last cell offset for the given date range
|
|
5710 // "last" implies inclusivity
|
|
5711 var rangeCellOffsetFirst = dayOffsetToCellOffset(rangeDayOffsetStart);
|
|
5712 var rangeCellOffsetLast = dayOffsetToCellOffset(rangeDayOffsetEnd) - 1;
|
|
5713
|
|
5714 // loop through all the rows in the view
|
|
5715 for (var row=0; row<rowCnt; row++) {
|
|
5716
|
|
5717 // first and last cell offset for the row
|
|
5718 var rowCellOffsetFirst = row * colCnt;
|
|
5719 var rowCellOffsetLast = rowCellOffsetFirst + colCnt - 1;
|
|
5720
|
|
5721 // get the segment's cell offsets by constraining the range's cell offsets to the bounds of the row
|
|
5722 var segmentCellOffsetFirst = Math.max(rangeCellOffsetFirst, rowCellOffsetFirst);
|
|
5723 var segmentCellOffsetLast = Math.min(rangeCellOffsetLast, rowCellOffsetLast);
|
|
5724
|
|
5725 // make sure segment's offsets are valid and in view
|
|
5726 if (segmentCellOffsetFirst <= segmentCellOffsetLast) {
|
|
5727
|
|
5728 // translate to cells
|
|
5729 var segmentCellFirst = cellOffsetToCell(segmentCellOffsetFirst);
|
|
5730 var segmentCellLast = cellOffsetToCell(segmentCellOffsetLast);
|
|
5731
|
|
5732 // view might be RTL, so order by leftmost column
|
|
5733 var cols = [ segmentCellFirst.col, segmentCellLast.col ].sort();
|
|
5734
|
|
5735 // Determine if segment's first/last cell is the beginning/end of the date range.
|
|
5736 // We need to compare "day offset" because "cell offsets" are often ambiguous and
|
|
5737 // can translate to multiple days, and an edge case reveals itself when we the
|
|
5738 // range's first cell is hidden (we don't want isStart to be true).
|
|
5739 var isStart = cellOffsetToDayOffset(segmentCellOffsetFirst) == rangeDayOffsetStart;
|
|
5740 var isEnd = cellOffsetToDayOffset(segmentCellOffsetLast) + 1 == rangeDayOffsetEnd; // +1 for comparing exclusively
|
|
5741
|
|
5742 segments.push({
|
|
5743 row: row,
|
|
5744 leftCol: cols[0],
|
|
5745 rightCol: cols[1],
|
|
5746 isStart: isStart,
|
|
5747 isEnd: isEnd
|
|
5748 });
|
|
5749 }
|
|
5750 }
|
|
5751
|
|
5752 return segments;
|
|
5753 }
|
|
5754
|
|
5755
|
|
5756 }
|
|
5757
|
|
5758 ;;
|
|
5759
|
|
5760 function DayEventRenderer() {
|
|
5761 var t = this;
|
|
5762
|
|
5763
|
|
5764 // exports
|
|
5765 t.renderDayEvents = renderDayEvents;
|
|
5766 t.draggableDayEvent = draggableDayEvent; // made public so that subclasses can override
|
|
5767 t.resizableDayEvent = resizableDayEvent; // "
|
|
5768
|
|
5769
|
|
5770 // imports
|
|
5771 var opt = t.opt;
|
|
5772 var trigger = t.trigger;
|
|
5773 var isEventDraggable = t.isEventDraggable;
|
|
5774 var isEventResizable = t.isEventResizable;
|
|
5775 var eventEnd = t.eventEnd;
|
|
5776 var reportEventElement = t.reportEventElement;
|
|
5777 var eventElementHandlers = t.eventElementHandlers;
|
|
5778 var showEvents = t.showEvents;
|
|
5779 var hideEvents = t.hideEvents;
|
|
5780 var eventDrop = t.eventDrop;
|
|
5781 var eventResize = t.eventResize;
|
|
5782 var getRowCnt = t.getRowCnt;
|
|
5783 var getColCnt = t.getColCnt;
|
|
5784 var getColWidth = t.getColWidth;
|
|
5785 var allDayRow = t.allDayRow; // TODO: rename
|
|
5786 var colLeft = t.colLeft;
|
|
5787 var colRight = t.colRight;
|
|
5788 var colContentLeft = t.colContentLeft;
|
|
5789 var colContentRight = t.colContentRight;
|
|
5790 var dateToCell = t.dateToCell;
|
|
5791 var getDaySegmentContainer = t.getDaySegmentContainer;
|
|
5792 var formatDates = t.calendar.formatDates;
|
|
5793 var renderDayOverlay = t.renderDayOverlay;
|
|
5794 var clearOverlays = t.clearOverlays;
|
|
5795 var clearSelection = t.clearSelection;
|
|
5796 var getHoverListener = t.getHoverListener;
|
|
5797 var rangeToSegments = t.rangeToSegments;
|
|
5798 var cellToDate = t.cellToDate;
|
|
5799 var cellToCellOffset = t.cellToCellOffset;
|
|
5800 var cellOffsetToDayOffset = t.cellOffsetToDayOffset;
|
|
5801 var dateToDayOffset = t.dateToDayOffset;
|
|
5802 var dayOffsetToCellOffset = t.dayOffsetToCellOffset;
|
|
5803
|
|
5804
|
|
5805 // Render `events` onto the calendar, attach mouse event handlers, and call the `eventAfterRender` callback for each.
|
|
5806 // Mouse event will be lazily applied, except if the event has an ID of `modifiedEventId`.
|
|
5807 // Can only be called when the event container is empty (because it wipes out all innerHTML).
|
|
5808 function renderDayEvents(events, modifiedEventId) {
|
|
5809
|
|
5810 // do the actual rendering. Receive the intermediate "segment" data structures.
|
|
5811 var segments = _renderDayEvents(
|
|
5812 events,
|
|
5813 false, // don't append event elements
|
|
5814 true // set the heights of the rows
|
|
5815 );
|
|
5816
|
|
5817 // report the elements to the View, for general drag/resize utilities
|
|
5818 segmentElementEach(segments, function(segment, element) {
|
|
5819 reportEventElement(segment.event, element);
|
|
5820 });
|
|
5821
|
|
5822 // attach mouse handlers
|
|
5823 attachHandlers(segments, modifiedEventId);
|
|
5824
|
|
5825 // call `eventAfterRender` callback for each event
|
|
5826 segmentElementEach(segments, function(segment, element) {
|
|
5827 trigger('eventAfterRender', segment.event, segment.event, element);
|
|
5828 });
|
|
5829 }
|
|
5830
|
|
5831
|
|
5832 // Render an event on the calendar, but don't report them anywhere, and don't attach mouse handlers.
|
|
5833 // Append this event element to the event container, which might already be populated with events.
|
|
5834 // If an event's segment will have row equal to `adjustRow`, then explicitly set its top coordinate to `adjustTop`.
|
|
5835 // This hack is used to maintain continuity when user is manually resizing an event.
|
|
5836 // Returns an array of DOM elements for the event.
|
|
5837 function renderTempDayEvent(event, adjustRow, adjustTop) {
|
|
5838
|
|
5839 // actually render the event. `true` for appending element to container.
|
|
5840 // Recieve the intermediate "segment" data structures.
|
|
5841 var segments = _renderDayEvents(
|
|
5842 [ event ],
|
|
5843 true, // append event elements
|
|
5844 false // don't set the heights of the rows
|
|
5845 );
|
|
5846
|
|
5847 var elements = [];
|
|
5848
|
|
5849 // Adjust certain elements' top coordinates
|
|
5850 segmentElementEach(segments, function(segment, element) {
|
|
5851 if (segment.row === adjustRow) {
|
|
5852 element.css('top', adjustTop);
|
|
5853 }
|
|
5854 elements.push(element[0]); // accumulate DOM nodes
|
|
5855 });
|
|
5856
|
|
5857 return elements;
|
|
5858 }
|
|
5859
|
|
5860
|
|
5861 // Render events onto the calendar. Only responsible for the VISUAL aspect.
|
|
5862 // Not responsible for attaching handlers or calling callbacks.
|
|
5863 // Set `doAppend` to `true` for rendering elements without clearing the existing container.
|
|
5864 // Set `doRowHeights` to allow setting the height of each row, to compensate for vertical event overflow.
|
|
5865 function _renderDayEvents(events, doAppend, doRowHeights) {
|
|
5866
|
|
5867 // where the DOM nodes will eventually end up
|
|
5868 var finalContainer = getDaySegmentContainer();
|
|
5869
|
|
5870 // the container where the initial HTML will be rendered.
|
|
5871 // If `doAppend`==true, uses a temporary container.
|
|
5872 var renderContainer = doAppend ? $("<div/>") : finalContainer;
|
|
5873
|
|
5874 var segments = buildSegments(events);
|
|
5875 var html;
|
|
5876 var elements;
|
|
5877
|
|
5878 // calculate the desired `left` and `width` properties on each segment object
|
|
5879 calculateHorizontals(segments);
|
|
5880
|
|
5881 // build the HTML string. relies on `left` property
|
|
5882 html = buildHTML(segments);
|
|
5883
|
|
5884 // render the HTML. innerHTML is considerably faster than jQuery's .html()
|
|
5885 renderContainer[0].innerHTML = html;
|
|
5886
|
|
5887 // retrieve the individual elements
|
|
5888 elements = renderContainer.children();
|
|
5889
|
|
5890 // if we were appending, and thus using a temporary container,
|
|
5891 // re-attach elements to the real container.
|
|
5892 if (doAppend) {
|
|
5893 finalContainer.append(elements);
|
|
5894 }
|
|
5895
|
|
5896 // assigns each element to `segment.event`, after filtering them through user callbacks
|
|
5897 resolveElements(segments, elements);
|
|
5898
|
|
5899 // Calculate the left and right padding+margin for each element.
|
|
5900 // We need this for setting each element's desired outer width, because of the W3C box model.
|
|
5901 // It's important we do this in a separate pass from acually setting the width on the DOM elements
|
|
5902 // because alternating reading/writing dimensions causes reflow for every iteration.
|
|
5903 segmentElementEach(segments, function(segment, element) {
|
|
5904 segment.hsides = hsides(element, true); // include margins = `true`
|
|
5905 });
|
|
5906
|
|
5907 // Set the width of each element
|
|
5908 segmentElementEach(segments, function(segment, element) {
|
|
5909 element.width(
|
|
5910 Math.max(0, segment.outerWidth - segment.hsides)
|
|
5911 );
|
|
5912 });
|
|
5913
|
|
5914 // Grab each element's outerHeight (setVerticals uses this).
|
|
5915 // To get an accurate reading, it's important to have each element's width explicitly set already.
|
|
5916 segmentElementEach(segments, function(segment, element) {
|
|
5917 segment.outerHeight = element.outerHeight(true); // include margins = `true`
|
|
5918 });
|
|
5919
|
|
5920 // Set the top coordinate on each element (requires segment.outerHeight)
|
|
5921 setVerticals(segments, doRowHeights);
|
|
5922
|
|
5923 return segments;
|
|
5924 }
|
|
5925
|
|
5926
|
|
5927 // Generate an array of "segments" for all events.
|
|
5928 function buildSegments(events) {
|
|
5929 var segments = [];
|
|
5930 for (var i=0; i<events.length; i++) {
|
|
5931 var eventSegments = buildSegmentsForEvent(events[i]);
|
|
5932 segments.push.apply(segments, eventSegments); // append an array to an array
|
|
5933 }
|
|
5934 return segments;
|
|
5935 }
|
|
5936
|
|
5937
|
|
5938 // Generate an array of segments for a single event.
|
|
5939 // A "segment" is the same data structure that View.rangeToSegments produces,
|
|
5940 // with the addition of the `event` property being set to reference the original event.
|
|
5941 function buildSegmentsForEvent(event) {
|
|
5942 var startDate = event.start;
|
|
5943 var endDate = exclEndDay(event);
|
|
5944 var segments = rangeToSegments(startDate, endDate);
|
|
5945 for (var i=0; i<segments.length; i++) {
|
|
5946 segments[i].event = event;
|
|
5947 }
|
|
5948 return segments;
|
|
5949 }
|
|
5950
|
|
5951
|
|
5952 // Sets the `left` and `outerWidth` property of each segment.
|
|
5953 // These values are the desired dimensions for the eventual DOM elements.
|
|
5954 function calculateHorizontals(segments) {
|
|
5955 var isRTL = opt('isRTL');
|
|
5956 for (var i=0; i<segments.length; i++) {
|
|
5957 var segment = segments[i];
|
|
5958
|
|
5959 // Determine functions used for calulating the elements left/right coordinates,
|
|
5960 // depending on whether the view is RTL or not.
|
|
5961 // NOTE:
|
|
5962 // colLeft/colRight returns the coordinate butting up the edge of the cell.
|
|
5963 // colContentLeft/colContentRight is indented a little bit from the edge.
|
|
5964 var leftFunc = (isRTL ? segment.isEnd : segment.isStart) ? colContentLeft : colLeft;
|
|
5965 var rightFunc = (isRTL ? segment.isStart : segment.isEnd) ? colContentRight : colRight;
|
|
5966
|
|
5967 var left = leftFunc(segment.leftCol);
|
|
5968 var right = rightFunc(segment.rightCol);
|
|
5969 segment.left = left;
|
|
5970 segment.outerWidth = right - left;
|
|
5971 }
|
|
5972 }
|
|
5973
|
|
5974
|
|
5975 // Build a concatenated HTML string for an array of segments
|
|
5976 function buildHTML(segments) {
|
|
5977 var html = '';
|
|
5978 for (var i=0; i<segments.length; i++) {
|
|
5979 html += buildHTMLForSegment(segments[i]);
|
|
5980 }
|
|
5981 return html;
|
|
5982 }
|
|
5983
|
|
5984
|
|
5985 // Build an HTML string for a single segment.
|
|
5986 // Relies on the following properties:
|
|
5987 // - `segment.event` (from `buildSegmentsForEvent`)
|
|
5988 // - `segment.left` (from `calculateHorizontals`)
|
|
5989 function buildHTMLForSegment(segment) {
|
|
5990 var html = '';
|
|
5991 var isRTL = opt('isRTL');
|
|
5992 var event = segment.event;
|
|
5993 var url = event.url;
|
|
5994
|
|
5995 // generate the list of CSS classNames
|
|
5996 var classNames = [ 'fc-event', 'fc-event-skin', 'fc-event-hori' ];
|
|
5997 if (isEventDraggable(event)) {
|
|
5998 classNames.push('fc-event-draggable');
|
|
5999 }
|
|
6000 if (segment.isStart) {
|
|
6001 classNames.push('fc-event-start');
|
|
6002 }
|
|
6003 if (segment.isEnd) {
|
|
6004 classNames.push('fc-event-end');
|
|
6005 }
|
|
6006 // use the event's configured classNames
|
|
6007 // guaranteed to be an array via `normalizeEvent`
|
|
6008 classNames = classNames.concat(event.className);
|
|
6009 if (event.source) {
|
|
6010 // use the event's source's classNames, if specified
|
|
6011 classNames = classNames.concat(event.source.className || []);
|
|
6012 }
|
|
6013
|
|
6014 // generate a semicolon delimited CSS string for any of the "skin" properties
|
|
6015 // of the event object (`backgroundColor`, `borderColor` and such)
|
|
6016 var skinCss = getSkinCss(event, opt);
|
|
6017
|
|
6018 if (url) {
|
|
6019 html += "<a href='" + htmlEscape(url) + "'";
|
|
6020 }else{
|
|
6021 html += "<div";
|
|
6022 }
|
|
6023 html +=
|
|
6024 " class='" + classNames.join(' ') + "'" +
|
|
6025 " style=" +
|
|
6026 "'" +
|
|
6027 "position:absolute;" +
|
|
6028 "left:" + segment.left + "px;" +
|
|
6029 skinCss +
|
|
6030 "'" +
|
|
6031 " tabindex='0'>" +
|
|
6032 "<div class='fc-event-inner'>";
|
|
6033 if (!event.allDay && segment.isStart) {
|
|
6034 html +=
|
|
6035 "<span class='fc-event-time'>" +
|
|
6036 htmlEscape(
|
|
6037 formatDates(event.start, event.end, opt('timeFormat'))
|
|
6038 ) +
|
|
6039 "</span>";
|
|
6040 }
|
|
6041 html +=
|
|
6042 "<span class='fc-event-title'>" +
|
|
6043 htmlEscape(event.title || '') +
|
|
6044 "</span>" +
|
|
6045 "</div>";
|
|
6046 if (segment.isEnd && isEventResizable(event)) {
|
|
6047 html +=
|
|
6048 "<div class='ui-resizable-handle ui-resizable-" + (isRTL ? 'w' : 'e') + "'>" +
|
|
6049 " " + // makes hit area a lot better for IE6/7
|
|
6050 "</div>";
|
|
6051 }
|
|
6052 html += "</" + (url ? "a" : "div") + ">";
|
|
6053
|
|
6054 // TODO:
|
|
6055 // When these elements are initially rendered, they will be briefly visibile on the screen,
|
|
6056 // even though their widths/heights are not set.
|
|
6057 // SOLUTION: initially set them as visibility:hidden ?
|
|
6058
|
|
6059 return html;
|
|
6060 }
|
|
6061
|
|
6062
|
|
6063 // Associate each segment (an object) with an element (a jQuery object),
|
|
6064 // by setting each `segment.element`.
|
|
6065 // Run each element through the `eventRender` filter, which allows developers to
|
|
6066 // modify an existing element, supply a new one, or cancel rendering.
|
|
6067 function resolveElements(segments, elements) {
|
|
6068 for (var i=0; i<segments.length; i++) {
|
|
6069 var segment = segments[i];
|
|
6070 var event = segment.event;
|
|
6071 var element = elements.eq(i);
|
|
6072
|
|
6073 // call the trigger with the original element
|
|
6074 var triggerRes = trigger('eventRender', event, event, element);
|
|
6075
|
|
6076 if (triggerRes === false) {
|
|
6077 // if `false`, remove the event from the DOM and don't assign it to `segment.event`
|
|
6078 element.remove();
|
|
6079 }
|
|
6080 else {
|
|
6081 if (triggerRes && triggerRes !== true) {
|
|
6082 // the trigger returned a new element, but not `true` (which means keep the existing element)
|
|
6083
|
|
6084 // re-assign the important CSS dimension properties that were already assigned in `buildHTMLForSegment`
|
|
6085 triggerRes = $(triggerRes)
|
|
6086 .css({
|
|
6087 position: 'absolute',
|
|
6088 left: segment.left
|
|
6089 });
|
|
6090
|
|
6091 element.replaceWith(triggerRes);
|
|
6092 element = triggerRes;
|
|
6093 }
|
|
6094
|
|
6095 segment.element = element;
|
|
6096 }
|
|
6097 }
|
|
6098 }
|
|
6099
|
|
6100
|
|
6101
|
|
6102 /* Top-coordinate Methods
|
|
6103 -------------------------------------------------------------------------------------------------*/
|
|
6104
|
|
6105
|
|
6106 // Sets the "top" CSS property for each element.
|
|
6107 // If `doRowHeights` is `true`, also sets each row's first cell to an explicit height,
|
|
6108 // so that if elements vertically overflow, the cell expands vertically to compensate.
|
|
6109 function setVerticals(segments, doRowHeights) {
|
|
6110 var overflowLinks = {};
|
|
6111 var rowContentHeights = calculateVerticals(segments, overflowLinks); // also sets segment.top
|
|
6112 var rowContentElements = getRowContentElements(); // returns 1 inner div per row
|
|
6113 var rowContentTops = [];
|
|
6114
|
|
6115 // Set each row's height by setting height of first inner div
|
|
6116 if (doRowHeights) {
|
|
6117 for (var i=0; i<rowContentElements.length; i++) {
|
|
6118 rowContentElements[i].height(rowContentHeights[i]);
|
|
6119 if (overflowLinks[i])
|
|
6120 renderOverflowLinks(overflowLinks[i], rowContentElements[i]);
|
|
6121 }
|
|
6122 }
|
|
6123
|
|
6124 // Get each row's top, relative to the views's origin.
|
|
6125 // Important to do this after setting each row's height.
|
|
6126 for (var i=0; i<rowContentElements.length; i++) {
|
|
6127 rowContentTops.push(
|
|
6128 rowContentElements[i].position().top
|
|
6129 );
|
|
6130 }
|
|
6131
|
|
6132 // Set each segment element's CSS "top" property.
|
|
6133 // Each segment object has a "top" property, which is relative to the row's top, but...
|
|
6134 segmentElementEach(segments, function(segment, element) {
|
|
6135 if (!segment.overflow) {
|
|
6136 element.css(
|
|
6137 'top',
|
|
6138 rowContentTops[segment.row] + segment.top // ...now, relative to views's origin
|
|
6139 );
|
|
6140 }
|
|
6141 else {
|
|
6142 element.hide();
|
|
6143 }
|
|
6144 });
|
|
6145 }
|
|
6146
|
|
6147
|
|
6148 // Calculate the "top" coordinate for each segment, relative to the "top" of the row.
|
|
6149 // Also, return an array that contains the "content" height for each row
|
|
6150 // (the height displaced by the vertically stacked events in the row).
|
|
6151 // Requires segments to have their `outerHeight` property already set.
|
|
6152 function calculateVerticals(segments, overflowLinks) {
|
|
6153 var rowCnt = getRowCnt();
|
|
6154 var colCnt = getColCnt();
|
|
6155 var rowContentHeights = []; // content height for each row
|
|
6156 var segmentRows = buildSegmentRows(segments); // an array of segment arrays, one for each row
|
|
6157 var maxHeight = opt('maxHeight');
|
|
6158 var top;
|
|
6159
|
|
6160 for (var rowI=0; rowI<rowCnt; rowI++) {
|
|
6161 var segmentRow = segmentRows[rowI];
|
|
6162
|
|
6163 // an array of running total heights for each column.
|
|
6164 // initialize with all zeros.
|
|
6165 overflowLinks[rowI] = {};
|
|
6166 var colHeights = [];
|
|
6167 var overflows = [];
|
|
6168 for (var colI=0; colI<colCnt; colI++) {
|
|
6169 colHeights.push(0);
|
|
6170 overflows.push(0);
|
|
6171 }
|
|
6172
|
|
6173 // loop through every segment
|
|
6174 for (var segmentI=0; segmentI<segmentRow.length; segmentI++) {
|
|
6175 var segment = segmentRow[segmentI];
|
|
6176
|
|
6177 // find the segment's top coordinate by looking at the max height
|
|
6178 // of all the columns the segment will be in.
|
|
6179 top = arrayMax(
|
|
6180 colHeights.slice(
|
|
6181 segment.leftCol,
|
|
6182 segment.rightCol + 1 // make exclusive for slice
|
|
6183 )
|
|
6184 );
|
|
6185
|
|
6186 if (maxHeight && top + segment.outerHeight > maxHeight) {
|
|
6187 segment.overflow = true;
|
|
6188 }
|
|
6189 else {
|
|
6190 segment.top = top;
|
|
6191 top += segment.outerHeight;
|
|
6192 }
|
|
6193
|
|
6194 // adjust the columns to account for the segment's height
|
|
6195 for (var colI=segment.leftCol; colI<=segment.rightCol; colI++) {
|
|
6196 if (overflows[colI]) {
|
|
6197 segment.overflow = true;
|
|
6198 }
|
|
6199 if (segment.overflow) {
|
|
6200 if (segment.isStart && !overflowLinks[rowI][colI])
|
|
6201 overflowLinks[rowI][colI] = { seg:segment, top:top, date:cloneDate(segment.event.start, true), count:0 };
|
|
6202 if (overflowLinks[rowI][colI])
|
|
6203 overflowLinks[rowI][colI].count++;
|
|
6204 overflows[colI]++;
|
|
6205 }
|
|
6206 else {
|
|
6207 colHeights[colI] = top;
|
|
6208 }
|
|
6209 }
|
|
6210 }
|
|
6211
|
|
6212 // the tallest column in the row should be the "content height"
|
|
6213 rowContentHeights.push(arrayMax(colHeights));
|
|
6214 }
|
|
6215
|
|
6216 return rowContentHeights;
|
|
6217 }
|
|
6218
|
|
6219
|
|
6220 // Build an array of segment arrays, each representing the segments that will
|
|
6221 // be in a row of the grid, sorted by which event should be closest to the top.
|
|
6222 function buildSegmentRows(segments) {
|
|
6223 var rowCnt = getRowCnt();
|
|
6224 var segmentRows = [];
|
|
6225 var segmentI;
|
|
6226 var segment;
|
|
6227 var rowI;
|
|
6228
|
|
6229 // group segments by row
|
|
6230 for (segmentI=0; segmentI<segments.length; segmentI++) {
|
|
6231 segment = segments[segmentI];
|
|
6232 rowI = segment.row;
|
|
6233 if (segment.element) { // was rendered?
|
|
6234 if (segmentRows[rowI]) {
|
|
6235 // already other segments. append to array
|
|
6236 segmentRows[rowI].push(segment);
|
|
6237 }
|
|
6238 else {
|
|
6239 // first segment in row. create new array
|
|
6240 segmentRows[rowI] = [ segment ];
|
|
6241 }
|
|
6242 }
|
|
6243 }
|
|
6244
|
|
6245 // sort each row
|
|
6246 for (rowI=0; rowI<rowCnt; rowI++) {
|
|
6247 segmentRows[rowI] = sortSegmentRow(
|
|
6248 segmentRows[rowI] || [] // guarantee an array, even if no segments
|
|
6249 );
|
|
6250 }
|
|
6251
|
|
6252 return segmentRows;
|
|
6253 }
|
|
6254
|
|
6255
|
|
6256 // Sort an array of segments according to which segment should appear closest to the top
|
|
6257 function sortSegmentRow(segments) {
|
|
6258 var sortedSegments = [];
|
|
6259
|
|
6260 // build the subrow array
|
|
6261 var subrows = buildSegmentSubrows(segments);
|
|
6262
|
|
6263 // flatten it
|
|
6264 for (var i=0; i<subrows.length; i++) {
|
|
6265 sortedSegments.push.apply(sortedSegments, subrows[i]); // append an array to an array
|
|
6266 }
|
|
6267
|
|
6268 return sortedSegments;
|
|
6269 }
|
|
6270
|
|
6271
|
|
6272 // Take an array of segments, which are all assumed to be in the same row,
|
|
6273 // and sort into subrows.
|
|
6274 function buildSegmentSubrows(segments) {
|
|
6275
|
|
6276 // Give preference to elements with certain criteria, so they have
|
|
6277 // a chance to be closer to the top.
|
|
6278 segments.sort(compareDaySegments);
|
|
6279
|
|
6280 var subrows = [];
|
|
6281 for (var i=0; i<segments.length; i++) {
|
|
6282 var segment = segments[i];
|
|
6283
|
|
6284 // loop through subrows, starting with the topmost, until the segment
|
|
6285 // doesn't collide with other segments.
|
|
6286 for (var j=0; j<subrows.length; j++) {
|
|
6287 if (!isDaySegmentCollision(segment, subrows[j])) {
|
|
6288 break;
|
|
6289 }
|
|
6290 }
|
|
6291 // `j` now holds the desired subrow index
|
|
6292 if (subrows[j]) {
|
|
6293 subrows[j].push(segment);
|
|
6294 }
|
|
6295 else {
|
|
6296 subrows[j] = [ segment ];
|
|
6297 }
|
|
6298 }
|
|
6299
|
|
6300 return subrows;
|
|
6301 }
|
|
6302
|
|
6303
|
|
6304 // Return an array of jQuery objects for the placeholder content containers of each row.
|
|
6305 // The content containers don't actually contain anything, but their dimensions should match
|
|
6306 // the events that are overlaid on top.
|
|
6307 function getRowContentElements() {
|
|
6308 var i;
|
|
6309 var rowCnt = getRowCnt();
|
|
6310 var rowDivs = [];
|
|
6311 for (i=0; i<rowCnt; i++) {
|
|
6312 rowDivs[i] = allDayRow(i)
|
|
6313 .find('div.fc-day-content > div');
|
|
6314 }
|
|
6315 return rowDivs;
|
|
6316 }
|
|
6317
|
|
6318
|
|
6319 function renderOverflowLinks(overflowLinks, rowDiv) {
|
|
6320 var container = getDaySegmentContainer();
|
|
6321 var colCnt = getColCnt();
|
|
6322 var element, triggerRes, link;
|
|
6323 for (var j=0; j<colCnt; j++) {
|
|
6324 if ((link = overflowLinks[j])) {
|
|
6325 if (link.count > 1) {
|
|
6326 element = $('<a>').addClass('fc-more-link').html('+'+link.count).appendTo(container);
|
|
6327 element[0].style.position = 'absolute';
|
|
6328 element[0].style.left = link.seg.left + 'px';
|
|
6329 element[0].style.top = (link.top + rowDiv[0].offsetTop) + 'px';
|
|
6330 triggerRes = trigger('overflowRender', link, { count:link.count, date:link.date }, element);
|
|
6331 if (triggerRes === false)
|
|
6332 element.remove();
|
|
6333 }
|
|
6334 else {
|
|
6335 link.seg.top = link.top;
|
|
6336 link.seg.overflow = false;
|
|
6337 }
|
|
6338 }
|
|
6339 }
|
|
6340 }
|
|
6341
|
|
6342
|
|
6343 /* Mouse Handlers
|
|
6344 ---------------------------------------------------------------------------------------------------*/
|
|
6345 // TODO: better documentation!
|
|
6346
|
|
6347
|
|
6348 function attachHandlers(segments, modifiedEventId) {
|
|
6349 var segmentContainer = getDaySegmentContainer();
|
|
6350
|
|
6351 segmentElementEach(segments, function(segment, element, i) {
|
|
6352 var event = segment.event;
|
|
6353 if (event._id === modifiedEventId) {
|
|
6354 bindDaySeg(event, element, segment);
|
|
6355 }else{
|
|
6356 element[0]._fci = i; // for lazySegBind
|
|
6357 }
|
|
6358 });
|
|
6359
|
|
6360 lazySegBind(segmentContainer, segments, bindDaySeg);
|
|
6361 }
|
|
6362
|
|
6363
|
|
6364 function bindDaySeg(event, eventElement, segment) {
|
|
6365
|
|
6366 if (isEventDraggable(event)) {
|
|
6367 t.draggableDayEvent(event, eventElement, segment); // use `t` so subclasses can override
|
|
6368 }
|
|
6369
|
|
6370 if (
|
|
6371 segment.isEnd && // only allow resizing on the final segment for an event
|
|
6372 isEventResizable(event)
|
|
6373 ) {
|
|
6374 t.resizableDayEvent(event, eventElement, segment); // use `t` so subclasses can override
|
|
6375 }
|
|
6376
|
|
6377 // attach all other handlers.
|
|
6378 // needs to be after, because resizableDayEvent might stopImmediatePropagation on click
|
|
6379 eventElementHandlers(event, eventElement);
|
|
6380 }
|
|
6381
|
|
6382
|
|
6383 function draggableDayEvent(event, eventElement) {
|
|
6384 var hoverListener = getHoverListener();
|
|
6385 var dayDelta;
|
|
6386 eventElement.draggable({
|
|
6387 delay: 50,
|
|
6388 opacity: opt('dragOpacity'),
|
|
6389 revertDuration: opt('dragRevertDuration'),
|
|
6390 start: function(ev, ui) {
|
|
6391 trigger('eventDragStart', eventElement, event, ev, ui);
|
|
6392 hideEvents(event, eventElement);
|
|
6393 hoverListener.start(function(cell, origCell, rowDelta, colDelta) {
|
|
6394 eventElement.draggable('option', 'revert', !cell || !rowDelta && !colDelta);
|
|
6395 clearOverlays();
|
|
6396 if (cell) {
|
|
6397 var origDate = cellToDate(origCell);
|
|
6398 var date = cellToDate(cell);
|
|
6399 dayDelta = dayDiff(date, origDate);
|
|
6400 renderDayOverlay(
|
|
6401 addDays(cloneDate(event.start), dayDelta),
|
|
6402 addDays(exclEndDay(event), dayDelta)
|
|
6403 );
|
|
6404 }else{
|
|
6405 dayDelta = 0;
|
|
6406 }
|
|
6407 }, ev, 'drag');
|
|
6408 },
|
|
6409 stop: function(ev, ui) {
|
|
6410 hoverListener.stop();
|
|
6411 clearOverlays();
|
|
6412 trigger('eventDragStop', eventElement, event, ev, ui);
|
|
6413 if (dayDelta) {
|
|
6414 eventDrop(this, event, dayDelta, 0, event.allDay, ev, ui);
|
|
6415 }else{
|
|
6416 eventElement.css('filter', ''); // clear IE opacity side-effects
|
|
6417 showEvents(event, eventElement);
|
|
6418 }
|
|
6419 }
|
|
6420 });
|
|
6421 }
|
|
6422
|
|
6423
|
|
6424 function resizableDayEvent(event, element, segment) {
|
|
6425 var isRTL = opt('isRTL');
|
|
6426 var direction = isRTL ? 'w' : 'e';
|
|
6427 var handle = element.find('.ui-resizable-' + direction); // TODO: stop using this class because we aren't using jqui for this
|
|
6428 var isResizing = false;
|
|
6429
|
|
6430 // TODO: look into using jquery-ui mouse widget for this stuff
|
|
6431 disableTextSelection(element); // prevent native <a> selection for IE
|
|
6432 element
|
|
6433 .mousedown(function(ev) { // prevent native <a> selection for others
|
|
6434 ev.preventDefault();
|
|
6435 })
|
|
6436 .click(function(ev) {
|
|
6437 if (isResizing) {
|
|
6438 ev.preventDefault(); // prevent link from being visited (only method that worked in IE6)
|
|
6439 ev.stopImmediatePropagation(); // prevent fullcalendar eventClick handler from being called
|
|
6440 // (eventElementHandlers needs to be bound after resizableDayEvent)
|
|
6441 }
|
|
6442 });
|
|
6443
|
|
6444 handle.mousedown(function(ev) {
|
|
6445 if (ev.which != 1) {
|
|
6446 return; // needs to be left mouse button
|
|
6447 }
|
|
6448 isResizing = true;
|
|
6449 var hoverListener = getHoverListener();
|
|
6450 var rowCnt = getRowCnt();
|
|
6451 var colCnt = getColCnt();
|
|
6452 var elementTop = element.css('top');
|
|
6453 var dayDelta;
|
|
6454 var helpers;
|
|
6455 var eventCopy = $.extend({}, event);
|
|
6456 var minCellOffset = dayOffsetToCellOffset( dateToDayOffset(event.start) );
|
|
6457 clearSelection();
|
|
6458 $('body')
|
|
6459 .css('cursor', direction + '-resize')
|
|
6460 .one('mouseup', mouseup);
|
|
6461 trigger('eventResizeStart', this, event, ev);
|
|
6462 hoverListener.start(function(cell, origCell) {
|
|
6463 if (cell) {
|
|
6464
|
|
6465 var origCellOffset = cellToCellOffset(origCell);
|
|
6466 var cellOffset = cellToCellOffset(cell);
|
|
6467
|
|
6468 // don't let resizing move earlier than start date cell
|
|
6469 cellOffset = Math.max(cellOffset, minCellOffset);
|
|
6470
|
|
6471 dayDelta =
|
|
6472 cellOffsetToDayOffset(cellOffset) -
|
|
6473 cellOffsetToDayOffset(origCellOffset);
|
|
6474
|
|
6475 if (dayDelta) {
|
|
6476 eventCopy.end = addDays(eventEnd(event), dayDelta, true);
|
|
6477 var oldHelpers = helpers;
|
|
6478
|
|
6479 helpers = renderTempDayEvent(eventCopy, segment.row, elementTop);
|
|
6480 helpers = $(helpers); // turn array into a jQuery object
|
|
6481
|
|
6482 helpers.find('*').css('cursor', direction + '-resize');
|
|
6483 if (oldHelpers) {
|
|
6484 oldHelpers.remove();
|
|
6485 }
|
|
6486
|
|
6487 hideEvents(event);
|
|
6488 }
|
|
6489 else {
|
|
6490 if (helpers) {
|
|
6491 showEvents(event);
|
|
6492 helpers.remove();
|
|
6493 helpers = null;
|
|
6494 }
|
|
6495 }
|
|
6496 clearOverlays();
|
|
6497 renderDayOverlay( // coordinate grid already rebuilt with hoverListener.start()
|
|
6498 event.start,
|
|
6499 addDays( exclEndDay(event), dayDelta )
|
|
6500 // TODO: instead of calling renderDayOverlay() with dates,
|
|
6501 // call _renderDayOverlay (or whatever) with cell offsets.
|
|
6502 );
|
|
6503 }
|
|
6504 }, ev);
|
|
6505
|
|
6506 function mouseup(ev) {
|
|
6507 trigger('eventResizeStop', this, event, ev);
|
|
6508 $('body').css('cursor', '');
|
|
6509 hoverListener.stop();
|
|
6510 clearOverlays();
|
|
6511 if (dayDelta) {
|
|
6512 eventResize(this, event, dayDelta, 0, ev);
|
|
6513 // event redraw will clear helpers
|
|
6514 }
|
|
6515 // otherwise, the drag handler already restored the old events
|
|
6516
|
|
6517 setTimeout(function() { // make this happen after the element's click event
|
|
6518 isResizing = false;
|
|
6519 },0);
|
|
6520 }
|
|
6521 });
|
|
6522 }
|
|
6523
|
|
6524
|
|
6525 }
|
|
6526
|
|
6527
|
|
6528
|
|
6529 /* Generalized Segment Utilities
|
|
6530 -------------------------------------------------------------------------------------------------*/
|
|
6531
|
|
6532
|
|
6533 function isDaySegmentCollision(segment, otherSegments) {
|
|
6534 for (var i=0; i<otherSegments.length; i++) {
|
|
6535 var otherSegment = otherSegments[i];
|
|
6536 if (
|
|
6537 otherSegment.leftCol <= segment.rightCol &&
|
|
6538 otherSegment.rightCol >= segment.leftCol
|
|
6539 ) {
|
|
6540 return true;
|
|
6541 }
|
|
6542 }
|
|
6543 return false;
|
|
6544 }
|
|
6545
|
|
6546
|
|
6547 function segmentElementEach(segments, callback) { // TODO: use in AgendaView?
|
|
6548 for (var i=0; i<segments.length; i++) {
|
|
6549 var segment = segments[i];
|
|
6550 var element = segment.element;
|
|
6551 if (element) {
|
|
6552 callback(segment, element, i);
|
|
6553 }
|
|
6554 }
|
|
6555 }
|
|
6556
|
|
6557
|
|
6558 // A cmp function for determining which segments should appear higher up
|
|
6559 function compareDaySegments(a, b) {
|
|
6560 return (b.rightCol - b.leftCol) - (a.rightCol - a.leftCol) || // put wider events first
|
|
6561 b.event.allDay - a.event.allDay || // if tie, put all-day events first (booleans cast to 0/1)
|
|
6562 a.event.start - b.event.start || // if a tie, sort by event start date
|
|
6563 (a.event.title || '').localeCompare(b.event.title) // if a tie, sort by event title
|
|
6564 }
|
|
6565
|
|
6566
|
|
6567 ;;
|
|
6568
|
|
6569 //BUG: unselect needs to be triggered when events are dragged+dropped
|
|
6570
|
|
6571 function SelectionManager() {
|
|
6572 var t = this;
|
|
6573
|
|
6574
|
|
6575 // exports
|
|
6576 t.select = select;
|
|
6577 t.unselect = unselect;
|
|
6578 t.reportSelection = reportSelection;
|
|
6579 t.daySelectionMousedown = daySelectionMousedown;
|
|
6580
|
|
6581
|
|
6582 // imports
|
|
6583 var opt = t.opt;
|
|
6584 var trigger = t.trigger;
|
|
6585 var defaultSelectionEnd = t.defaultSelectionEnd;
|
|
6586 var renderSelection = t.renderSelection;
|
|
6587 var clearSelection = t.clearSelection;
|
|
6588
|
|
6589
|
|
6590 // locals
|
|
6591 var selected = false;
|
|
6592
|
|
6593
|
|
6594
|
|
6595 // unselectAuto
|
|
6596 if (opt('selectable') && opt('unselectAuto')) {
|
|
6597 $(document).mousedown(function(ev) {
|
|
6598 var ignore = opt('unselectCancel');
|
|
6599 if (ignore) {
|
|
6600 if ($(ev.target).parents(ignore).length) { // could be optimized to stop after first match
|
|
6601 return;
|
|
6602 }
|
|
6603 }
|
|
6604 unselect(ev);
|
|
6605 });
|
|
6606 }
|
|
6607
|
|
6608
|
|
6609 function select(startDate, endDate, allDay) {
|
|
6610 unselect();
|
|
6611 if (!endDate) {
|
|
6612 endDate = defaultSelectionEnd(startDate, allDay);
|
|
6613 }
|
|
6614 renderSelection(startDate, endDate, allDay);
|
|
6615 reportSelection(startDate, endDate, allDay);
|
|
6616 }
|
|
6617
|
|
6618
|
|
6619 function unselect(ev) {
|
|
6620 if (selected) {
|
|
6621 selected = false;
|
|
6622 clearSelection();
|
|
6623 trigger('unselect', null, ev);
|
|
6624 }
|
|
6625 }
|
|
6626
|
|
6627
|
|
6628 function reportSelection(startDate, endDate, allDay, ev) {
|
|
6629 selected = true;
|
|
6630 trigger('select', null, startDate, endDate, allDay, ev);
|
|
6631 }
|
|
6632
|
|
6633
|
|
6634 function daySelectionMousedown(ev) { // not really a generic manager method, oh well
|
|
6635 var cellToDate = t.cellToDate;
|
|
6636 var getIsCellAllDay = t.getIsCellAllDay;
|
|
6637 var hoverListener = t.getHoverListener();
|
|
6638 var reportDayClick = t.reportDayClick; // this is hacky and sort of weird
|
|
6639 if (ev.which == 1 && opt('selectable')) { // which==1 means left mouse button
|
|
6640 unselect(ev);
|
|
6641 var _mousedownElement = this;
|
|
6642 var dates;
|
|
6643 hoverListener.start(function(cell, origCell) { // TODO: maybe put cellToDate/getIsCellAllDay info in cell
|
|
6644 clearSelection();
|
|
6645 if (cell && getIsCellAllDay(cell)) {
|
|
6646 dates = [ cellToDate(origCell), cellToDate(cell) ].sort(dateCompare);
|
|
6647 renderSelection(dates[0], dates[1], true);
|
|
6648 }else{
|
|
6649 dates = null;
|
|
6650 }
|
|
6651 }, ev);
|
|
6652 $(document).one('mouseup', function(ev) {
|
|
6653 hoverListener.stop();
|
|
6654 if (dates) {
|
|
6655 if (+dates[0] == +dates[1]) {
|
|
6656 reportDayClick(dates[0], true, ev);
|
|
6657 }
|
|
6658 reportSelection(dates[0], dates[1], true, ev);
|
|
6659 }
|
|
6660 });
|
|
6661 }
|
|
6662 }
|
|
6663
|
|
6664
|
|
6665 }
|
|
6666
|
|
6667 ;;
|
|
6668
|
|
6669 function OverlayManager() {
|
|
6670 var t = this;
|
|
6671
|
|
6672
|
|
6673 // exports
|
|
6674 t.renderOverlay = renderOverlay;
|
|
6675 t.clearOverlays = clearOverlays;
|
|
6676
|
|
6677
|
|
6678 // locals
|
|
6679 var usedOverlays = [];
|
|
6680 var unusedOverlays = [];
|
|
6681
|
|
6682
|
|
6683 function renderOverlay(rect, parent) {
|
|
6684 var e = unusedOverlays.shift();
|
|
6685 if (!e) {
|
|
6686 e = $("<div class='fc-cell-overlay' style='position:absolute;z-index:3'/>");
|
|
6687 }
|
|
6688 if (e[0].parentNode != parent[0]) {
|
|
6689 e.appendTo(parent);
|
|
6690 }
|
|
6691 usedOverlays.push(e.css(rect).show());
|
|
6692 return e;
|
|
6693 }
|
|
6694
|
|
6695
|
|
6696 function clearOverlays() {
|
|
6697 var e;
|
|
6698 while (e = usedOverlays.shift()) {
|
|
6699 unusedOverlays.push(e.hide().unbind());
|
|
6700 }
|
|
6701 }
|
|
6702
|
|
6703
|
|
6704 }
|
|
6705
|
|
6706 ;;
|
|
6707
|
|
6708 function CoordinateGrid(buildFunc) {
|
|
6709
|
|
6710 var t = this;
|
|
6711 var rows;
|
|
6712 var cols;
|
|
6713
|
|
6714
|
|
6715 t.build = function() {
|
|
6716 rows = [];
|
|
6717 cols = [];
|
|
6718 buildFunc(rows, cols);
|
|
6719 };
|
|
6720
|
|
6721
|
|
6722 t.cell = function(x, y) {
|
|
6723 var rowCnt = rows.length;
|
|
6724 var colCnt = cols.length;
|
|
6725 var i, r=-1, c=-1;
|
|
6726 for (i=0; i<rowCnt; i++) {
|
|
6727 if (y >= rows[i][0] && y < rows[i][1]) {
|
|
6728 r = i;
|
|
6729 break;
|
|
6730 }
|
|
6731 }
|
|
6732 for (i=0; i<colCnt; i++) {
|
|
6733 if (x >= cols[i][0] && x < cols[i][1]) {
|
|
6734 c = i;
|
|
6735 break;
|
|
6736 }
|
|
6737 }
|
|
6738 return (r>=0 && c>=0) ? { row:r, col:c } : null;
|
|
6739 };
|
|
6740
|
|
6741
|
|
6742 t.rect = function(row0, col0, row1, col1, originElement) { // row1,col1 is inclusive
|
|
6743 var origin = originElement.offset();
|
|
6744 return {
|
|
6745 top: rows[row0][0] - origin.top,
|
|
6746 left: cols[col0][0] - origin.left,
|
|
6747 width: cols[col1][1] - cols[col0][0],
|
|
6748 height: rows[row1][1] - rows[row0][0]
|
|
6749 };
|
|
6750 };
|
|
6751
|
|
6752 }
|
|
6753
|
|
6754 ;;
|
|
6755
|
|
6756 function HoverListener(coordinateGrid) {
|
|
6757
|
|
6758
|
|
6759 var t = this;
|
|
6760 var bindType;
|
|
6761 var change;
|
|
6762 var firstCell;
|
|
6763 var cell;
|
|
6764
|
|
6765
|
|
6766 t.start = function(_change, ev, _bindType) {
|
|
6767 change = _change;
|
|
6768 firstCell = cell = null;
|
|
6769 coordinateGrid.build();
|
|
6770 mouse(ev);
|
|
6771 bindType = _bindType || 'mousemove';
|
|
6772 $(document).bind(bindType, mouse);
|
|
6773 };
|
|
6774
|
|
6775
|
|
6776 function mouse(ev) {
|
|
6777 _fixUIEvent(ev); // see below
|
|
6778 var newCell = coordinateGrid.cell(ev.pageX, ev.pageY);
|
|
6779 if (!newCell != !cell || newCell && (newCell.row != cell.row || newCell.col != cell.col)) {
|
|
6780 if (newCell) {
|
|
6781 if (!firstCell) {
|
|
6782 firstCell = newCell;
|
|
6783 }
|
|
6784 change(newCell, firstCell, newCell.row-firstCell.row, newCell.col-firstCell.col);
|
|
6785 }else{
|
|
6786 change(newCell, firstCell);
|
|
6787 }
|
|
6788 cell = newCell;
|
|
6789 }
|
|
6790 }
|
|
6791
|
|
6792
|
|
6793 t.stop = function() {
|
|
6794 $(document).unbind(bindType, mouse);
|
|
6795 return cell;
|
|
6796 };
|
|
6797
|
|
6798
|
|
6799 }
|
|
6800
|
|
6801
|
|
6802
|
|
6803 // this fix was only necessary for jQuery UI 1.8.16 (and jQuery 1.7 or 1.7.1)
|
|
6804 // upgrading to jQuery UI 1.8.17 (and using either jQuery 1.7 or 1.7.1) fixed the problem
|
|
6805 // but keep this in here for 1.8.16 users
|
|
6806 // and maybe remove it down the line
|
|
6807
|
|
6808 function _fixUIEvent(event) { // for issue 1168
|
|
6809 if (event.pageX === undefined) {
|
|
6810 event.pageX = event.originalEvent.pageX;
|
|
6811 event.pageY = event.originalEvent.pageY;
|
|
6812 }
|
|
6813 }
|
|
6814 ;;
|
|
6815
|
|
6816 function HorizontalPositionCache(getElement) {
|
|
6817
|
|
6818 var t = this,
|
|
6819 elements = {},
|
|
6820 lefts = {},
|
|
6821 rights = {};
|
|
6822
|
|
6823 function e(i) {
|
|
6824 return elements[i] = elements[i] || getElement(i);
|
|
6825 }
|
|
6826
|
|
6827 t.left = function(i) {
|
|
6828 return lefts[i] = lefts[i] === undefined ? e(i).position().left : lefts[i];
|
|
6829 };
|
|
6830
|
|
6831 t.right = function(i) {
|
|
6832 return rights[i] = rights[i] === undefined ? t.left(i) + e(i).width() : rights[i];
|
|
6833 };
|
|
6834
|
|
6835 t.clear = function() {
|
|
6836 elements = {};
|
|
6837 lefts = {};
|
|
6838 rights = {};
|
|
6839 };
|
|
6840
|
|
6841 }
|
|
6842
|
|
6843 ;;
|
|
6844
|
|
6845 })(jQuery); |