Mercurial > hg > rc1
comparison plugins/calendar/lib/js/fullcalendar.js @ 3:f6fe4b6ae66a
calendar plugin nearly as distributed
author | Charlie Root |
---|---|
date | Sat, 13 Jan 2018 08:56:12 -0500 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
2:c828b0fd4a6e | 3:f6fe4b6ae66a |
---|---|
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); |