From d0d0a97c6468bf3f8317b6ccc4d28ad46a41a022 Mon Sep 17 00:00:00 2001 From: Adam Shaw Date: Fri, 24 Jan 2014 22:51:24 -0800 Subject: [PATCH] [2.0] source file changes --- src/Calendar.js | 300 +++++++++--- src/EventManager.js | 298 ++++++++---- src/Header.js | 26 +- src/agenda/AgendaDayView.js | 33 +- src/agenda/AgendaEventRenderer.js | 268 +++++++---- src/agenda/AgendaView.js | 325 +++++++------ src/agenda/AgendaWeekView.js | 59 +-- src/agenda/agenda.css | 5 +- src/basic/BasicDayView.js | 33 +- src/basic/BasicView.js | 72 ++- src/basic/BasicWeekView.js | 44 +- src/basic/MonthView.js | 51 +- src/common/DayEventRenderer.js | 65 ++- src/common/SelectionManager.js | 35 +- src/common/View.js | 286 +++++++---- src/common/common.css | 32 +- src/date_util.js | 756 +++++++++++++++++------------- src/defaults.js | 106 +++-- src/gcal/gcal.js | 51 +- src/intro.js | 16 +- src/lang.js | 72 +++ src/main.css | 3 + src/main.js | 57 ++- src/outro.js | 2 +- src/util.js | 45 +- 25 files changed, 1886 insertions(+), 1154 deletions(-) create mode 100644 src/lang.js diff --git a/src/Calendar.js b/src/Calendar.js index fe859ee..18b78e5 100644 --- a/src/Calendar.js +++ b/src/Calendar.js @@ -1,10 +1,38 @@ -function Calendar(element, options, eventSources) { +function Calendar(element, instanceOptions) { var t = this; + + + + // Build options object + // ----------------------------------------------------------------------------------- + // Precedence (lowest to highest): defaults, rtlDefaults, langOptions, instanceOptions + + var options = mergeOptions({}, defaults, instanceOptions); + var langOptions; + + // determine language options + if (options.lang in langOptionHash) { + langOptions = langOptionHash[options.lang]; + } + else { + langOptions = langOptionHash[defaults.lang]; + } + + if (langOptions) { // if language options exist, rebuild... + options = mergeOptions({}, defaults, langOptions, instanceOptions); + } + + if (options.isRTL) { // is isRTL, rebuild... + options = mergeOptions({}, defaults, rtlDefaults, langOptions || {}, instanceOptions); + } + + - - // exports + // Exports + // ----------------------------------------------------------------------------------- + t.options = options; t.render = render; t.destroy = destroy; @@ -22,21 +50,186 @@ function Calendar(element, options, eventSources) { t.today = today; t.gotoDate = gotoDate; t.incrementDate = incrementDate; - t.formatDate = function(format, date) { return formatDate(format, date, options) }; - t.formatDates = function(format, date1, date2) { return formatDates(format, date1, date2, options) }; t.getDate = getDate; + t.getCalendar = getCalendar; t.getView = getView; t.option = option; t.trigger = trigger; + + + + // Language-data Internals + // ----------------------------------------------------------------------------------- + // Apply overrides to the current language's data + + var langData = createObject( // make a cheap clone + moment.langData(options.lang) + ); + + if (options.monthNames) { + langData._months = options.monthNames; + } + if (options.monthNamesShort) { + langData._monthsShort = options.monthNamesShort; + } + if (options.dayNames) { + langData._weekdays = options.dayNames; + } + if (options.dayNamesShort) { + langData._weekdaysShort = options.dayNamesShort; + } + if (options.firstDay) { + var _week = createObject(langData._week); // _week: { dow: # } + _week.dow = options.firstDay; + langData._week = _week; + } + + + + // Calendar-specific Date Utilities + // ----------------------------------------------------------------------------------- + + + t.defaultAllDayEventDuration = moment.duration(options.defaultAllDayEventDuration); + t.defaultTimedEventDuration = moment.duration(options.defaultTimedEventDuration); + + + // Builds a moment using the settings of the current calendar: timezone and language. + // Accepts anything the vanilla moment() constructor accepts. + t.moment = function() { + var mom; + + if (options.timezone === 'local') { + mom = fc.moment.apply(null, arguments); + } + else if (options.timezone === 'UTC') { + mom = fc.moment.utc.apply(null, arguments); + } + else { + mom = fc.moment.parseZone.apply(null, arguments); + } + + mom._lang = langData; + + return mom; + }; + + + // Returns a boolean about whether or not the calendar knows how to calculate + // the timezone offset of arbitrary dates in the current timezone. + t.getIsAmbigTimezone = function() { + return options.timezone !== 'local' && options.timezone !== 'UTC'; + }; + + + // Returns a copy of the given date in the current timezone of it is ambiguously zoned. + // This will also give the date an unambiguous time. + t.rezoneDate = function(date) { + return t.moment(date.toArray()); + }; + + + // Returns a moment for the current date, as defined by the client's computer, + // or overridden by the `now` option. + t.getNow = function() { + var now = options.now; + if (typeof now === 'function') { + now = now(); + } + return t.moment(now); + }; + + + // Calculates the week number for a moment according to the calendar's + // `weekNumberCalculation` setting. + t.calculateWeekNumber = function(mom) { + var calc = options.weekNumberCalculation; + if (calc == 'local') { + return mom.week(); + } + else if (calc.toUpperCase() == 'ISO') { + return mom.isoWeek(); + } + else if (typeof calc === 'function') { + return calc(mom); + } + }; + + + // Get an event's normalized end date. If not present, calculate it from the defaults. + t.getEventEnd = function(event) { + if (event.end) { + return event.end.clone(); + } + else { + return t.getDefaultEventEnd(event.allDay, event.start); + } + }; + + + // Given an event's allDay status and start date, return swhat its fallback end date should be. + t.getDefaultEventEnd = function(allDay, start) { // TODO: rename to computeDefaultEventEnd + var end = start.clone(); + + if (allDay) { + end.stripTime().add(t.defaultAllDayEventDuration); + } + else { + end.add(t.defaultTimedEventDuration); + } + + if (t.getIsAmbigTimezone()) { + end.stripZone(); // we don't know what the tzo should be + } + + return end; + }; + + + + // Date-formatting Utilities + // ----------------------------------------------------------------------------------- + + + // Like the vanilla formatRange, but with calendar-specific settings applied. + t.formatRange = function(m1, m2, formatStr) { + + // a function that returns a formatStr // TODO: in future, precompute this + if (typeof formatStr === 'function') { + formatStr = formatStr.call(t, options, langData); + } + + return formatRange(m1, m2, formatStr, null, options.isRTL); + }; + + + // Like the vanilla formatDate, but with calendar-specific settings applied. + t.formatDate = function(mom, formatStr) { + + // a function that returns a formatStr // TODO: in future, precompute this + if (typeof formatStr === 'function') { + formatStr = formatStr.call(t, options, langData); + } + + return formatDate(mom, formatStr); + }; + + - - // imports - EventManager.call(t, options, eventSources); + // Imports + // ----------------------------------------------------------------------------------- + + + EventManager.call(t, options); var isFetchNeeded = t.isFetchNeeded; var fetchEvents = t.fetchEvents; - - - // locals + + + + // Locals + // ----------------------------------------------------------------------------------- + + var _element = element[0]; var header; var headerElement; @@ -47,17 +240,22 @@ function Calendar(element, options, eventSources) { var suggestedViewHeight; var resizeUID = 0; var ignoreWindowResize = 0; - var date = new Date(); + var date; var events = []; var _dragElement; - /* Main Rendering - -----------------------------------------------------------------------------*/ - - - setYMD(date, options.year, options.month, options.date); + // Main Rendering + // ----------------------------------------------------------------------------------- + + + if (options.defaultDate != null) { + date = t.moment(options.defaultDate); + } + else { + date = t.getNow(); + } function render(inc) { @@ -85,7 +283,7 @@ function Calendar(element, options, eventSources) { element.addClass('ui-widget'); } - content = $("
") + content = $("
") .prependTo(element); header = new Header(t, options); @@ -143,9 +341,9 @@ function Calendar(element, options, eventSources) { } - - /* View Rendering - -----------------------------------------------------------------------------*/ + + // View Rendering + // ----------------------------------------------------------------------------------- function changeView(newViewName) { @@ -170,7 +368,7 @@ function Calendar(element, options, eventSources) { header.activateButton(newViewName); currentView = new fcViews[newViewName]( - $("
") + $("
") .appendTo(content), t // the calendar object ); @@ -185,7 +383,8 @@ function Calendar(element, options, eventSources) { function renderView(inc) { if ( !currentView.start || // never rendered before - inc || date < currentView.start || date >= currentView.end // or new date range + inc || // explicit date window change + !date.isWithin(currentView.intervalStart, currentView.intervalEnd) // implicit date window change ) { if (elementVisible()) { _renderView(inc); @@ -204,7 +403,10 @@ function Calendar(element, options, eventSources) { } freezeContentHeight(); - currentView.render(date, inc || 0); // the view's render method ONLY renders the skeleton, nothing else + if (inc) { + date = currentView.incrementDate(date, inc); + } + currentView.render(date.clone()); // the view's render method ONLY renders the skeleton, nothing else setSize(); unfreezeContentHeight(); (currentView.afterRender || noop)(); @@ -213,7 +415,6 @@ function Calendar(element, options, eventSources) { updateTodayButton(); trigger('viewRender', currentView, currentView, currentView.element); - currentView.trigger('viewDisplay', _element); // deprecated ignoreWindowResize--; @@ -222,8 +423,8 @@ function Calendar(element, options, eventSources) { - /* Resizing - -----------------------------------------------------------------------------*/ + // Resizing + // ----------------------------------------------------------------------------------- function updateSize() { @@ -324,7 +525,7 @@ function Calendar(element, options, eventSources) { function getAndRenderEvents() { - if (!options.lazyFetching || isFetchNeeded(currentView.visStart, currentView.visEnd)) { + if (!options.lazyFetching || isFetchNeeded(currentView.start, currentView.end)) { fetchAndRenderEvents(); } else { @@ -334,7 +535,7 @@ function Calendar(element, options, eventSources) { function fetchAndRenderEvents() { - fetchEvents(currentView.visStart, currentView.visEnd); + fetchEvents(currentView.start, currentView.end); // ... will call reportEvents // ... which will call renderEvents } @@ -364,8 +565,8 @@ function Calendar(element, options, eventSources) { function updateTodayButton() { - var today = new Date(); - if (today >= currentView.start && today < currentView.end) { + var now = t.getNow(); + if (now.isWithin(currentView.intervalStart, currentView.intervalEnd)) { header.disableButton('today'); } else { @@ -379,8 +580,8 @@ function Calendar(element, options, eventSources) { -----------------------------------------------------------------------------*/ - function select(start, end, allDay) { - currentView.select(start, end, allDay===undefined ? true : allDay); + function select(start, end) { + currentView.select(start, end); } @@ -407,49 +608,37 @@ function Calendar(element, options, eventSources) { function prevYear() { - addYears(date, -1); + date.add('years', -1); renderView(); } function nextYear() { - addYears(date, 1); + date.add('years', 1); renderView(); } function today() { - date = new Date(); + date = t.getNow(); renderView(); } - function gotoDate(year, month, dateOfMonth) { - if (year instanceof Date) { - date = cloneDate(year); // provided 1 argument, a Date - }else{ - setYMD(date, year, month, dateOfMonth); - } + function gotoDate(dateInput) { + date = t.moment(dateInput); renderView(); } - function incrementDate(years, months, days) { - if (years !== undefined) { - addYears(date, years); - } - if (months !== undefined) { - addMonths(date, months); - } - if (days !== undefined) { - addDays(date, days); - } + function incrementDate() { + date.add.apply(date, arguments); renderView(); } function getDate() { - return cloneDate(date); + return date.clone(); } @@ -480,6 +669,11 @@ function Calendar(element, options, eventSources) { /* Misc -----------------------------------------------------------------------------*/ + + function getCalendar() { + return t; + } + function getView() { return currentView; diff --git a/src/EventManager.js b/src/EventManager.js index 59558d3..500da23 100644 --- a/src/EventManager.js +++ b/src/EventManager.js @@ -10,7 +10,7 @@ var ajaxDefaults = { var eventGUID = 1; -function EventManager(options, _sources) { +function EventManager(options) { // assumed to be a calendar var t = this; @@ -23,13 +23,14 @@ function EventManager(options, _sources) { t.renderEvent = renderEvent; t.removeEvents = removeEvents; t.clientEvents = clientEvents; - t.normalizeEvent = normalizeEvent; // imports var trigger = t.trigger; var getView = t.getView; var reportEvents = t.reportEvents; + var getEventEnd = t.getEventEnd; + var mutateEvent = t.mutateEvent; // locals @@ -40,7 +41,14 @@ function EventManager(options, _sources) { var pendingSourceCnt = 0; var loadingLevel = 0; var cache = []; - + + + + var _sources = options.eventSources || []; + + if (options.events) { + _sources.push(options.events); + } for (var i=0; i<_sources.length; i++) { _addEventSource(_sources[i]); @@ -53,7 +61,10 @@ function EventManager(options, _sources) { function isFetchNeeded(start, end) { - return !rangeStart || start < rangeStart || end > rangeEnd; + return !rangeStart || // nothing has been fetched yet? + // or, a part of the new range is outside of the old range? (after normalizing) + start.clone().stripZone() < rangeStart.clone().stripZone() || + end.clone().stripZone() > rangeEnd.clone().stripZone(); } @@ -73,23 +84,16 @@ function EventManager(options, _sources) { function fetchEventSource(source, fetchID) { _fetchEventSource(source, function(events) { if (fetchID == currentFetchID) { - if (events) { - if (options.eventDataTransform) { - events = $.map(events, options.eventDataTransform); - } - if (source.eventDataTransform) { - events = $.map(events, source.eventDataTransform); - } - // TODO: this technique is not ideal for static array event sources. - // For arrays, we'll want to process all events right in the beginning, then never again. - + if (events) { for (var i=0; i"; + } + else if (normalIcon && !options.theme) { + html = ""; + } + else { + html = htmlEscape(text || buttonName); + } + var button = $( "" + - (icon ? - "" + - "" + - "" : - text - ) + + html + "" ) .click(function() { diff --git a/src/agenda/AgendaDayView.js b/src/agenda/AgendaDayView.js index a6526b5..fdbaf8f 100644 --- a/src/agenda/AgendaDayView.js +++ b/src/agenda/AgendaDayView.js @@ -1,39 +1,34 @@ fcViews.agendaDay = AgendaDayView; - -function AgendaDayView(element, calendar) { +function AgendaDayView(element, calendar) { // TODO: make a DayView mixin var t = this; // exports + t.incrementDate = incrementDate; t.render = render; // imports AgendaView.call(t, element, calendar, 'agendaDay'); - var opt = t.opt; - var renderAgenda = t.renderAgenda; - var skipHiddenDays = t.skipHiddenDays; - var formatDate = calendar.formatDate; - - - function render(date, delta) { - if (delta) { - addDays(date, delta); - } - skipHiddenDays(date, delta < 0 ? -1 : 1); - var start = cloneDate(date, true); - var end = addDays(cloneDate(start), 1); + function incrementDate(date, delta) { + var out = date.clone().stripTime().add('days', delta); + out = t.skipHiddenDays(out, delta < 0 ? -1 : 1); + return out; + } - t.title = formatDate(date, opt('titleFormat')); - t.start = t.visStart = start; - t.end = t.visEnd = end; + function render(date) { - renderAgenda(1); + t.start = t.intervalStart = date.clone().stripTime(); + t.end = t.intervalEnd = t.start.clone().add('days', 1); + + t.title = calendar.formatDate(t.start, t.opt('titleFormat')); + + t.renderAgenda(1); } diff --git a/src/agenda/AgendaEventRenderer.js b/src/agenda/AgendaEventRenderer.js index 3105b3d..c2b279d 100644 --- a/src/agenda/AgendaEventRenderer.js +++ b/src/agenda/AgendaEventRenderer.js @@ -15,15 +15,12 @@ function AgendaEventRenderer() { 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 computeDateTop = t.computeDateTop; var getIsCellAllDay = t.getIsCellAllDay; var colContentLeft = t.colContentLeft; var colContentRight = t.colContentRight; @@ -31,7 +28,9 @@ function AgendaEventRenderer() { var getColCnt = t.getColCnt; var getColWidth = t.getColWidth; var getSnapHeight = t.getSnapHeight; - var getSnapMinutes = t.getSnapMinutes; + var getSnapDuration = t.getSnapDuration; + var getSlotHeight = t.getSlotHeight; + var getSlotDuration = t.getSlotDuration; var getSlotContainer = t.getSlotContainer; var reportEventElement = t.reportEventElement; var showEvents = t.showEvents; @@ -41,9 +40,12 @@ function AgendaEventRenderer() { var renderDayOverlay = t.renderDayOverlay; var clearOverlays = t.clearOverlays; var renderDayEvents = t.renderDayEvents; + var getMinTime = t.getMinTime; + var getMaxTime = t.getMaxTime; var calendar = t.calendar; var formatDate = calendar.formatDate; - var formatDates = calendar.formatDates; + var formatRange = calendar.formatRange; + var getEventEnd = calendar.getEventEnd; // overrides @@ -84,25 +86,21 @@ function AgendaEventRenderer() { function compileSlotSegs(events) { var colCnt = getColCnt(), - minMinute = getMinMinute(), - maxMinute = getMaxMinute(), - d, - visEventEnds = $.map(events, slotEventEnd), + minTime = getMinTime(), + maxTime = getMaxTime(), + cellDate, i, j, seg, colSegs, segs = []; for (i=0; i start && eventStart < end) { - if (eventStart < start) { - segStart = cloneDate(start); + + // get dates, make copies, then strip zone to normalize + eventStart = event.start.clone().stripZone(); + eventEnd = getEventEnd(event).stripZone(); + + if (eventEnd > rangeStart && eventStart < rangeEnd) { + + if (eventStart < rangeStart) { + segStart = rangeStart.clone(); isStart = false; - }else{ + } + else { segStart = eventStart; isStart = true; } - if (eventEnd > end) { - segEnd = cloneDate(end); + + if (eventEnd > rangeEnd) { + segEnd = rangeEnd.clone(); isEnd = false; - }else{ + } + else { segEnd = eventEnd; isEnd = true; } + segs.push({ event: event, start: segStart, @@ -152,17 +164,9 @@ function AgendaEventRenderer() { }); } } + return segs.sort(compareSlotSegs); } - - - 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 @@ -194,8 +198,8 @@ function AgendaEventRenderer() { for (i=0; i" + "
" + - "
" + - htmlEscape(formatDates(event.start, event.end, opt('timeFormat'))) + + "
"; + + if (event.end) { + html += htmlEscape(formatRange(event.start, event.end, opt('timeFormat'))); + }else{ + html += htmlEscape(formatDate(event.start, opt('timeFormat'))); + } + + html += "
" + "
" + htmlEscape(event.title || '') + "
" + "
" + "
"; + if (seg.isEnd && isEventResizable(event)) { html += "
=
"; @@ -386,79 +401,98 @@ function AgendaEventRenderer() { var revert; var allDay = true; var dayDelta; + var hoverListener = getHoverListener(); var colWidth = getColWidth(); + var minTime = getMinTime(); + var slotDuration = getSlotDuration(); + var slotHeight = getSlotHeight(); + var snapDuration = getSnapDuration(); 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 + dayDelta = date.diff(origDate, 'days'); + + if (!cell.row) { // on full-days + renderDayOverlay( - addDays(cloneDate(event.start), dayDelta), - addDays(exclEndDay(event), dayDelta) + event.start.clone().add('days', dayDelta), + getEventEnd(event).add('days', dayDelta) ); + resetElement(); - }else{ - // mouse is over bottom slots + } + 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]); + setOuterHeight(eventElement, calendar.defaultTimedEventDuration / slotDuration * slotHeight); // the default height + eventElement.draggable('option', 'grid', [ colWidth, 1 ]); allDay = false; } - }else{ + } + else { revert = true; } } + revert = revert || (allDay && !dayDelta); - }else{ + } + 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) + + 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; + } + else { // changed! + + var eventStart = event.start.clone().add('days', dayDelta); // already assumed to have a stripped time + var snapTime; + var snapIndex; if (!allDay) { - minuteDelta = Math.round((eventElement.offset().top - getSlotContainer().offset().top) / snapHeight) - * snapMinutes - + minMinute - - (event.start.getHours() * 60 + event.start.getMinutes()); + snapIndex = Math.round((eventElement.offset().top - getSlotContainer().offset().top) / snapHeight); // why not use ui.offset.top? + snapTime = moment.duration(minTime + snapIndex * snapDuration); + eventStart = calendar.rezoneDate(eventStart.clone().time(snapTime)); } - eventDrop(this, event, dayDelta, minuteDelta, allDay, ev, ui); + + eventDrop( + this, // el + event, + eventStart, + ev, + ui + ); } } }); @@ -481,7 +515,7 @@ function AgendaEventRenderer() { var colCnt = getColCnt(); var colWidth = getColWidth(); var snapHeight = getSnapHeight(); - var snapMinutes = getSnapMinutes(); + var snapDuration = getSnapDuration(); // states var origPosition; // original position of the element, not the mouse @@ -490,7 +524,10 @@ function AgendaEventRenderer() { var isAllDay, prevIsAllDay; var colDelta, prevColDelta; var dayDelta; // derived from colDelta - var minuteDelta, prevMinuteDelta; + var snapDelta, prevSnapDelta; // the number of snaps away from the original position + + // newly computed + var eventStart, eventEnd; eventElement.draggable({ scroll: false, @@ -512,8 +549,10 @@ function AgendaEventRenderer() { isAllDay = prevIsAllDay = getIsCellAllDay(origCell); colDelta = prevColDelta = 0; dayDelta = 0; - minuteDelta = prevMinuteDelta = 0; + snapDelta = prevSnapDelta = 0; + eventStart = null; + eventEnd = null; }, drag: function(ev, ui) { @@ -539,12 +578,12 @@ function AgendaEventRenderer() { col = Math.max(0, col); col = Math.min(colCnt-1, col); var date = cellToDate(0, col); - dayDelta = dayDiff(date, origDate); + dayDelta = date.diff(origDate, 'days'); } // calculate minute delta (only if over slots) if (!isAllDay) { - minuteDelta = Math.round((ui.position.top - origPosition.top) / snapHeight) * snapMinutes; + snapDelta = Math.round((ui.position.top - origPosition.top) / snapHeight); } } @@ -553,16 +592,26 @@ function AgendaEventRenderer() { isInBounds != prevIsInBounds || isAllDay != prevIsAllDay || colDelta != prevColDelta || - minuteDelta != prevMinuteDelta + snapDelta != prevSnapDelta ) { + // compute new dates + if (isAllDay) { + eventStart = event.start.clone().stripTime().add('days', dayDelta); + eventEnd = eventStart.clone().add(calendar.defaultAllDayEventDuration); + } + else { + eventStart = event.start.clone().add(snapDelta * snapDuration).add('days', dayDelta); + eventEnd = getEventEnd(event).add(snapDelta * snapDuration).add('days', dayDelta); + } + updateUI(); // update previous states for next time prevIsInBounds = isInBounds; prevIsAllDay = isAllDay; prevColDelta = colDelta; - prevMinuteDelta = minuteDelta; + prevSnapDelta = snapDelta; } // if out-of-bounds, revert when done, and vice versa. @@ -574,8 +623,14 @@ function AgendaEventRenderer() { clearOverlays(); trigger('eventDragStop', eventElement, event, ev, ui); - if (isInBounds && (isAllDay || dayDelta || minuteDelta)) { // changed! - eventDrop(this, event, dayDelta, isAllDay ? 0 : minuteDelta, isAllDay, ev, ui); + if (isInBounds && (isAllDay || dayDelta || snapDelta)) { // changed! + eventDrop( + this, // el + event, + eventStart, + ev, + ui + ); } else { // either no change or out-of-bounds (draggable has already reverted) @@ -584,7 +639,7 @@ function AgendaEventRenderer() { isAllDay = false; colDelta = 0; dayDelta = 0; - minuteDelta = 0; + snapDelta = 0; updateUI(); eventElement.css('filter', ''); // clear IE opacity side-effects @@ -605,26 +660,27 @@ function AgendaEventRenderer() { if (isAllDay) { timeElement.hide(); eventElement.draggable('option', 'grid', null); // disable grid snapping - renderDayOverlay( - addDays(cloneDate(event.start), dayDelta), - addDays(exclEndDay(event), dayDelta) - ); + renderDayOverlay(eventStart, eventEnd); } else { - updateTimeText(minuteDelta); + updateTimeText(); 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); + function updateTimeText() { + var text; + if (eventStart) { // must of had a state change + if (event.end) { + text = formatRange(eventStart, eventEnd, opt('timeFormat')); + } + else { + text = formatDate(eventStart, opt('timeFormat')); + } + timeElement.text(text); } - timeElement.text(formatDates(newStart, newEnd, opt('timeFormat'))); } } @@ -638,7 +694,9 @@ function AgendaEventRenderer() { function resizableSlotEvent(event, eventElement, timeElement) { var snapDelta, prevSnapDelta; var snapHeight = getSnapHeight(); - var snapMinutes = getSnapMinutes(); + var snapDuration = getSnapDuration(); + var eventEnd; + eventElement.resizable({ handles: { s: '.ui-resizable-handle' @@ -653,22 +711,34 @@ function AgendaEventRenderer() { // 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( + eventEnd = getEventEnd(event).add(snapDuration * snapDelta); + var text; + if (snapDelta || event.end) { + text = formatRange( event.start, - (!snapDelta && !event.end) ? null : // no change, so don't display time range - addMinutes(eventEnd(event), snapMinutes*snapDelta), + eventEnd, opt('timeFormat') - ) - ); + ); + } + else { + text = formatDate(event.start, opt('timeFormat')); + } + timeElement.text(text); prevSnapDelta = snapDelta; } }, stop: function(ev, ui) { trigger('eventResizeStop', this, event, ev, ui); if (snapDelta) { - eventResize(this, event, 0, snapMinutes*snapDelta, ev, ui); - }else{ + eventResize( + this, + event, + eventEnd, + ev, + ui + ); + } + else { showEvents(event, eventElement); // BUG: if event was really short, need to put title back in span } diff --git a/src/agenda/AgendaView.js b/src/agenda/AgendaView.js index 45c4e88..ec9a17a 100644 --- a/src/agenda/AgendaView.js +++ b/src/agenda/AgendaView.js @@ -2,22 +2,39 @@ setDefaults({ allDaySlot: true, allDayText: 'all-day', - firstHour: 6, - slotMinutes: 30, - defaultEventMinutes: 120, - axisFormat: 'h(:mm)tt', + + scrollTime: '06:00:00', + + slotDuration: '00:30:00', + + axisFormat: generateAgendaAxisFormat, timeFormat: { - agenda: 'h:mm{ - h:mm}' + agenda: generateAgendaTimeFormat }, + dragOpacity: { agenda: .5 }, - minTime: 0, - maxTime: 24, + minTime: '00:00:00', + maxTime: '24:00:00', slotEventOverlap: true }); +function generateAgendaAxisFormat(options, langData) { + return langData.longDateFormat('LT') + .replace(':mm', '(:mm)') + .replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs + .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand +} + + +function generateAgendaTimeFormat(options, langData) { + return langData.longDateFormat('LT') + .replace(/\s*a$/i, ''); // remove trailing AM/PM +} + + // TODO: make it work in quirks mode (event corners, all-day height) // TODO: test liquid width, especially in IE6 @@ -31,10 +48,9 @@ function AgendaView(element, calendar, viewName) { t.setWidth = setWidth; t.setHeight = setHeight; t.afterRender = afterRender; - t.defaultEventEnd = defaultEventEnd; - t.timePosition = timePosition; + t.computeDateTop = computeDateTop; t.getIsCellAllDay = getIsCellAllDay; - t.allDayRow = getAllDayRow; + t.allDayRow = function() { return allDayRow }; // badly named t.getCoordinateGrid = function() { return coordinateGrid }; // specifically for AgendaEventRenderer t.getHoverListener = function() { return hoverListener }; t.colLeft = colLeft; @@ -43,14 +59,16 @@ function AgendaView(element, calendar, viewName) { 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.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; @@ -76,7 +94,9 @@ function AgendaView(element, calendar, viewName) { var cellToDate = t.cellToDate; var dateToCell = t.dateToCell; var rangeToSegments = t.rangeToSegments; + var calendar = t.calendar; var formatDate = calendar.formatDate; + var calculateWeekNumber = calendar.calculateWeekNumber; // locals @@ -105,9 +125,11 @@ function AgendaView(element, calendar, viewName) { var axisWidth; var colWidth; var gutterWidth; + + var slotDuration; var slotHeight; // TODO: what if slotHeight changes? (see issue 650) - var snapMinutes; + 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 @@ -121,11 +143,9 @@ function AgendaView(element, calendar, viewName) { var tm; var rtl; - var minMinute, maxMinute; + var minTime; + var maxTime; var colFormat; - var showWeekNumbers; - var weekNumberTitle; - var weekNumberFormat; @@ -153,21 +173,14 @@ function AgendaView(element, calendar, viewName) { 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"; - } + minTime = moment.duration(opt('minTime')); + maxTime = moment.duration(opt('maxTime')); - snapMinutes = opt('snapMinutes') || opt('slotMinutes'); + slotDuration = moment.duration(opt('slotDuration')); + snapDuration = opt('snapDuration'); + snapDuration = snapDuration ? moment.duration(snapDuration) : slotDuration; } @@ -177,14 +190,13 @@ function AgendaView(element, calendar, viewName) { function buildSkeleton() { + var s; var headerClass = tm + "-widget-header"; var contentClass = tm + "-widget-content"; - var s; - var d; - var i; - var maxd; + var slotTime; + var slotDate; var minutes; - var slotNormal = opt('slotMinutes') % 15 == 0; + var slotNormal = slotDuration.asMinutes() % 15 == 0; buildDayTable(); @@ -201,7 +213,12 @@ function AgendaView(element, calendar, viewName) { s = "" + "" + - "" + + "" + "" + @@ -240,27 +257,32 @@ function AgendaView(element, calendar, viewName) { s = "
" + opt('allDayText') + "" + + ( + opt('allDayHTML') || + htmlEscape(opt('allDayText')) + ) + + "" + "
" + "
" + ""; - d = zeroDate(); - maxd = addMinutes(cloneDate(d), maxMinute); - addMinutes(d, minMinute); + + slotTime = moment.duration(+minTime); // i wish there was .clone() for durations slotCnt = 0; - for (i=0; d < maxd; i++) { - minutes = d.getMinutes(); + 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 += - "" + + "" + "" + "" + ""; - addMinutes(d, opt('slotMinutes')); + slotTime.add(slotDuration); slotCnt++; } + s += "" + "
" + - ((!slotNormal || !minutes) ? formatDate(d, opt('axisFormat')) : ' ') + + ((!slotNormal || !minutes) ? + htmlEscape(formatDate(slotDate, opt('axisFormat'))) : + ' ' + ) + "" + "
 
" + "
"; + slotTable = $(s).appendTo(slotContainer); slotBind(slotTable.find('td')); @@ -319,14 +341,14 @@ function AgendaView(element, calendar, viewName) { "" + ""; - if (showWeekNumbers) { + if (opt('weekNumbers')) { date = cellToDate(0, 0); - weekText = formatDate(date, weekNumberFormat); + weekText = calculateWeekNumber(date); if (rtl) { - weekText += weekNumberTitle; + weekText += opt('weekNumberTitle'); } else { - weekText = weekNumberTitle + weekText; + weekText = opt('weekNumberTitle') + weekText; } html += "" + @@ -340,7 +362,7 @@ function AgendaView(element, calendar, viewName) { for (col=0; col" + + "" + htmlEscape(formatDate(date, colFormat)) + ""; } @@ -358,7 +380,7 @@ function AgendaView(element, calendar, viewName) { var headerClass = tm + "-widget-header"; // TODO: make these when updateOptions() called var contentClass = tm + "-widget-content"; var date; - var today = clearTime(new Date()); + var today = calendar.getNow().stripTime(); var col; var cellsHTML; var cellHTML; @@ -378,10 +400,10 @@ function AgendaView(element, calendar, viewName) { classNames = [ 'fc-col' + col, - 'fc-' + dayIDs[date.getDay()], + 'fc-' + dayIDs[date.day()], contentClass ]; - if (+date == +today) { + if (date.isSame(today, 'day')) { classNames.push( tm + '-state-highlight', 'fc-today' @@ -447,9 +469,12 @@ function AgendaView(element, calendar, viewName) { // the stylesheet guarantees that the first row has no border. // this allows .height() to work well cross-browser. - slotHeight = slotTable.find('tr:first').height() + 1; // +1 for bottom border + 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 = opt('slotMinutes') / snapMinutes; + snapRatio = slotDuration / snapDuration; snapHeight = slotHeight / snapRatio; } @@ -507,13 +532,14 @@ function AgendaView(element, calendar, viewName) { function resetScroll() { - var d0 = zeroDate(); - var scrollDate = cloneDate(d0); - scrollDate.setHours(opt('firstHour')); - var top = timePosition(d0, scrollDate) + 1; // +1 for the border + 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 } @@ -545,15 +571,24 @@ function AgendaView(element, calendar, viewName) { 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); + var match = this.parentNode.className.match(/fc-slot(\d+)/); // TODO: maybe use data + if (match) { + var slotIndex = parseInt(match[1]); + date.add(minTime + slotIndex * slotDuration); + date = calendar.rezoneDate(date); + trigger( + 'dayClick', + dayBodyCells[col], + date, + ev + ); }else{ - trigger('dayClick', dayBodyCells[col], date, true, ev); + trigger( + 'dayClick', + dayBodyCells[col], + date, + ev + ); } } } @@ -594,15 +629,24 @@ function AgendaView(element, calendar, viewName) { function renderSlotOverlay(overlayStart, overlayEnd) { - for (var i=0; i= 0) { - addMinutes(d, minMinute + slotIndex * snapMinutes); + date.time(moment.duration(minTime + slotIndex * slotDuration)); + date = calendar.rezoneDate(date); } - return d; + + return date; } - - - // 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)) { + + + 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 >= addMinutes(cloneDate(day), maxMinute)) { + if (time >= maxTime) { return slotTable.height(); } - var slotMinutes = opt('slotMinutes'), - minutes = time.getHours()*60 + time.getMinutes() - minMinute, - slotI = Math.floor(minutes / slotMinutes), - slotTop = slotTopCache[slotI]; + + 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[slotI] = - slotTable.find('tr').eq(slotI).find('td div')[0].offsetTop; + 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 } - 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')); + + 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; } /* Selection ---------------------------------------------------------------------------------*/ + - - function defaultSelectionEnd(startDate, allDay) { - if (allDay) { - return cloneDate(startDate); + function defaultSelectionEnd(start) { + if (start.hasTime()) { + return start.clone().add(slotDuration); + } + else { + return start.clone().add('days', 1); } - 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 renderSelection(start, end) { + if (start.hasTime() || end.hasTime()) { + renderSlotSelection(start, end); + } + else if (opt('allDaySlot')) { + renderDayOverlay(start, end, true); // true for refreshing coordinate grid } } @@ -770,8 +822,8 @@ function AgendaView(element, calendar, viewName) { 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); + var top = computeDateTop(startDate, startDate); + var bottom = computeDateTop(endDate, startDate); if (bottom > top) { // protect against selections that are entirely before or after visible range rect.top = top; rect.height = bottom - top; @@ -834,9 +886,9 @@ function AgendaView(element, calendar, viewName) { var d2 = realCellToDate(cell); dates = [ d1, - addMinutes(cloneDate(d1), snapMinutes), // calculate minutes depending on selection slot minutes + d1.clone().add(snapDuration), // calculate minutes depending on selection slot minutes d2, - addMinutes(cloneDate(d2), snapMinutes) + d2.clone().add(snapDuration) ].sort(dateCompare); renderSlotSelection(dates[0], dates[3]); }else{ @@ -847,17 +899,17 @@ function AgendaView(element, calendar, viewName) { hoverListener.stop(); if (dates) { if (+dates[0] == +dates[1]) { - reportDayClick(dates[0], false, ev); + reportDayClick(dates[0], ev); } - reportSelection(dates[0], dates[3], false, ev); + reportSelection(dates[0], dates[3], ev); } }); } } - function reportDayClick(date, allDay, ev) { - trigger('dayClick', dayBodyCells[dateToCell(date).col], date, allDay, ev); + function reportDayClick(date, ev) { + trigger('dayClick', dayBodyCells[dateToCell(date).col], date, ev); } @@ -870,13 +922,16 @@ function AgendaView(element, calendar, viewName) { 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')); + var d1 = realCellToDate(cell); + var d2 = d1.clone(); + if (d1.hasTime()) { + d2.add(calendar.defaultTimedEventDuration); renderSlotOverlay(d1, d2); } + else { + d2.add(calendar.defaultAllDayEventDuration); + renderDayOverlay(d1, d2); + } } }, ev); } @@ -886,7 +941,13 @@ function AgendaView(element, calendar, viewName) { var cell = hoverListener.stop(); clearOverlays(); if (cell) { - trigger('drop', _dragElement, realCellToDate(cell), getIsCellAllDay(cell), ev, ui); + trigger( + 'drop', + _dragElement, + realCellToDate(cell), + ev, + ui + ); } } diff --git a/src/agenda/AgendaWeekView.js b/src/agenda/AgendaWeekView.js index 3352cc5..4b101c8 100644 --- a/src/agenda/AgendaWeekView.js +++ b/src/agenda/AgendaWeekView.js @@ -1,52 +1,41 @@ fcViews.agendaWeek = AgendaWeekView; -function AgendaWeekView(element, calendar) { +function AgendaWeekView(element, calendar) { // TODO: do a WeekView mixin var t = this; // exports + t.incrementDate = incrementDate; t.render = render; // imports AgendaView.call(t, element, calendar, 'agendaWeek'); - var opt = t.opt; - var renderAgenda = t.renderAgenda; - var skipHiddenDays = t.skipHiddenDays; - var getCellsPerWeek = t.getCellsPerWeek; - var formatDates = calendar.formatDates; - - function render(date, delta) { - if (delta) { - addDays(date, delta * 7); - } - - var start = addDays(cloneDate(date), -((date.getDay() - opt('firstDay') + 7) % 7)); - var end = addDays(cloneDate(start), 7); - - var visStart = cloneDate(start); - skipHiddenDays(visStart); - - var visEnd = cloneDate(end); - skipHiddenDays(visEnd, -1, true); - - var colCnt = getCellsPerWeek(); - - t.title = formatDates( - visStart, - addDays(cloneDate(visEnd), -1), - opt('titleFormat') - ); - - t.start = start; - t.end = end; - t.visStart = visStart; - t.visEnd = visEnd; - - renderAgenda(colCnt); + function incrementDate(date, delta) { + return date.clone().stripTime().add('weeks', delta).startOf('week'); } + + function render(date) { + + t.intervalStart = date.clone().stripTime().startOf('week'); + t.intervalEnd = t.intervalStart.clone().add('weeks', 1); + + t.start = t.skipHiddenDays(t.intervalStart); + t.end = t.skipHiddenDays(t.intervalEnd, -1, true); + + t.title = calendar.formatRange( + t.start, + t.end.clone().subtract(1), // make inclusive by subtracting 1 ms + t.opt('titleFormat'), + ' \u2014 ' // emphasized dash + ); + + t.renderAgenda(t.getCellsPerWeek()); + } + + } diff --git a/src/agenda/agenda.css b/src/agenda/agenda.css index 3038357..fe85498 100644 --- a/src/agenda/agenda.css +++ b/src/agenda/agenda.css @@ -15,10 +15,13 @@ padding: 0 4px; vertical-align: middle; text-align: right; - white-space: nowrap; font-weight: normal; } +.fc-agenda-slots .fc-agenda-axis { + white-space: nowrap; + } + .fc-agenda .fc-week-number { font-weight: bold; } diff --git a/src/basic/BasicDayView.js b/src/basic/BasicDayView.js index 2c94d8c..00feab8 100644 --- a/src/basic/BasicDayView.js +++ b/src/basic/BasicDayView.js @@ -1,39 +1,34 @@ fcViews.basicDay = BasicDayView; - -function BasicDayView(element, calendar) { +function BasicDayView(element, calendar) { // TODO: make a DayView mixin var t = this; // exports + t.incrementDate = incrementDate; t.render = render; // imports BasicView.call(t, element, calendar, 'basicDay'); - var opt = t.opt; - var renderBasic = t.renderBasic; - var skipHiddenDays = t.skipHiddenDays; - var formatDate = calendar.formatDate; - - - function render(date, delta) { - if (delta) { - addDays(date, delta); - } - skipHiddenDays(date, delta < 0 ? -1 : 1); - var start = cloneDate(date, true); - var end = addDays(cloneDate(start), 1); + function incrementDate(date, delta) { + var out = date.clone().stripTime().add('days', delta); + out = t.skipHiddenDays(out, delta < 0 ? -1 : 1); + return out; + } - t.title = formatDate(date, opt('titleFormat')); - t.start = t.visStart = start; - t.end = t.visEnd = end; + function render(date) { - renderBasic(1, 1, false); + t.start = t.intervalStart = date.clone().stripTime(); + t.end = t.intervalEnd = t.start.clone().add('days', 1); + + t.title = calendar.formatDate(t.start, t.opt('titleFormat')); + + t.renderBasic(1, 1, false); } diff --git a/src/basic/BasicView.js b/src/basic/BasicView.js index c3ba0a3..204db3f 100644 --- a/src/basic/BasicView.js +++ b/src/basic/BasicView.js @@ -19,7 +19,6 @@ function BasicView(element, calendar, viewName) { t.reportDayClick = reportDayClick; // for selection (kinda hacky) t.dragStart = dragStart; t.dragStop = dragStop; - t.defaultEventEnd = defaultEventEnd; t.getHoverListener = function() { return hoverListener }; t.colLeft = colLeft; t.colRight = colRight; @@ -46,7 +45,9 @@ function BasicView(element, calendar, viewName) { var cellToDate = t.cellToDate; var dateToCell = t.dateToCell; var rangeToSegments = t.rangeToSegments; + var calendar = t.calendar; var formatDate = calendar.formatDate; + var calculateWeekNumber = calendar.calculateWeekNumber; // locals @@ -77,8 +78,6 @@ function BasicView(element, calendar, viewName) { var tm; var colFormat; var showWeekNumbers; - var weekNumberTitle; - var weekNumberFormat; @@ -106,16 +105,7 @@ function BasicView(element, calendar, viewName) { function updateOptions() { tm = opt('theme') ? 'ui' : 'fc'; 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"; - } } @@ -188,14 +178,14 @@ function BasicView(element, calendar, viewName) { if (showWeekNumbers) { html += "" + - htmlEscape(weekNumberTitle) + + htmlEscape(opt('weekNumberTitle')) + ""; } for (col=0; col" + + "" + htmlEscape(formatDate(date, colFormat)) + ""; } @@ -224,7 +214,7 @@ function BasicView(element, calendar, viewName) { html += "" + "
" + - htmlEscape(formatDate(date, weekNumberFormat)) + + htmlEscape(calculateWeekNumber(date)) + "
" + ""; } @@ -243,21 +233,21 @@ function BasicView(element, calendar, viewName) { } - function buildCellHTML(date) { - var contentClass = tm + "-widget-content"; - var month = t.start.getMonth(); - var today = clearTime(new Date()); + function buildCellHTML(date) { // date assumed to have stripped time + var month = t.intervalStart.month(); + var today = calendar.getNow().stripTime(); var html = ''; + var contentClass = tm + "-widget-content"; var classNames = [ 'fc-day', - 'fc-' + dayIDs[date.getDay()], + 'fc-' + dayIDs[date.day()], contentClass ]; - if (date.getMonth() != month) { + if (date.month() != month) { classNames.push('fc-other-month'); } - if (+date == +today) { + if (date.isSame(today, 'day')) { classNames.push( 'fc-today', tm + '-state-highlight' @@ -273,12 +263,12 @@ function BasicView(element, calendar, viewName) { html += "" + "
"; if (showNumbers) { - html += "
" + date.getDate() + "
"; + html += "
" + date.date() + "
"; } html += @@ -353,8 +343,8 @@ function BasicView(element, calendar, viewName) { function dayClick(ev) { if (!opt('selectable')) { // if selectable, SelectionManager will worry about dayClick - var date = parseISO8601($(this).data('date')); - trigger('dayClick', this, date, true, ev); + var date = calendar.moment($(this).data('date')); + trigger('dayClick', this, date, ev); } } @@ -398,13 +388,13 @@ function BasicView(element, calendar, viewName) { -----------------------------------------------------------------------*/ - function defaultSelectionEnd(startDate, allDay) { - return cloneDate(startDate); + function defaultSelectionEnd(start) { + return start.clone().stripTime().add('days', 1); } - function renderSelection(startDate, endDate, allDay) { - renderDayOverlay(startDate, addDays(cloneDate(endDate), 1), true); // rebuild every time??? + function renderSelection(start, end) { // end is exclusive + renderDayOverlay(start, end, true); // true = rebuild every time } @@ -413,10 +403,10 @@ function BasicView(element, calendar, viewName) { } - function reportDayClick(date, allDay, ev) { + function reportDayClick(date, ev) { var cell = dateToCell(date); var _element = bodyCells[cell.row*colCnt + cell.col]; - trigger('dayClick', _element, date, allDay, ev); + trigger('dayClick', _element, date, ev); } @@ -429,7 +419,9 @@ function BasicView(element, calendar, viewName) { hoverListener.start(function(cell) { clearOverlays(); if (cell) { - renderCellOverlay(cell.row, cell.col, cell.row, cell.col); + var d1 = cellToDate(cell); + var d2 = d1.clone().add(calendar.defaultAllDayEventDuration); + renderDayOverlay(d1, d2); } }, ev); } @@ -439,8 +431,13 @@ function BasicView(element, calendar, viewName) { var cell = hoverListener.stop(); clearOverlays(); if (cell) { - var d = cellToDate(cell); - trigger('drop', _dragElement, d, true, ev, ui); + trigger( + 'drop', + _dragElement, + cellToDate(cell), + ev, + ui + ); } } @@ -450,11 +447,6 @@ function BasicView(element, calendar, viewName) { --------------------------------------------------------*/ - function defaultEventEnd(event) { - return cloneDate(event.start); - } - - coordinateGrid = new CoordinateGrid(function(rows, cols) { var e, n, p; headCells.each(function(i, _e) { diff --git a/src/basic/BasicWeekView.js b/src/basic/BasicWeekView.js index 241be0c..39b3b84 100644 --- a/src/basic/BasicWeekView.js +++ b/src/basic/BasicWeekView.js @@ -1,52 +1,40 @@ fcViews.basicWeek = BasicWeekView; -function BasicWeekView(element, calendar) { +function BasicWeekView(element, calendar) { // TODO: do a WeekView mixin var t = this; // exports + t.incrementDate = incrementDate; t.render = render; // imports BasicView.call(t, element, calendar, 'basicWeek'); - var opt = t.opt; - var renderBasic = t.renderBasic; - var skipHiddenDays = t.skipHiddenDays; - var getCellsPerWeek = t.getCellsPerWeek; - var formatDates = calendar.formatDates; - - - function render(date, delta) { - if (delta) { - addDays(date, delta * 7); - } - var start = addDays(cloneDate(date), -((date.getDay() - opt('firstDay') + 7) % 7)); - var end = addDays(cloneDate(start), 7); + function incrementDate(date, delta) { + return date.clone().stripTime().add('weeks', delta).startOf('week'); + } - var visStart = cloneDate(start); - skipHiddenDays(visStart); - var visEnd = cloneDate(end); - skipHiddenDays(visEnd, -1, true); + function render(date) { - var colCnt = getCellsPerWeek(); + t.intervalStart = date.clone().stripTime().startOf('week'); + t.intervalEnd = t.intervalStart.clone().add('weeks', 1); - t.start = start; - t.end = end; - t.visStart = visStart; - t.visEnd = visEnd; + t.start = t.skipHiddenDays(t.intervalStart); + t.end = t.skipHiddenDays(t.intervalEnd, -1, true); - t.title = formatDates( - visStart, - addDays(cloneDate(visEnd), -1), - opt('titleFormat') + t.title = calendar.formatRange( + t.start, + t.end.clone().subtract(1), // make inclusive by subtracting 1 ms + t.opt('titleFormat'), + ' \u2014 ' // emphasized dash ); - renderBasic(1, colCnt, false); + t.renderBasic(1, t.getCellsPerWeek(), false); } diff --git a/src/basic/MonthView.js b/src/basic/MonthView.js index bad09b1..8502a67 100644 --- a/src/basic/MonthView.js +++ b/src/basic/MonthView.js @@ -6,56 +6,41 @@ function MonthView(element, calendar) { // exports + t.incrementDate = incrementDate; t.render = render; // imports BasicView.call(t, element, calendar, 'month'); - var opt = t.opt; - var renderBasic = t.renderBasic; - var skipHiddenDays = t.skipHiddenDays; - var getCellsPerWeek = t.getCellsPerWeek; - var formatDate = calendar.formatDate; - - - function render(date, delta) { - if (delta) { - addMonths(date, delta); - date.setDate(1); - } - var firstDay = opt('firstDay'); + function incrementDate(date, delta) { + return date.clone().stripTime().add('months', delta).startOf('month'); + } - var start = cloneDate(date, true); - start.setDate(1); - var end = addMonths(cloneDate(start), 1); + function render(date) { - var visStart = cloneDate(start); - addDays(visStart, -((visStart.getDay() - firstDay + 7) % 7)); - skipHiddenDays(visStart); + t.intervalStart = date.clone().stripTime().startOf('month'); + t.intervalEnd = t.intervalStart.clone().add('months', 1); - var visEnd = cloneDate(end); - addDays(visEnd, (7 - visEnd.getDay() + firstDay) % 7); - skipHiddenDays(visEnd, -1, true); + t.start = t.intervalStart.clone().startOf('week'); + t.start = t.skipHiddenDays(t.start); - var colCnt = getCellsPerWeek(); - var rowCnt = Math.round(dayDiff(visEnd, visStart) / 7); // should be no need for Math.round + t.end = t.intervalEnd.clone().add('days', (7 - t.intervalEnd.weekday()) % 7); + t.end = t.skipHiddenDays(t.end, -1, true); - if (opt('weekMode') == 'fixed') { - addDays(visEnd, (6 - rowCnt) * 7); // add weeks to make up for it + var rowCnt = Math.ceil( // need to ceil in case there are hidden days + t.end.diff(t.start, 'weeks', true) // returnfloat=true + ); + if (t.opt('weekMode') == 'fixed') { + t.end.add('weeks', 6 - rowCnt); rowCnt = 6; } - t.title = formatDate(start, opt('titleFormat')); + t.title = calendar.formatDate(t.intervalStart, t.opt('titleFormat')); - t.start = start; - t.end = end; - t.visStart = visStart; - t.visEnd = visEnd; - - renderBasic(rowCnt, colCnt, true); + t.renderBasic(rowCnt, t.getCellsPerWeek(), true); } diff --git a/src/common/DayEventRenderer.js b/src/common/DayEventRenderer.js index 2286a9e..c2f3ed0 100644 --- a/src/common/DayEventRenderer.js +++ b/src/common/DayEventRenderer.js @@ -14,7 +14,6 @@ function DayEventRenderer() { var trigger = t.trigger; var isEventDraggable = t.isEventDraggable; var isEventResizable = t.isEventResizable; - var eventEnd = t.eventEnd; var reportEventElement = t.reportEventElement; var eventElementHandlers = t.eventElementHandlers; var showEvents = t.showEvents; @@ -31,7 +30,6 @@ function DayEventRenderer() { var colContentRight = t.colContentRight; var dateToCell = t.dateToCell; var getDaySegmentContainer = t.getDaySegmentContainer; - var formatDates = t.calendar.formatDates; var renderDayOverlay = t.renderDayOverlay; var clearOverlays = t.clearOverlays; var clearSelection = t.clearSelection; @@ -42,6 +40,9 @@ function DayEventRenderer() { var cellOffsetToDayOffset = t.cellOffsetToDayOffset; var dateToDayOffset = t.dateToDayOffset; var dayOffsetToCellOffset = t.dayOffsetToCellOffset; + var calendar = t.calendar; + var getEventEnd = calendar.getEventEnd; + var formatDate = calendar.formatDate; // Render `events` onto the calendar, attach mouse event handlers, and call the `eventAfterRender` callback for each. @@ -181,9 +182,7 @@ function DayEventRenderer() { // A "segment" is the same data structure that View.rangeToSegments produces, // with the addition of the `event` property being set to reference the original event. function buildSegmentsForEvent(event) { - var startDate = event.start; - var endDate = exclEndDay(event); - var segments = rangeToSegments(startDate, endDate); + var segments = rangeToSegments(event.start, getEventEnd(event)); for (var i=0; i"; } @@ -285,7 +284,7 @@ function DayEventRenderer() { htmlEscape(event.title || '') + "" + "
"; - if (segment.isEnd && isEventResizable(event)) { + if (event.allDay && segment.isEnd && isEventResizable(event)) { html += "
" + "   " + // makes hit area a lot better for IE6/7 @@ -554,6 +553,7 @@ function DayEventRenderer() { } if ( + event.allDay && segment.isEnd && // only allow resizing on the final segment for an event isEventResizable(event) ) { @@ -565,10 +565,11 @@ function DayEventRenderer() { eventElementHandlers(event, eventElement); } - + function draggableDayEvent(event, eventElement) { var hoverListener = getHoverListener(); var dayDelta; + var eventStart; eventElement.draggable({ delay: 50, opacity: opt('dragOpacity'), @@ -580,14 +581,16 @@ function DayEventRenderer() { eventElement.draggable('option', 'revert', !cell || !rowDelta && !colDelta); clearOverlays(); if (cell) { - var origDate = cellToDate(origCell); - var date = cellToDate(cell); - dayDelta = dayDiff(date, origDate); + var origCellDate = cellToDate(origCell); + var cellDate = cellToDate(cell); + dayDelta = cellDate.diff(origCellDate, 'days'); + eventStart = event.start.clone().add('days', dayDelta); renderDayOverlay( - addDays(cloneDate(event.start), dayDelta), - addDays(exclEndDay(event), dayDelta) + eventStart, + getEventEnd(event).add('days', dayDelta) ); - }else{ + } + else { dayDelta = 0; } }, ev, 'drag'); @@ -597,8 +600,15 @@ function DayEventRenderer() { clearOverlays(); trigger('eventDragStop', eventElement, event, ev, ui); if (dayDelta) { - eventDrop(this, event, dayDelta, 0, event.allDay, ev, ui); - }else{ + eventDrop( + this, // el + event, + eventStart, + ev, + ui + ); + } + else { eventElement.css('filter', ''); // clear IE opacity side-effects showEvents(event, eventElement); } @@ -637,6 +647,7 @@ function DayEventRenderer() { var colCnt = getColCnt(); var elementTop = element.css('top'); var dayDelta; + var eventEnd; var helpers; var eventCopy = $.extend({}, event); var minCellOffset = dayOffsetToCellOffset( dateToDayOffset(event.start) ); @@ -658,18 +669,17 @@ function DayEventRenderer() { cellOffsetToDayOffset(cellOffset) - cellOffsetToDayOffset(origCellOffset); - if (dayDelta) { - eventCopy.end = addDays(eventEnd(event), dayDelta, true); - var oldHelpers = helpers; + eventEnd = getEventEnd(event).add('days', dayDelta); // assumed to already have a stripped time + if (dayDelta) { + eventCopy.end = eventEnd; + var oldHelpers = helpers; helpers = renderTempDayEvent(eventCopy, segment.row, elementTop); helpers = $(helpers); // turn array into a jQuery object - helpers.find('*').css('cursor', direction + '-resize'); if (oldHelpers) { oldHelpers.remove(); } - hideEvents(event); } else { @@ -679,10 +689,11 @@ function DayEventRenderer() { helpers = null; } } + clearOverlays(); renderDayOverlay( // coordinate grid already rebuilt with hoverListener.start() event.start, - addDays( exclEndDay(event), dayDelta ) + eventEnd // TODO: instead of calling renderDayOverlay() with dates, // call _renderDayOverlay (or whatever) with cell offsets. ); @@ -694,8 +705,14 @@ function DayEventRenderer() { $('body').css('cursor', ''); hoverListener.stop(); clearOverlays(); + if (dayDelta) { - eventResize(this, event, dayDelta, 0, ev); + eventResize( + this, // el + event, + eventEnd, + ev + ); // event redraw will clear helpers } // otherwise, the drag handler already restored the old events diff --git a/src/common/SelectionManager.js b/src/common/SelectionManager.js index d7b4e97..9b9716c 100644 --- a/src/common/SelectionManager.js +++ b/src/common/SelectionManager.js @@ -13,6 +13,7 @@ function SelectionManager() { // imports + var calendar = t.calendar; var opt = t.opt; var trigger = t.trigger; var defaultSelectionEnd = t.defaultSelectionEnd; @@ -39,13 +40,19 @@ function SelectionManager() { } - function select(startDate, endDate, allDay) { + function select(start, end) { unselect(); - if (!endDate) { - endDate = defaultSelectionEnd(startDate, allDay); + + start = calendar.moment(start); + if (end) { + end = calendar.moment(end); } - renderSelection(startDate, endDate, allDay); - reportSelection(startDate, endDate, allDay); + else { + end = defaultSelectionEnd(start); + } + + renderSelection(start, end); + reportSelection(start, end); } @@ -58,9 +65,9 @@ function SelectionManager() { } - function reportSelection(startDate, endDate, allDay, ev) { + function reportSelection(start, end, ev) { selected = true; - trigger('select', null, startDate, endDate, allDay, ev); + trigger('select', null, start, end, ev); } @@ -69,6 +76,7 @@ function SelectionManager() { var getIsCellAllDay = t.getIsCellAllDay; var hoverListener = t.getHoverListener(); var reportDayClick = t.reportDayClick; // this is hacky and sort of weird + if (ev.which == 1 && opt('selectable')) { // which==1 means left mouse button unselect(ev); var _mousedownElement = this; @@ -77,7 +85,10 @@ function SelectionManager() { clearSelection(); if (cell && getIsCellAllDay(cell)) { dates = [ cellToDate(origCell), cellToDate(cell) ].sort(dateCompare); - renderSelection(dates[0], dates[1], true); + renderSelection( + dates[0], + dates[1].clone().add('days', 1) // make exclusive + ); }else{ dates = null; } @@ -86,9 +97,13 @@ function SelectionManager() { hoverListener.stop(); if (dates) { if (+dates[0] == +dates[1]) { - reportDayClick(dates[0], true, ev); + reportDayClick(dates[0], ev); } - reportSelection(dates[0], dates[1], true, ev); + reportSelection( + dates[0], + dates[1].clone().add('days', 1), // make exclusive + ev + ); } }); } diff --git a/src/common/View.js b/src/common/View.js index c861acd..08a8dd3 100644 --- a/src/common/View.js +++ b/src/common/View.js @@ -14,7 +14,6 @@ function View(element, calendar, viewName) { t.isEventResizable = isEventResizable; t.setEventData = setEventData; t.clearEventData = clearEventData; - t.eventEnd = eventEnd; t.reportEventElement = reportEventElement; t.triggerEventDestroy = triggerEventDestroy; t.eventElementHandlers = eventElementHandlers; @@ -22,15 +21,14 @@ function View(element, calendar, viewName) { t.hideEvents = hideEvents; t.eventDrop = eventDrop; t.eventResize = eventResize; - // t.title - // t.start, t.end - // t.visStart, t.visEnd + t.mutateEvent = mutateEvent; + // t.start, t.end // moments with ambiguous-time + // t.intervalStart, t.intervalEnd // moments with ambiguous-time // imports - var defaultEventEnd = t.defaultEventEnd; - var normalizeEvent = calendar.normalizeEvent; // in EventManager var reportEventChange = calendar.reportEventChange; + var getEventEnd = calendar.getEventEnd; // locals @@ -38,12 +36,14 @@ function View(element, calendar, viewName) { var eventElementsByID = {}; // eventID mapped to array of jQuery elements var eventElementCouples = []; // array of objects, { event, element } // TODO: unify with segment system var options = calendar.options; + var nextDayThreshold = moment.duration(options.nextDayThreshold); + function opt(name, viewNameOverride) { var v = options[name]; - if ($.isPlainObject(v)) { + if ($.isPlainObject(v) && !isForcedAtomicOption(name)) { return smartProperty(v, viewNameOverride || viewName); } return v; @@ -72,8 +72,7 @@ function View(element, calendar, viewName) { event.editable, source.editable, opt('editable') - ) - && !opt('disableDragging'); // deprecated + ); } @@ -86,8 +85,7 @@ function View(element, calendar, viewName) { event.editable, source.editable, opt('editable') - ) - && !opt('disableResizing'); // deprecated + ); } @@ -117,12 +115,6 @@ function View(element, calendar, viewName) { } - // returns a Date object for an event's end - function eventEnd(event) { - return event.end ? cloneDate(event.end) : defaultEventEnd(event); - } - - /* Event Elements ------------------------------------------------------------------------------*/ @@ -194,83 +186,206 @@ function View(element, calendar, viewName) { /* Event Modification Reporting ---------------------------------------------------------------------------------*/ + - - function eventDrop(e, event, dayDelta, minuteDelta, allDay, ev, ui) { - var oldAllDay = event.allDay; - var eventId = event._id; - moveEvents(eventsByID[eventId], dayDelta, minuteDelta, allDay); + function eventDrop(el, event, newStart, ev, ui) { + var undoMutation = mutateEvent(event, newStart, null); + trigger( 'eventDrop', - e, + el, event, - dayDelta, - minuteDelta, - allDay, function() { - // TODO: investigate cases where this inverse technique might not work - moveEvents(eventsByID[eventId], -dayDelta, -minuteDelta, oldAllDay); - reportEventChange(eventId); + undoMutation(); + reportEventChange(event._id); }, ev, ui ); - reportEventChange(eventId); + + reportEventChange(event._id); } - - - function eventResize(e, event, dayDelta, minuteDelta, ev, ui) { - var eventId = event._id; - elongateEvents(eventsByID[eventId], dayDelta, minuteDelta); + + + function eventResize(el, event, newEnd, ev, ui) { + var undoMutation = mutateEvent(event, null, newEnd); + trigger( 'eventResize', - e, + el, event, - dayDelta, - minuteDelta, function() { - // TODO: investigate cases where this inverse technique might not work - elongateEvents(eventsByID[eventId], -dayDelta, -minuteDelta); - reportEventChange(eventId); + undoMutation(); + reportEventChange(event._id); }, ev, ui ); - reportEventChange(eventId); + + reportEventChange(event._id); } /* Event Modification Math - ---------------------------------------------------------------------------------*/ - - - function moveEvents(events, dayDelta, minuteDelta, allDay) { - minuteDelta = minuteDelta || 0; - for (var e, len=events.length, i=0; i