setDefaults({ allDaySlot: true, allDayText: 'all-day', firstHour: 6, slotMinutes: 30, defaultEventMinutes: 120, axisFormat: 'h(:mm)tt', timeFormat: { agenda: 'h:mm{ - h:mm}' }, dragOpacity: { agenda: .5 }, minTime: 0, maxTime: 24 }); // TODO: make it work in quirks mode (event corners, all-day height) // TODO: test liquid width, especially in IE6 function ResourceView(element, calendar, viewName) { var t = this; // exports t.renderResource = renderResource; t.setWidth = setWidth; t.setHeight = setHeight; t.afterRender = afterRender; t.defaultEventEnd = defaultEventEnd; t.timePosition = timePosition; t.getIsCellAllDay = getIsCellAllDay; t.allDayRow = getAllDayRow; t.getCoordinateGrid = function() { return coordinateGrid }; // specifically for AgendaEventRenderer t.getHoverListener = function() { return hoverListener }; t.colLeft = colLeft; t.colRight = colRight; t.colContentLeft = colContentLeft; t.colContentRight = colContentRight; t.getDaySegmentContainer = function() { return daySegmentContainer }; t.getSlotSegmentContainer = function() { return slotSegmentContainer }; t.getMinMinute = function() { return minMinute }; t.getMaxMinute = function() { return maxMinute }; t.getSlotContainer = function() { return slotContainer }; t.getRowCnt = function() { return 1 }; t.getColCnt = function() { return colCnt }; t.getColWidth = function() { return colWidth }; t.getSnapHeight = function() { return snapHeight }; t.getSnapMinutes = function() { return snapMinutes }; t.defaultSelectionEnd = defaultSelectionEnd; t.renderDayOverlay = renderDayOverlay; t.renderSelection = renderSelection; t.clearSelection = clearSelection; t.reportDayClick = reportDayClick; // selection mousedown hack t.dragStart = dragStart; t.dragStop = dragStop; t.getResources = calendar.fetchResources(); // imports View.call(t, element, calendar, viewName); OverlayManager.call(t); SelectionManager.call(t); ResourceEventRenderer.call(t); var opt = t.opt; var trigger = t.trigger; var renderOverlay = t.renderOverlay; var clearOverlays = t.clearOverlays; var reportSelection = t.reportSelection; var unselect = t.unselect; var daySelectionMousedown = t.daySelectionMousedown; var slotSegHtml = t.slotSegHtml; var cellToDate = t.cellToDate; var dateToCell = t.dateToCell; var rangeToSegments = t.rangeToSegments; var formatDate = calendar.formatDate; // locals var dayTable; var dayHead; var dayHeadCells; var dayBody; var dayBodyCells; var dayBodyCellInners; var dayBodyCellContentInners; var dayBodyFirstCell; var dayBodyFirstCellStretcher; var slotLayer; var daySegmentContainer; var allDayTable; var allDayRow; var slotScroller; var slotContainer; var slotSegmentContainer; var slotTable; var slotTableFirstInner; var selectionHelper; var viewWidth; var viewHeight; var axisWidth; var colWidth; var gutterWidth; var slotHeight; // TODO: what if slotHeight changes? (see issue 650) var snapMinutes; var snapRatio; // ratio of number of "selection" slots to normal slots. (ex: 1, 2, 4) var snapHeight; // holds the pixel hight of a "selection" slot var colCnt; var slotCnt; var coordinateGrid; var hoverListener; var colPositions; var colContentPositions; var slotTopCache = {}; var tm; var rtl; var minMinute, maxMinute; var colFormat; var showWeekNumbers; var weekNumberTitle; var weekNumberFormat; var resources = t.getResources; /* Rendering -----------------------------------------------------------------------------*/ disableTextSelection(element.addClass('fc-agenda')); function renderResource(c) { colCnt = c; updateOptions(); if (!dayTable) { // first time rendering? buildSkeleton(); // builds day table, slot area, events containers } else { buildDayTable(); // rebuilds day table } } function updateOptions() { tm = opt('theme') ? 'ui' : 'fc'; rtl = opt('isRTL') minMinute = parseTime(opt('minTime')); maxMinute = parseTime(opt('maxTime')); colFormat = opt('columnFormat'); // week # options. (TODO: bad, logic also in other views) showWeekNumbers = opt('weekNumbers'); weekNumberTitle = opt('weekNumberTitle'); if (opt('weekNumberCalculation') != 'iso') { weekNumberFormat = "w"; } else { weekNumberFormat = "W"; } snapMinutes = opt('snapMinutes') || opt('slotMinutes'); } /* Build DOM -----------------------------------------------------------------------*/ function buildSkeleton() { var headerClass = tm + "-widget-header"; var contentClass = tm + "-widget-content"; var s; var d; var i; var maxd; var minutes; var slotNormal = opt('slotMinutes') % 15 == 0; buildDayTable(); slotLayer = $("
") .appendTo(element); if (opt('allDaySlot')) { daySegmentContainer = $("
") .appendTo(slotLayer); s = "" + "" + "" + "" + "" + "" + "
" + opt('allDayText') + "" + "
" + "
 
"; allDayTable = $(s).appendTo(slotLayer); allDayRow = allDayTable.find('tr'); dayBind(allDayRow.find('td')); slotLayer.append( "
" + "
" + "
" ); }else{ daySegmentContainer = $([]); // in jQuery 1.4, we can just do $() } slotScroller = $("
") .appendTo(slotLayer); slotContainer = $("
") .appendTo(slotScroller); slotSegmentContainer = $("
") .appendTo(slotContainer); s = "" + ""; d = zeroDate(); maxd = addMinutes(cloneDate(d), maxMinute); addMinutes(d, minMinute); slotCnt = 0; for (i=0; d < maxd; i++) { minutes = d.getMinutes(); s += "" + "" + "" + ""; addMinutes(d, opt('slotMinutes')); slotCnt++; } s += "" + "
" + ((!slotNormal || !minutes) ? formatDate(d, opt('axisFormat')) : ' ') + "" + "
 
" + "
"; slotTable = $(s).appendTo(slotContainer); slotTableFirstInner = slotTable.find('div:first'); slotBind(slotTable.find('td')); } /* Build Day Table -----------------------------------------------------------------------*/ function buildDayTable() { var html = buildDayTableHTML(); if (dayTable) { dayTable.remove(); } dayTable = $(html).appendTo(element); dayHead = dayTable.find('thead'); dayHeadCells = dayHead.find('th').slice(1, -1); // exclude gutter dayBody = dayTable.find('tbody'); dayBodyCells = dayBody.find('td').slice(0, -1); // exclude gutter dayBodyCellInners = dayBodyCells.find('> div'); dayBodyCellContentInners = dayBodyCells.find('.fc-day-content > div'); dayBodyFirstCell = dayBodyCells.eq(0); dayBodyFirstCellStretcher = dayBodyCellInners.eq(0); markFirstLast(dayHead.add(dayHead.find('tr'))); markFirstLast(dayBody.add(dayBody.find('tr'))); // TODO: now that we rebuild the cells every time, we should call dayRender } function buildDayTableHTML() { var html = "" + buildDayTableHeadHTML() + buildDayTableBodyHTML() + "
"; return html; } function buildDayTableHeadHTML() { var headerClass = tm + "-widget-header"; var date; var html = ''; var weekText; var col; html += "" + ""; if (showWeekNumbers) { weekText = formatDate(date, weekNumberFormat); if (rtl) { weekText += weekNumberTitle; } else { weekText = weekNumberTitle + weekText; } html += "" + htmlEscape(weekText) + ""; } else { html += " "; } for (col=0; col" + htmlEscape(resources[col].name) + ""; } html += " " + "" + ""; return html; } function buildDayTableBodyHTML() { var headerClass = tm + "-widget-header"; // TODO: make these when updateOptions() called var contentClass = tm + "-widget-content"; var date; var today = clearTime(new Date()); var col; var cellsHTML; var cellHTML; var classNames; var html = ''; html += "" + "" + " "; cellsHTML = ''; for (col=0; col" + "
" + "
" + "
 
" + "
" + "
" + ""; cellsHTML += cellHTML; } html += cellsHTML; html += " " + "" + ""; return html; } // TODO: data-date on the cells /* Dimensions -----------------------------------------------------------------------*/ function setHeight(height) { if (height === undefined) { height = viewHeight; } viewHeight = height; slotTopCache = {}; var headHeight = dayBody.position().top; var allDayHeight = slotScroller.position().top; // including divider var bodyHeight = Math.min( // total body height, including borders height - headHeight, // when scrollbars slotTable.height() + allDayHeight + 1 // when no scrollbars. +1 for bottom border ); dayBodyFirstCellStretcher .height(bodyHeight - vsides(dayBodyFirstCell)); slotLayer.css('top', headHeight); slotScroller.height(bodyHeight - allDayHeight - 1); slotHeight = slotTableFirstInner.height() + 1; // +1 for border snapRatio = opt('slotMinutes') / snapMinutes; snapHeight = slotHeight / snapRatio; } function setWidth(width) { viewWidth = width; colPositions.clear(); colContentPositions.clear(); var axisFirstCells = dayHead.find('th:first'); if (allDayTable) { axisFirstCells = axisFirstCells.add(allDayTable.find('th:first')); } axisFirstCells = axisFirstCells.add(slotTable.find('th:first')); axisWidth = 0; setOuterWidth( axisFirstCells .width('') .each(function(i, _cell) { axisWidth = Math.max(axisWidth, $(_cell).outerWidth()); }), axisWidth ); var gutterCells = dayTable.find('.fc-agenda-gutter'); if (allDayTable) { gutterCells = gutterCells.add(allDayTable.find('th.fc-agenda-gutter')); } var slotTableWidth = slotScroller[0].clientWidth; // needs to be done after axisWidth (for IE7) gutterWidth = slotScroller.width() - slotTableWidth; if (gutterWidth) { setOuterWidth(gutterCells, gutterWidth); gutterCells .show() .prev() .removeClass('fc-last'); }else{ gutterCells .hide() .prev() .addClass('fc-last'); } colWidth = Math.floor((slotTableWidth - axisWidth) / colCnt); setOuterWidth(dayHeadCells.slice(0, -1), colWidth); } /* Scrolling -----------------------------------------------------------------------*/ function resetScroll() { var d0 = zeroDate(); var scrollDate = cloneDate(d0); scrollDate.setHours(opt('firstHour')); var top = timePosition(d0, scrollDate) + 1; // +1 for the border function scroll() { slotScroller.scrollTop(top); } scroll(); setTimeout(scroll, 0); // overrides any previous scroll state made by the browser } function afterRender() { // after the view has been freshly rendered and sized resetScroll(); } /* Slot/Day clicking and binding -----------------------------------------------------------------------*/ function dayBind(cells) { cells.click(slotClick) .mousedown(daySelectionMousedown); } function slotBind(cells) { cells.click(slotClick) .mousedown(slotSelectionMousedown); } function slotClick(ev) { if (!opt('selectable')) { // if selectable, SelectionManager will worry about dayClick var col = Math.min(colCnt-1, Math.floor((ev.pageX - dayTable.offset().left - axisWidth) / colWidth)); var date = cellToDate(0, col); var rowMatch = this.parentNode.className.match(/fc-slot(\d+)/); // TODO: maybe use data if (rowMatch) { var mins = parseInt(rowMatch[1]) * opt('slotMinutes'); var hours = Math.floor(mins/60); date.setHours(hours); date.setMinutes(mins%60 + minMinute); trigger('dayClick', dayBodyCells[col], date, false, ev); }else{ trigger('dayClick', dayBodyCells[col], date, true, ev); } } } /* Semi-transparent Overlay Helpers -----------------------------------------------------*/ // TODO: should be consolidated with BasicView's methods function renderDayOverlay(overlayStart, overlayEnd, refreshCoordinateGrid) { // overlayEnd is exclusive if (refreshCoordinateGrid) { coordinateGrid.build(); } var segments = rangeToSegments(overlayStart, overlayEnd); for (var i=0; i= 0) { addMinutes(d, minMinute + slotIndex * snapMinutes); } return d; } // get the Y coordinate of the given time on the given day (both Date objects) function timePosition(day, time) { // both date objects. day holds 00:00 of current day day = cloneDate(day, true); if (time < addMinutes(cloneDate(day), minMinute)) { return 0; } if (time >= addMinutes(cloneDate(day), maxMinute)) { return slotTable.height(); } var slotMinutes = opt('slotMinutes'), minutes = time.getHours()*60 + time.getMinutes() - minMinute, slotI = Math.floor(minutes / slotMinutes), slotTop = slotTopCache[slotI]; if (slotTop === undefined) { slotTop = slotTopCache[slotI] = slotTable.find('tr').eq(slotI).find('td div')[0].offsetTop; // .eq() is faster than ":eq()" selector // [0].offsetTop is faster than .position().top (do we really need this optimization?) // a better optimization would be to cache all these divs } return Math.max(0, Math.round( slotTop - 1 + slotHeight * ((minutes % slotMinutes) / slotMinutes) )); } function getAllDayRow(index) { return allDayRow; } function defaultEventEnd(event) { var start = cloneDate(event.start); if (event.allDay) { return start; } return addMinutes(start, opt('defaultEventMinutes')); } /* Selection ---------------------------------------------------------------------------------*/ function defaultSelectionEnd(startDate, allDay) { if (allDay) { return cloneDate(startDate); } return addMinutes(cloneDate(startDate), opt('slotMinutes')); } function renderSelection(startDate, endDate, allDay) { // only for all-day if (allDay) { if (opt('allDaySlot')) { renderDayOverlay(startDate, addDays(cloneDate(endDate), 1), true); } }else{ renderSlotSelection(startDate, endDate); } } function renderSlotSelection(startDate, endDate) { var helperOption = opt('selectHelper'); coordinateGrid.build(); if (helperOption) { var col = dateToCell(startDate).col; if (col >= 0 && col < colCnt) { // only works when times are on same day var rect = coordinateGrid.rect(0, col, 0, col, slotContainer); // only for horizontal coords var top = timePosition(startDate, startDate); var bottom = timePosition(startDate, endDate); if (bottom > top) { // protect against selections that are entirely before or after visible range rect.top = top; rect.height = bottom - top; rect.left += 2; rect.width -= 5; if ($.isFunction(helperOption)) { var helperRes = helperOption(startDate, endDate); if (helperRes) { rect.position = 'absolute'; selectionHelper = $(helperRes) .css(rect) .appendTo(slotContainer); } }else{ rect.isStart = true; // conside rect a "seg" now rect.isEnd = true; // selectionHelper = $(slotSegHtml( { title: '', start: startDate, end: endDate, className: ['fc-select-helper'], editable: false }, rect )); selectionHelper.css('opacity', opt('dragOpacity')); } if (selectionHelper) { slotBind(selectionHelper); slotContainer.append(selectionHelper); setOuterWidth(selectionHelper, rect.width, true); // needs to be after appended setOuterHeight(selectionHelper, rect.height, true); } } } }else{ renderSlotOverlay(startDate, endDate); } } function clearSelection() { clearOverlays(); if (selectionHelper) { selectionHelper.remove(); selectionHelper = null; } } function slotSelectionMousedown(ev) { if (ev.which == 1 && opt('selectable')) { // ev.which==1 means left mouse button unselect(ev); var dates; hoverListener.start(function(cell, origCell) { clearSelection(); if (cell && cell.col == origCell.col && !getIsCellAllDay(cell)) { var d1 = realCellToDate(origCell); var d2 = realCellToDate(cell); dates = [ d1, addMinutes(cloneDate(d1), snapMinutes), // calculate minutes depending on selection slot minutes d2, addMinutes(cloneDate(d2), snapMinutes) ].sort(dateCompare); renderSlotSelection(dates[0], dates[3]); }else{ dates = null; } }, ev); $(document).one('mouseup', function(ev) { hoverListener.stop(); if (dates) { if (+dates[0] == +dates[1]) { reportDayClick(dates[0], false, ev); } reportSelection(dates[0], dates[3], false, ev); } }); } } function reportDayClick(date, allDay, ev) { trigger('dayClick', dayBodyCells[dateToCell(date).col], date, allDay, ev); } /* External Dragging --------------------------------------------------------------------------------*/ function dragStart(_dragElement, ev, ui) { hoverListener.start(function(cell) { clearOverlays(); if (cell) { if (getIsCellAllDay(cell)) { renderCellOverlay(cell.row, cell.col, cell.row, cell.col); }else{ var d1 = realCellToDate(cell); var d2 = addMinutes(cloneDate(d1), opt('defaultEventMinutes')); renderSlotOverlay(d1, d2); } } }, ev); } function dragStop(_dragElement, ev, ui) { var cell = hoverListener.stop(); clearOverlays(); if (cell) { trigger('drop', _dragElement, realCellToDate(cell), getIsCellAllDay(cell), ev, ui); } } }