function ResourceEventRenderer() { var t = this; // exports t.renderEvents = renderEvents; t.clearEvents = clearEvents; t.slotSegHtml = slotSegHtml; // imports DayEventRenderer.call(t); var opt = t.opt; var trigger = t.trigger; var isEventDraggable = t.isEventDraggable; var isEventResizable = t.isEventResizable; var eventEnd = t.eventEnd; var eventElementHandlers = t.eventElementHandlers; var setHeight = t.setHeight; var getDaySegmentContainer = t.getDaySegmentContainer; var getSlotSegmentContainer = t.getSlotSegmentContainer; var getHoverListener = t.getHoverListener; var getMaxMinute = t.getMaxMinute; var getMinMinute = t.getMinMinute; var timePosition = t.timePosition; var getIsCellAllDay = t.getIsCellAllDay; var colContentLeft = t.colContentLeft; var colContentRight = t.colContentRight; var cellToDate = t.cellToDate; var segmentCompare = t.segmentCompare; var getColCnt = t.getColCnt; var getColWidth = t.getColWidth; var getSnapHeight = t.getSnapHeight; var getSnapMinutes = t.getSnapMinutes; var getSlotContainer = t.getSlotContainer; var reportEventElement = t.reportEventElement; var showEvents = t.showEvents; var hideEvents = t.hideEvents; var eventDrop = t.eventDrop; var eventResize = t.eventResize; var renderDayOverlay = t.renderDayOverlay; var clearOverlays = t.clearOverlays; var renderDayEvents = t.renderDayEvents; var calendar = t.calendar; var formatDate = calendar.formatDate; var formatDates = calendar.formatDates; var resources = t.getResources; // overrides t.draggableDayEvent = draggableDayEvent; /* Rendering ----------------------------------------------------------------------------*/ function renderEvents(events, modifiedEventId) { var i, len=events.length, dayEvents=[], slotEvents=[]; for (i=0; i start && eventStart < end) { if (eventStart < start) { segStart = cloneDate(start); isStart = false; }else{ segStart = eventStart; isStart = true; } if (eventEnd > end) { segEnd = cloneDate(end); isEnd = false; }else{ segEnd = eventEnd; isEnd = true; } segs.push({ event: event, start: segStart, end: segEnd, isStart: isStart, isEnd: isEnd, msLength: segEnd - segStart }); } } return segs.sort(segmentCompare); } function eventsForResource(resource, events) { var resourceEvents = []; for (var i = 0; i < events.length; i++) { if (events[i].resources && $.inArray(resource.id, events[i].resources) >= 0) { resourceEvents.push(events[i]) } } return resourceEvents; } function slotEventEnd(event) { if (event.end) { return cloneDate(event.end); }else{ return addMinutes(cloneDate(event.start), opt('defaultEventMinutes')); } } // renders events in the 'time slots' at the bottom // TODO: when we refactor this, when user returns `false` eventRender, don't have empty space // TODO: refactor will include using pixels to detect collisions instead of dates (handy for seg cmp) function renderSlotSegs(segs, modifiedEventId) { var i, segCnt=segs.length, seg, event, classes, top, bottom, colI, levelI, forward, leftmost, availWidth, outerWidth, left, html='', eventElements, eventElement, triggerRes, titleElement, height, slotSegmentContainer = getSlotSegmentContainer(), rtl, dis; if (rtl = opt('isRTL')) { dis = -1; }else{ dis = 1; } // calculate position/dimensions, create html for (i=0; i" + "
" + "
" + htmlEscape(formatDates(event.start, event.end, opt('timeFormat'))) + "
" + "
" + htmlEscape(event.title || '') + "
" + "
" + "
"; if (seg.isEnd && isEventResizable(event)) { html += "
=
"; } html += ""; return html; } function bindSlotSeg(event, eventElement, seg) { var timeElement = eventElement.find('div.fc-event-time'); if (isEventDraggable(event)) { draggableSlotEvent(event, eventElement, timeElement); } if (seg.isEnd && isEventResizable(event)) { resizableSlotEvent(event, eventElement, timeElement); } eventElementHandlers(event, eventElement); } /* Dragging -----------------------------------------------------------------------------------*/ // when event starts out FULL-DAY // overrides DayEventRenderer's version because it needs to account for dragging elements // to and from the slot area. function draggableDayEvent(event, eventElement, seg) { var isStart = seg.isStart; var origWidth; var revert; var allDay = true; var dayDelta; var hoverListener = getHoverListener(); var colWidth = getColWidth(); var snapHeight = getSnapHeight(); var snapMinutes = getSnapMinutes(); var minMinute = getMinMinute(); eventElement.draggable({ opacity: opt('dragOpacity', 'month'), // use whatever the month view was using revertDuration: opt('dragRevertDuration'), start: function(ev, ui) { trigger('eventDragStart', eventElement, event, ev, ui); hideEvents(event, eventElement); origWidth = eventElement.width(); hoverListener.start(function(cell, origCell) { clearOverlays(); if (cell) { revert = false; var origDate = cellToDate(0, origCell.col); var date = cellToDate(0, cell.col); dayDelta = dayDiff(date, origDate); if (!cell.row) { // on full-days renderDayOverlay( addDays(cloneDate(event.start), dayDelta), addDays(exclEndDay(event), dayDelta) ); resetElement(); }else{ // mouse is over bottom slots if (isStart) { if (allDay) { // convert event to temporary slot-event eventElement.width(colWidth - 10); // don't use entire width setOuterHeight( eventElement, snapHeight * Math.round( (event.end ? ((event.end - event.start) / MINUTE_MS) : opt('defaultEventMinutes')) / snapMinutes ) ); eventElement.draggable('option', 'grid', [colWidth, 1]); allDay = false; } }else{ revert = true; } } revert = revert || (allDay && !dayDelta); }else{ resetElement(); revert = true; } eventElement.draggable('option', 'revert', revert); }, ev, 'drag'); }, stop: function(ev, ui) { hoverListener.stop(); clearOverlays(); trigger('eventDragStop', eventElement, event, ev, ui); if (revert) { // hasn't moved or is out of bounds (draggable has already reverted) resetElement(); eventElement.css('filter', ''); // clear IE opacity side-effects showEvents(event, eventElement); }else{ // changed! var minuteDelta = 0; if (!allDay) { minuteDelta = Math.round((eventElement.offset().top - getSlotContainer().offset().top) / snapHeight) * snapMinutes + minMinute - (event.start.getHours() * 60 + event.start.getMinutes()); } eventDrop(this, event, dayDelta, minuteDelta, allDay, ev, ui); } } }); function resetElement() { if (!allDay) { eventElement .width(origWidth) .height('') .draggable('option', 'grid', null); allDay = true; } } } // when event starts out IN TIMESLOTS function draggableSlotEvent(event, eventElement, timeElement) { var coordinateGrid = t.getCoordinateGrid(); var colCnt = getColCnt(); var colWidth = getColWidth(); var snapHeight = getSnapHeight(); var snapMinutes = getSnapMinutes(); // states var origPosition; // original position of the element, not the mouse var origCell; var isInBounds, prevIsInBounds; var isAllDay, prevIsAllDay; var colDelta, prevColDelta; var dayDelta; // derived from colDelta var minuteDelta, prevMinuteDelta; eventElement.draggable({ scroll: false, grid: [ colWidth, snapHeight ], axis: colCnt==1 ? 'y' : false, opacity: opt('dragOpacity'), revertDuration: opt('dragRevertDuration'), start: function(ev, ui) { trigger('eventDragStart', eventElement, event, ev, ui); hideEvents(event, eventElement); coordinateGrid.build(); // initialize states origPosition = eventElement.position(); origCell = coordinateGrid.cell(ev.pageX, ev.pageY); isInBounds = prevIsInBounds = true; isAllDay = prevIsAllDay = getIsCellAllDay(origCell); colDelta = prevColDelta = 0; dayDelta = 0; minuteDelta = prevMinuteDelta = 0; }, drag: function(ev, ui) { // NOTE: this `cell` value is only useful for determining in-bounds and all-day. // Bad for anything else due to the discrepancy between the mouse position and the // element position while snapping. (problem revealed in PR #55) // // PS- the problem exists for draggableDayEvent() when dragging an all-day event to a slot event. // We should overhaul the dragging system and stop relying on jQuery UI. var cell = coordinateGrid.cell(ev.pageX, ev.pageY); // update states isInBounds = !!cell; if (isInBounds) { isAllDay = getIsCellAllDay(cell); // calculate column delta colDelta = Math.round((ui.position.left - origPosition.left) / colWidth); if (colDelta != prevColDelta) { // calculate the day delta based off of the original clicked column and the column delta var origDate = cellToDate(0, origCell.col); var col = origCell.col + colDelta; col = Math.max(0, col); col = Math.min(colCnt-1, col); var date = cellToDate(0, col); dayDelta = dayDiff(date, origDate); } // calculate minute delta (only if over slots) if (!isAllDay) { minuteDelta = Math.round((ui.position.top - origPosition.top) / snapHeight) * snapMinutes; } } // any state changes? if ( isInBounds != prevIsInBounds || isAllDay != prevIsAllDay || colDelta != prevColDelta || minuteDelta != prevMinuteDelta ) { updateUI(); // update previous states for next time prevIsInBounds = isInBounds; prevIsAllDay = isAllDay; prevColDelta = colDelta; prevMinuteDelta = minuteDelta; } // if out-of-bounds, revert when done, and vice versa. eventElement.draggable('option', 'revert', !isInBounds); }, stop: function(ev, ui) { clearOverlays(); trigger('eventDragStop', eventElement, event, ev, ui); if (isInBounds && (isAllDay || dayDelta || minuteDelta)) { // changed! eventDrop(this, event, dayDelta, isAllDay ? 0 : minuteDelta, isAllDay, ev, ui); } else { // either no change or out-of-bounds (draggable has already reverted) // reset states for next time, and for updateUI() isInBounds = true; isAllDay = false; colDelta = 0; dayDelta = 0; minuteDelta = 0; updateUI(); eventElement.css('filter', ''); // clear IE opacity side-effects // sometimes fast drags make event revert to wrong position, so reset. // also, if we dragged the element out of the area because of snapping, // but the *mouse* is still in bounds, we need to reset the position. eventElement.css(origPosition); showEvents(event, eventElement); } } }); function updateUI() { clearOverlays(); if (isInBounds) { if (isAllDay) { timeElement.hide(); eventElement.draggable('option', 'grid', null); // disable grid snapping renderDayOverlay( addDays(cloneDate(event.start), dayDelta), addDays(exclEndDay(event), dayDelta) ); } else { updateTimeText(minuteDelta); timeElement.css('display', ''); // show() was causing display=inline eventElement.draggable('option', 'grid', [colWidth, snapHeight]); // re-enable grid snapping } } } function updateTimeText(minuteDelta) { var newStart = addMinutes(cloneDate(event.start), minuteDelta); var newEnd; if (event.end) { newEnd = addMinutes(cloneDate(event.end), minuteDelta); } timeElement.text(formatDates(newStart, newEnd, opt('timeFormat'))); } } /* Resizing --------------------------------------------------------------------------------------*/ function resizableSlotEvent(event, eventElement, timeElement) { var snapDelta, prevSnapDelta; var snapHeight = getSnapHeight(); var snapMinutes = getSnapMinutes(); eventElement.resizable({ handles: { s: '.ui-resizable-handle' }, grid: snapHeight, start: function(ev, ui) { snapDelta = prevSnapDelta = 0; hideEvents(event, eventElement); trigger('eventResizeStart', this, event, ev, ui); }, resize: function(ev, ui) { // don't rely on ui.size.height, doesn't take grid into account snapDelta = Math.round((Math.max(snapHeight, eventElement.height()) - ui.originalSize.height) / snapHeight); if (snapDelta != prevSnapDelta) { timeElement.text( formatDates( event.start, (!snapDelta && !event.end) ? null : // no change, so don't display time range addMinutes(eventEnd(event), snapMinutes*snapDelta), opt('timeFormat') ) ); prevSnapDelta = snapDelta; } }, stop: function(ev, ui) { trigger('eventResizeStop', this, event, ev, ui); if (snapDelta) { eventResize(this, event, 0, snapMinutes*snapDelta, ev, ui); }else{ showEvents(event, eventElement); // BUG: if event was really short, need to put title back in span } } }); } } /* Agenda Event Segment Utilities -----------------------------------------------------------------------------*/ // TODO: maybe somehow consolidate this with DayEventRenderer's segment system function stackAgendaSegs(segs) { var levels = [], i, len = segs.length, seg, j, collide, k; for (i=0; i0; i--) { level = levels[i]; for (j=0; j seg2.start && seg1.start < seg2.end; }