// 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 // }); setDefaults({ allDaySlot: true, allDayText: 'all-day', scrollTime: '06:00:00', slotMinutes: 30, slotDuration: '00:30:00', // axisFormat: generateAgendaAxisFormat, // timeFormat: { // agenda: generateAgendaTimeFormat // }, dragOpacity: { agenda: .5 }, minTime: '00:00:00', maxTime: '24:00:00', slotEventOverlap: true }); // 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.computeDateTop = computeDateTop; // //t.defaultEventEnd = defaultEventEnd; // //t.timePosition = timePosition; // t.getIsCellAllDay = getIsCellAllDay; // t.allDayRow = function() { return allDayRow; }; // badly named // //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.getMinTime = function() { return minTime; }; // t.getMaxTime = function() { return maxTime; }; // 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.renderAgenda = renderAgenda; t.renderResource = renderResource; t.setWidth = setWidth; t.setHeight = setHeight; t.afterRender = afterRender; t.computeDateTop = computeDateTop; t.getIsCellAllDay = getIsCellAllDay; t.allDayRow = function() { return allDayRow; }; // badly named 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.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.getSnapDuration = function() { return snapDuration; }; t.getSlotHeight = function() { return slotHeight; }; t.getSlotDuration = function() { return slotDuration; }; t.getMinTime = function() { return minTime; }; t.getMaxTime = function() { return maxTime; }; 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; // overridden var slotSegHtml = t.slotSegHtml; var cellToDate = t.cellToDate; var dateToCell = t.dateToCell; var rangeToSegments = t.rangeToSegments; var formatDate = calendar.formatDate; var calculateWeekNumber = calendar.calculateWeekNumber; // 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 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 selectionHelper; var viewWidth; var viewHeight; var axisWidth; var colWidth; var gutterWidth; var slotDuration; var slotHeight; // TODO: what if slotHeight changes? (see issue 650) var snapDuration; 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 minTime; var maxTime; var colFormat; var resources = t.getResources; // 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 slotDuration; // var tm; // var rtl; // var minMinute, maxMinute; // var colFormat; // var showWeekNumbers; // var weekNumberTitle; // var weekNumberFormat; // var resources = t.getResources; // var minTime; // var maxTime; /* Rendering -----------------------------------------------------------------------------*/ disableTextSelection(element.addClass('fc-agenda')); function renderResource(resourceColumnsCnt) { colCnt = resourceColumnsCnt; 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'); colFormat = opt('columnFormat'); minTime = moment.duration(opt('minTime')); maxTime = moment.duration(opt('maxTime')); slotDuration = moment.duration(opt('slotDuration')); snapDuration = opt('snapDuration'); snapDuration = snapDuration ? moment.duration(snapDuration) : slotDuration; } /* Build DOM -----------------------------------------------------------------------*/ function buildSkeleton() { var s; var headerClass = tm + "-widget-header"; var contentClass = tm + "-widget-content"; var slotTime; var slotDate; var minutes; var slotNormal = slotDuration.asMinutes() % 15 === 0; buildDayTable(); slotLayer = $("
") .appendTo(element); if (opt('allDaySlot')) { daySegmentContainer = $("
") .appendTo(slotLayer); s = "" + "" + "" + "" + "" + "" + "
" + ( opt('allDayHTML') || htmlEscape(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 = "" + ""; slotTime = moment.duration(+minTime); // i wish there was .clone() for durations slotCnt = 0; while (slotTime < maxTime) { slotDate = t.start.clone().time(slotTime); // will be in UTC but that's good. to avoid DST issues minutes = slotDate.minutes(); s += "" + "" + "" + ""; slotTime.add(slotDuration); slotCnt++; } s += "" + "
" + ((!slotNormal || !minutes) ? htmlEscape(formatDate(slotDate, opt('axisFormat'))) : ' ' ) + "" + "
 
" + "
"; // slotTable = $(s).appendTo(slotContainer); // slotTableFirstInner = slotTable.find('div:first'); slotTable = $(s).appendTo(slotContainer); 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 (opt('weekNumbers')) { date = cellToDate(0, 0); weekText = calculateWeekNumber(date); if (rtl) { weekText += opt('weekNumberTitle'); } else { weekText = opt('weekNumberTitle') + weekText; } html += "" + htmlEscape(weekText) + ""; } else { html += " "; } for (col=0; col" + htmlEscape(resource.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 = makeMoment(new Date()).stripTime(); 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); // the stylesheet guarantees that the first row has no border. // this allows .height() to work well cross-browser. var slotHeight0 = slotTable.find('tr:first').height() + 1; // +1 for bottom border var slotHeight1 = slotTable.find('tr:eq(1)').height(); // HACK: i forget why we do this, but i think a cross-browser issue slotHeight = (slotHeight0 + slotHeight1) / 2; snapRatio = slotDuration / snapDuration; snapHeight = slotHeight / snapRatio; // 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 top = computeTimeTop( moment.duration(opt('scrollTime')) ) + 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 match = this.parentNode.className.match(/fc-slot(\d+)/); // TODO: maybe use data ev.data = resources()[col]; // added if (match) { var slotIndex = parseInt(match[1], 10); date.add(minTime + slotIndex * slotDuration); date = calendar.rezoneDate(date); trigger( 'dayClick', dayBodyCells[col], date, ev ); }else{ trigger( 'dayClick', dayBodyCells[col], date, ev ); } } } /* Semi-transparent Overlay Helpers -----------------------------------------------------*/ // TODO: should be consolidated with BasicView's methods function renderDayOverlay(overlayStart, overlayEnd, refreshCoordinateGrid, col) { // overlayEnd is exclusive if (refreshCoordinateGrid) { coordinateGrid.build(); } var segments = rangeToSegments(overlayStart, overlayEnd); for (var i=0; i= 0) { date.time(moment.duration(minTime + snapIndex * snapDuration)); date = calendar.rezoneDate(date); } return date; } function computeDateTop(date, startOfDayDate) { return computeTimeTop( moment.duration( date.clone().stripZone() - startOfDayDate.clone().stripTime() ) ); } function computeTimeTop(time) { // time is a duration if (time < minTime) { return 0; } if (time >= maxTime) { return slotTable.height(); } var slots = (time - minTime) / slotDuration; var slotIndex = Math.floor(slots); var slotPartial = slots - slotIndex; var slotTop = slotTopCache[slotIndex]; // find the position of the corresponding // need to use this tecnhique because not all rows are rendered at same height sometimes. if (slotTop === undefined) { slotTop = slotTopCache[slotIndex] = slotTable.find('tr').eq(slotIndex).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 } var top = slotTop - 1 + // because first row doesn't have a top border slotPartial * slotHeight; // part-way through the row top = Math.max(top, 0); return top; } // // 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 = day.clone().stripTime(); // if (time < day.clone().add('m', minMinute)) { // return 0; // } // if (time >= day.clone().add('m', 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 = event.start.clone(); // if (event.allDay) { // return start; // } // return start.add('m', opt('defaultEventMinutes')); // } /* Selection ---------------------------------------------------------------------------------*/ function defaultSelectionEnd(startDate, allDay) { if (allDay) { return startDate.clone(); } return startDate.clone().add('m', opt('slotMinutes')); } function renderSelection(startDate, endDate, allDay, col) { // only for all-day if (allDay) { if (opt('allDaySlot')) { renderDayOverlay(startDate, endDate, true, col); } }else{ renderSlotSelection(startDate, endDate); } } function renderSlotSelection(startDate, endDate, col) { var helperOption = opt('selectHelper'); coordinateGrid.build(); if (helperOption) { col = 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 = computeDateTop(startDate, startDate); var bottom = computeDateTop(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, col); } } function clearSelection() { clearOverlays(); if (selectionHelper) { selectionHelper.remove(); selectionHelper = null; } } function daySelectionMousedown(ev) { // var cellToDate = t.cellToDate; var getIsCellAllDay = t.getIsCellAllDay; var hoverListener = t.getHoverListener(); var reportDayClick = t.reportDayClick; // this is hacky and sort of weird var col; if (ev.which == 1 && opt('selectable')) { // which==1 means left mouse button unselect(ev); // var _mousedownElement = this; var dates; hoverListener.start(function(cell, origCell) { // TODO: maybe put cellToDate/getIsCellAllDay info in cell clearSelection(); if (cell && getIsCellAllDay(cell)) { col = cell.col; dates = [ realCellToDate(origCell), realCellToDate(cell) ].sort(dateCompare); renderSelection(dates[0], dates[1], true, col); }else{ dates = null; } }, ev); $(document).one('mouseup', function(ev) { hoverListener.stop(); if (dates) { if (+dates[0] == +dates[1]) { reportDayClick(dates[0], true, ev); } ev.data = resources()[col]; reportSelection(dates[0], dates[1], true, ev); } }); } } // select on the calendar somewhere function slotSelectionMousedown(ev) { if (ev.which == 1 && opt('selectable')) { // ev.which==1 means left mouse button unselect(ev); var dates; var col; hoverListener.start(function(cell, origCell) { clearSelection(); if (cell && cell.col == origCell.col && !getIsCellAllDay(cell)) { col = cell.col; var d1 = realCellToDate(origCell); var d2 = realCellToDate(cell); dates = [ d1, d1.clone().add(snapDuration), // calculate minutes depending on selection slot minutes d2, d2.clone().add(snapDuration) ].sort(dateCompare); renderSlotSelection(dates[0], dates[3], cell.col); // updated }else{ dates = null; } }, ev); $(document).one('mouseup', function(ev) { hoverListener.stop(); if (dates) { if (+dates[0] == +dates[1]) { reportDayClick(dates[0], false, ev); } ev.data = resources()[col]; // added 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 = d1.clone().add('m', opt('defaultEventMinutes')); renderSlotOverlay(d1, d2, cell.col); } } }, ev); } function dragStop(_dragElement, ev, ui) { var cell = hoverListener.stop(); clearOverlays(); if (cell) { ev.data = resources()[cell.col]; trigger('drop', _dragElement, realCellToDate(cell), getIsCellAllDay(cell), ev, ui); } } }