diff --git a/src/main/config/component/listeners.ent b/src/main/config/component/listeners.ent index d52fcdc..9406a41 100644 --- a/src/main/config/component/listeners.ent +++ b/src/main/config/component/listeners.ent @@ -177,6 +177,41 @@ The fully qualified Java class name for the listener + + dateRangeSelectedListener + + binding + org.richfaces.scheduleDateRangeChangedListener + The attribute takes a value-binding expression for a component property of + a backing bean + + + + org.richfaces.component.event.ScheduleDateRangeSelectedListener + + + org.richfaces.component.event.ScheduleListenerEventsProducer + + + org.richfaces.component.event.ScheduleDateRangeSelectedEvent + + + + org.richfaces.taglib.ScheduleDateRangeSelectedListenerTagHandler + + + + + org.richfaces.taglib.DateRangeSelectedListenerTag + + + + + type + java.lang.String + The fully qualified Java class name for the listener + + dateSelectedListener diff --git a/src/main/config/component/schedule.xml b/src/main/config/component/schedule.xml index 66b8c49..f8a3d12 100644 --- a/src/main/config/component/schedule.xml +++ b/src/main/config/component/schedule.xml @@ -364,6 +364,56 @@ null + selectable + java.lang.Boolean + + Allows a user to highlight multiple days or timeslots by clicking and dragging. + To let the user make selections by clicking and dragging, this option must be set to true. + default: false + + null + + + selectHelper + java.lang.Boolean + + Whether to draw a "placeholder" event while the user is dragging. + A value of true will draw a "placeholder" event while the user + is dragging (similar to what Google Calendar does for its week + and day views). A value of false (the default) will draw + the standard highlighting over each cell. + default: false + + null + + + unselectAuto + java.lang.Boolean + + Whether clicking elsewhere on the page will cause the current + selection to be cleared. + This option can only take effect when selectable is set to true. + default: true + + null + + + unselectCancel + java.lang.String + + A way to specify elements that will ignore the unselectAuto option. + Clicking on elements that match this jQuery selector will prevent + the current selection from being cleared (due to the unselectAuto option). + This option is useful if you have a "Create an event" form that + shows up in response to the user making a selection. When the user + clicks on this form, you probably don't want to the current selection + to go away. Thus, you should add a class to your form such as "my-form", + and set the unselectAuto option to ".my-form". + default: '' + + null + + disableDragging java.lang.Boolean @@ -491,6 +541,14 @@ null + onDateRangeSelected + java.lang.String + + + + null + + styleClass java.lang.String @@ -527,6 +585,11 @@ + dateRangeSelectedListener + javax.faces.el.MethodBinding + + + dateSelectedListener javax.faces.el.MethodBinding diff --git a/src/main/java/org/richfaces/component/UISchedule.java b/src/main/java/org/richfaces/component/UISchedule.java index 98f6904..19b5801 100644 --- a/src/main/java/org/richfaces/component/UISchedule.java +++ b/src/main/java/org/richfaces/component/UISchedule.java @@ -1,12 +1,10 @@ package org.richfaces.component; -import org.richfaces.component.event.ScheduleViewChangedEvent; -import org.richfaces.component.event.ScheduleDateRangeChangedEvent; -import org.richfaces.component.event.ScheduleItemResizeEvent; -import org.richfaces.component.event.ScheduleItemSelectedEvent; import org.ajax4jsf.context.AjaxContext; import org.ajax4jsf.model.DataVisitor; import org.ajax4jsf.model.ExtendedDataModel; +import org.richfaces.component.event.*; +import org.richfaces.component.model.DateRange; import javax.el.ELContext; import javax.el.ValueExpression; @@ -24,17 +22,6 @@ import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.*; -import org.richfaces.component.event.ScheduleDateRangeChangedListener; -import org.richfaces.component.event.ScheduleDateSelectedEvent; -import org.richfaces.component.event.ScheduleDateSelectedListener; -import org.richfaces.component.event.ScheduleItemSelectedListener; -import org.richfaces.component.event.ScheduleItemMoveEvent; -import org.richfaces.component.event.ScheduleItemMoveListener; -import org.richfaces.component.event.ScheduleItemResizeListener; -import org.richfaces.component.event.ScheduleListenerEventsProducer; -import org.richfaces.component.event.ScheduleViewChangedListener; -import org.richfaces.component.model.DateRange; - public abstract class UISchedule extends UIComponentBase implements ScheduleCommonViewAttributes, ScheduleListenerEventsProducer { public static final String COMPONENT_TYPE = "org.richfaces.Schedule"; @@ -147,6 +134,22 @@ public abstract class UISchedule extends UIComponentBase implements ScheduleComm public abstract void setEditable(Boolean editable); + public abstract Boolean getSelectable(); + + public abstract void setSelectable(Boolean selectable); + + public abstract Boolean getSelectHelper(); + + public abstract void setSelectHelper(Boolean selectHelper); + + public abstract Boolean getUnselectAuto(); + + public abstract void setUnselectAuto(Boolean unselectAuto); + + public abstract String getUnselectCancel(); + + public abstract void setUnselectCancel(String unselectCancel); + public abstract Boolean getDisableDragging(); public abstract void setDisableDragging(Boolean disableDragging); @@ -211,7 +214,12 @@ public abstract class UISchedule extends UIComponentBase implements ScheduleComm public abstract void setOnDateSelected(String onDaySelected); + public abstract String getOnDateRangeSelected(); + + public abstract void setOnDateRangeSelected(String onDateRangeSelected); + // TODO do we use MethodBinding or MethodExpression? + public abstract MethodBinding getItemMoveListener(); public abstract void setItemMoveListener(MethodBinding listener); @@ -232,6 +240,10 @@ public abstract class UISchedule extends UIComponentBase implements ScheduleComm public abstract void setDateRangeChangedListener(MethodBinding listener); + public abstract MethodBinding getDateRangeSelectedListener(); + + public abstract void setDateRangeSelectedListener(MethodBinding listener); + public abstract MethodBinding getDateSelectedListener(); public abstract void setDateSelectedListener(MethodBinding listener); @@ -295,6 +307,13 @@ public abstract class UISchedule extends UIComponentBase implements ScheduleComm if (expression != null) { expression.invoke(facesContext, new Object[]{event}); } + } else if (event instanceof ScheduleDateRangeSelectedEvent) { + super.broadcast(event); + FacesContext facesContext = getFacesContext(); + MethodBinding expression = getDateRangeSelectedListener(); + if (expression != null) { + expression.invoke(facesContext, new Object[]{event}); + } } } @@ -316,7 +335,7 @@ public abstract class UISchedule extends UIComponentBase implements ScheduleComm DataModel dataModel = getDataModel(); if (dataModel instanceof ExtendedDataModel) { DataVisitor visitor = new DataVisitor() { - //TODO Is this fine? or should we stack rowKeys and not use dataModel later on. I'don't know business rules of extendedDataModel, just used it to did once pagination with underlying EntityQuery from Seam + //TODO Is this fine? or should we stack rowKeys and not use dataModel later on. I'don't know business rules of extendedDataModel, just used it once to do pagination with underlying EntityQuery from Seam public void process(FacesContext context, Object rowKey, Object argument) throws IOException { } @@ -595,6 +614,18 @@ public abstract class UISchedule extends UIComponentBase implements ScheduleComm return (ScheduleDateRangeChangedListener[]) getFacesListeners(ScheduleDateRangeChangedListener.class); } + public void addDateRangeSelectedListener(ScheduleDateRangeSelectedListener listener) { + addFacesListener(listener); + } + + public void removeDateRangeSelectedListener(ScheduleDateRangeSelectedListener listener) { + removeFacesListener(listener); + } + + public ScheduleDateRangeSelectedListener[] getDateRangeSelectedListeners() { + return (ScheduleDateRangeSelectedListener[]) getFacesListeners(ScheduleDateRangeSelectedListener.class); + } + public void addDateSelectedListener(ScheduleDateSelectedListener listener) { addFacesListener(listener); } diff --git a/src/main/java/org/richfaces/component/event/ScheduleDateRangeSelectedEvent.java b/src/main/java/org/richfaces/component/event/ScheduleDateRangeSelectedEvent.java new file mode 100644 index 0000000..b0a4094 --- /dev/null +++ b/src/main/java/org/richfaces/component/event/ScheduleDateRangeSelectedEvent.java @@ -0,0 +1,45 @@ +package org.richfaces.component.event; + +import javax.faces.component.UIComponent; +import javax.faces.event.FacesEvent; +import javax.faces.event.FacesListener; +import java.util.Date; + +public class ScheduleDateRangeSelectedEvent extends FacesEvent { + + private Date startDate; + private Date endDate; + private boolean allDay; + + public ScheduleDateRangeSelectedEvent(UIComponent component, Date startDate, Date endDate, boolean allDay) { + super(component); + this.startDate = startDate; + this.endDate = endDate; + this.allDay = allDay; + } + + public boolean isAppropriateListener(FacesListener facesListener) { + return facesListener instanceof ScheduleDateSelectedListener; + } + + public void processListener(FacesListener facesListener) { + ((ScheduleDateRangeSelectedListener) facesListener).dateRangeSelected(this); + } + + public Date getStartDate() { + return startDate; + } + + public Date getEndDate() { + return endDate; + } + + public boolean isAllDay() { + return allDay; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "[startDate=" + startDate + ";endDate=" + endDate + ";allDay=" + allDay + "]"; + } +} diff --git a/src/main/java/org/richfaces/component/event/ScheduleDateRangeSelectedListener.java b/src/main/java/org/richfaces/component/event/ScheduleDateRangeSelectedListener.java new file mode 100644 index 0000000..2615d90 --- /dev/null +++ b/src/main/java/org/richfaces/component/event/ScheduleDateRangeSelectedListener.java @@ -0,0 +1,8 @@ +package org.richfaces.component.event; + +import javax.faces.event.FacesListener; + +public interface ScheduleDateRangeSelectedListener extends FacesListener { + + void dateRangeSelected(ScheduleDateRangeSelectedEvent event); +} diff --git a/src/main/java/org/richfaces/component/event/ScheduleListenerEventsProducer.java b/src/main/java/org/richfaces/component/event/ScheduleListenerEventsProducer.java index 0b1b43d..32443bb 100644 --- a/src/main/java/org/richfaces/component/event/ScheduleListenerEventsProducer.java +++ b/src/main/java/org/richfaces/component/event/ScheduleListenerEventsProducer.java @@ -3,26 +3,44 @@ package org.richfaces.component.event; public interface ScheduleListenerEventsProducer { void addItemSelectedListener(ScheduleItemSelectedListener listener); + void removeItemSelectedListener(ScheduleItemSelectedListener listener); + ScheduleItemSelectedListener[] getItemSelectedListeners(); void addItemMoveListener(ScheduleItemMoveListener listener); + void removeItemMoveListener(ScheduleItemMoveListener listener); + ScheduleItemMoveListener[] getItemMoveListeners(); void addItemResizeListener(ScheduleItemResizeListener listener); + void removeItemResizeListener(ScheduleItemResizeListener listener); + ScheduleItemResizeListener[] getItemResizeListeners(); void addViewChangedListener(ScheduleViewChangedListener listener); + void removeViewChangedListener(ScheduleViewChangedListener listener); + ScheduleViewChangedListener[] getViewChangedListeners(); void addDateRangeChangedListener(ScheduleDateRangeChangedListener listener); + void removeDateRangeChangedListener(ScheduleDateRangeChangedListener listener); + ScheduleDateRangeChangedListener[] getDateRangeChangedListeners(); + void addDateRangeSelectedListener(ScheduleDateRangeSelectedListener listener); + + void removeDateRangeSelectedListener(ScheduleDateRangeSelectedListener listener); + + ScheduleDateRangeSelectedListener[] getDateRangeSelectedListeners(); + void addDateSelectedListener(ScheduleDateSelectedListener listener); + void removeDateSelectedListener(ScheduleDateSelectedListener listener); + ScheduleDateSelectedListener[] getDateSelectedListeners(); } diff --git a/src/main/java/org/richfaces/renderkit/ScheduleRendererBase.java b/src/main/java/org/richfaces/renderkit/ScheduleRendererBase.java index 6663043..11e4b87 100644 --- a/src/main/java/org/richfaces/renderkit/ScheduleRendererBase.java +++ b/src/main/java/org/richfaces/renderkit/ScheduleRendererBase.java @@ -1,10 +1,5 @@ package org.richfaces.renderkit; -import java.util.Locale; -import org.richfaces.component.event.ScheduleViewChangedEvent; -import org.richfaces.component.event.ScheduleDateRangeChangedEvent; -import org.richfaces.component.event.ScheduleItemResizeEvent; -import org.richfaces.component.event.ScheduleItemSelectedEvent; import org.ajax4jsf.javascript.JSFunction; import org.ajax4jsf.javascript.JSFunctionDefinition; import org.ajax4jsf.javascript.JSObject; @@ -12,21 +7,15 @@ import org.ajax4jsf.javascript.JSReference; import org.ajax4jsf.renderkit.AjaxComponentRendererBase; import org.ajax4jsf.renderkit.AjaxRendererUtils; import org.richfaces.component.*; +import org.richfaces.component.event.*; +import javax.faces.FacesException; import javax.faces.component.NamingContainer; import javax.faces.component.UIComponent; import javax.faces.context.FacesContext; import javax.faces.context.ResponseWriter; import java.io.IOException; -import java.util.Calendar; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; - -import javax.faces.FacesException; - -import org.richfaces.component.event.ScheduleDateSelectedEvent; -import org.richfaces.component.event.ScheduleItemMoveEvent; +import java.util.*; public abstract class ScheduleRendererBase extends AjaxComponentRendererBase { @@ -36,6 +25,7 @@ public abstract class ScheduleRendererBase extends AjaxComponentRendererBase { public static final String DATE_RANGE_CHANGED_EVENT = "dateRangeChanged"; public static final String VIEW_CHANGED_EVENT = "viewChanged"; public static final String DATE_SELECTED_EVENT = "dateSelected"; + public static final String DATE_RANGE_SELECTED_EVENT = "dateRangeSelected"; public static final String AJAX_MODE = "ajax"; public static final String SERVER_MODE = "server"; public static final String CLIENT_MODE = "client"; @@ -50,7 +40,7 @@ public abstract class ScheduleRendererBase extends AjaxComponentRendererBase { private static final String VIEW_PARAM = "view"; protected Object createSubmitEventFunction(FacesContext context, UISchedule component) { - JSFunction jsFunction = null; + JSFunction jsFunction; Map params = new HashMap(); params.put(getFieldId(context, component, START_DATE_PARAM), new JSReference(START_DATE_PARAM)); params.put(getFieldId(context, component, END_DATE_PARAM), new JSReference(END_DATE_PARAM)); @@ -115,6 +105,11 @@ public abstract class ScheduleRendererBase extends AjaxComponentRendererBase { Date startDate = new Date(Long.parseLong(startDateParam) * 1000); boolean allDay = Boolean.parseBoolean(allDayParam); new ScheduleDateSelectedEvent(component, startDate, allDay).queue(); + } else if (DATE_RANGE_SELECTED_EVENT.equals(eventTypeParam)) { + Date startDate = new Date(Long.parseLong(startDateParam) * 1000); + Date endDate = new Date(Long.parseLong(endDateParam) * 1000); + boolean allDay = Boolean.parseBoolean(allDayParam); + new ScheduleDateRangeSelectedEvent(component, startDate, endDate, allDay).queue(); } } catch (NumberFormatException ex) { throw new FacesException("Cannot convert request parmeters", ex); @@ -126,7 +121,7 @@ public abstract class ScheduleRendererBase extends AjaxComponentRendererBase { ResponseWriter writer = context.getResponseWriter(); String clientId = component.getClientId(context); Locale locale = context.getViewRoot().getLocale(); - writer.writeText(new JSObject("RichFaces.Schedule", clientId, locale.getLanguage(), + writer.writeText(new JSObject("RichFaces.Schedule", clientId, locale.toString(), getOptions(component), DATE_RANGE_CHANGED_EVENT, ITEM_SELECTED_EVENT, @@ -134,6 +129,7 @@ public abstract class ScheduleRendererBase extends AjaxComponentRendererBase { ITEM_RESIZE_EVENT, VIEW_CHANGED_EVENT, DATE_SELECTED_EVENT, + DATE_RANGE_SELECTED_EVENT, createSubmitEventFunction(context, component)).toScript(), null); } @@ -190,6 +186,10 @@ public abstract class ScheduleRendererBase extends AjaxComponentRendererBase { addOptionIfSet("minTime", schedule.getMinTime(), options); addOptionIfSet("maxTime", schedule.getMaxTime(), options); addOptionIfSet("editable", schedule.getEditable(), options); + addOptionIfSet("selectable", schedule.getSelectable(), options); + addOptionIfSet("selectHelper", schedule.getSelectHelper(), options); + addOptionIfSet("unselectAuto", schedule.getUnselectAuto(), options); + addOptionIfSet("unselectCancel", schedule.getUnselectCancel(), options); addOptionIfSet("disableDragging", schedule.getDisableDragging(), options); addOptionIfSet("disableResizing", schedule.getDisableResizing(), options); addOptionIfSet("dragRevertDuration", schedule.getDragRevertDuration(), options); @@ -216,6 +216,7 @@ public abstract class ScheduleRendererBase extends AjaxComponentRendererBase { addOptionIfSet("onItemMouseout", schedule.getOnItemMouseout(), options); addOptionIfSet("onViewDisplay", schedule.getOnViewDisplay(), options); addOptionIfSet("onDateSelected", schedule.getOnDateSelected(), options); + addOptionIfSet("onDateRangeSelected", schedule.getOnDateRangeSelected(), options); if (schedule.getDate() != null) { Calendar calendar = Calendar.getInstance(); calendar.setTime(schedule.getDate()); diff --git a/src/main/java/org/richfaces/renderkit/html/scripts/ScheduleMessages.java b/src/main/java/org/richfaces/renderkit/html/scripts/ScheduleMessages.java index 7719455..6beb3ac 100644 --- a/src/main/java/org/richfaces/renderkit/html/scripts/ScheduleMessages.java +++ b/src/main/java/org/richfaces/renderkit/html/scripts/ScheduleMessages.java @@ -1,49 +1,128 @@ package org.richfaces.renderkit.html.scripts; +import org.ajax4jsf.resource.ClientScript; +import org.ajax4jsf.resource.ResourceContext; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import javax.faces.application.Application; +import javax.faces.context.FacesContext; import java.io.ByteArrayInputStream; import java.io.InputStream; +import java.io.UnsupportedEncodingException; import java.util.Locale; import java.util.MissingResourceException; import java.util.ResourceBundle; -import javax.faces.FactoryFinder; -import javax.faces.context.ExternalContext; -import javax.faces.context.FacesContext; -import javax.faces.context.FacesContextFactory; -import org.ajax4jsf.resource.InternetResourceBase; -import org.ajax4jsf.resource.ResourceContext; -public class ScheduleMessages extends InternetResourceBase { +public class ScheduleMessages extends ClientScript { + + public static final String BUNDLE_NAME = "org.richfaces.component.UIScheduleMessages"; + private static final String MESSAGE_KEY_BASE = "org.richfaces.component.UISchedule."; + private static final Log log = LogFactory.getLog(ClientScript.class); +// private Locale recentLocale; @Override public InputStream getResourceAsStream(ResourceContext context) { //TODO how to access messages from here if user keeps them somewhere else? - String language = "pl"; - Locale locale = new Locale(language); ClassLoader loader = Thread.currentThread().getContextClassLoader(); -//TODO load default locale if specific is null - ResourceBundle bundle = ResourceBundle.getBundle("org.richfaces.component.scheduleMessages", locale, loader); + FacesContext facesContext = FacesContext.getCurrentInstance(); + Application application = facesContext.getApplication(); + Locale locale = application.getViewHandler().calculateLocale(facesContext); +// recentLocale = locale; + ResourceBundle applicationBundle = ResourceBundle.getBundle(application.getMessageBundle(), locale, loader); + ResourceBundle stockBundle = ResourceBundle.getBundle(BUNDLE_NAME, locale, loader); + String[] months = new String[]{"JANUARY", "FEBRUARY", "MARCH", "APRIL", "MAY", "JUNE", "JULY", "AUGUST", "SEPTEMBER", "OCTOBER", "NOVEMBER", "DECEMBER"}; + String[] days = new String[]{"SUNDAY", "MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY"}; StringBuilder out = new StringBuilder(); - out.append("RichFaces.Schedule.prototype.messages=jQuery.extend(RichFaces.Schedule.prototype.messages,{").append("'").append(locale).append("':{").append("monthNames:["); - String[] months = new String[]{"JANUARY","FEBRUARY"}; - for (int i = 0; i < months.length; i++) { - out.append("'"); -//TODO Handle MissingResourceException -//TODO Escape message strings - try { - out.append(escape(bundle.getString("org.richfaces.component.UISchedule.monthNames." + months[i]))); - } catch (MissingResourceException e) { - out.append(escape(months[i])); + out.append("RichFaces.Schedule.prototype.messages=jQuery.extend(RichFaces.Schedule.prototype.messages,{").append("'").append(locale.toString()).append("':{"); + out.append("allDayText:'").append(escape(getMessageFromBundle(MESSAGE_KEY_BASE + "allDay", applicationBundle, stockBundle))).append("',"); + appendArray(out, applicationBundle, stockBundle, "monthNames", "monthNames", months); + out.append(","); + appendArray(out, applicationBundle, stockBundle, "monthNamesShort", "monthNamesShort", months); + out.append(","); + appendArray(out, applicationBundle, stockBundle, "dayNames", "dayNames", days); + out.append(","); + appendArray(out, applicationBundle, stockBundle, "dayNamesShort", "dayNamesShort", days); + out.append(","); + appendMap(out, applicationBundle, stockBundle, "buttonText", "buttonTexts", new String[]{"prev", "next", "prevYear", "nextYear", "today", "month", "day", "week"}); + out.append("}").append("})"); + try { +// TODO where to get encoding from? It should match properties file's encoding, but probably be converted to response encoding + return new ByteArrayInputStream(out.toString().getBytes(application.getViewHandler().calculateCharacterEncoding(facesContext))); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + private void appendArray(StringBuilder out, ResourceBundle applicationBundle, ResourceBundle stockBundle, String jsPropertyName, String prefix, String[] keys) { + String key; + out.append(jsPropertyName).append(":["); + for (int i = 0; i < keys.length; i++) { + key = MESSAGE_KEY_BASE + prefix + "." + keys[i]; + out.append("'").append(escape(getMessageFromBundle(key, applicationBundle, stockBundle))).append("'"); + if (i + 1 < keys.length) { + out.append(","); } - out.append("'"); - if (i + 1 < months.length) { + } + out.append("]"); + } + + private void appendMap(StringBuilder out, ResourceBundle applicationBundle, ResourceBundle stockBundle, String jsPropertyName, String prefix, String[] keys) { + String key; + out.append(jsPropertyName).append(":{"); + for (int i = 0; i < keys.length; i++) { + key = MESSAGE_KEY_BASE + prefix + "." + keys[i]; + out.append(keys[i]).append(":").append("'").append(escape(getMessageFromBundle(key, applicationBundle, stockBundle))).append("'"); + if (i + 1 < keys.length) { out.append(","); } } - out.append("]").append("}").append("})"); - return new ByteArrayInputStream(out.toString().getBytes()); + out.append("}"); + } + + private String getMessageFromBundle(String key, ResourceBundle applicationBundle, ResourceBundle stockBundle) { + try { + return applicationBundle.getString(key); + } catch (MissingResourceException e) { + try { + return stockBundle.getString(key); + } catch (MissingResourceException e1) { + if (log.isWarnEnabled()) { + log.warn("Cannot find resource " + e1.getKey() + " in bundle " + e1.getClassName()); + } + return ""; + } + } } private String escape(String message) { - return message.replaceAll("'", "\\'"); + return message.replaceAll("'", "\\\\'"); + } + + @Override + public String getJavaScript() { + return getClass().getCanonicalName(); + } + + @Override + public boolean requireFacesContext() { + return true; } +//TODO if locale changes then recreate this resource +// @Override +// public boolean isCacheable(ResourceContext resourceContext) { +// if (resourceContext instanceof FacesResourceContext) { +// FacesContext facesContext = ((FacesResourceContext) resourceContext).getFacesContext(); +// Locale locale = facesContext.getApplication().getViewHandler().calculateLocale(facesContext); + + // if (locale != null && !locale.equals(recentLocale)) { +// return false; +// } +// } +// return super.isCacheable(resourceContext); +// } + public boolean isCacheable(ResourceContext resourceContext) { + return false; + } + } diff --git a/src/main/resources/org/richfaces/component/UIScheduleMessages_en.properties b/src/main/resources/org/richfaces/component/UIScheduleMessages_en.properties new file mode 100644 index 0000000..8a85c05 --- /dev/null +++ b/src/main/resources/org/richfaces/component/UIScheduleMessages_en.properties @@ -0,0 +1,47 @@ +org.richfaces.component.UISchedule.allDay=All day +org.richfaces.component.UISchedule.monthNames.JANUARY=January +org.richfaces.component.UISchedule.monthNames.FEBRUARY=Fabruary +org.richfaces.component.UISchedule.monthNames.MARCH=March +org.richfaces.component.UISchedule.monthNames.APRIL=April +org.richfaces.component.UISchedule.monthNames.MAY=May +org.richfaces.component.UISchedule.monthNames.JUNE=June +org.richfaces.component.UISchedule.monthNames.JULY=July +org.richfaces.component.UISchedule.monthNames.AUGUST=August +org.richfaces.component.UISchedule.monthNames.SEPTEMBER=September +org.richfaces.component.UISchedule.monthNames.OCTOBER=October +org.richfaces.component.UISchedule.monthNames.NOVEMBER=November +org.richfaces.component.UISchedule.monthNames.DECEMBER=December +org.richfaces.component.UISchedule.monthNamesShort.JANUARY=Jan +org.richfaces.component.UISchedule.monthNamesShort.FEBRUARY=Feb +org.richfaces.component.UISchedule.monthNamesShort.MARCH=Mar +org.richfaces.component.UISchedule.monthNamesShort.APRIL=Apr +org.richfaces.component.UISchedule.monthNamesShort.MAY=May +org.richfaces.component.UISchedule.monthNamesShort.JUNE=Jun +org.richfaces.component.UISchedule.monthNamesShort.JULY=Jul +org.richfaces.component.UISchedule.monthNamesShort.AUGUST=Aug +org.richfaces.component.UISchedule.monthNamesShort.SEPTEMBER=Sep +org.richfaces.component.UISchedule.monthNamesShort.OCTOBER=Oct +org.richfaces.component.UISchedule.monthNamesShort.NOVEMBER=Nov +org.richfaces.component.UISchedule.monthNamesShort.DECEMBER=Dec +org.richfaces.component.UISchedule.dayNames.SUNDAY=Sunday +org.richfaces.component.UISchedule.dayNames.MONDAY=Monday +org.richfaces.component.UISchedule.dayNames.TUESDAY=Tuesday +org.richfaces.component.UISchedule.dayNames.WEDNESDAY=Wednesday +org.richfaces.component.UISchedule.dayNames.THURSDAY=Thursday +org.richfaces.component.UISchedule.dayNames.FRIDAY=Friday +org.richfaces.component.UISchedule.dayNames.SATURDAY=Saturday +org.richfaces.component.UISchedule.dayNamesShort.SUNDAY=Sun +org.richfaces.component.UISchedule.dayNamesShort.MONDAY=Mon +org.richfaces.component.UISchedule.dayNamesShort.TUESDAY=Tue +org.richfaces.component.UISchedule.dayNamesShort.WEDNESDAY=Wed +org.richfaces.component.UISchedule.dayNamesShort.THURSDAY=Thu +org.richfaces.component.UISchedule.dayNamesShort.FRIDAY=Fri +org.richfaces.component.UISchedule.dayNamesShort.SATURDAY=Sat +org.richfaces.component.UISchedule.buttonTexts.prev= ◄  +org.richfaces.component.UISchedule.buttonTexts.next= ►  +org.richfaces.component.UISchedule.buttonTexts.prevYear= <<  +org.richfaces.component.UISchedule.buttonTexts.nextYear= >>  +org.richfaces.component.UISchedule.buttonTexts.today=today +org.richfaces.component.UISchedule.buttonTexts.month=month +org.richfaces.component.UISchedule.buttonTexts.week=week +org.richfaces.component.UISchedule.buttonTexts.day=day \ No newline at end of file diff --git a/src/main/resources/org/richfaces/component/UIScheduleMessages_pl.properties b/src/main/resources/org/richfaces/component/UIScheduleMessages_pl.properties new file mode 100644 index 0000000..361ca72 --- /dev/null +++ b/src/main/resources/org/richfaces/component/UIScheduleMessages_pl.properties @@ -0,0 +1,47 @@ +org.richfaces.component.UISchedule.allDay=Ca\u0142y dzie\u0144 +org.richfaces.component.UISchedule.monthNames.JANUARY=Stycze\u0144 +org.richfaces.component.UISchedule.monthNames.FEBRUARY=Luty +org.richfaces.component.UISchedule.monthNames.MARCH=Marzec +org.richfaces.component.UISchedule.monthNames.APRIL=Kwiecie\u0144 +org.richfaces.component.UISchedule.monthNames.MAY=Maj +org.richfaces.component.UISchedule.monthNames.JUNE=Czerwiec +org.richfaces.component.UISchedule.monthNames.JULY=Lipiec +org.richfaces.component.UISchedule.monthNames.AUGUST=Sierpie\u0144 +org.richfaces.component.UISchedule.monthNames.SEPTEMBER=Wrzesie\u0144 +org.richfaces.component.UISchedule.monthNames.OCTOBER=Pa\u017Adziernik +org.richfaces.component.UISchedule.monthNames.NOVEMBER=Listopad +org.richfaces.component.UISchedule.monthNames.DECEMBER=Grudzie\u0144 +org.richfaces.component.UISchedule.monthNamesShort.JANUARY=Sty +org.richfaces.component.UISchedule.monthNamesShort.FEBRUARY=Lut +org.richfaces.component.UISchedule.monthNamesShort.MARCH=Mar +org.richfaces.component.UISchedule.monthNamesShort.APRIL=Kwi +org.richfaces.component.UISchedule.monthNamesShort.MAY=Maj +org.richfaces.component.UISchedule.monthNamesShort.JUNE=Cze +org.richfaces.component.UISchedule.monthNamesShort.JULY=Lip +org.richfaces.component.UISchedule.monthNamesShort.AUGUST=Sie +org.richfaces.component.UISchedule.monthNamesShort.SEPTEMBER=Wrz +org.richfaces.component.UISchedule.monthNamesShort.OCTOBER=Pa\u017A +org.richfaces.component.UISchedule.monthNamesShort.NOVEMBER=Lis +org.richfaces.component.UISchedule.monthNamesShort.DECEMBER=Gru +org.richfaces.component.UISchedule.dayNames.SUNDAY=Niedziela +org.richfaces.component.UISchedule.dayNames.MONDAY=Poniedzia\u0142ek +org.richfaces.component.UISchedule.dayNames.TUESDAY=Wtorek +org.richfaces.component.UISchedule.dayNames.WEDNESDAY=\u015Aroda +org.richfaces.component.UISchedule.dayNames.THURSDAY=Czwartek +org.richfaces.component.UISchedule.dayNames.FRIDAY=Pi\u0105tek +org.richfaces.component.UISchedule.dayNames.SATURDAY=Sobota +org.richfaces.component.UISchedule.dayNamesShort.SUNDAY=Nie +org.richfaces.component.UISchedule.dayNamesShort.MONDAY=Pon +org.richfaces.component.UISchedule.dayNamesShort.TUESDAY=Wto +org.richfaces.component.UISchedule.dayNamesShort.WEDNESDAY=\u015Aro +org.richfaces.component.UISchedule.dayNamesShort.THURSDAY=Czw +org.richfaces.component.UISchedule.dayNamesShort.FRIDAY=Pi\u0105 +org.richfaces.component.UISchedule.dayNamesShort.SATURDAY=Sob +org.richfaces.component.UISchedule.buttonTexts.prev= ◄  +org.richfaces.component.UISchedule.buttonTexts.next= ►  +org.richfaces.component.UISchedule.buttonTexts.prevYear= <<  +org.richfaces.component.UISchedule.buttonTexts.nextYear= >>  +org.richfaces.component.UISchedule.buttonTexts.today=Dzi\u015B +org.richfaces.component.UISchedule.buttonTexts.month=Miesi\u0105c +org.richfaces.component.UISchedule.buttonTexts.week=Tydzie\u0144 +org.richfaces.component.UISchedule.buttonTexts.day=Dzie\u0144 \ No newline at end of file diff --git a/src/main/resources/org/richfaces/component/scheduleMessages.properties b/src/main/resources/org/richfaces/component/scheduleMessages.properties deleted file mode 100644 index 47bed51..0000000 --- a/src/main/resources/org/richfaces/component/scheduleMessages.properties +++ /dev/null @@ -1 +0,0 @@ -org.richfaces.component.UISchedule.monthNames.JANUARY=January \ No newline at end of file diff --git a/src/main/resources/org/richfaces/renderkit/html/scripts/fullcalendar.js b/src/main/resources/org/richfaces/renderkit/html/scripts/fullcalendar.js index 644f4ad..8cab6d8 100644 --- a/src/main/resources/org/richfaces/renderkit/html/scripts/fullcalendar.js +++ b/src/main/resources/org/richfaces/renderkit/html/scripts/fullcalendar.js @@ -1,5 +1,6 @@ -/*! - * FullCalendar v1.4.5 +/** + * @preserve + * FullCalendar v1.4.6 * http://arshaw.com/fullcalendar/ * * Use fullcalendar.css for basic styling. @@ -11,3408 +12,3944 @@ * http://www.opensource.org/licenses/mit-license.php * http://www.gnu.org/licenses/gpl.html * - * Date: Sun Feb 21 20:30:11 2010 -0800 + * Date: Mon May 31 10:18:29 2010 -0700 * */ (function($) { -var fc = $.fullCalendar = {}; -var views = fc.views = {}; - - -/* Defaults ------------------------------------------------------------------------------*/ - -var defaults = { - - // display - defaultView: 'month', - aspectRatio: 1.35, - header: { - left: 'title', - center: '', - right: 'today prev,next' - }, - weekends: true, - - // editing - //editable: false, - //disableDragging: false, - //disableResizing: false, - - allDayDefault: true, - - // event ajax - lazyFetching: true, - startParam: 'start', - endParam: 'end', - - // time formats - titleFormat: { - month: 'MMMM yyyy', - week: "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", - day: 'dddd, MMM d, yyyy' - }, - columnFormat: { - month: 'ddd', - week: 'ddd M/d', - day: 'dddd M/d' - }, - timeFormat: { // for event elements - '': 'h(:mm)t' // default - }, - - // locale - isRTL: false, - firstDay: 0, - monthNames: ['January','February','March','April','May','June','July','August','September','October','November','December'], - monthNamesShort: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'], - dayNames: ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'], - dayNamesShort: ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'], - buttonText: { - prev: ' ◄ ', - next: ' ► ', - prevYear: ' << ', - nextYear: ' >> ', - today: 'today', - month: 'month', - week: 'week', - day: 'day' - }, - - // jquery-ui theming - theme: false, - buttonIcons: { - prev: 'circle-triangle-w', - next: 'circle-triangle-e' - } - -}; - -// right-to-left defaults -var rtlDefaults = { - header: { - left: 'next,prev today', - center: '', - right: 'title' - }, - buttonText: { - prev: ' ► ', - next: ' ◄ ', - prevYear: ' >> ', - nextYear: ' << ' - }, - buttonIcons: { - prev: 'circle-triangle-e', - next: 'circle-triangle-w' - } -}; - -// function for adding/overriding defaults -var setDefaults = fc.setDefaults = function(d) { - $.extend(true, defaults, d); -} - - - -/* .fullCalendar jQuery function ------------------------------------------------------------------------------*/ - -$.fn.fullCalendar = function(options) { - - // method calling - if (typeof options == 'string') { - var args = Array.prototype.slice.call(arguments, 1), - res; - this.each(function() { - var data = $.data(this, 'fullCalendar'); - if (data) { - var r = data[options].apply(this, args); - if (res == undefined) { - res = r; - } - } - }); - if (res != undefined) { - return res; - } - return this; - } - - // pluck the 'events' and 'eventSources' options - var eventSources = options.eventSources || []; - delete options.eventSources; - if (options.events) { - eventSources.push(options.events); - delete options.events; - } - - // first event source reserved for 'sticky' events - eventSources.unshift([]); - - // initialize options - options = $.extend(true, {}, - defaults, - (options.isRTL || options.isRTL==undefined && defaults.isRTL) ? rtlDefaults : {}, - options - ); - var tm = options.theme ? 'ui' : 'fc'; // for making theme classes - - - this.each(function() { - - - /* Instance Initialization - -----------------------------------------------------------------------------*/ - - // element - var _element = this, - element = $(_element).addClass('fc'), - elementOuterWidth, - content = $("
").prependTo(_element), - suggestedViewHeight, - resizeUID = 0, - ignoreWindowResize = 0, - date = new Date(), - viewName, // the current view name (TODO: look into getting rid of) - view, // the current view - viewInstances = {}, - absoluteViewElement; - - - - if (options.isRTL) { - element.addClass('fc-rtl'); - } - if (options.theme) { - element.addClass('ui-widget'); - } - - if (options.year != undefined && options.year != date.getFullYear()) { - date.setDate(1); - date.setMonth(0); - date.setFullYear(options.year); - } - if (options.month != undefined && options.month != date.getMonth()) { - date.setDate(1); - date.setMonth(options.month); - } - if (options.date != undefined) { - date.setDate(options.date); - } - - - - /* View Rendering - -----------------------------------------------------------------------------*/ - - function changeView(v) { - if (v != viewName) { - ignoreWindowResize++; // because setMinHeight might change the height before render (and subsequently setSize) is reached - - var oldView = view, - newViewElement; - - if (oldView) { - if (oldView.eventsChanged) { - eventsDirty(); - oldView.eventDirty = oldView.eventsChanged = false; - } - if (oldView.beforeHide) { - oldView.beforeHide(); // called before changing min-height. if called after, scroll state is reset (in Opera) - } - setMinHeight(content, content.height()); - oldView.element.hide(); - }else{ - setMinHeight(content, 1); // needs to be 1 (not 0) for IE7, or else view dimensions miscalculated - } - content.css('overflow', 'hidden'); - - if (viewInstances[v]) { - (view = viewInstances[v]).element.show(); - }else{ - view = viewInstances[v] = $.fullCalendar.views[v]( - newViewElement = absoluteViewElement = - $("
") - .appendTo(content), - options - ); - } - - if (header) { - // update 'active' view button - header.find('div.fc-button-' + viewName).removeClass(tm + '-state-active'); - header.find('div.fc-button-' + v).addClass(tm + '-state-active'); - } - - view.name = viewName = v; - render(); // after height has been set, will make absoluteViewElement's position=relative, then set to null - content.css('overflow', ''); - if (oldView) { - setMinHeight(content, 1); - } - if (!newViewElement && view.afterShow) { - view.afterShow(); // called after setting min-height/overflow, so in final scroll state (for Opera) - } - - ignoreWindowResize--; - } - } - - - function render(inc) { - if (elementVisible()) { - ignoreWindowResize++; // because view.renderEvents might temporarily change the height before setSize is reached - - if (suggestedViewHeight == undefined) { - calcSize(); - } - - if (!view.start || inc || date < view.start || date >= view.end) { - view.render(date, inc || 0); // responsible for clearing events - setSize(true); - if (!eventStart || !options.lazyFetching || view.visStart < eventStart || view.visEnd > eventEnd) { - fetchAndRenderEvents(); - }else{ - view.renderEvents(events); // don't refetch - } - } - else if (view.sizeDirty || view.eventsDirty || !options.lazyFetching) { - view.clearEvents(); - if (view.sizeDirty) { - setSize(); - } - if (options.lazyFetching) { - view.renderEvents(events); // don't refetch - }else{ - fetchAndRenderEvents(); - } - } - elementOuterWidth = element.outerWidth(); - view.sizeDirty = false; - view.eventsDirty = false; - - if (header) { - // update title text - header.find('h2.fc-header-title').html(view.title); - // enable/disable 'today' button - var today = new Date(); - if (today >= view.start && today < view.end) { - header.find('div.fc-button-today').addClass(tm + '-state-disabled'); - }else{ - header.find('div.fc-button-today').removeClass(tm + '-state-disabled'); - } - } - - ignoreWindowResize--; - view.trigger('viewDisplay', _element); - } - } - - - function elementVisible() { - return _element.offsetWidth !== 0; - } - - function bodyVisible() { - return $('body')[0].offsetWidth !== 0; - } - - - // called when any event objects have been added/removed/changed, rerenders - function eventsChanged() { - eventsDirty(); - if (elementVisible()) { - view.clearEvents(); - view.renderEvents(events); - view.eventsDirty = false; - } - } - - // marks other views' events as dirty - function eventsDirty() { - $.each(viewInstances, function() { - this.eventsDirty = true; - }); - } - - // called when we know the element size has changed - function sizeChanged() { - sizesDirty(); - if (elementVisible()) { - calcSize(); - setSize(); - view.rerenderEvents(); - view.sizeDirty = false; - } - } - - // marks other views' sizes as dirty - function sizesDirty() { - $.each(viewInstances, function() { - this.sizeDirty = true; - }); - } - - - - - /* Event Sources and Fetching - -----------------------------------------------------------------------------*/ - - var events = [], - eventStart, eventEnd; - - // Fetch from ALL sources. Clear 'events' array and populate - function fetchEvents(callback) { - events = []; - eventStart = cloneDate(view.visStart); - eventEnd = cloneDate(view.visEnd); - var queued = eventSources.length, - sourceDone = function() { - if (--queued == 0) { - if (callback) { - callback(events); - } - } - }, i=0; - for (; i") - .append($("") - .append($("").append(buildSection(sections.left))) - .append($("").append(buildSection(sections.center))) - .append($("").append(buildSection(sections.right)))) - .prependTo(element); - } - function buildSection(buttonStr) { - if (buttonStr) { - var tr = $(""); - $.each(buttonStr.split(' '), function(i) { - if (i > 0) { - tr.append(""); - } - var prevButton; - $.each(this.split(','), function(j, buttonName) { - if (buttonName == 'title') { - tr.append("

 

"); - if (prevButton) { - prevButton.addClass(tm + '-corner-right'); - } - prevButton = null; - }else{ - var buttonClick; - if (publicMethods[buttonName]) { - buttonClick = publicMethods[buttonName]; - } - else if (views[buttonName]) { - buttonClick = function() { - button.removeClass(tm + '-state-hover'); - changeView(buttonName) - }; - } - if (buttonClick) { - if (prevButton) { - prevButton.addClass(tm + '-no-right'); - } - var button, - icon = options.theme ? smartProperty(options.buttonIcons, buttonName) : null, - text = smartProperty(options.buttonText, buttonName); - if (icon) { - button = $("
" + - "
"); - } - else if (text) { - button = $(""); - } - if (button) { - button - .click(function() { - if (!button.hasClass(tm + '-state-disabled')) { - buttonClick(); - } - }) - .mousedown(function() { - button - .not('.' + tm + '-state-active') - .not('.' + tm + '-state-disabled') - .addClass(tm + '-state-down'); - }) - .mouseup(function() { - button.removeClass(tm + '-state-down'); - }) - .hover( - function() { - button - .not('.' + tm + '-state-active') - .not('.' + tm + '-state-disabled') - .addClass(tm + '-state-hover'); - }, - function() { - button - .removeClass(tm + '-state-hover') - .removeClass(tm + '-state-down'); - } - ) - .appendTo($("").appendTo(tr)); - if (prevButton) { - prevButton.addClass(tm + '-no-right'); - }else{ - button.addClass(tm + '-corner-left'); - } - prevButton = button; - } - } - } - }); - if (prevButton) { - prevButton.addClass(tm + '-corner-right'); - } - }); - return $("").append(tr); - } - } - - - - /* Resizing - -----------------------------------------------------------------------------*/ - - - function calcSize() { - if (options.contentHeight) { - suggestedViewHeight = options.contentHeight; - } - else if (options.height) { - suggestedViewHeight = options.height - (header ? header.height() : 0) - vsides(content[0]); - } - else { - suggestedViewHeight = Math.round(content.width() / Math.max(options.aspectRatio, .5)); - } - } - - - function setSize(dateChanged) { - ignoreWindowResize++; - view.setHeight(suggestedViewHeight, dateChanged); - if (absoluteViewElement) { - absoluteViewElement.css('position', 'relative'); - absoluteViewElement = null; - } - view.setWidth(content.width(), dateChanged); - ignoreWindowResize--; - } - - - function windowResize() { - if (!ignoreWindowResize) { - if (view.start) { // view has already been rendered - var uid = ++resizeUID; - setTimeout(function() { // add a delay - if (uid == resizeUID && !ignoreWindowResize && elementVisible()) { - if (elementOuterWidth != (elementOuterWidth = element.outerWidth())) { - ignoreWindowResize++; // in case the windowResize callback changes the height - sizeChanged(); - view.trigger('windowResize', _element); - ignoreWindowResize--; - } - } - }, 200); - }else{ - // calendar must have been initialized in a 0x0 iframe that has just been resized - lateRender(); - } - } - }; - $(window).resize(windowResize); - - - // let's begin... - changeView(options.defaultView); - - - // needed for IE in a 0x0 iframe, b/c when it is resized, never triggers a windowResize - if (!bodyVisible()) { - lateRender(); - } - - - // called when we know the calendar couldn't be rendered when it was initialized, - // but we think it's ready now - function lateRender() { - setTimeout(function() { // IE7 needs this so dimensions are calculated correctly - if (!view.start && bodyVisible()) { // !view.start makes sure this never happens more than once - render(); - } - },0); - } - - - }); - - return this; - -}; - - - -/* Important Event Utilities ------------------------------------------------------------------------------*/ - -var fakeID = 0; - -function normalizeEvent(event, options) { - event._id = event._id || (event.id == undefined ? '_fc' + fakeID++ : event.id + ''); - if (event.date) { - if (!event.start) { - event.start = event.date; - } - delete event.date; - } - event._start = cloneDate(event.start = parseDate(event.start)); - event.end = parseDate(event.end); - if (event.end && event.end <= event.start) { - event.end = null; - } - event._end = event.end ? cloneDate(event.end) : null; - if (event.allDay == undefined) { - event.allDay = options.allDayDefault; - } - if (event.className) { - if (typeof event.className == 'string') { - event.className = event.className.split(/\s+/); - } - }else{ - event.className = []; - } -} -// TODO: if there is no title or start date, return false to indicate an invalid event - - -/* Grid-based Views: month, basicWeek, basicDay ------------------------------------------------------------------------------*/ - -setDefaults({ - weekMode: 'fixed' -}); - -views.month = function(element, options) { - return new Grid(element, options, { - render: function(date, delta) { - if (delta) { - addMonths(date, delta); - date.setDate(1); - } - // start/end - var start = this.start = cloneDate(date, true); - start.setDate(1); - this.end = addMonths(cloneDate(start), 1); - // visStart/visEnd - var visStart = this.visStart = cloneDate(start), - visEnd = this.visEnd = cloneDate(this.end), - nwe = options.weekends ? 0 : 1; - if (nwe) { - skipWeekend(visStart); - skipWeekend(visEnd, -1, true); - } - addDays(visStart, -((visStart.getDay() - Math.max(options.firstDay, nwe) + 7) % 7)); - addDays(visEnd, (7 - visEnd.getDay() + Math.max(options.firstDay, nwe)) % 7); - // row count - var rowCnt = Math.round((visEnd - visStart) / (DAY_MS * 7)); - if (options.weekMode == 'fixed') { - addDays(visEnd, (6 - rowCnt) * 7); - rowCnt = 6; - } - // title - this.title = formatDate( - start, - this.option('titleFormat'), - options - ); - // render - this.renderGrid( - rowCnt, options.weekends ? 7 : 5, - this.option('columnFormat'), - true - ); - } - }); -} - -views.basicWeek = function(element, options) { - return new Grid(element, options, { - render: function(date, delta) { - if (delta) { - addDays(date, delta * 7); - } - var visStart = this.visStart = cloneDate( - this.start = addDays(cloneDate(date), -((date.getDay() - options.firstDay + 7) % 7)) - ), - visEnd = this.visEnd = cloneDate( - this.end = addDays(cloneDate(visStart), 7) - ); - if (!options.weekends) { - skipWeekend(visStart); - skipWeekend(visEnd, -1, true); - } - this.title = formatDates( - visStart, - addDays(cloneDate(visEnd), -1), - this.option('titleFormat'), - options - ); - this.renderGrid( - 1, options.weekends ? 7 : 5, - this.option('columnFormat'), - false - ); - } - }); -}; - -views.basicDay = function(element, options) { - return new Grid(element, options, { - render: function(date, delta) { - if (delta) { - addDays(date, delta); - if (!options.weekends) { - skipWeekend(date, delta < 0 ? -1 : 1); - } - } - this.title = formatDate(date, this.option('titleFormat'), options); - this.start = this.visStart = cloneDate(date, true); - this.end = this.visEnd = addDays(cloneDate(this.start), 1); - this.renderGrid( - 1, 1, - this.option('columnFormat'), - false - ); - } - }); -} - - -// rendering bugs - -var tdHeightBug; - - -function Grid(element, options, methods) { - - var tm, firstDay, - nwe, // no weekends (int) - rtl, dis, dit, // day index sign / translate - viewWidth, viewHeight, - rowCnt, colCnt, - colWidth, - thead, tbody, - cachedEvents=[], - segmentContainer, - dayContentPositions = new HorizontalPositionCache(function(dayOfWeek) { - return tbody.find('td:eq(' + ((dayOfWeek - Math.max(firstDay,nwe)+colCnt) % colCnt) + ') div div') - }), - // ... - - // initialize superclass - view = $.extend(this, viewMethods, methods, { - renderGrid: renderGrid, - renderEvents: renderEvents, - rerenderEvents: rerenderEvents, - clearEvents: clearEvents, - setHeight: setHeight, - setWidth: setWidth, - defaultEventEnd: function(event) { // calculates an end if event doesnt have one, mostly for resizing - return cloneDate(event.start); - } - }); - view.init(element, options); - - - - /* Grid Rendering - -----------------------------------------------------------------------------*/ - - - element.addClass('fc-grid'); - if (element.disableSelection) { - element.disableSelection(); - } - - function renderGrid(r, c, colFormat, showNumbers) { - rowCnt = r; - colCnt = c; - - // update option-derived variables - tm = options.theme ? 'ui' : 'fc'; - nwe = options.weekends ? 0 : 1; - firstDay = options.firstDay; - if (rtl = options.isRTL) { - dis = -1; - dit = colCnt - 1; - }else{ - dis = 1; - dit = 0; - } - - var month = view.start.getMonth(), - today = clearTime(new Date()), - s, i, j, d = cloneDate(view.visStart); - - if (!tbody) { // first time, build all cells from scratch - - var table = $("
").appendTo(element); - - s = ""; - for (i=0; i" + formatDate(d, colFormat, options) + ""; - addDays(d, 1); - if (nwe) { - skipWeekend(d); - } - } - thead = $(s + "").appendTo(table); - - s = ""; - d = cloneDate(view.visStart); - for (i=0; i"; - for (j=0; j1 && d.getMonth() != month ? ' fc-other-month' : '') + - (+d == +today ? - ' fc-today '+tm+'-state-highlight' : - ' fc-not-today') + "'>" + - (showNumbers ? "
" + d.getDate() + "
" : '') + - "
 
"; - addDays(d, 1); - if (nwe) { - skipWeekend(d); - } - } - s += ""; - } - tbody = $(s + "
").appendTo(table); - tbody.find('td').click(dayClick); - - segmentContainer = $("
").appendTo(element); - - }else{ // NOT first time, reuse as many cells as possible - - clearEvents(); - - var prevRowCnt = tbody.find('tr').length; - if (rowCnt < prevRowCnt) { - tbody.find('tr:gt(' + (rowCnt-1) + ')').remove(); // remove extra rows - } - else if (rowCnt > prevRowCnt) { // needs to create new rows... - s = ''; - for (i=prevRowCnt; i"; - for (j=0; j" + - (showNumbers ? "
" : '') + - "
 
" + - ""; - addDays(d, 1); - if (nwe) { - skipWeekend(d); - } - } - s += ""; - } - tbody.append(s); - } - tbody.find('td.fc-new').removeClass('fc-new').click(dayClick); - - // re-label and re-class existing cells - d = cloneDate(view.visStart); - tbody.find('td').each(function() { - var td = $(this); - if (rowCnt > 1) { - if (d.getMonth() == month) { - td.removeClass('fc-other-month'); - }else{ - td.addClass('fc-other-month'); - } - } - if (+d == +today) { - td.removeClass('fc-not-today') - .addClass('fc-today') - .addClass(tm + '-state-highlight'); - }else{ - td.addClass('fc-not-today') - .removeClass('fc-today') - .removeClass(tm + '-state-highlight'); - } - td.find('div.fc-day-number').text(d.getDate()); - addDays(d, 1); - if (nwe) { - skipWeekend(d); - } - }); - - if (rowCnt == 1) { // more changes likely (week or day view) - - // redo column header text and class - d = cloneDate(view.visStart); - thead.find('th').each(function() { - $(this).text(formatDate(d, colFormat, options)); - this.className = this.className.replace(/^fc-\w+(?= )/, 'fc-' + dayIDs[d.getDay()]); - addDays(d, 1); - if (nwe) { - skipWeekend(d); - } - }); - - // redo cell day-of-weeks - d = cloneDate(view.visStart); - tbody.find('td').each(function() { - this.className = this.className.replace(/^fc-\w+(?= )/, 'fc-' + dayIDs[d.getDay()]); - addDays(d, 1); - if (nwe) { - skipWeekend(d); - } - }); - - } - - } - - }; - - - function dayClick(ev) { - var n = parseInt(this.className.match(/fc\-day(\d+)/)[1]), - date = addDays( - cloneDate(view.visStart), - Math.floor(n/colCnt) * 7 + n % colCnt - ); - view.trigger('dayClick', this, date, true, ev); - } - - - - function setHeight(height) { - viewHeight = height; - var leftTDs = tbody.find('tr td:first-child'), - tbodyHeight = viewHeight - thead.height(), - rowHeight1, rowHeight2; - if (options.weekMode == 'variable') { - rowHeight1 = rowHeight2 = Math.floor(tbodyHeight / (rowCnt==1 ? 2 : 6)); - }else{ - rowHeight1 = Math.floor(tbodyHeight / rowCnt); - rowHeight2 = tbodyHeight - rowHeight1*(rowCnt-1); - } - if (tdHeightBug == undefined) { - // bug in firefox where cell height includes padding - var tr = tbody.find('tr:first'), - td = tr.find('td:first'); - td.height(rowHeight1); - tdHeightBug = rowHeight1 != td.height(); - } - if (tdHeightBug) { - leftTDs.slice(0, -1).height(rowHeight1); - leftTDs.slice(-1).height(rowHeight2); - }else{ - setOuterHeight(leftTDs.slice(0, -1), rowHeight1); - setOuterHeight(leftTDs.slice(-1), rowHeight2); - } - } - - - function setWidth(width) { - viewWidth = width; - dayContentPositions.clear(); - setOuterWidth( - thead.find('th').slice(0, -1), - colWidth = Math.floor(viewWidth / colCnt) - ); - } - - - - /* Event Rendering - -----------------------------------------------------------------------------*/ - - - function renderEvents(events) { - view.reportEvents(cachedEvents = events); - renderSegs(compileSegs(events)); - } - - - function rerenderEvents(modifiedEventId) { - clearEvents(); - renderSegs(compileSegs(cachedEvents), modifiedEventId); - } - - - function clearEvents() { - view._clearEvents(); // only clears the hashes - segmentContainer.empty(); - } - - - function compileSegs(events) { - var d1 = cloneDate(view.visStart), - d2 = addDays(cloneDate(d1), colCnt), - visEventsEnds = $.map(events, visEventEnd), - i, row, - j, level, - k, seg, - segs=[]; - for (i=0; i" + - "" + - (!event.allDay && seg.isStart ? - "" + - htmlEscape(formatDates(event.start, event.end, view.option('timeFormat'), options)) + - "" - :'') + - "" + htmlEscape(event.title) + "" + - "" + - ((event.editable || event.editable == undefined && options.editable) && !options.disableResizing && $.fn.resizable ? - "
" - : '') + - "
"; - seg.left = left; - seg.outerWidth = right - left; - } - segmentContainer[0].innerHTML = html; // faster than html() - eventElements = segmentContainer.children(); - - // retrieve elements, run through eventRender callback, bind handlers - for (i=0; i div') // optimal selector? - .height(top + levelHeight); - } - - // calculate row tops - for (rowI=0; rowI" + - "" + - ""; - for (i=0; i" + formatDate(d, colFormat, options) + ""; - addDays(d, dis); - if (nwe) { - skipWeekend(d, dis); - } - } - s += ""; - if (options.allDaySlot) { - s += "" + - "" + - "" + - "" + - ""; - } - s+= "
  
" + options.allDayText + "" + - "
 
 
"; - head = $(s).appendTo(element); - head.find('td').click(slotClick); - - // all-day event container - daySegmentContainer = $("
").appendTo(head); - - // body - d = zeroDate(); - var maxd = addMinutes(cloneDate(d), maxMinute); - addMinutes(d, minMinute); - s = ""; - for (i=0; d < maxd; i++) { - minutes = d.getMinutes(); - s += ""; - addMinutes(d, options.slotMinutes); - } - s += "
" + - ((!slotNormal || minutes==0) ? formatDate(d, options.axisFormat) : ' ') + - "
 
"; - body = $("
") - .append(bodyContent = $("
") - .append(bodyTable = $(s))) - .appendTo(element); - body.find('td').click(slotClick); - - // slot event container - slotSegmentContainer = $("
").appendTo(bodyContent); - - // background stripes - d = cloneDate(d0); - s = "
" + - ""; - for (i=0; i
 
"; - addDays(d, dis); - if (nwe) { - skipWeekend(d, dis); - } - } - s += "
"; - bg = $(s).appendTo(element); - - }else{ // skeleton already built, just modify it - - clearEvents(); - - // redo column header text and class - head.find('tr:first th').slice(1, -1).each(function() { - $(this).text(formatDate(d, colFormat, options)); - this.className = this.className.replace(/^fc-\w+(?= )/, 'fc-' + dayIDs[d.getDay()]); - addDays(d, dis); - if (nwe) { - skipWeekend(d, dis); - } - }); - - // change classes of background stripes - d = cloneDate(d0); - bg.find('td').each(function() { - this.className = this.className.replace(/^fc-\w+(?= )/, 'fc-' + dayIDs[d.getDay()]); - if (+d == +today) { - $(this) - .removeClass('fc-not-today') - .addClass('fc-today') - .addClass(tm + '-state-highlight'); - }else{ - $(this) - .addClass('fc-not-today') - .removeClass('fc-today') - .removeClass(tm + '-state-highlight'); - } - addDays(d, dis); - if (nwe) { - skipWeekend(d, dis); - } - }); - - } - - }; - - - function resetScroll() { - var d0 = zeroDate(), - scrollDate = cloneDate(d0); - scrollDate.setHours(options.firstHour); - var top = timePosition(d0, scrollDate) + 1, // +1 for the border - scroll = function() { - body.scrollTop(top); - }; - scroll(); - setTimeout(scroll, 0); // overrides any previous scroll state made by the browser - } - - - function setHeight(height, dateChanged) { - viewHeight = height; - slotTopCache = {}; - - body.height(height - head.height()); - - slotHeight = body.find('tr:first div').height() + 1; - - bg.css({ - top: head.find('tr').height(), - height: height - }); - - if (dateChanged) { - resetScroll(); - } - } - - - function setWidth(width) { - viewWidth = width; - colContentPositions.clear(); - - body.width(width); - bodyTable.width(''); - - var topTDs = head.find('tr:first th'), - stripeTDs = bg.find('td'), - clientWidth = body[0].clientWidth; - - bodyTable.width(clientWidth); - - // time-axis width - axisWidth = 0; - setOuterWidth( - head.find('tr:lt(2) th:first').add(body.find('tr:first th')) - .width('') - .each(function() { - axisWidth = Math.max(axisWidth, $(this).outerWidth()); - }), - axisWidth - ); - - // column width - colWidth = Math.floor((clientWidth - axisWidth) / colCnt); - setOuterWidth(stripeTDs.slice(0, -1), colWidth); - setOuterWidth(topTDs.slice(1, -2), colWidth); - setOuterWidth(topTDs.slice(-2, -1), clientWidth - axisWidth - colWidth*(colCnt-1)); - - bg.css({ - left: axisWidth, - width: clientWidth - axisWidth - }); - } - - - - - function slotClick(ev) { - var col = Math.floor((ev.pageX - bg.offset().left) / colWidth), - date = addDays(cloneDate(view.visStart), dit + dis*col), - rowMatch = this.className.match(/fc-slot(\d+)/); - if (rowMatch) { - var mins = parseInt(rowMatch[1]) * options.slotMinutes, - hours = Math.floor(mins/60); - date.setHours(hours); - date.setMinutes(mins%60 + minMinute); - view.trigger('dayClick', this, date, false, ev); - }else{ - view.trigger('dayClick', this, date, true, ev); - } - } - - - - /* Event Rendering - -----------------------------------------------------------------------------*/ - - function renderEvents(events, modifiedEventId) { - view.reportEvents(cachedEvents = events); - var i, len=events.length, - dayEvents=[], - slotEvents=[]; - for (i=0; i" + - "" + - "" + - "" + htmlEscape(formatDates(event.start, event.end, view.option('timeFormat'))) + "" + - "" + htmlEscape(event.title) + "" + - "" + - ((event.editable || event.editable == undefined && options.editable) && !options.disableResizing && $.fn.resizable ? - "
=
" - : '') + - "
"; - } - slotSegmentContainer[0].innerHTML = html; // faster than html() - eventElements = slotSegmentContainer.children(); - - // retrieve elements, run through eventRender callback, bind event handlers - for (i=0; i= addMinutes(cloneDate(day), maxMinute)) { - return bodyContent.height(); - } - var slotMinutes = options.slotMinutes, - minutes = time.getHours()*60 + time.getMinutes() - minMinute, - slotI = Math.floor(minutes / slotMinutes), - slotTop = slotTopCache[slotI]; - if (slotTop == undefined) { - slotTop = slotTopCache[slotI] = body.find('tr:eq(' + slotI + ') td div')[0].offsetTop; - } - return Math.max(0, Math.round( - slotTop - 1 + slotHeight * ((minutes % slotMinutes) / slotMinutes) - )); - } - - - - - function day2col(dayOfWeek) { - return ((dayOfWeek - Math.max(firstDay,nwe)+colCnt) % colCnt)*dis+dit; - } - - -} - - -// count the number of colliding, higher-level segments (for event squishing) - -function countForwardSegs(levels) { - var i, j, k, level, segForward, segBack; - for (i=levels.length-1; i>0; i--) { - level = levels[i]; - for (j=0; j").prependTo(_element), + suggestedViewHeight, + resizeUID = 0, + ignoreWindowResize = 0, + date = new Date(), + viewName, // the current view name (TODO: look into getting rid of) + view, // the current view + viewInstances = {}, + absoluteViewElement; + + + if (options.isRTL) { + element.addClass('fc-rtl'); + } + if (options.theme) { + element.addClass('ui-widget'); + } + + if (options.year !== undefined && options.year != date.getFullYear()) { + date.setDate(1); + date.setMonth(0); + date.setFullYear(options.year); + } + if (options.month !== undefined && options.month != date.getMonth()) { + date.setDate(1); + date.setMonth(options.month); + } + if (options.date !== undefined) { + date.setDate(options.date); + } + + + /* View Rendering + -----------------------------------------------------------------------------*/ + + function changeView(v) { + if (v != viewName) { + ignoreWindowResize++; // because setMinHeight might change the height before render (and subsequently setSize) is reached + + viewUnselect(); + + var oldView = view, + newViewElement; + + if (oldView) { + if (oldView.eventsChanged) { + eventsDirty(); + oldView.eventDirty = oldView.eventsChanged = false; + } + if (oldView.beforeHide) { + oldView.beforeHide(); // called before changing min-height. if called after, scroll state is reset (in Opera) + } + setMinHeight(content, content.height()); + oldView.element.hide(); + } else { + setMinHeight(content, 1); // needs to be 1 (not 0) for IE7, or else view dimensions miscalculated + } + content.css('overflow', 'hidden'); + + if (viewInstances[v]) { + (view = viewInstances[v]).element.show(); + } else { + view = viewInstances[v] = fc.views[v]( + newViewElement = absoluteViewElement = + $("
") + .appendTo(content), + options + ); + } + + if (header) { + // update 'active' view button + header.find('div.fc-button-' + viewName).removeClass(tm + '-state-active'); + header.find('div.fc-button-' + v).addClass(tm + '-state-active'); + } + + view.name = viewName = v; + render(); // after height has been set, will make absoluteViewElement's position=relative, then set to null + content.css('overflow', ''); + if (oldView) { + setMinHeight(content, 1); + } + if (!newViewElement && view.afterShow) { + view.afterShow(); // called after setting min-height/overflow, so in final scroll state (for Opera) + } + + ignoreWindowResize--; + } + } + + + function render(inc) { + if (elementVisible()) { + ignoreWindowResize++; // because view.renderEvents might temporarily change the height before setSize is reached + + viewUnselect(); + + if (suggestedViewHeight === undefined) { + calcSize(); + } + + if (!view.start || inc || date < view.start || date >= view.end) { + view.render(date, inc || 0); // responsible for clearing events + setSize(true); + if (!eventStart || !options.lazyFetching || view.visStart < eventStart || view.visEnd > eventEnd) { + fetchAndRenderEvents(); + } else { + view.renderEvents(events); // don't refetch + } + } + else if (view.sizeDirty || view.eventsDirty || !options.lazyFetching) { + view.clearEvents(); + if (view.sizeDirty) { + setSize(); + } + if (options.lazyFetching) { + view.renderEvents(events); // don't refetch + } else { + fetchAndRenderEvents(); + } + } + elementOuterWidth = element.outerWidth(); + view.sizeDirty = false; + view.eventsDirty = false; + + if (header) { + // update title text + header.find('h2.fc-header-title').html(view.title); + // enable/disable 'today' button + var today = new Date(); + if (today >= view.start && today < view.end) { + header.find('div.fc-button-today').addClass(tm + '-state-disabled'); + } else { + header.find('div.fc-button-today').removeClass(tm + '-state-disabled'); + } + } + + ignoreWindowResize--; + view.trigger('viewDisplay', _element); + } + } + + + function elementVisible() { + return _element.offsetWidth !== 0; + } + + function bodyVisible() { + return $('body')[0].offsetWidth !== 0; + } + + function viewUnselect() { + if (view) { + view.unselect(); + } + } + + + // called when any event objects have been added/removed/changed, rerenders + function eventsChanged() { + eventsDirty(); + if (elementVisible()) { + view.clearEvents(); + view.renderEvents(events); + view.eventsDirty = false; + } + } + + // marks other views' events as dirty + function eventsDirty() { + $.each(viewInstances, function() { + this.eventsDirty = true; + }); + } + + // called when we know the element size has changed + function sizeChanged() { + sizesDirty(); + if (elementVisible()) { + calcSize(); + setSize(); + viewUnselect(); + view.rerenderEvents(); + view.sizeDirty = false; + } + } + + // marks other views' sizes as dirty + function sizesDirty() { + $.each(viewInstances, function() { + this.sizeDirty = true; + }); + } + + + /* Event Sources and Fetching + -----------------------------------------------------------------------------*/ + + var events = [], + eventStart, eventEnd; + + // Fetch from ALL sources. Clear 'events' array and populate + function fetchEvents(callback) { + events = []; + eventStart = cloneDate(view.visStart); + eventEnd = cloneDate(view.visEnd); + var queued = eventSources.length, + sourceDone = function() { + if (!--queued) { + if (callback) { + callback(events); + } + } + }, i = 0; + for (; i < eventSources.length; i++) { + fetchEventSource(eventSources[i], sourceDone); + } + } + + // Fetch from a particular source. Append to the 'events' array + function fetchEventSource(src, callback) { + var prevViewName = view.name, + prevDate = cloneDate(date), + reportEvents = function(a) { + if (prevViewName == view.name && +prevDate == +date && // protects from fast switching + $.inArray(src, eventSources) != -1) { // makes sure source hasn't been removed + for (var i = 0; i < a.length; i++) { + normalizeEvent(a[i], options); + a[i].source = src; + } + events = events.concat(a); + if (callback) { + callback(a); + } + } + }, + reportEventsAndPop = function(a) { + reportEvents(a); + popLoading(); + }; + if (typeof src == 'string') { + var params = {}; + params[options.startParam] = Math.round(eventStart.getTime() / 1000); + params[options.endParam] = Math.round(eventEnd.getTime() / 1000); + if (options.cacheParam) { + params[options.cacheParam] = (new Date()).getTime(); // TODO: deprecate cacheParam + } + pushLoading(); + $.ajax({ + url: src, + dataType: 'json', + data: params, + cache: options.cacheParam || false, // don't let jquery prevent caching if cacheParam is being used + success: reportEventsAndPop + }); + } + else if ($.isFunction(src)) { + pushLoading(); + src(cloneDate(eventStart), cloneDate(eventEnd), reportEventsAndPop); + } + else { + reportEvents(src); // src is an array + } + } + + + // for convenience + function fetchAndRenderEvents() { + fetchEvents(function(events) { + view.renderEvents(events); // maintain `this` in view + }); + } + + + /* Loading State + -----------------------------------------------------------------------------*/ + + var loadingLevel = 0; + + function pushLoading() { + if (!loadingLevel++) { + view.trigger('loading', _element, true); + } + } + + function popLoading() { + if (!--loadingLevel) { + view.trigger('loading', _element, false); + } + } + + + /* Public Methods + -----------------------------------------------------------------------------*/ + + var publicMethods = { + + render: function() { + calcSize(); + sizesDirty(); + eventsDirty(); + render(); + }, + + changeView: changeView, + + getView: function() { + return view; + }, + + getDate: function() { + return date; + }, + + option: function(name, value) { + if (value === undefined) { + return options[name]; + } + if (name == 'height' || name == 'contentHeight' || name == 'aspectRatio') { + options[name] = value; + sizeChanged(); + } + }, + + destroy: function() { + $(window).unbind('resize', windowResize); + if (header) { + header.remove(); + } + content.remove(); + $.removeData(_element, 'fullCalendar'); + }, + + // + // Navigation + // + + prev: function() { + render(-1); + }, + + next: function() { + render(1); + }, + + prevYear: function() { + addYears(date, -1); + render(); + }, + + nextYear: function() { + addYears(date, 1); + render(); + }, + + today: function() { + date = new Date(); + render(); + }, + + gotoDate: function(year, month, dateNum) { + if (typeof year == 'object') { + date = cloneDate(year); // provided 1 argument, a Date + } else { + if (year !== undefined) { + date.setFullYear(year); + } + if (month !== undefined) { + date.setMonth(month); + } + if (dateNum !== undefined) { + date.setDate(dateNum); + } + } + render(); + }, + + incrementDate: function(years, months, days) { + if (years !== undefined) { + addYears(date, years); + } + if (months !== undefined) { + addMonths(date, months); + } + if (days !== undefined) { + addDays(date, days); + } + render(); + }, + + // + // Event Manipulation + // + + updateEvent: function(event) { // update an existing event + var i, len = events.length, e, + startDelta = event.start - event._start, + endDelta = event.end ? + (event.end - (event._end || view.defaultEventEnd(event))) // event._end would be null if event.end + : 0; // was null and event was just resized + for (i = 0; i < len; i++) { + e = events[i]; + if (e._id == event._id && e != event) { + e.start = new Date(+e.start + startDelta); + if (event.end) { + if (e.end) { + e.end = new Date(+e.end + endDelta); + } else { + e.end = new Date(+view.defaultEventEnd(e) + endDelta); + } + } else { + e.end = null; + } + e.title = event.title; + e.url = event.url; + e.allDay = event.allDay; + e.className = event.className; + e.editable = event.editable; + normalizeEvent(e, options); + } + } + normalizeEvent(event, options); + eventsChanged(); + }, + + renderEvent: function(event, stick) { // render a new event + normalizeEvent(event, options); + if (!event.source) { + if (stick) { + (event.source = eventSources[0]).push(event); + } + events.push(event); + } + eventsChanged(); + }, + + removeEvents: function(filter) { + if (!filter) { // remove all + events = []; + // clear all array sources + for (var i = 0; i < eventSources.length; i++) { + if (typeof eventSources[i] == 'object') { + eventSources[i] = []; + } + } + } else { + if (!$.isFunction(filter)) { // an event ID + var id = filter + ''; + filter = function(e) { + return e._id == id; + }; + } + events = $.grep(events, filter, true); + // remove events from array sources + for (var i = 0; i < eventSources.length; i++) { + if (typeof eventSources[i] == 'object') { + eventSources[i] = $.grep(eventSources[i], filter, true); + } + } + } + eventsChanged(); + }, + + clientEvents: function(filter) { + if ($.isFunction(filter)) { + return $.grep(events, filter); + } + else if (filter) { // an event ID + filter += ''; + return $.grep(events, function(e) { + return e._id == filter; + }); + } + return events; // else, return all + }, + + rerenderEvents: eventsChanged, // TODO: think of renaming eventsChanged + + // + // Event Source + // + + addEventSource: function(source) { + eventSources.push(source); + fetchEventSource(source, eventsChanged); + }, + + removeEventSource: function(source) { + eventSources = $.grep(eventSources, function(src) { + return src != source; + }); + // remove all client events from that source + events = $.grep(events, function(e) { + return e.source != source; + }); + eventsChanged(); + }, + + refetchEvents: function() { + fetchEvents(eventsChanged); + }, + + // + // selection + // + + select: function(start, end, allDay) { + view.select(start, end, allDay === undefined ? true : allDay); + }, + + unselect: function() { + view.unselect(); + } + + }; + + $.data(this, 'fullCalendar', publicMethods); // TODO: look into memory leak implications + + + /* Header + -----------------------------------------------------------------------------*/ + + var header, + sections = options.header; + if (sections) { + header = $("") + .append($("") + .append($(""); + $.each(buttonStr.split(' '), function(i) { + if (i > 0) { + tr.append(""); + } + var prevButton; + $.each(this.split(','), function(j, buttonName) { + if (buttonName == 'title') { + tr.append(""); + if (prevButton) { + prevButton.addClass(tm + '-corner-right'); + } + prevButton = null; + } else { + var buttonClick; + if (publicMethods[buttonName]) { + buttonClick = publicMethods[buttonName]; + } + else if (views[buttonName]) { + buttonClick = function() { + button.removeClass(tm + '-state-hover'); + changeView(buttonName); + }; + } + if (buttonClick) { + if (prevButton) { + prevButton.addClass(tm + '-no-right'); + } + var button, + icon = options.theme ? smartProperty(options.buttonIcons, buttonName) : null, + text = smartProperty(options.buttonText, buttonName); + if (icon) { + button = $("
" + + "
"); + } + else if (text) { + button = $(""); + } + if (button) { + button + .click(function() { + if (!button.hasClass(tm + '-state-disabled')) { + buttonClick(); + } + }) + .mousedown(function() { + button + .not('.' + tm + '-state-active') + .not('.' + tm + '-state-disabled') + .addClass(tm + '-state-down'); + }) + .mouseup(function() { + button.removeClass(tm + '-state-down'); + }) + .hover( + function() { + button + .not('.' + tm + '-state-active') + .not('.' + tm + '-state-disabled') + .addClass(tm + '-state-hover'); + }, + function() { + button + .removeClass(tm + '-state-hover') + .removeClass(tm + '-state-down'); + } + ) + .appendTo($("
").append(buildSection(sections.left))) + .append($("").append(buildSection(sections.center))) + .append($("").append(buildSection(sections.right)))) + .prependTo(element); + } + function buildSection(buttonStr) { + if (buttonStr) { + var tr = $("

 

").appendTo(tr)); + if (prevButton) { + prevButton.addClass(tm + '-no-right'); + } else { + button.addClass(tm + '-corner-left'); + } + prevButton = button; + } + } + } + }); + if (prevButton) { + prevButton.addClass(tm + '-corner-right'); + } + }); + return $("").append(tr); + } + } + + + /* Resizing + -----------------------------------------------------------------------------*/ + + + function calcSize() { + if (options.contentHeight) { + suggestedViewHeight = options.contentHeight; + } + else if (options.height) { + suggestedViewHeight = options.height - (header ? header.height() : 0) - vsides(content[0]); + } + else { + suggestedViewHeight = Math.round(content.width() / Math.max(options.aspectRatio, .5)); + } + } + + + function setSize(dateChanged) { + ignoreWindowResize++; + view.setHeight(suggestedViewHeight, dateChanged); + if (absoluteViewElement) { + absoluteViewElement.css('position', 'relative'); + absoluteViewElement = null; + } + view.setWidth(content.width(), dateChanged); + ignoreWindowResize--; + } + + + function windowResize() { + if (!ignoreWindowResize) { + if (view.start) { // view has already been rendered + var uid = ++resizeUID; + setTimeout(function() { // add a delay + if (uid == resizeUID && !ignoreWindowResize && elementVisible()) { + if (elementOuterWidth != (elementOuterWidth = element.outerWidth())) { + ignoreWindowResize++; // in case the windowResize callback changes the height + sizeChanged(); + view.trigger('windowResize', _element); + ignoreWindowResize--; + } + } + }, 200); + } else { + // calendar must have been initialized in a 0x0 iframe that has just been resized + lateRender(); + } + } + } + + $(window).resize(windowResize); + + + // let's begin... + changeView(options.defaultView); + + + // needed for IE in a 0x0 iframe, b/c when it is resized, never triggers a windowResize + if (!bodyVisible()) { + lateRender(); + } + + + // called when we know the calendar couldn't be rendered when it was initialized, + // but we think it's ready now + function lateRender() { + setTimeout(function() { // IE7 needs this so dimensions are calculated correctly + if (!view.start && bodyVisible()) { // !view.start makes sure this never happens more than once + render(); + } + }, 0); + } + + + }); + + return this; + + }; + + + /* Important Event Utilities + -----------------------------------------------------------------------------*/ + + var fakeID = 0; + + function normalizeEvent(event, options) { + event._id = event._id || (event.id === undefined ? '_fc' + fakeID++ : event.id + ''); + if (event.date) { + if (!event.start) { + event.start = event.date; + } + delete event.date; + } + event._start = cloneDate(event.start = parseDate(event.start)); + event.end = parseDate(event.end); + if (event.end && event.end <= event.start) { + event.end = null; + } + event._end = event.end ? cloneDate(event.end) : null; + if (event.allDay === undefined) { + event.allDay = options.allDayDefault; + } + if (event.className) { + if (typeof event.className == 'string') { + event.className = event.className.split(/\s+/); + } + } else { + event.className = []; + } + } + + // TODO: if there is no start date, return false to indicate an invalid event + + + /* Grid-based Views: month, basicWeek, basicDay + -----------------------------------------------------------------------------*/ + + setDefaults({ + weekMode: 'fixed' + }); + + views.month = function(element, options) { + return new Grid(element, options, { + render: function(date, delta) { + if (delta) { + addMonths(date, delta); + date.setDate(1); + } + // start/end + var start = this.start = cloneDate(date, true); + start.setDate(1); + this.end = addMonths(cloneDate(start), 1); + // visStart/visEnd + var visStart = this.visStart = cloneDate(start), + visEnd = this.visEnd = cloneDate(this.end), + nwe = options.weekends ? 0 : 1; + if (nwe) { + skipWeekend(visStart); + skipWeekend(visEnd, -1, true); + } + addDays(visStart, -((visStart.getDay() - Math.max(options.firstDay, nwe) + 7) % 7)); + addDays(visEnd, (7 - visEnd.getDay() + Math.max(options.firstDay, nwe)) % 7); + // row count + var rowCnt = Math.round((visEnd - visStart) / (DAY_MS * 7)); + if (options.weekMode == 'fixed') { + addDays(visEnd, (6 - rowCnt) * 7); + rowCnt = 6; + } + // title + this.title = formatDate( + start, + this.option('titleFormat'), + options + ); + // render + this.renderGrid( + rowCnt, options.weekends ? 7 : 5, + this.option('columnFormat'), + true + ); + } + }); + }; + + views.basicWeek = function(element, options) { + return new Grid(element, options, { + render: function(date, delta) { + if (delta) { + addDays(date, delta * 7); + } + var visStart = this.visStart = cloneDate( + this.start = addDays(cloneDate(date), -((date.getDay() - options.firstDay + 7) % 7)) + ), + visEnd = this.visEnd = cloneDate( + this.end = addDays(cloneDate(visStart), 7) + ); + if (!options.weekends) { + skipWeekend(visStart); + skipWeekend(visEnd, -1, true); + } + this.title = formatDates( + visStart, + addDays(cloneDate(visEnd), -1), + this.option('titleFormat'), + options + ); + this.renderGrid( + 1, options.weekends ? 7 : 5, + this.option('columnFormat'), + false + ); + } + }); + }; + + views.basicDay = function(element, options) { + return new Grid(element, options, { + render: function(date, delta) { + if (delta) { + addDays(date, delta); + if (!options.weekends) { + skipWeekend(date, delta < 0 ? -1 : 1); + } + } + this.title = formatDate(date, this.option('titleFormat'), options); + this.start = this.visStart = cloneDate(date, true); + this.end = this.visEnd = addDays(cloneDate(this.start), 1); + this.renderGrid( + 1, 1, + this.option('columnFormat'), + false + ); + } + }); + }; + + + // rendering bugs + + var tdHeightBug; + + + function Grid(element, options, methods) { + + var tm, firstDay, + nwe, // no weekends (int) + rtl, dis, dit, // day index sign / translate + viewWidth, viewHeight, + rowCnt, colCnt, + colWidth, + thead, tbody, + cachedEvents = [], + segmentContainer, + dayContentPositions = new HorizontalPositionCache(function(dayOfWeek) { + return tbody.find('td:eq(' + ((dayOfWeek - Math.max(firstDay, nwe) + colCnt) % colCnt) + ') div div'); + }), + selectionManager, + selectionMatrix, + // ... + + // initialize superclass + view = $.extend(this, viewMethods, methods, { + renderGrid: renderGrid, + renderEvents: renderEvents, + rerenderEvents: rerenderEvents, + clearEvents: clearEvents, + setHeight: setHeight, + setWidth: setWidth, + defaultEventEnd: function(event) { // calculates an end if event doesnt have one, mostly for resizing + return cloneDate(event.start); + } + }); + view.init(element, options); + + + /* Grid Rendering + -----------------------------------------------------------------------------*/ + + + disableTextSelection(element.addClass('fc-grid')); + + + function renderGrid(r, c, colFormat, showNumbers) { + + rowCnt = r; + colCnt = c; + + // update option-derived variables + tm = options.theme ? 'ui' : 'fc'; + nwe = options.weekends ? 0 : 1; + firstDay = options.firstDay; + if (rtl = options.isRTL) { + dis = -1; + dit = colCnt - 1; + } else { + dis = 1; + dit = 0; + } + + var month = view.start.getMonth(), + today = clearTime(new Date()), + s, i, j, d = cloneDate(view.visStart); + + if (!tbody) { // first time, build all cells from scratch + + var table = $("
").appendTo(element); + + s = ""; + for (i = 0; i < colCnt; i++) { + s += ""; + addDays(d, 1); + if (nwe) { + skipWeekend(d); + } + } + thead = $(s + "").appendTo(table); + + s = ""; + d = cloneDate(view.visStart); + for (i = 0; i < rowCnt; i++) { + s += ""; + for (j = 0; j < colCnt; j++) { + s += ""; + addDays(d, 1); + if (nwe) { + skipWeekend(d); + } + } + s += ""; + } + tbody = $(s + "").appendTo(table); + dayBind(tbody.find('td')); + + segmentContainer = $("
").appendTo(element); + + } else { // NOT first time, reuse as many cells as possible + + clearEvents(); + + var prevRowCnt = tbody.find('tr').length; + if (rowCnt < prevRowCnt) { + tbody.find('tr:gt(' + (rowCnt - 1) + ')').remove(); // remove extra rows + } + else if (rowCnt > prevRowCnt) { // needs to create new rows... + s = ''; + for (i = prevRowCnt; i < rowCnt; i++) { + s += "
"; + for (j = 0; j < colCnt; j++) { + s += ""; + addDays(d, 1); + if (nwe) { + skipWeekend(d); + } + } + s += ""; + } + tbody.append(s); + } + dayBind(tbody.find('td.fc-new').removeClass('fc-new')); + + // re-label and re-class existing cells + d = cloneDate(view.visStart); + tbody.find('td').each(function() { + var td = $(this); + if (rowCnt > 1) { + if (d.getMonth() == month) { + td.removeClass('fc-other-month'); + } else { + td.addClass('fc-other-month'); + } + } + if (+d == +today) { + td.removeClass('fc-not-today') + .addClass('fc-today') + .addClass(tm + '-state-highlight'); + } else { + td.addClass('fc-not-today') + .removeClass('fc-today') + .removeClass(tm + '-state-highlight'); + } + td.find('div.fc-day-number').text(d.getDate()); + addDays(d, 1); + if (nwe) { + skipWeekend(d); + } + }); + + if (rowCnt == 1) { // more changes likely (week or day view) + + // redo column header text and class + d = cloneDate(view.visStart); + thead.find('th').each(function() { + $(this).text(formatDate(d, colFormat, options)); + this.className = this.className.replace(/^fc-\w+(?= )/, 'fc-' + dayIDs[d.getDay()]); + addDays(d, 1); + if (nwe) { + skipWeekend(d); + } + }); + + // redo cell day-of-weeks + d = cloneDate(view.visStart); + tbody.find('td').each(function() { + this.className = this.className.replace(/^fc-\w+(?= )/, 'fc-' + dayIDs[d.getDay()]); + addDays(d, 1); + if (nwe) { + skipWeekend(d); + } + }); + + } + + } + + } + + + function setHeight(height) { + viewHeight = height; + var leftTDs = tbody.find('tr td:first-child'), + tbodyHeight = viewHeight - thead.height(), + rowHeight1, rowHeight2; + if (options.weekMode == 'variable') { + rowHeight1 = rowHeight2 = Math.floor(tbodyHeight / (rowCnt == 1 ? 2 : 6)); + } else { + rowHeight1 = Math.floor(tbodyHeight / rowCnt); + rowHeight2 = tbodyHeight - rowHeight1 * (rowCnt - 1); + } + if (tdHeightBug === undefined) { + // bug in firefox where cell height includes padding + var tr = tbody.find('tr:first'), + td = tr.find('td:first'); + td.height(rowHeight1); + tdHeightBug = rowHeight1 != td.height(); + } + if (tdHeightBug) { + leftTDs.slice(0, -1).height(rowHeight1); + leftTDs.slice(-1).height(rowHeight2); + } else { + setOuterHeight(leftTDs.slice(0, -1), rowHeight1); + setOuterHeight(leftTDs.slice(-1), rowHeight2); + } + } + + + function setWidth(width) { + viewWidth = width; + dayContentPositions.clear(); + setOuterWidth( + thead.find('th').slice(0, -1), + colWidth = Math.floor(viewWidth / colCnt) + ); + } + + + /* Event Rendering + -----------------------------------------------------------------------------*/ + + + function renderEvents(events) { + view.reportEvents(cachedEvents = events); + renderSegs(compileSegs(events)); + } + + + function rerenderEvents(modifiedEventId) { + clearEvents(); + renderSegs(compileSegs(cachedEvents), modifiedEventId); + } + + + function clearEvents() { + view._clearEvents(); // only clears the hashes + segmentContainer.empty(); + } + + + function compileSegs(events) { + var d1 = cloneDate(view.visStart), + d2 = addDays(cloneDate(d1), colCnt), + visEventsEnds = $.map(events, exclEndDay), + i, row, + j, level, + k, seg, + segs = []; + for (i = 0; i < rowCnt; i++) { + row = stackSegs(view.sliceSegs(events, visEventsEnds, d1, d2)); + for (j = 0; j < row.length; j++) { + level = row[j]; + for (k = 0; k < level.length; k++) { + seg = level[k]; + seg.row = i; + seg.level = j; + segs.push(seg); + } + } + addDays(d1, 7); + addDays(d2, 7); + } + return segs; + } + + + function renderSegs(segs, modifiedEventId) { + _renderDaySegs( + segs, + rowCnt, + view, + 0, + viewWidth, + function(i) { + return tbody.find('tr:eq(' + i + ')') + }, + dayContentPositions.left, + dayContentPositions.right, + segmentContainer, + bindSegHandlers, + modifiedEventId + ); + } + + + function bindSegHandlers(event, eventElement, seg) { + view.eventElementHandlers(event, eventElement); + if (event.editable || event.editable === undefined && options.editable) { + draggableEvent(event, eventElement); + if (seg.isEnd) { + view.resizableDayEvent(event, eventElement, colWidth); + } + } + } + + + /* Event Dragging + -----------------------------------------------------------------------------*/ + + + function draggableEvent(event, eventElement) { + if (!options.disableDragging && eventElement.draggable) { + var matrix, + dayDelta = 0; + eventElement.draggable({ + zIndex: 9, + delay: 50, + opacity: view.option('dragOpacity'), + revertDuration: options.dragRevertDuration, + start: function(ev, ui) { + view.hideEvents(event, eventElement); + view.trigger('eventDragStart', eventElement, event, ev, ui); + matrix = buildDayMatrix(function(cell) { + eventElement.draggable('option', 'revert', !cell || !cell.rowDelta && !cell.colDelta); + clearOverlays(); + if (cell) { + dayDelta = cell.rowDelta * 7 + cell.colDelta * dis; + renderDayOverlays( + matrix, + addDays(cloneDate(event.start), dayDelta), + addDays(exclEndDay(event), dayDelta) + ); + } else { + dayDelta = 0; + } + }); + matrix.mouse(ev); + }, + drag: function(ev) { + matrix.mouse(ev); + }, + stop: function(ev, ui) { + clearOverlays(); + view.trigger('eventDragStop', eventElement, event, ev, ui); + if (dayDelta) { + eventElement.find('a').removeAttr('href'); // prevents safari from visiting the link + view.eventDrop(this, event, dayDelta, 0, event.allDay, ev, ui); + } else { + if ($.browser.msie) { + eventElement.css('filter', ''); // clear IE opacity side-effects + } + view.showEvents(event, eventElement); + } + } + }); + } + } + + + /* Day clicking and binding + ---------------------------------------------------------*/ + + function dayBind(days) { + days.click(dayClick) + .mousedown(selectionMousedown); + } + + function dayClick(ev) { + if (!view.option('selectable')) { // SelectionManager will worry about dayClick + var n = parseInt(this.className.match(/fc\-day(\d+)/)[1]), + date = addDays( + cloneDate(view.visStart), + Math.floor(n / colCnt) * 7 + n % colCnt + ); + // TODO: what about weekends in middle of week? + view.trigger('dayClick', this, date, true, ev); + } + } + + + /* Selecting + --------------------------------------------------------*/ + + selectionManager = new SelectionManager( + view, + unselect, + function(startDate, endDate, allDay) { + renderDayOverlays( + selectionMatrix, + startDate, + addDays(cloneDate(endDate), 1) + ); + }, + clearOverlays + ); + + function selectionMousedown(ev) { + if (view.option('selectable')) { + selectionMatrix = buildDayMatrix(function(cell) { + if (cell) { + var d = cellDate(cell.row, cell.col); + selectionManager.drag(d, d, true); + } else { + selectionManager.drag(); + } + }); + documentDragHelp( + function(ev) { + selectionMatrix.mouse(ev); + }, + function(ev) { + selectionManager.dragStop(ev); + } + ); + selectionManager.dragStart(ev); + selectionMatrix.mouse(ev); + return false; // prevent auto-unselect and text selection + } + } + + documentUnselectAuto(view, unselect); + + view.select = function(start, end, allDay) { + if (!end) { + end = cloneDate(start); + } + selectionMatrix = buildDayMatrix(); + selectionManager.select(start, end, allDay); + }; + + function unselect() { + selectionManager.unselect(); + } + + view.unselect = unselect; + + + /* Semi-transparent Overlay Helpers + ------------------------------------------------------*/ + + function renderDayOverlays(matrix, overlayStart, overlayEnd) { // overlayEnd is exclusive + var rowStart = cloneDate(view.visStart); + var rowEnd = addDays(cloneDate(rowStart), colCnt); + for (var i = 0; i < rowCnt; i++) { + var stretchStart = new Date(Math.max(rowStart, overlayStart)); + var stretchEnd = new Date(Math.min(rowEnd, overlayEnd)); + if (stretchStart < stretchEnd) { + var colStart, colEnd; + if (rtl) { + colStart = dayDiff(stretchEnd, rowStart) * dis + dit + 1; + colEnd = dayDiff(stretchStart, rowStart) * dis + dit + 1; + } else { + colStart = dayDiff(stretchStart, rowStart); + colEnd = dayDiff(stretchEnd, rowStart); + } + var rect = matrix.rect(i, colStart, i + 1, colEnd, element); + dayBind( + view.renderOverlay(rect, element) + ); + } + addDays(rowStart, 7); + addDays(rowEnd, 7); + } + } + + function clearOverlays() { + view.clearOverlays(); + } + + + /* Utils + ---------------------------------------------------*/ + + + function buildDayMatrix(changeCallback) { + var tds = tbody.find('tr:first td'); + if (rtl) { + tds = $(tds.get().reverse()); + } + return new HoverMatrix(tbody.find('tr'), tds, changeCallback); + } + + + function cellDate(r, c) { // convert r,c to date + return addDays(cloneDate(view.visStart), r * 7 + c * dis + dit); + // TODO: what about weekends in middle of week? + } + + + } + + + function _renderDaySegs(segs, rowCnt, view, minLeft, maxLeft, getRow, dayContentLeft, dayContentRight, segmentContainer, bindSegHandlers, modifiedEventId) { + + var options = view.options, + rtl = options.isRTL, + i, segCnt = segs.length, seg, + event, + className, + left, right, + html = '', + eventElements, + eventElement, + triggerRes, + hsideCache = {}, + vmarginCache = {}, + key, val, + rowI, top, levelI, levelHeight, + rowDivs = [], + rowDivTops = []; + + // calculate desired position/dimensions, create html + for (i = 0; i < segCnt; i++) { + seg = segs[i]; + event = seg.event; + className = 'fc-event fc-event-hori '; + if (rtl) { + if (seg.isStart) { + className += 'fc-corner-right '; + } + if (seg.isEnd) { + className += 'fc-corner-left '; + } + left = seg.isEnd ? dayContentLeft(seg.end.getDay() - 1) : minLeft; + right = seg.isStart ? dayContentRight(seg.start.getDay()) : maxLeft; + } else { + if (seg.isStart) { + className += 'fc-corner-left '; + } + if (seg.isEnd) { + className += 'fc-corner-right '; + } + left = seg.isStart ? dayContentLeft(seg.start.getDay()) : minLeft; + right = seg.isEnd ? dayContentRight(seg.end.getDay() - 1) : maxLeft; + } + html += + "
" + + "" + + (!event.allDay && seg.isStart ? + "" + + htmlEscape(formatDates(event.start, event.end, view.option('timeFormat'), options)) + + "" + : '') + + "" + htmlEscape(event.title) + "" + + "" + + ((event.editable || event.editable === undefined && options.editable) && !options.disableResizing && $.fn.resizable ? + "
" + : '') + + "
"; + seg.left = left; + seg.outerWidth = right - left; + } + segmentContainer[0].innerHTML = html; // faster than html() + eventElements = segmentContainer.children(); + + // retrieve elements, run through eventRender callback, bind handlers + for (i = 0; i < segCnt; i++) { + seg = segs[i]; + eventElement = $(eventElements[i]); // faster than eq() + event = seg.event; + triggerRes = view.trigger('eventRender', event, event, eventElement); + if (triggerRes === false) { + eventElement.remove(); + } else { + if (triggerRes && triggerRes !== true) { + eventElement.remove(); + eventElement = $(triggerRes) + .css({ + position: 'absolute', + left: seg.left + }) + .appendTo(segmentContainer); + } + seg.element = eventElement; + if (event._id === modifiedEventId) { + bindSegHandlers(event, eventElement, seg); + } else { + eventElement[0]._fci = i; // for lazySegBind + } + view.reportEventElement(event, eventElement); + } + } + + lazySegBind(segmentContainer, segs, bindSegHandlers); + + // record event horizontal sides + for (i = 0; i < segCnt; i++) { + seg = segs[i]; + if (eventElement = seg.element) { + val = hsideCache[key = seg.key = cssKey(eventElement[0])]; + seg.hsides = val === undefined ? (hsideCache[key] = hsides(eventElement[0], true)) : val; + } + } + + // set event widths + for (i = 0; i < segCnt; i++) { + seg = segs[i]; + if (eventElement = seg.element) { + eventElement[0].style.width = seg.outerWidth - seg.hsides + 'px'; + } + } + + // record event heights + for (i = 0; i < segCnt; i++) { + seg = segs[i]; + if (eventElement = seg.element) { + val = vmarginCache[key = seg.key]; + seg.outerHeight = eventElement[0].offsetHeight + ( + val === undefined ? (vmarginCache[key] = vmargins(eventElement[0])) : val + ); + } + } + + // set row heights, calculate event tops (in relation to row top) + for (i = 0,rowI = 0; rowI < rowCnt; rowI++) { + top = levelI = levelHeight = 0; + while (i < segCnt && (seg = segs[i]).row == rowI) { + if (seg.level != levelI) { + top += levelHeight; + levelHeight = 0; + levelI++; + } + levelHeight = Math.max(levelHeight, seg.outerHeight || 0); + seg.top = top; + i++; + } + rowDivs[rowI] = getRow(rowI).find('td:first div.fc-day-content > div')// optimal selector? + .height(top + levelHeight); + } + + // calculate row tops + for (rowI = 0; rowI < rowCnt; rowI++) { + rowDivTops[rowI] = rowDivs[rowI][0].offsetTop; + } + + // set event tops + for (i = 0; i < segCnt; i++) { + seg = segs[i]; + if (eventElement = seg.element) { + eventElement[0].style.top = rowDivTops[seg.row] + seg.top + 'px'; + event = seg.event; + view.trigger('eventAfterRender', event, event, eventElement); + } + } + + } + + + /* Agenda Views: agendaWeek/agendaDay + -----------------------------------------------------------------------------*/ + + 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 + }); + + views.agendaWeek = function(element, options) { + return new Agenda(element, options, { + render: function(date, delta) { + if (delta) { + addDays(date, delta * 7); + } + var visStart = this.visStart = cloneDate( + this.start = addDays(cloneDate(date), -((date.getDay() - options.firstDay + 7) % 7)) + ), + visEnd = this.visEnd = cloneDate( + this.end = addDays(cloneDate(visStart), 7) + ); + if (!options.weekends) { + skipWeekend(visStart); + skipWeekend(visEnd, -1, true); + } + this.title = formatDates( + visStart, + addDays(cloneDate(visEnd), -1), + this.option('titleFormat'), + options + ); + this.renderAgenda( + options.weekends ? 7 : 5, + this.option('columnFormat') + ); + } + }); + }; + + views.agendaDay = function(element, options) { + return new Agenda(element, options, { + render: function(date, delta) { + if (delta) { + addDays(date, delta); + if (!options.weekends) { + skipWeekend(date, delta < 0 ? -1 : 1); + } + } + this.title = formatDate(date, this.option('titleFormat'), options); + this.start = this.visStart = cloneDate(date, true); + this.end = this.visEnd = addDays(cloneDate(this.start), 1); + this.renderAgenda( + 1, + this.option('columnFormat') + ); + } + }); + }; + + function Agenda(element, options, methods) { + + var head, body, bodyContent, bodyTable, bg, + colCnt, + slotCnt = 0, // spanning all the way across + axisWidth, colWidth, slotHeight, + viewWidth, viewHeight, + savedScrollTop, + cachedEvents = [], + daySegmentContainer, + slotSegmentContainer, + tm, firstDay, + nwe, // no weekends (int) + rtl, dis, dit, // day index sign / translate + minMinute, maxMinute, + colContentPositions = new HorizontalPositionCache(function(col) { + return bg.find('td:eq(' + col + ') div div'); + }), + slotTopCache = {}, + daySelectionManager, + slotSelectionManager, + selectionHelper, + selectionMatrix, + // ... + + view = $.extend(this, viewMethods, methods, { + renderAgenda: renderAgenda, + renderEvents: renderEvents, + rerenderEvents: rerenderEvents, + clearEvents: clearEvents, + setHeight: setHeight, + setWidth: setWidth, + beforeHide: function() { + savedScrollTop = body.scrollTop(); + }, + afterShow: function() { + body.scrollTop(savedScrollTop); + }, + defaultEventEnd: function(event) { + var start = cloneDate(event.start); + if (event.allDay) { + return start; + } + return addMinutes(start, options.defaultEventMinutes); + } + }); + view.init(element, options); + + + /* Time-slot rendering + -----------------------------------------------------------------------------*/ + + + disableTextSelection(element.addClass('fc-agenda')); + + + function renderAgenda(c, colFormat) { + + colCnt = c; + + // update option-derived variables + tm = options.theme ? 'ui' : 'fc'; + nwe = options.weekends ? 0 : 1; + firstDay = options.firstDay; + if (rtl = options.isRTL) { + dis = -1; + dit = colCnt - 1; + } else { + dis = 1; + dit = 0; + } + minMinute = parseTime(options.minTime); + maxMinute = parseTime(options.maxTime); + + var d0 = rtl ? addDays(cloneDate(view.visEnd), -1) : cloneDate(view.visStart), + d = cloneDate(d0), + today = clearTime(new Date()); + + if (!head) { // first time rendering, build from scratch + + var i, + minutes, + slotNormal = options.slotMinutes % 15 == 0, //... + + // head + s = "
" + + "
" + formatDate(d, colFormat, options) + "
1 && d.getMonth() != month ? ' fc-other-month' : '') + + (+d == +today ? + ' fc-today ' + tm + '-state-highlight' : + ' fc-not-today') + "'>" + + (showNumbers ? "
" + d.getDate() + "
" : '') + + "
 
" + + (showNumbers ? "
" : '') + + "
 
" + + "
" + + "" + + ""; + for (i = 0; i < colCnt; i++) { + s += ""; + addDays(d, dis); + if (nwe) { + skipWeekend(d, dis); + } + } + s += ""; + if (options.allDaySlot) { + s += "" + + "" + + "" + + "" + + ""; + } + s += "
 " + formatDate(d, colFormat, options) + " 
" + options.allDayText + "" + + "
 
 
"; + head = $(s).appendTo(element); + dayBind(head.find('td')); + + // all-day event container + daySegmentContainer = $("
").appendTo(head); + + // body + d = zeroDate(); + var maxd = addMinutes(cloneDate(d), maxMinute); + addMinutes(d, minMinute); + s = ""; + for (i = 0; d < maxd; i++) { + minutes = d.getMinutes(); + s += ""; + addMinutes(d, options.slotMinutes); + slotCnt++; + } + s += "
" + + ((!slotNormal || !minutes) ? formatDate(d, options.axisFormat) : ' ') + + "
 
"; + body = $("
") + .append(bodyContent = $("
") + .append(bodyTable = $(s))) + .appendTo(element); + slotBind(body.find('td')); + + // slot event container + slotSegmentContainer = $("
").appendTo(bodyContent); + + // background stripes + d = cloneDate(d0); + s = "
" + + ""; + for (i = 0; i < colCnt; i++) { + s += ""; + addDays(d, dis); + if (nwe) { + skipWeekend(d, dis); + } + } + s += "
 
"; + bg = $(s).appendTo(element); + + } else { // skeleton already built, just modify it + + clearEvents(); + + // redo column header text and class + head.find('tr:first th').slice(1, -1).each(function() { + $(this).text(formatDate(d, colFormat, options)); + this.className = this.className.replace(/^fc-\w+(?= )/, 'fc-' + dayIDs[d.getDay()]); + addDays(d, dis); + if (nwe) { + skipWeekend(d, dis); + } + }); + + // change classes of background stripes + d = cloneDate(d0); + bg.find('td').each(function() { + this.className = this.className.replace(/^fc-\w+(?= )/, 'fc-' + dayIDs[d.getDay()]); + if (+d == +today) { + $(this) + .removeClass('fc-not-today') + .addClass('fc-today') + .addClass(tm + '-state-highlight'); + } else { + $(this) + .addClass('fc-not-today') + .removeClass('fc-today') + .removeClass(tm + '-state-highlight'); + } + addDays(d, dis); + if (nwe) { + skipWeekend(d, dis); + } + }); + + } + + } + + + function resetScroll() { + var d0 = zeroDate(), + scrollDate = cloneDate(d0); + scrollDate.setHours(options.firstHour); + var top = timePosition(d0, scrollDate) + 1, // +1 for the border + scroll = function() { + body.scrollTop(top); + }; + scroll(); + setTimeout(scroll, 0); // overrides any previous scroll state made by the browser + } + + + function setHeight(height, dateChanged) { + viewHeight = height; + slotTopCache = {}; + + body.height(height - head.height()); + + slotHeight = body.find('tr:first div').height() + 1; + + bg.css({ + top: head.find('tr').height(), + height: height + }); + + if (dateChanged) { + resetScroll(); + } + } + + + function setWidth(width) { + viewWidth = width; + colContentPositions.clear(); + + body.width(width); + bodyTable.width(''); + + var topTDs = head.find('tr:first th'), + stripeTDs = bg.find('td'), + clientWidth = body[0].clientWidth; + + bodyTable.width(clientWidth); + + // time-axis width + axisWidth = 0; + setOuterWidth( + head.find('tr:lt(2) th:first').add(body.find('tr:first th')) + .width('') + .each(function() { + axisWidth = Math.max(axisWidth, $(this).outerWidth()); + }), + axisWidth + ); + + // column width + colWidth = Math.floor((clientWidth - axisWidth) / colCnt); + setOuterWidth(stripeTDs.slice(0, -1), colWidth); + setOuterWidth(topTDs.slice(1, -2), colWidth); + setOuterWidth(topTDs.slice(-2, -1), clientWidth - axisWidth - colWidth * (colCnt - 1)); + + bg.css({ + left: axisWidth, + width: clientWidth - axisWidth + }); + } + + + /* Slot/Day clicking and binding + -----------------------------------------------------------------------*/ + + + function dayBind(tds) { + tds.click(slotClick) + .mousedown(daySelectionMousedown); + } + + + function slotBind(tds) { + tds.click(slotClick) + .mousedown(slotSelectionMousedown); + } + + + function slotClick(ev) { + if (!view.option('selectable')) { // SelectionManager will worry about dayClick + var col = Math.floor((ev.pageX - bg.offset().left) / colWidth), + date = addDays(cloneDate(view.visStart), dit + dis * col), + rowMatch = this.className.match(/fc-slot(\d+)/); + if (rowMatch) { + var mins = parseInt(rowMatch[1]) * options.slotMinutes, + hours = Math.floor(mins / 60); + date.setHours(hours); + date.setMinutes(mins % 60 + minMinute); + view.trigger('dayClick', this, date, false, ev); + } else { + view.trigger('dayClick', this, date, true, ev); + } + } + } + + + /* Event Rendering + -----------------------------------------------------------------------------*/ + + function renderEvents(events, modifiedEventId) { + view.reportEvents(cachedEvents = events); + var i, len = events.length, + dayEvents = [], + slotEvents = []; + for (i = 0; i < len; i++) { + if (events[i].allDay) { + dayEvents.push(events[i]); + } else { + slotEvents.push(events[i]); + } + } + renderDaySegs(compileDaySegs(dayEvents), modifiedEventId); + renderSlotSegs(compileSlotSegs(slotEvents), modifiedEventId); + } + + + function rerenderEvents(modifiedEventId) { + clearEvents(); + renderEvents(cachedEvents, modifiedEventId); + } + + + function clearEvents() { + view._clearEvents(); // only clears the hashes + daySegmentContainer.empty(); + slotSegmentContainer.empty(); + } + + + function compileDaySegs(events) { + var levels = stackSegs(view.sliceSegs(events, $.map(events, exclEndDay), view.visStart, view.visEnd)), + i, levelCnt = levels.length, level, + j, seg, + segs = []; + for (i = 0; i < levelCnt; i++) { + level = levels[i]; + for (j = 0; j < level.length; j++) { + seg = level[j]; + seg.row = 0; + seg.level = i; + segs.push(seg); + } + } + return segs; + } + + + function compileSlotSegs(events) { + var d = addMinutes(cloneDate(view.visStart), minMinute), + visEventEnds = $.map(events, slotEventEnd), + i, col, + j, level, + k, seg, + segs = []; + for (i = 0; i < colCnt; i++) { + col = stackSegs(view.sliceSegs(events, visEventEnds, d, addMinutes(cloneDate(d), maxMinute - minMinute))); + countForwardSegs(col); + for (j = 0; j < col.length; j++) { + level = col[j]; + for (k = 0; k < level.length; k++) { + seg = level[k]; + seg.col = i; + seg.level = j; + segs.push(seg); + } + } + addDays(d, 1, true); + } + return segs; + } + + + // renders 'all-day' events at the top + + function renderDaySegs(segs, modifiedEventId) { + if (options.allDaySlot) { + _renderDaySegs( + segs, + 1, + view, + axisWidth, + viewWidth, + function() { + return head.find('tr.fc-all-day'); + }, + function(dayOfWeek) { + return axisWidth + colContentPositions.left(dayOfWeekCol(dayOfWeek)); + }, + function(dayOfWeek) { + return axisWidth + colContentPositions.right(dayOfWeekCol(dayOfWeek)); + }, + daySegmentContainer, + daySegBind, + modifiedEventId + ); + setHeight(viewHeight); // might have pushed the body down, so resize + } + } + + + // renders events in the 'time slots' at the bottom + + function renderSlotSegs(segs, modifiedEventId) { + + var i, segCnt = segs.length, seg, + event, + className, + top, bottom, + colI, levelI, forward, + leftmost, + availWidth, + outerWidth, + left, + html = '', + eventElements, + eventElement, + triggerRes, + vsideCache = {}, + hsideCache = {}, + key, val, + titleSpan, + height; + + // calculate position/dimensions, create html + for (i = 0; i < segCnt; i++) { + seg = segs[i]; + event = seg.event; + className = 'fc-event fc-event-vert '; + if (seg.isStart) { + className += 'fc-corner-top '; + } + if (seg.isEnd) { + className += 'fc-corner-bottom '; + } + top = timePosition(seg.start, seg.start); + bottom = timePosition(seg.start, seg.end); + colI = seg.col; + levelI = seg.level; + forward = seg.forward || 0; + leftmost = axisWidth + colContentPositions.left(colI * dis + dit); + availWidth = axisWidth + colContentPositions.right(colI * dis + dit) - leftmost; + availWidth = Math.min(availWidth - 6, availWidth * .95); // TODO: move this to CSS + if (levelI) { + // indented and thin + outerWidth = availWidth / (levelI + forward + 1); + } else { + if (forward) { + // moderately wide, aligned left still + outerWidth = ((availWidth / (forward + 1)) - (12 / 2)) * 2; // 12 is the predicted width of resizer = + } else { + // can be entire width, aligned left + outerWidth = availWidth; + } + } + left = leftmost + // leftmost possible + (availWidth / (levelI + forward + 1) * levelI) // indentation + * dis + (rtl ? availWidth - outerWidth : 0); // rtl + seg.top = top; + seg.left = left; + seg.outerWidth = outerWidth; + seg.outerHeight = bottom - top; + html += slotSegHtml(event, seg, className); + } + slotSegmentContainer[0].innerHTML = html; // faster than html() + eventElements = slotSegmentContainer.children(); + + // retrieve elements, run through eventRender callback, bind event handlers + for (i = 0; i < segCnt; i++) { + seg = segs[i]; + event = seg.event; + eventElement = $(eventElements[i]); // faster than eq() + triggerRes = view.trigger('eventRender', event, event, eventElement); + if (triggerRes === false) { + eventElement.remove(); + } else { + if (triggerRes && triggerRes !== true) { + eventElement.remove(); + eventElement = $(triggerRes) + .css({ + position: 'absolute', + top: seg.top, + left: seg.left + }) + .appendTo(slotSegmentContainer); + } + seg.element = eventElement; + if (event._id === modifiedEventId) { + slotSegBind(event, eventElement, seg); + } else { + eventElement[0]._fci = i; // for lazySegBind + } + view.reportEventElement(event, eventElement); + } + } + + lazySegBind(slotSegmentContainer, segs, slotSegBind); + + // record event sides and title positions + for (i = 0; i < segCnt; i++) { + seg = segs[i]; + if (eventElement = seg.element) { + val = vsideCache[key = seg.key = cssKey(eventElement[0])]; + seg.vsides = val === undefined ? (vsideCache[key] = vsides(eventElement[0], true)) : val; + val = hsideCache[key]; + seg.hsides = val === undefined ? (hsideCache[key] = hsides(eventElement[0], true)) : val; + titleSpan = eventElement.find('span.fc-event-title'); + if (titleSpan.length) { + seg.titleTop = titleSpan[0].offsetTop; + } + } + } + + // set all positions/dimensions at once + for (i = 0; i < segCnt; i++) { + seg = segs[i]; + if (eventElement = seg.element) { + eventElement[0].style.width = seg.outerWidth - seg.hsides + 'px'; + eventElement[0].style.height = (height = seg.outerHeight - seg.vsides) + 'px'; + event = seg.event; + if (seg.titleTop !== undefined && height - seg.titleTop < 10) { + // not enough room for title, put it in the time header + eventElement.find('span.fc-event-time') + .text(formatDate(event.start, view.option('timeFormat')) + ' - ' + event.title); + eventElement.find('span.fc-event-title') + .remove(); + } + view.trigger('eventAfterRender', event, event, eventElement); + } + } + + } + + function slotSegHtml(event, seg, className) { + return "
" + + "" + + "" + + "" + htmlEscape(formatDates(event.start, event.end, view.option('timeFormat'))) + "" + + "" + htmlEscape(event.title) + "" + + "" + + ((event.editable || event.editable === undefined && options.editable) && !options.disableResizing && $.fn.resizable ? + "
=
" + : '') + + "
"; + } + + + function daySegBind(event, eventElement, seg) { + view.eventElementHandlers(event, eventElement); + if (event.editable || event.editable === undefined && options.editable) { + draggableDayEvent(event, eventElement, seg.isStart); + if (seg.isEnd) { + view.resizableDayEvent(event, eventElement, colWidth); + } + } + } + + + function slotSegBind(event, eventElement, seg) { + view.eventElementHandlers(event, eventElement); + if (event.editable || event.editable === undefined && options.editable) { + var timeElement = eventElement.find('span.fc-event-time'); + draggableSlotEvent(event, eventElement, timeElement); + if (seg.isEnd) { + resizableSlotEvent(event, eventElement, timeElement); + } + } + } + + + /* Event Dragging + -----------------------------------------------------------------------------*/ + + + // when event starts out FULL-DAY + + function draggableDayEvent(event, eventElement, isStart) { + if (!options.disableDragging && eventElement.draggable) { + var origPosition, origWidth, + resetElement, + allDay = true, + matrix; + eventElement.draggable({ + zIndex: 9, + opacity: view.option('dragOpacity', 'month'), // use whatever the month view was using + revertDuration: options.dragRevertDuration, + start: function(ev, ui) { + view.hideEvents(event, eventElement); + view.trigger('eventDragStart', eventElement, event, ev, ui); + origPosition = eventElement.position(); + origWidth = eventElement.width(); + resetElement = function() { + if (!allDay) { + eventElement + .width(origWidth) + .height('') + .draggable('option', 'grid', null); + allDay = true; + } + }; + matrix = buildDayMatrix(function(cell) { + eventElement.draggable('option', 'revert', !cell || !cell.rowDelta && !cell.colDelta); + view.clearOverlays(); + if (cell) { + if (!cell.row) { + // on full-days + renderDayOverlay( + matrix, + addDays(cloneDate(event.start), cell.colDelta), + addDays(exclEndDay(event), cell.colDelta) + ); + resetElement(); + } else { + // mouse is over bottom slots + if (isStart && allDay) { + // convert event to temporary slot-event + setOuterHeight( + eventElement.width(colWidth - 10), // don't use entire width + slotHeight * Math.round( + (event.end ? ((event.end - event.start) / MINUTE_MS) : options.defaultEventMinutes) + / options.slotMinutes) + ); + eventElement.draggable('option', 'grid', [colWidth, 1]); + allDay = false; + } + } + } + }, true); + matrix.mouse(ev); + }, + drag: function(ev, ui) { + matrix.mouse(ev); + }, + stop: function(ev, ui) { + view.trigger('eventDragStop', eventElement, event, ev, ui); + view.clearOverlays(); + var cell = matrix.cell; + var dayDelta = dis * ( + allDay ? // can't trust cell.colDelta when using slot grid + (cell ? cell.colDelta : 0) : + Math.floor((ui.position.left - origPosition.left) / colWidth) + ); + if (!cell || !dayDelta && !cell.rowDelta) { + // over nothing (has reverted) + resetElement(); + if ($.browser.msie) { + eventElement.css('filter', ''); // clear IE opacity side-effects + } + view.showEvents(event, eventElement); + } else { + eventElement.find('a').removeAttr('href'); // prevents safari from visiting the link + view.eventDrop( + this, event, dayDelta, + allDay ? 0 : // minute delta + Math.round((eventElement.offset().top - bodyContent.offset().top) / slotHeight) + * options.slotMinutes + + minMinute + - (event.start.getHours() * 60 + event.start.getMinutes()), + allDay, ev, ui + ); + } + } + }); + } + } + + + // when event starts out IN TIMESLOTS + + function draggableSlotEvent(event, eventElement, timeElement) { + if (!options.disableDragging && eventElement.draggable) { + var origPosition, + resetElement, + prevSlotDelta, slotDelta, + allDay = false, + matrix; + eventElement.draggable({ + zIndex: 9, + scroll: false, + grid: [colWidth, slotHeight], + axis: colCnt == 1 ? 'y' : false, + opacity: view.option('dragOpacity'), + revertDuration: options.dragRevertDuration, + start: function(ev, ui) { + view.hideEvents(event, eventElement); + view.trigger('eventDragStart', eventElement, event, ev, ui); + if ($.browser.msie) { + eventElement.find('span.fc-event-bg').hide(); // nested opacities mess up in IE, just hide + } + origPosition = eventElement.position(); + resetElement = function() { + // convert back to original slot-event + if (allDay) { + timeElement.css('display', ''); // show() was causing display=inline + eventElement.draggable('option', 'grid', [colWidth, slotHeight]); + allDay = false; + } + }; + prevSlotDelta = 0; + matrix = buildDayMatrix(function(cell) { + eventElement.draggable('option', 'revert', !cell); + view.clearOverlays(); + if (cell) { + if (!cell.row && options.allDaySlot) { // over full days + if (!allDay) { + // convert to temporary all-day event + allDay = true; + timeElement.hide(); + eventElement.draggable('option', 'grid', null); + } + renderDayOverlay( + matrix, + addDays(cloneDate(event.start), cell.colDelta), + addDays(exclEndDay(event), cell.colDelta) + ); + } else { // on slots + resetElement(); + } + } + }, true); + matrix.mouse(ev); + }, + drag: function(ev, ui) { + slotDelta = Math.round((ui.position.top - origPosition.top) / slotHeight); + if (slotDelta != prevSlotDelta) { + if (!allDay) { + // update time header + var minuteDelta = slotDelta * options.slotMinutes, + newStart = addMinutes(cloneDate(event.start), minuteDelta), + newEnd; + if (event.end) { + newEnd = addMinutes(cloneDate(event.end), minuteDelta); + } + timeElement.text(formatDates(newStart, newEnd, view.option('timeFormat'))); + } + prevSlotDelta = slotDelta; + } + matrix.mouse(ev); + }, + stop: function(ev, ui) { + view.clearOverlays(); + view.trigger('eventDragStop', eventElement, event, ev, ui); + var cell = matrix.cell, + dayDelta = dis * ( + allDay ? // can't trust cell.colDelta when using slot grid + (cell ? cell.colDelta : 0) : + Math.floor((ui.position.left - origPosition.left) / colWidth) + ); + if (!cell || !slotDelta && !dayDelta) { + resetElement(); + if ($.browser.msie) { + eventElement + .css('filter', '')// clear IE opacity side-effects + .find('span.fc-event-bg').css('display', ''); // .show() made display=inline + } + eventElement.css(origPosition); // sometimes fast drags make event revert to wrong position + view.showEvents(event, eventElement); + } else { + view.eventDrop( + this, event, dayDelta, + allDay ? 0 : slotDelta * options.slotMinutes, // minute delta + allDay, ev, ui + ); + } + } + }); + } + } + + + /* Event Resizing + -----------------------------------------------------------------------------*/ + + // for TIMESLOT events + + function resizableSlotEvent(event, eventElement, timeElement) { + if (!options.disableResizing && eventElement.resizable) { + var slotDelta, prevSlotDelta; + eventElement.resizable({ + handles: { + s: 'div.ui-resizable-s' + }, + grid: slotHeight, + start: function(ev, ui) { + slotDelta = prevSlotDelta = 0; + view.hideEvents(event, eventElement); + if ($.browser.msie && $.browser.version == '6.0') { + eventElement.css('overflow', 'hidden'); + } + eventElement.css('z-index', 9); + view.trigger('eventResizeStart', this, event, ev, ui); + }, + resize: function(ev, ui) { + // don't rely on ui.size.height, doesn't take grid into account + slotDelta = Math.round((Math.max(slotHeight, eventElement.height()) - ui.originalSize.height) / slotHeight); + if (slotDelta != prevSlotDelta) { + timeElement.text( + formatDates( + event.start, + (!slotDelta && !event.end) ? null : // no change, so don't display time range + addMinutes(view.eventEnd(event), options.slotMinutes * slotDelta), + view.option('timeFormat') + ) + ); + prevSlotDelta = slotDelta; + } + }, + stop: function(ev, ui) { + view.trigger('eventResizeStop', this, event, ev, ui); + if (slotDelta) { + view.eventResize(this, event, 0, options.slotMinutes * slotDelta, ev, ui); + } else { + eventElement.css('z-index', 8); + view.showEvents(event, eventElement); + // BUG: if event was really short, need to put title back in span + } + } + }); + } + } + + + /* Selecting + -----------------------------------------------------------------------------*/ + + daySelectionManager = new SelectionManager( + view, + unselect, + function(startDate, endDate, allDay) { + renderDayOverlay( + selectionMatrix, + startDate, + addDays(cloneDate(endDate), 1) + ); + }, + clearSelection + ); + + function daySelectionMousedown(ev) { + if (view.option('selectable')) { + selectionMatrix = buildDayMatrix(function(cell) { + if (cell) { + var d = dayColDate(cell.col); + daySelectionManager.drag(d, d, true); + } else { + daySelectionManager.drag(); + } + }); + documentDragHelp( + function(ev) { + selectionMatrix.mouse(ev); + }, + function(ev) { + daySelectionManager.dragStop(ev); + } + ); + daySelectionManager.dragStart(ev); + selectionMatrix.mouse(ev); + return false; // prevent auto-unselect and text selection + } + } + + slotSelectionManager = new SelectionManager( + view, + unselect, + renderSlotSelection, + clearSelection + ); + + function slotSelectionMousedown(ev) { + if (view.option('selectable')) { + selectionMatrix = buildSlotMatrix(function(cell) { + if (cell) { + var d = slotCellDate(cell.row, cell.origCol); + slotSelectionManager.drag(d, addMinutes(cloneDate(d), options.slotMinutes), false); + } else { + slotSelectionManager.drag(); + } + }); + documentDragHelp( + function(ev) { + selectionMatrix.mouse(ev); + }, + function(ev) { + slotSelectionManager.dragStop(ev); + } + ); + slotSelectionManager.dragStart(ev); + selectionMatrix.mouse(ev); + return false; // prevent auto-unselect and text selection + } + } + + documentUnselectAuto(view, unselect); + + this.select = function(start, end, allDay) { + if (allDay) { + if (options.allDaySlot) { + if (!end) { + end = cloneDate(start); + } + selectionMatrix = buildDayMatrix(); + daySelectionManager.select(start, end, allDay); + } + } else { + if (!end) { + end = addMinutes(cloneDate(start), options.slotMinutes); + } + selectionMatrix = buildSlotMatrix(); + slotSelectionManager.select(start, end, allDay); + } + }; + + function unselect() { + slotSelectionManager.unselect(); + daySelectionManager.unselect(); + } + + this.unselect = unselect; + + + /* Selecting drawing utils + -----------------------------------------------------------------------------*/ + + function renderSlotSelection(startDate, endDate) { + var helperOption = view.option('selectHelper'); + if (helperOption) { + var col = dayDiff(startDate, view.visStart); + if (col >= 0 && col < colCnt) { // only works when times are on same day + var rect = selectionMatrix.rect(0, col * dis + dit, 1, col * dis + dit + 1, bodyContent); // 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'; + rect.zIndex = 8; + selectionHelper = $(helperRes) + .css(rect) + .appendTo(bodyContent); + } + } else { + selectionHelper = $(slotSegHtml( + { + title: '', + start: startDate, + end: endDate, + className: [], + editable: false + }, + rect, + 'fc-event fc-event-vert fc-corner-top fc-corner-bottom ' + )); + if ($.browser.msie) { + selectionHelper.find('span.fc-event-bg').hide(); // nested opacities mess up in IE, just hide + } + selectionHelper.css('opacity', view.option('dragOpacity')); + } + if (selectionHelper) { + slotBind(selectionHelper); + bodyContent.append(selectionHelper); + setOuterWidth(selectionHelper, rect.width, true); // needs to be after appended + setOuterHeight(selectionHelper, rect.height, true); + } + } + } + } else { + renderSlotOverlay(selectionMatrix, startDate, endDate); + } + } + + function clearSelection() { + clearOverlays(); + if (selectionHelper) { + selectionHelper.remove(); + selectionHelper = null; + } + } + + + /* Semi-transparent Overlay Helpers + -----------------------------------------------------*/ + + function renderDayOverlay(matrix, startDate, endDate) { + var startCol, endCol; + if (rtl) { + startCol = dayDiff(endDate, view.visStart) * dis + dit + 1; + endCol = dayDiff(startDate, view.visStart) * dis + dit + 1; + } else { + startCol = dayDiff(startDate, view.visStart); + endCol = dayDiff(endDate, view.visStart); + } + startCol = Math.max(0, startCol); + endCol = Math.min(colCnt, endCol); + if (startCol < endCol) { + var rect = matrix.rect(0, startCol, 1, endCol, head); + dayBind( + view.renderOverlay(rect, head) + ); + } + } + + function renderSlotOverlay(matrix, overlayStart, overlayEnd) { + var dayStart = cloneDate(view.visStart); + var dayEnd = addDays(cloneDate(dayStart), 1); + for (var i = 0; i < colCnt; i++) { + var stretchStart = new Date(Math.max(dayStart, overlayStart)); + var stretchEnd = new Date(Math.min(dayEnd, overlayEnd)); + if (stretchStart < stretchEnd) { + var rect = matrix.rect(0, i * dis + dit, 1, i * dis + dit + 1, bodyContent); // only use it for horizontal coords + var top = timePosition(dayStart, stretchStart); + var bottom = timePosition(dayStart, stretchEnd); + rect.top = top; + rect.height = bottom - top; + slotBind( + view.renderOverlay(rect, bodyContent) + ); + } + addDays(dayStart, 1); + addDays(dayEnd, 1); + } + } + + function clearOverlays() { + view.clearOverlays(); + } + + + /* Coordinate Utilities + -----------------------------------------------------------------------------*/ + + // 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 bodyContent.height(); + } + var slotMinutes = options.slotMinutes, + minutes = time.getHours() * 60 + time.getMinutes() - minMinute, + slotI = Math.floor(minutes / slotMinutes), + slotTop = slotTopCache[slotI]; + if (slotTop === undefined) { + slotTop = slotTopCache[slotI] = body.find('tr:eq(' + slotI + ') td div')[0].offsetTop; + } + return Math.max(0, Math.round( + slotTop - 1 + slotHeight * ((minutes % slotMinutes) / slotMinutes) + )); + } + + function buildDayMatrix(changeCallback, includeSlotArea) { + var rowElements = options.allDaySlot ? head.find('td') : $([]); + if (includeSlotArea) { + rowElements = rowElements.add(body); + } + return new HoverMatrix(rowElements, bg.find('td'), changeCallback); + } + + function buildSlotMatrix(changeCallback) { + return new HoverMatrix(bodyTable.find('td'), bg.find('td'), changeCallback); + } + + + /* Date Utilities + ----------------------------------------------------*/ + + function slotEventEnd(event) { + if (event.end) { + return cloneDate(event.end); + } else { + return addMinutes(cloneDate(event.start), options.defaultEventMinutes); + } + } + + function dayOfWeekCol(dayOfWeek) { + return ((dayOfWeek - Math.max(firstDay, nwe) + colCnt) % colCnt) * dis + dit; + } + + + // generating dates from cell row & columns + + function dayColDate(col) { + return addDays(cloneDate(view.visStart), col * dis + dit); + } + + function slotCellDate(row, col) { + var d = dayColDate(col); + addMinutes(d, minMinute + row * options.slotMinutes); + return d; + } + + + } + + + // count the number of colliding, higher-level segments (for event squishing) + + function countForwardSegs(levels) { + var i, j, k, level, segForward, segBack; + for (i = levels.length - 1; i > 0; i--) { + level = levels[i]; + for (j = 0; j < level.length; j++) { + segForward = level[j]; + for (k = 0; k < levels[i - 1].length; k++) { + segBack = levels[i - 1][k]; + if (segsCollide(segForward, segBack)) { + segBack.forward = Math.max(segBack.forward || 0, (segForward.forward || 0) + 1); + } + } + } + } + } + + + /* Methods & Utilities for All Views + -----------------------------------------------------------------------------*/ + + var viewMethods = { + + /* + * Objects inheriting these methods must implement the following properties/methods: + * - title + * - start + * - end + * - visStart + * - visEnd + * - defaultEventEnd(event) + * - render(events) + * - rerenderEvents() + * + * + * z-index reservations: + * 3 - day-overlay + * 8 - events + * 9 - dragging/resizing events + * + */ + + + + init: function(element, options) { + this.element = element; + this.options = options; + this.eventsByID = {}; + this.eventElements = []; + this.eventElementsByID = {}; + this.usedOverlays = []; + this.unusedOverlays = []; + }, - init: function(element, options) { - this.element = element; - this.options = options; - this.eventsByID = {}; - this.eventElements = []; - this.eventElementsByID = {}; - }, - - - - // triggers an event handler, always append view as last arg - - trigger: function(name, thisObj) { - if (this.options[name]) { - return this.options[name].apply(thisObj || this, Array.prototype.slice.call(arguments, 2).concat([this])); - } - }, - - - - // returns a Date object for an event's end - - eventEnd: function(event) { - return event.end ? cloneDate(event.end) : this.defaultEventEnd(event); // TODO: make sure always using copies - }, - - - - // report when view receives new events - - reportEvents: function(events) { // events are already normalized at this point - var i, len=events.length, event, - eventsByID = this.eventsByID = {}; - for (i=0; i