From e4dd8d37ee6db46e97f467bd5c6d5f2a5dab4dc0 Mon Sep 17 00:00:00 2001 From: sean kenny Date: Tue, 23 Sep 2014 13:49:34 +0100 Subject: [PATCH] Fix for issue 34 --- dist/cdnjs/2.0.2/fullcalendar.css | 601 ++ dist/cdnjs/2.0.2/fullcalendar.js | 9289 +++++++++++++++++++++++ dist/cdnjs/2.0.2/fullcalendar.min.js | 8 + dist/cdnjs/2.0.2/fullcalendar.print.css | 32 + dist/cdnjs/2.0.2/gcal.js | 100 + dist/cdnjs/2.0.2/lang-all.js | 3 + dist/cdnjs/2.0.2/lang/ar-ma.js | 1 + dist/cdnjs/2.0.2/lang/ar-sa.js | 1 + dist/cdnjs/2.0.2/lang/ar.js | 1 + dist/cdnjs/2.0.2/lang/bg.js | 1 + dist/cdnjs/2.0.2/lang/ca.js | 1 + dist/cdnjs/2.0.2/lang/cs.js | 1 + dist/cdnjs/2.0.2/lang/da.js | 1 + dist/cdnjs/2.0.2/lang/de-at.js | 1 + dist/cdnjs/2.0.2/lang/de.js | 1 + dist/cdnjs/2.0.2/lang/el.js | 1 + dist/cdnjs/2.0.2/lang/en-au.js | 1 + dist/cdnjs/2.0.2/lang/en-ca.js | 1 + dist/cdnjs/2.0.2/lang/en-gb.js | 1 + dist/cdnjs/2.0.2/lang/es.js | 1 + dist/cdnjs/2.0.2/lang/fa.js | 1 + dist/cdnjs/2.0.2/lang/fi.js | 1 + dist/cdnjs/2.0.2/lang/fr-ca.js | 1 + dist/cdnjs/2.0.2/lang/fr.js | 1 + dist/cdnjs/2.0.2/lang/hi.js | 1 + dist/cdnjs/2.0.2/lang/hr.js | 1 + dist/cdnjs/2.0.2/lang/hu.js | 1 + dist/cdnjs/2.0.2/lang/is.js | 1 + dist/cdnjs/2.0.2/lang/it.js | 1 + dist/cdnjs/2.0.2/lang/ja.js | 1 + dist/cdnjs/2.0.2/lang/ko.js | 1 + dist/cdnjs/2.0.2/lang/lt.js | 1 + dist/cdnjs/2.0.2/lang/lv.js | 1 + dist/cdnjs/2.0.2/lang/nl.js | 1 + dist/cdnjs/2.0.2/lang/pl.js | 1 + dist/cdnjs/2.0.2/lang/pt-br.js | 1 + dist/cdnjs/2.0.2/lang/pt.js | 1 + dist/cdnjs/2.0.2/lang/ro.js | 1 + dist/cdnjs/2.0.2/lang/ru.js | 1 + dist/cdnjs/2.0.2/lang/sk.js | 1 + dist/cdnjs/2.0.2/lang/sl.js | 1 + dist/cdnjs/2.0.2/lang/sr-cyrl.js | 1 + dist/cdnjs/2.0.2/lang/sr.js | 1 + dist/cdnjs/2.0.2/lang/sv.js | 1 + dist/cdnjs/2.0.2/lang/th.js | 1 + dist/cdnjs/2.0.2/lang/tr.js | 1 + dist/cdnjs/2.0.2/lang/uk.js | 1 + dist/cdnjs/2.0.2/lang/vi.js | 1 + dist/cdnjs/2.0.2/lang/zh-cn.js | 1 + dist/cdnjs/2.0.2/lang/zh-tw.js | 1 + dist/cdnjs/package.json | 37 + dist/fullcalendar-2.0.2.zip | Bin 0 -> 311775 bytes dist/fullcalendar.css | 6 +- dist/fullcalendar.js | 11 +- dist/fullcalendar.min.js | 2 +- dist/fullcalendar.print.css | 6 +- dist/gcal.js | 6 +- dist/lang-all.js | 6 +- dist/lang/ar-ma.js | 2 +- dist/lang/ar-sa.js | 2 +- dist/lang/ar.js | 2 +- dist/lang/bg.js | 2 +- dist/lang/ca.js | 2 +- dist/lang/cs.js | 2 +- dist/lang/de-at.js | 2 +- dist/lang/de.js | 2 +- dist/lang/el.js | 2 +- dist/lang/en-au.js | 2 +- dist/lang/en-ca.js | 2 +- dist/lang/en-gb.js | 2 +- dist/lang/es.js | 2 +- dist/lang/fa.js | 2 +- dist/lang/fi.js | 2 +- dist/lang/fr-ca.js | 2 +- dist/lang/fr.js | 2 +- dist/lang/hi.js | 2 +- dist/lang/hr.js | 2 +- dist/lang/hu.js | 2 +- dist/lang/is.js | 2 +- dist/lang/it.js | 2 +- dist/lang/ja.js | 2 +- dist/lang/ko.js | 2 +- dist/lang/lt.js | 2 +- dist/lang/lv.js | 2 +- dist/lang/nl.js | 2 +- dist/lang/pl.js | 2 +- dist/lang/pt-br.js | 2 +- dist/lang/pt.js | 2 +- dist/lang/ro.js | 2 +- dist/lang/ru.js | 2 +- dist/lang/sk.js | 2 +- dist/lang/sl.js | 2 +- dist/lang/sr-cyrl.js | 2 +- dist/lang/sr.js | 2 +- dist/lang/sv.js | 2 +- dist/lang/th.js | 2 +- dist/lang/tr.js | 2 +- dist/lang/uk.js | 2 +- dist/lang/vi.js | 2 +- dist/lang/zh-cn.js | 2 +- dist/lang/zh-tw.js | 2 +- src/resource/ResourceView.js | 3 +- tests/resourceDayView.html | 7 +- 103 files changed, 10182 insertions(+), 65 deletions(-) create mode 100644 dist/cdnjs/2.0.2/fullcalendar.css create mode 100644 dist/cdnjs/2.0.2/fullcalendar.js create mode 100644 dist/cdnjs/2.0.2/fullcalendar.min.js create mode 100644 dist/cdnjs/2.0.2/fullcalendar.print.css create mode 100644 dist/cdnjs/2.0.2/gcal.js create mode 100644 dist/cdnjs/2.0.2/lang-all.js create mode 100644 dist/cdnjs/2.0.2/lang/ar-ma.js create mode 100644 dist/cdnjs/2.0.2/lang/ar-sa.js create mode 100644 dist/cdnjs/2.0.2/lang/ar.js create mode 100644 dist/cdnjs/2.0.2/lang/bg.js create mode 100644 dist/cdnjs/2.0.2/lang/ca.js create mode 100644 dist/cdnjs/2.0.2/lang/cs.js create mode 100644 dist/cdnjs/2.0.2/lang/da.js create mode 100644 dist/cdnjs/2.0.2/lang/de-at.js create mode 100644 dist/cdnjs/2.0.2/lang/de.js create mode 100644 dist/cdnjs/2.0.2/lang/el.js create mode 100644 dist/cdnjs/2.0.2/lang/en-au.js create mode 100644 dist/cdnjs/2.0.2/lang/en-ca.js create mode 100644 dist/cdnjs/2.0.2/lang/en-gb.js create mode 100644 dist/cdnjs/2.0.2/lang/es.js create mode 100644 dist/cdnjs/2.0.2/lang/fa.js create mode 100644 dist/cdnjs/2.0.2/lang/fi.js create mode 100644 dist/cdnjs/2.0.2/lang/fr-ca.js create mode 100644 dist/cdnjs/2.0.2/lang/fr.js create mode 100644 dist/cdnjs/2.0.2/lang/hi.js create mode 100644 dist/cdnjs/2.0.2/lang/hr.js create mode 100644 dist/cdnjs/2.0.2/lang/hu.js create mode 100644 dist/cdnjs/2.0.2/lang/is.js create mode 100644 dist/cdnjs/2.0.2/lang/it.js create mode 100644 dist/cdnjs/2.0.2/lang/ja.js create mode 100644 dist/cdnjs/2.0.2/lang/ko.js create mode 100644 dist/cdnjs/2.0.2/lang/lt.js create mode 100644 dist/cdnjs/2.0.2/lang/lv.js create mode 100644 dist/cdnjs/2.0.2/lang/nl.js create mode 100644 dist/cdnjs/2.0.2/lang/pl.js create mode 100644 dist/cdnjs/2.0.2/lang/pt-br.js create mode 100644 dist/cdnjs/2.0.2/lang/pt.js create mode 100644 dist/cdnjs/2.0.2/lang/ro.js create mode 100644 dist/cdnjs/2.0.2/lang/ru.js create mode 100644 dist/cdnjs/2.0.2/lang/sk.js create mode 100644 dist/cdnjs/2.0.2/lang/sl.js create mode 100644 dist/cdnjs/2.0.2/lang/sr-cyrl.js create mode 100644 dist/cdnjs/2.0.2/lang/sr.js create mode 100644 dist/cdnjs/2.0.2/lang/sv.js create mode 100644 dist/cdnjs/2.0.2/lang/th.js create mode 100644 dist/cdnjs/2.0.2/lang/tr.js create mode 100644 dist/cdnjs/2.0.2/lang/uk.js create mode 100644 dist/cdnjs/2.0.2/lang/vi.js create mode 100644 dist/cdnjs/2.0.2/lang/zh-cn.js create mode 100644 dist/cdnjs/2.0.2/lang/zh-tw.js create mode 100644 dist/cdnjs/package.json create mode 100644 dist/fullcalendar-2.0.2.zip diff --git a/dist/cdnjs/2.0.2/fullcalendar.css b/dist/cdnjs/2.0.2/fullcalendar.css new file mode 100644 index 0000000..4a65f08 --- /dev/null +++ b/dist/cdnjs/2.0.2/fullcalendar.css @@ -0,0 +1,601 @@ +/*! + * FullCalendar v2.0.2 Stylesheet + * Docs & License: http://arshaw.com/fullcalendar/ + * (c) 2014 Adam Shaw, Sean Kenny + */ + + +.fc { + direction: ltr; + text-align: left; + } + +.fc table { + border-collapse: collapse; + border-spacing: 0; + } + +html .fc, +.fc table { + font-size: 1em; + } + +.fc td, +.fc th { + padding: 0; + vertical-align: top; + } + + + +/* Header +------------------------------------------------------------------------*/ + +.fc-header td { + white-space: nowrap; + } + +.fc-header-left { + width: 25%; + text-align: left; + } + +.fc-header-center { + text-align: center; + } + +.fc-header-right { + width: 25%; + text-align: right; + } + +.fc-header-title { + display: inline-block; + vertical-align: top; + } + +.fc-header-title h2 { + margin-top: 0; + white-space: nowrap; + } + +.fc .fc-header-space { + padding-left: 10px; + } + +.fc-header .fc-button { + margin-bottom: 1em; + vertical-align: top; + } + +/* buttons edges butting together */ + +.fc-header .fc-button { + margin-right: -1px; + } + +.fc-header .fc-corner-right, /* non-theme */ +.fc-header .ui-corner-right { /* theme */ + margin-right: 0; /* back to normal */ + } + +/* button layering (for border precedence) */ + +.fc-header .fc-state-hover, +.fc-header .ui-state-hover { + z-index: 2; + } + +.fc-header .fc-state-down { + z-index: 3; + } + +.fc-header .fc-state-active, +.fc-header .ui-state-active { + z-index: 4; + } + + + +/* Content +------------------------------------------------------------------------*/ + +.fc-content { + position: relative; + z-index: 1; /* scopes all other z-index's to be inside this container */ + clear: both; + zoom: 1; /* for IE7, gives accurate coordinates for [un]freezeContentHeight */ + } + +.fc-view { + position: relative; + width: 100%; + overflow: hidden; + } + + + +/* Cell Styles +------------------------------------------------------------------------*/ + +.fc-widget-header, /* , usually */ +.fc-widget-content { /* , usually */ + border: 1px solid #ddd; + } + +.fc-state-highlight { /* today cell */ /* TODO: add .fc-today to */ + background: #fcf8e3; + } + +.fc-cell-overlay { /* semi-transparent rectangle while dragging */ + background: #bce8f1; + opacity: .3; + filter: alpha(opacity=30); /* for IE */ + } + + + +/* Buttons +------------------------------------------------------------------------*/ + +.fc-button { + position: relative; + display: inline-block; + padding: 0 .6em; + overflow: hidden; + height: 1.9em; + line-height: 1.9em; + white-space: nowrap; + cursor: pointer; + } + +.fc-state-default { /* non-theme */ + border: 1px solid; + } + +.fc-state-default.fc-corner-left { /* non-theme */ + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; + } + +.fc-state-default.fc-corner-right { /* non-theme */ + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + } + +/* + Our default prev/next buttons use HTML entities like ‹ › « » + and we'll try to make them look good cross-browser. +*/ + +.fc-button .fc-icon { + margin: 0 .1em; + font-size: 2em; + font-family: "Courier New", Courier, monospace; + vertical-align: baseline; /* for IE7 */ + } + +.fc-icon-left-single-arrow:after { + content: "\02039"; + font-weight: bold; + } + +.fc-icon-right-single-arrow:after { + content: "\0203A"; + font-weight: bold; + } + +.fc-icon-left-double-arrow:after { + content: "\000AB"; + } + +.fc-icon-right-double-arrow:after { + content: "\000BB"; + } + +/* icon (for jquery ui) */ + +.fc-button .ui-icon { + position: relative; + top: 50%; + float: left; + margin-top: -8px; /* we know jqui icons are always 16px tall */ + } + +/* + button states + borrowed from twitter bootstrap (http://twitter.github.com/bootstrap/) +*/ + +.fc-state-default { + background-color: #f5f5f5; + background-image: -moz-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), to(#e6e6e6)); + background-image: -webkit-linear-gradient(top, #ffffff, #e6e6e6); + background-image: -o-linear-gradient(top, #ffffff, #e6e6e6); + background-image: linear-gradient(to bottom, #ffffff, #e6e6e6); + background-repeat: repeat-x; + border-color: #e6e6e6 #e6e6e6 #bfbfbf; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + color: #333; + text-shadow: 0 1px 1px rgba(255, 255, 255, 0.75); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); + } + +.fc-state-hover, +.fc-state-down, +.fc-state-active, +.fc-state-disabled { + color: #333333; + background-color: #e6e6e6; + } + +.fc-state-hover { + color: #333333; + text-decoration: none; + background-position: 0 -15px; + -webkit-transition: background-position 0.1s linear; + -moz-transition: background-position 0.1s linear; + -o-transition: background-position 0.1s linear; + transition: background-position 0.1s linear; + } + +.fc-state-down, +.fc-state-active { + background-color: #cccccc; + background-image: none; + outline: 0; + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); + } + +.fc-state-disabled { + cursor: default; + background-image: none; + opacity: 0.65; + filter: alpha(opacity=65); + box-shadow: none; + } + + + +/* Global Event Styles +------------------------------------------------------------------------*/ + +.fc-event-container > * { + z-index: 8; + } + +.fc-event-container > .ui-draggable-dragging, +.fc-event-container > .ui-resizable-resizing { + z-index: 9; + } + +.fc-event { + border: 1px solid #3a87ad; /* default BORDER color */ + background-color: #3a87ad; /* default BACKGROUND color */ + color: #fff; /* default TEXT color */ + font-size: .85em; + cursor: default; + } + +a.fc-event { + text-decoration: none; + } + +a.fc-event, +.fc-event-draggable { + cursor: pointer; + } + +.fc-rtl .fc-event { + text-align: right; + } + +.fc-event-inner { + width: 100%; + height: 100%; + overflow: hidden; + } + +.fc-event-time, +.fc-event-title { + padding: 0 1px; + } + +.fc .ui-resizable-handle { + display: block; + position: absolute; + z-index: 99999; + overflow: hidden; /* hacky spaces (IE6/7) */ + font-size: 300%; /* */ + line-height: 50%; /* */ + } + + + +/* Horizontal Events +------------------------------------------------------------------------*/ + +.fc-event-hori { + border-width: 1px 0; + margin-bottom: 1px; + } + +.fc-ltr .fc-event-hori.fc-event-start, +.fc-rtl .fc-event-hori.fc-event-end { + border-left-width: 1px; + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; + } + +.fc-ltr .fc-event-hori.fc-event-end, +.fc-rtl .fc-event-hori.fc-event-start { + border-right-width: 1px; + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; + } + +/* resizable */ + +.fc-event-hori .ui-resizable-e { + top: 0 !important; /* importants override pre jquery ui 1.7 styles */ + right: -3px !important; + width: 7px !important; + height: 100% !important; + cursor: e-resize; + } + +.fc-event-hori .ui-resizable-w { + top: 0 !important; + left: -3px !important; + width: 7px !important; + height: 100% !important; + cursor: w-resize; + } + +.fc-event-hori .ui-resizable-handle { + _padding-bottom: 14px; /* IE6 had 0 height */ + } + + + +/* Reusable Separate-border Table +------------------------------------------------------------*/ + +table.fc-border-separate { + border-collapse: separate; + } + +.fc-border-separate th, +.fc-border-separate td { + border-width: 1px 0 0 1px; + } + +.fc-border-separate th.fc-last, +.fc-border-separate td.fc-last { + border-right-width: 1px; + } + +.fc-border-separate tr.fc-last th, +.fc-border-separate tr.fc-last td { + border-bottom-width: 1px; + } + +.fc-border-separate tbody tr.fc-first td, +.fc-border-separate tbody tr.fc-first th { + border-top-width: 0; + } + + + +/* Month View, Basic Week View, Basic Day View +------------------------------------------------------------------------*/ + +.fc-grid th { + text-align: center; + } + +.fc .fc-week-number { + width: 22px; + text-align: center; + } + +.fc .fc-week-number div { + padding: 0 2px; + } + +.fc-grid .fc-day-number { + float: right; + padding: 0 2px; + } + +.fc-grid .fc-other-month .fc-day-number { + opacity: 0.3; + filter: alpha(opacity=30); /* for IE */ + /* opacity with small font can sometimes look too faded + might want to set the 'color' property instead + making day-numbers bold also fixes the problem */ + } + +.fc-grid .fc-day-content { + clear: both; + padding: 2px 2px 1px; /* distance between events and day edges */ + } + +/* event styles */ + +.fc-grid .fc-event-time { + font-weight: bold; + } + +/* right-to-left */ + +.fc-rtl .fc-grid .fc-day-number { + float: left; + } + +.fc-rtl .fc-grid .fc-event-time { + float: right; + } + + + +/* Agenda Week View, Agenda Day View +------------------------------------------------------------------------*/ + +.fc-agenda table { + border-collapse: separate; + } + +.fc-agenda-days th { + text-align: center; + } + +.fc-agenda .fc-agenda-axis { + width: 50px; + padding: 0 4px; + vertical-align: middle; + text-align: right; + font-weight: normal; + } + +.fc-agenda-slots .fc-agenda-axis { + white-space: nowrap; + } + +.fc-agenda .fc-week-number { + font-weight: bold; + } + +.fc-agenda .fc-day-content { + padding: 2px 2px 1px; + } + +/* make axis border take precedence */ + +.fc-agenda-days .fc-agenda-axis { + border-right-width: 1px; + } + +.fc-agenda-days .fc-col0 { + border-left-width: 0; + } + +/* all-day area */ + +.fc-agenda-allday th { + border-width: 0 1px; + } + +.fc-agenda-allday .fc-day-content { + min-height: 34px; /* TODO: doesnt work well in quirksmode */ + _height: 34px; + } + +/* divider (between all-day and slots) */ + +.fc-agenda-divider-inner { + height: 2px; + overflow: hidden; + } + +.fc-widget-header .fc-agenda-divider-inner { + background: #eee; + } + +/* slot rows */ + +.fc-agenda-slots th { + border-width: 1px 1px 0; + } + +.fc-agenda-slots td { + border-width: 1px 0 0; + background: none; + } + +.fc-agenda-slots td div { + height: 20px; + } + +.fc-agenda-slots tr.fc-slot0 th, +.fc-agenda-slots tr.fc-slot0 td { + border-top-width: 0; + } + +.fc-agenda-slots tr.fc-minor th, +.fc-agenda-slots tr.fc-minor td { + border-top-style: dotted; + } + +.fc-agenda-slots tr.fc-minor th.ui-widget-header { + *border-top-style: solid; /* doesn't work with background in IE6/7 */ + } + + + +/* Vertical Events +------------------------------------------------------------------------*/ + +.fc-event-vert { + border-width: 0 1px; + } + +.fc-event-vert.fc-event-start { + border-top-width: 1px; + border-top-left-radius: 3px; + border-top-right-radius: 3px; + } + +.fc-event-vert.fc-event-end { + border-bottom-width: 1px; + border-bottom-left-radius: 3px; + border-bottom-right-radius: 3px; + } + +.fc-event-vert .fc-event-time { + white-space: nowrap; + font-size: 10px; + } + +.fc-event-vert .fc-event-inner { + position: relative; + z-index: 2; + } + +.fc-event-vert .fc-event-bg { /* makes the event lighter w/ a semi-transparent overlay */ + position: absolute; + z-index: 1; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: #fff; + opacity: .25; + filter: alpha(opacity=25); + } + +.fc .ui-draggable-dragging .fc-event-bg, /* TODO: something nicer like .fc-opacity */ +.fc-select-helper .fc-event-bg { + display: none\9; /* for IE6/7/8. nested opacity filters while dragging don't work */ + } + +/* resizable */ + +.fc-event-vert .ui-resizable-s { + bottom: 0 !important; /* importants override pre jquery ui 1.7 styles */ + width: 100% !important; + height: 8px !important; + overflow: hidden !important; + line-height: 8px !important; + font-size: 11px !important; + font-family: monospace; + text-align: center; + cursor: s-resize; + } + +.fc-agenda .ui-resizable-resizing { /* TODO: better selector */ + _overflow: hidden; + } + + diff --git a/dist/cdnjs/2.0.2/fullcalendar.js b/dist/cdnjs/2.0.2/fullcalendar.js new file mode 100644 index 0000000..c1bed9a --- /dev/null +++ b/dist/cdnjs/2.0.2/fullcalendar.js @@ -0,0 +1,9289 @@ +/*! + * FullCalendar v2.0.2 + * Docs & License: http://arshaw.com/fullcalendar/ + * (c) 2014 Adam Shaw, Sean Kenny + */ + +(function(factory) { + if (typeof define === 'function' && define.amd) { + define([ 'jquery', 'moment' ], factory); + } + else { + factory(jQuery, moment); + } +})(function($, moment) { + +;; + +var defaults = { + + lang: 'en', + + defaultTimedEventDuration: '02:00:00', + defaultAllDayEventDuration: { days: 1 }, + forceEventDuration: false, + nextDayThreshold: '09:00:00', // 9am + + // display + defaultView: 'month', + aspectRatio: 1.35, + header: { + left: 'title', + center: '', + right: 'today prev,next' + }, + weekends: true, + weekNumbers: false, + + weekNumberTitle: 'W', + weekNumberCalculation: 'local', + + //editable: false, + + // event ajax + lazyFetching: true, + startParam: 'start', + endParam: 'end', + timezoneParam: 'timezone', + + timezone: false, + + //allDayDefault: undefined, + + // time formats + titleFormat: { + month: 'MMMM YYYY', // like "September 1986". each language will override this + week: 'll', // like "Sep 4 1986" + day: 'LL' // like "September 4 1986" + }, + columnFormat: { + month: 'ddd', // like "Sat" + week: generateWeekColumnFormat, + day: 'dddd' // like "Saturday" + }, + timeFormat: { // for event elements + 'default': generateShortTimeFormat + }, + + displayEventEnd: { + month: false, + basicWeek: false, + 'default': true + }, + + // locale + isRTL: false, + defaultButtonText: { + prev: "prev", + next: "next", + prevYear: "prev year", + nextYear: "next year", + today: 'today', + month: 'month', + week: 'week', + day: 'day' + }, + + buttonIcons: { + prev: 'left-single-arrow', + next: 'right-single-arrow', + prevYear: 'left-double-arrow', + nextYear: 'right-double-arrow' + }, + + // jquery-ui theming + theme: false, + themeButtonIcons: { + prev: 'circle-triangle-w', + next: 'circle-triangle-e', + prevYear: 'seek-prev', + nextYear: 'seek-next' + }, + + //selectable: false, + unselectAuto: true, + + dropAccept: '*', + + handleWindowResize: true, + windowResizeDelay: 200 // milliseconds before a rerender happens + +}; + + +function generateShortTimeFormat(options, langData) { + return langData.longDateFormat('LT') + .replace(':mm', '(:mm)') + .replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs + .replace(/\s*a$/i, 't'); // convert to AM/PM/am/pm to lowercase one-letter. remove any spaces beforehand +} + + +function generateWeekColumnFormat(options, langData) { + var format = langData.longDateFormat('L'); // for the format like "MM/DD/YYYY" + format = format.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g, ''); // strip the year off the edge, as well as other misc non-whitespace chars + if (options.isRTL) { + format += ' ddd'; // for RTL, add day-of-week to end + } + else { + format = 'ddd ' + format; // for LTR, add day-of-week to beginning + } + return format; +} + + +var langOptionHash = { + en: { + columnFormat: { + week: 'ddd M/D' // override for english. different from the generated default, which is MM/DD + } + } +}; + + +// right-to-left defaults +var rtlDefaults = { + header: { + left: 'next,prev today', + center: '', + right: 'title' + }, + buttonIcons: { + prev: 'right-single-arrow', + next: 'left-single-arrow', + prevYear: 'right-double-arrow', + nextYear: 'left-double-arrow' + }, + themeButtonIcons: { + prev: 'circle-triangle-e', + next: 'circle-triangle-w', + nextYear: 'seek-prev', + prevYear: 'seek-next' + } +}; + + + +;; + +var fc = $.fullCalendar = { version: "2.0.2" }; +var fcViews = fc.views = {}; + + +$.fn.fullCalendar = function(options) { + var args = Array.prototype.slice.call(arguments, 1); // for a possible method call + var res = this; // what this function will return (this jQuery object by default) + + this.each(function(i, _element) { // loop each DOM element involved + var element = $(_element); + var calendar = element.data('fullCalendar'); // get the existing calendar object (if any) + var singleRes; // the returned value of this single method call + + // a method call + if (typeof options === 'string') { + if (calendar && $.isFunction(calendar[options])) { + singleRes = calendar[options].apply(calendar, args); + if (!i) { + res = singleRes; // record the first method call result + } + if (options === 'destroy') { // for the destroy method, must remove Calendar object data + element.removeData('fullCalendar'); + } + } + } + // a new calendar initialization + else if (!calendar) { // don't initialize twice + calendar = new Calendar(element, options); + element.data('fullCalendar', calendar); + calendar.render(); + } + }); + + return res; +}; + + +// function for adding/overriding defaults +function setDefaults(d) { + mergeOptions(defaults, d); +} + + +// Recursively combines option hash-objects. +// Better than `$.extend(true, ...)` because arrays are not traversed/copied. +// +// called like: +// mergeOptions(target, obj1, obj2, ...) +// +function mergeOptions(target) { + + function mergeIntoTarget(name, value) { + if ($.isPlainObject(value) && $.isPlainObject(target[name]) && !isForcedAtomicOption(name)) { + // merge into a new object to avoid destruction + target[name] = mergeOptions({}, target[name], value); // combine. `value` object takes precedence + } + else if (value !== undefined) { // only use values that are set and not undefined + target[name] = value; + } + } + + for (var i=1; i") + .prependTo(element); + + header = new Header(t, options); + headerElement = header.render(); + if (headerElement) { + element.prepend(headerElement); + } + + changeView(options.defaultView); + + if (options.handleWindowResize) { + $(window).resize(windowResize); + } + + // 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 (!currentView.start && bodyVisible()) { // !currentView.start makes sure this never happens more than once + renderView(); + } + },0); + } + + + function destroy() { + + if (currentView) { + trigger('viewDestroy', currentView, currentView, currentView.element); + currentView.triggerEventDestroy(); + } + + $(window).unbind('resize', windowResize); + + if (options.droppable) { + $(document) + .off('dragstart', droppableDragStart) + .off('dragstop', droppableDragStop); + } + + if (currentView.selectionManagerDestroy) { + currentView.selectionManagerDestroy(); + } + + header.destroy(); + content.remove(); + element.removeClass('fc fc-ltr fc-rtl ui-widget'); + } + + + function elementVisible() { + return element.is(':visible'); + } + + + function bodyVisible() { + return $('body').is(':visible'); + } + + + + // View Rendering + // ----------------------------------------------------------------------------------- + + + function changeView(newViewName) { + if (!currentView || newViewName != currentView.name) { + _changeView(newViewName); + } + } + + + function _changeView(newViewName) { + ignoreWindowResize++; + + if (currentView) { + trigger('viewDestroy', currentView, currentView, currentView.element); + unselect(); + currentView.triggerEventDestroy(); // trigger 'eventDestroy' for each event + freezeContentHeight(); + currentView.element.remove(); + header.deactivateButton(currentView.name); + } + + header.activateButton(newViewName); + + currentView = new fcViews[newViewName]( + $("
") + .appendTo(content), + t // the calendar object + ); + + renderView(); + unfreezeContentHeight(); + + ignoreWindowResize--; + } + + + function renderView(inc) { + if ( + !currentView.start || // never rendered before + inc || // explicit date window change + !date.isWithin(currentView.intervalStart, currentView.intervalEnd) // implicit date window change + ) { + if (elementVisible()) { + _renderView(inc); + } + } + } + + + function _renderView(inc) { // assumes elementVisible + ignoreWindowResize++; + + if (currentView.start) { // already been rendered? + trigger('viewDestroy', currentView, currentView, currentView.element); + unselect(); + clearEvents(); + } + + freezeContentHeight(); + if (inc) { + date = currentView.incrementDate(date, inc); + } + currentView.render(date.clone()); // the view's render method ONLY renders the skeleton, nothing else + setSize(); + unfreezeContentHeight(); + (currentView.afterRender || noop)(); + + updateTitle(); + updateTodayButton(); + + trigger('viewRender', currentView, currentView, currentView.element); + + ignoreWindowResize--; + + getAndRenderEvents(); + } + + + + // Resizing + // ----------------------------------------------------------------------------------- + + + function updateSize() { + if (elementVisible()) { + unselect(); + clearEvents(); + calcSize(); + setSize(); + renderEvents(); + } + } + + + function calcSize() { // assumes elementVisible + if (options.contentHeight) { + suggestedViewHeight = options.contentHeight; + } + else if (options.height) { + suggestedViewHeight = options.height - (headerElement ? headerElement.height() : 0) - vsides(content); + } + else { + suggestedViewHeight = Math.round(content.width() / Math.max(options.aspectRatio, .5)); + } + } + + + function setSize() { // assumes elementVisible + + if (suggestedViewHeight === undefined) { + calcSize(); // for first time + // NOTE: we don't want to recalculate on every renderView because + // it could result in oscillating heights due to scrollbars. + } + + ignoreWindowResize++; + currentView.setHeight(suggestedViewHeight); + currentView.setWidth(content.width()); + ignoreWindowResize--; + + elementOuterWidth = element.outerWidth(); + } + + + function windowResize(ev) { + if ( + !ignoreWindowResize && + ev.target === window // so we don't process jqui "resize" events that have bubbled up + ) { + if (currentView.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 + updateSize(); + currentView.trigger('windowResize', _element); + ignoreWindowResize--; + } + } + }, options.windowResizeDelay); + }else{ + // calendar must have been initialized in a 0x0 iframe that has just been resized + lateRender(); + } + } + } + + + + /* Event Fetching/Rendering + -----------------------------------------------------------------------------*/ + // TODO: going forward, most of this stuff should be directly handled by the view + + + function refetchEvents() { // can be called as an API method + clearEvents(); + fetchAndRenderEvents(); + } + + + function rerenderEvents(modifiedEventID) { // can be called as an API method + clearEvents(); + renderEvents(modifiedEventID); + } + + + function renderEvents(modifiedEventID) { // TODO: remove modifiedEventID hack + if (elementVisible()) { + currentView.renderEvents(events, modifiedEventID); // actually render the DOM elements + currentView.trigger('eventAfterAllRender'); + } + } + + + function clearEvents() { + currentView.triggerEventDestroy(); // trigger 'eventDestroy' for each event + currentView.clearEvents(); // actually remove the DOM elements + currentView.clearEventData(); // for View.js, TODO: unify with clearEvents + } + + + function getAndRenderEvents() { + if (!options.lazyFetching || isFetchNeeded(currentView.start, currentView.end)) { + fetchAndRenderEvents(); + } + else { + renderEvents(); + } + } + + + function fetchAndRenderEvents() { + fetchEvents(currentView.start, currentView.end); + // ... will call reportEvents + // ... which will call renderEvents + } + + + // called when event data arrives + function reportEvents(_events) { + events = _events; + renderEvents(); + } + + + // called when a single event's data has been changed + function reportEventChange(eventID) { + rerenderEvents(eventID); + } + + + + /* Header Updating + -----------------------------------------------------------------------------*/ + + + function updateTitle() { + header.updateTitle(currentView.title); + } + + + function updateTodayButton() { + var now = t.getNow(); + if (now.isWithin(currentView.intervalStart, currentView.intervalEnd)) { + header.disableButton('today'); + } + else { + header.enableButton('today'); + } + } + + + + /* Selection + -----------------------------------------------------------------------------*/ + + + function select(start, end) { + currentView.select(start, end); + } + + + function unselect() { // safe to be called before renderView + if (currentView) { + currentView.unselect(); + } + } + + + + /* Date + -----------------------------------------------------------------------------*/ + + + function prev() { + renderView(-1); + } + + + function next() { + renderView(1); + } + + + function prevYear() { + date.add('years', -1); + renderView(); + } + + + function nextYear() { + date.add('years', 1); + renderView(); + } + + + function today() { + date = t.getNow(); + renderView(); + } + + + function gotoDate(dateInput) { + date = t.moment(dateInput); + renderView(); + } + + + function incrementDate(delta) { + date.add(moment.duration(delta)); + renderView(); + } + + + function getDate() { + return date.clone(); + } + + + + /* Height "Freezing" + -----------------------------------------------------------------------------*/ + + + function freezeContentHeight() { + content.css({ + width: '100%', + height: content.height(), + overflow: 'hidden' + }); + } + + + function unfreezeContentHeight() { + content.css({ + width: '', + height: '', + overflow: '' + }); + } + + + + /* Misc + -----------------------------------------------------------------------------*/ + + + function getCalendar() { + return t; + } + + + function getView() { + return currentView; + } + + + function option(name, value) { + if (value === undefined) { + return options[name]; + } + if (name == 'height' || name == 'contentHeight' || name == 'aspectRatio') { + options[name] = value; + updateSize(); + } + } + + + function trigger(name, thisObj) { + if (options[name]) { + return options[name].apply( + thisObj || _element, + Array.prototype.slice.call(arguments, 2) + ); + } + } + + + + /* External Dragging + ------------------------------------------------------------------------*/ + + if (options.droppable) { + // TODO: unbind on destroy + $(document) + .on('dragstart', droppableDragStart) + .on('dragstop', droppableDragStop); + // this is undone in destroy + } + + function droppableDragStart(ev, ui) { + var _e = ev.target; + var e = $(_e); + if (!e.parents('.fc').length) { // not already inside a calendar + var accept = options.dropAccept; + if ($.isFunction(accept) ? accept.call(_e, e) : e.is(accept)) { + _dragElement = _e; + currentView.dragStart(_dragElement, ev, ui); + } + } + } + + function droppableDragStop(ev, ui) { + if (_dragElement) { + currentView.dragStop(_dragElement, ev, ui); + _dragElement = null; + } + } + + +} + +;; + +function Header(calendar, options) { + var t = this; + + + // exports + t.render = render; + t.destroy = destroy; + t.updateTitle = updateTitle; + t.activateButton = activateButton; + t.deactivateButton = deactivateButton; + t.disableButton = disableButton; + t.enableButton = enableButton; + + + // locals + var element = $([]); + var tm; + + + + function render() { + tm = options.theme ? 'ui' : 'fc'; + var sections = options.header; + if (sections) { + element = $("") + .append( + $("") + .append(renderSection('left')) + .append(renderSection('center')) + .append(renderSection('right')) + ); + return element; + } + } + + + function destroy() { + element.remove(); + } + + + function renderSection(position) { + var e = $(""; + + if (showWeekNumbers) { + html += + ""; + } + + for (col=0; col" + + htmlEscape(formatDate(date, colFormat)) + + ""; + } + + html += ""; + + return html; + } + + + function buildBodyHTML() { + var contentClass = tm + "-widget-content"; + var html = ''; + var row; + var col; + var date; + + html += ""; + + for (row=0; row" + + "
" + + htmlEscape(calculateWeekNumber(date)) + + "
" + + ""; + } + + for (col=0; col" + + "
"; + + if (showNumbers) { + html += "
" + date.date() + "
"; + } + + html += + "
" + + "
 
" + + "
" + + "
" + + ""; + + return html; + } + + + + /* Dimensions + -----------------------------------------------------------*/ + + + function setHeight(height) { + viewHeight = height; + + var bodyHeight = Math.max(viewHeight - head.height(), 0); + var rowHeight; + var rowHeightLast; + var cell; + + if (opt('weekMode') == 'variable') { + rowHeight = rowHeightLast = Math.floor(bodyHeight / (rowCnt==1 ? 2 : 6)); + }else{ + rowHeight = Math.floor(bodyHeight / rowCnt); + rowHeightLast = bodyHeight - rowHeight * (rowCnt-1); + } + + bodyFirstCells.each(function(i, _cell) { + if (i < rowCnt) { + cell = $(_cell); + cell.find('> div').css( + 'min-height', + (i==rowCnt-1 ? rowHeightLast : rowHeight) - vsides(cell) + ); + } + }); + + } + + + function setWidth(width) { + viewWidth = width; + colPositions.clear(); + colContentPositions.clear(); + + weekNumberWidth = 0; + if (showWeekNumbers) { + weekNumberWidth = head.find('th.fc-week-number').outerWidth(); + } + + colWidth = Math.floor((viewWidth - weekNumberWidth) / colCnt); + setOuterWidth(headCells.slice(0, -1), colWidth); + } + + + + /* Day clicking and binding + -----------------------------------------------------------*/ + + + function dayBind(days) { + days.click(dayClick) + .mousedown(daySelectionMousedown); + } + + + function dayClick(ev) { + if (!opt('selectable')) { // if selectable, SelectionManager will worry about dayClick + var date = calendar.moment($(this).data('date')); + trigger('dayClick', this, date, ev); + } + } + + + + /* Semi-transparent Overlay Helpers + ------------------------------------------------------*/ + // TODO: should be consolidated with AgendaView's methods + + + function renderDayOverlay(overlayStart, overlayEnd, refreshCoordinateGrid) { // overlayEnd is exclusive + + if (refreshCoordinateGrid) { + coordinateGrid.build(); + } + + var segments = rangeToSegments(overlayStart, overlayEnd); + + for (var i=0; i") + .appendTo(element); + + if (opt('allDaySlot')) { + + daySegmentContainer = + $("
") + .appendTo(slotLayer); + + s = + "
"); + var buttonStr = options.header[position]; + if (buttonStr) { + $.each(buttonStr.split(' '), function(i) { + if (i > 0) { + e.append(""); + } + var prevButton; + $.each(this.split(','), function(j, buttonName) { + if (buttonName == 'title') { + e.append("

 

"); + if (prevButton) { + prevButton.addClass(tm + '-corner-right'); + } + prevButton = null; + }else{ + var buttonClick; + if (calendar[buttonName]) { + buttonClick = calendar[buttonName]; // calendar method + } + else if (fcViews[buttonName]) { + buttonClick = function() { + button.removeClass(tm + '-state-hover'); // forget why + calendar.changeView(buttonName); + }; + } + if (buttonClick) { + + // smartProperty allows different text per view button (ex: "Agenda Week" vs "Basic Week") + var themeIcon = smartProperty(options.themeButtonIcons, buttonName); + var normalIcon = smartProperty(options.buttonIcons, buttonName); + var defaultText = smartProperty(options.defaultButtonText, buttonName); + var customText = smartProperty(options.buttonText, buttonName); + var html; + + if (customText) { + html = htmlEscape(customText); + } + else if (themeIcon && options.theme) { + html = ""; + } + else if (normalIcon && !options.theme) { + html = ""; + } + else { + html = htmlEscape(defaultText || buttonName); + } + + var button = $( + "" + + html + + "" + ) + .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(e); + disableTextSelection(button); + if (!prevButton) { + button.addClass(tm + '-corner-left'); + } + prevButton = button; + } + } + }); + if (prevButton) { + prevButton.addClass(tm + '-corner-right'); + } + }); + } + return e; + } + + + function updateTitle(html) { + element.find('h2') + .html(html); + } + + + function activateButton(buttonName) { + element.find('span.fc-button-' + buttonName) + .addClass(tm + '-state-active'); + } + + + function deactivateButton(buttonName) { + element.find('span.fc-button-' + buttonName) + .removeClass(tm + '-state-active'); + } + + + function disableButton(buttonName) { + element.find('span.fc-button-' + buttonName) + .addClass(tm + '-state-disabled'); + } + + + function enableButton(buttonName) { + element.find('span.fc-button-' + buttonName) + .removeClass(tm + '-state-disabled'); + } + + +} + +;; + +fc.sourceNormalizers = []; +fc.sourceFetchers = []; + +var ajaxDefaults = { + dataType: 'json', + cache: false +}; + +var eventGUID = 1; + + +function EventManager(options) { // assumed to be a calendar + var t = this; + + + // exports + t.isFetchNeeded = isFetchNeeded; + t.fetchEvents = fetchEvents; + t.addEventSource = addEventSource; + t.removeEventSource = removeEventSource; + t.updateEvent = updateEvent; + t.renderEvent = renderEvent; + t.removeEvents = removeEvents; + t.clientEvents = clientEvents; + t.mutateEvent = mutateEvent; + + + // imports + var trigger = t.trigger; + var getView = t.getView; + var reportEvents = t.reportEvents; + var getEventEnd = t.getEventEnd; + + + // locals + var stickySource = { events: [] }; + var sources = [ stickySource ]; + var rangeStart, rangeEnd; + var currentFetchID = 0; + var pendingSourceCnt = 0; + var loadingLevel = 0; + var cache = []; + + + $.each( + (options.events ? [ options.events ] : []).concat(options.eventSources || []), + function(i, sourceInput) { + var source = buildEventSource(sourceInput); + if (source) { + sources.push(source); + } + } + ); + + + + /* Fetching + -----------------------------------------------------------------------------*/ + + + function isFetchNeeded(start, end) { + return !rangeStart || // nothing has been fetched yet? + // or, a part of the new range is outside of the old range? (after normalizing) + start.clone().stripZone() < rangeStart.clone().stripZone() || + end.clone().stripZone() > rangeEnd.clone().stripZone(); + } + + + function fetchEvents(start, end) { + rangeStart = start; + rangeEnd = end; + cache = []; + var fetchID = ++currentFetchID; + var len = sources.length; + pendingSourceCnt = len; + for (var i=0; i=0; i--) { + res = obj[parts[i].toLowerCase()]; + if (res !== undefined) { + return res; + } + } + return obj['default']; +} + + +function htmlEscape(s) { + return (s + '').replace(/&/g, '&') + .replace(//g, '>') + .replace(/'/g, ''') + .replace(/"/g, '"') + .replace(/\n/g, '
'); +} + + +function stripHTMLEntities(text) { + return text.replace(/&.*?;/g, ''); +} + + +function disableTextSelection(element) { + element + .attr('unselectable', 'on') + .css('MozUserSelect', 'none') + .bind('selectstart.ui', function() { return false; }); +} + + +/* +function enableTextSelection(element) { + element + .attr('unselectable', 'off') + .css('MozUserSelect', '') + .unbind('selectstart.ui'); +} +*/ + + +function markFirstLast(e) { // TODO: use CSS selectors instead + e.children() + .removeClass('fc-first fc-last') + .filter(':first-child') + .addClass('fc-first') + .end() + .filter(':last-child') + .addClass('fc-last'); +} + + +function getSkinCss(event, opt) { + var source = event.source || {}; + var eventColor = event.color; + var sourceColor = source.color; + var optionColor = opt('eventColor'); + var backgroundColor = + event.backgroundColor || + eventColor || + source.backgroundColor || + sourceColor || + opt('eventBackgroundColor') || + optionColor; + var borderColor = + event.borderColor || + eventColor || + source.borderColor || + sourceColor || + opt('eventBorderColor') || + optionColor; + var textColor = + event.textColor || + source.textColor || + opt('eventTextColor'); + var statements = []; + if (backgroundColor) { + statements.push('background-color:' + backgroundColor); + } + if (borderColor) { + statements.push('border-color:' + borderColor); + } + if (textColor) { + statements.push('color:' + textColor); + } + return statements.join(';'); +} + + +function applyAll(functions, thisObj, args) { + if ($.isFunction(functions)) { + functions = [ functions ]; + } + if (functions) { + var i; + var ret; + for (i=0; i= a[1] && a[0] < a[2]; +}; + +// Make these query methods work with ambiguous moments +$.each([ + 'isBefore', + 'isAfter', + 'isSame' +], function(i, methodName) { + FCMoment.prototype[methodName] = function(input, units) { + var a = commonlyAmbiguate([ this, input ]); + return moment.fn[methodName].call(a[0], a[1], units); + }; +}); + + +// Misc Internals +// ------------------------------------------------------------------------------------------------- + +// given an array of moment-like inputs, return a parallel array w/ moments similarly ambiguated. +// for example, of one moment has ambig time, but not others, all moments will have their time stripped. +function commonlyAmbiguate(inputs) { + var outputs = []; + var anyAmbigTime = false; + var anyAmbigZone = false; + var i; + + for (i=0; i "MMMM D YYYY" + formatStr = localeData.longDateFormat(formatStr) || formatStr; + // BTW, this is not important for `formatDate` because it is impossible to put custom tokens + // or non-zero areas in Moment's localized format strings. + + separator = separator || ' - '; + + return formatRangeWithChunks( + date1, + date2, + getFormatStringChunks(formatStr), + separator, + isRTL + ); +} +fc.formatRange = formatRange; // expose + + +function formatRangeWithChunks(date1, date2, chunks, separator, isRTL) { + var chunkStr; // the rendering of the chunk + var leftI; + var leftStr = ''; + var rightI; + var rightStr = ''; + var middleI; + var middleStr1 = ''; + var middleStr2 = ''; + var middleStr = ''; + + // Start at the leftmost side of the formatting string and continue until you hit a token + // that is not the same between dates. + for (leftI=0; leftIleftI; rightI--) { + chunkStr = formatSimilarChunk(date1, date2, chunks[rightI]); + if (chunkStr === false) { + break; + } + rightStr = chunkStr + rightStr; + } + + // The area in the middle is different for both of the dates. + // Collect them distinctly so we can jam them together later. + for (middleI=leftI; middleI<=rightI; middleI++) { + middleStr1 += formatDateWithChunk(date1, chunks[middleI]); + middleStr2 += formatDateWithChunk(date2, chunks[middleI]); + } + + if (middleStr1 || middleStr2) { + if (isRTL) { + middleStr = middleStr2 + separator + middleStr1; + } + else { + middleStr = middleStr1 + separator + middleStr2; + } + } + + return leftStr + middleStr + rightStr; +} + + +var similarUnitMap = { + Y: 'year', + M: 'month', + D: 'day', // day of month + d: 'day', // day of week + // prevents a separator between anything time-related... + A: 'second', // AM/PM + a: 'second', // am/pm + T: 'second', // A/P + t: 'second', // a/p + H: 'second', // hour (24) + h: 'second', // hour (12) + m: 'second', // minute + s: 'second' // second +}; +// TODO: week maybe? + + +// Given a formatting chunk, and given that both dates are similar in the regard the +// formatting chunk is concerned, format date1 against `chunk`. Otherwise, return `false`. +function formatSimilarChunk(date1, date2, chunk) { + var token; + var unit; + + if (typeof chunk === 'string') { // a literal string + return chunk; + } + else if ((token = chunk.token)) { + unit = similarUnitMap[token.charAt(0)]; + // are the dates the same for this unit of measurement? + if (unit && date1.isSame(date2, unit)) { + return momentFormat(date1, token); // would be the same if we used `date2` + // BTW, don't support custom tokens + } + } + + return false; // the chunk is NOT the same for the two dates + // BTW, don't support splitting on non-zero areas +} + + +// Chunking Utils +// ------------------------------------------------------------------------------------------------- + + +var formatStringChunkCache = {}; + + +function getFormatStringChunks(formatStr) { + if (formatStr in formatStringChunkCache) { + return formatStringChunkCache[formatStr]; + } + return (formatStringChunkCache[formatStr] = chunkFormatString(formatStr)); +} + + +// Break the formatting string into an array of chunks +function chunkFormatString(formatStr) { + var chunks = []; + var chunker = /\[([^\]]*)\]|\(([^\)]*)\)|(LT|(\w)\4*o?)|([^\w\[\(]+)/g; // TODO: more descrimination + var match; + + while ((match = chunker.exec(formatStr))) { + if (match[1]) { // a literal string inside [ ... ] + chunks.push(match[1]); + } + else if (match[2]) { // non-zero formatting inside ( ... ) + chunks.push({ maybe: chunkFormatString(match[2]) }); + } + else if (match[3]) { // a formatting token + chunks.push({ token: match[3] }); + } + else if (match[5]) { // an unenclosed literal string + chunks.push(match[5]); + } + } + + return chunks; +} + +;; + +fcViews.month = MonthView; + +function MonthView(element, calendar) { + var t = this; + + + // exports + t.incrementDate = incrementDate; + t.render = render; + + + // imports + BasicView.call(t, element, calendar, 'month'); + + + function incrementDate(date, delta) { + return date.clone().stripTime().add('months', delta).startOf('month'); + } + + + function render(date) { + + t.intervalStart = date.clone().stripTime().startOf('month'); + t.intervalEnd = t.intervalStart.clone().add('months', 1); + + t.start = t.intervalStart.clone(); + t.start = t.skipHiddenDays(t.start); // move past the first week if no visible days + t.start.startOf('week'); + t.start = t.skipHiddenDays(t.start); // move past the first invisible days of the week + + t.end = t.intervalEnd.clone(); + t.end = t.skipHiddenDays(t.end, -1, true); // move in from the last week if no visible days + t.end.add((7 - t.end.weekday()) % 7, 'days'); // move to end of week if not already + t.end = t.skipHiddenDays(t.end, -1, true); // move in from the last invisible days of the week + + var rowCnt = Math.ceil( // need to ceil in case there are hidden days + t.end.diff(t.start, 'weeks', true) // returnfloat=true + ); + if (t.opt('weekMode') == 'fixed') { + t.end.add('weeks', 6 - rowCnt); + rowCnt = 6; + } + + t.title = calendar.formatDate(t.intervalStart, t.opt('titleFormat')); + + t.renderBasic(rowCnt, t.getCellsPerWeek(), true); + } + + +} + +;; + +fcViews.basicWeek = BasicWeekView; + +function BasicWeekView(element, calendar) { // TODO: do a WeekView mixin + var t = this; + + + // exports + t.incrementDate = incrementDate; + t.render = render; + + + // imports + BasicView.call(t, element, calendar, 'basicWeek'); + + + function incrementDate(date, delta) { + return date.clone().stripTime().add('weeks', delta).startOf('week'); + } + + + function render(date) { + + t.intervalStart = date.clone().stripTime().startOf('week'); + t.intervalEnd = t.intervalStart.clone().add('weeks', 1); + + t.start = t.skipHiddenDays(t.intervalStart); + t.end = t.skipHiddenDays(t.intervalEnd, -1, true); + + t.title = calendar.formatRange( + t.start, + t.end.clone().subtract(1), // make inclusive by subtracting 1 ms + t.opt('titleFormat'), + ' \u2014 ' // emphasized dash + ); + + t.renderBasic(1, t.getCellsPerWeek(), false); + } + + +} + +;; + +fcViews.basicDay = BasicDayView; + +function BasicDayView(element, calendar) { // TODO: make a DayView mixin + var t = this; + + + // exports + t.incrementDate = incrementDate; + t.render = render; + + + // imports + BasicView.call(t, element, calendar, 'basicDay'); + + + function incrementDate(date, delta) { + var out = date.clone().stripTime().add(delta, 'days'); + out = t.skipHiddenDays(out, delta < 0 ? -1 : 1); + return out; + } + + + function render(date) { + + t.start = t.intervalStart = date.clone().stripTime(); + t.end = t.intervalEnd = t.start.clone().add(1, 'days'); + + t.title = calendar.formatDate(t.start, t.opt('titleFormat')); + + t.renderBasic(1, 1, false); + } + + +} + +;; + +setDefaults({ + weekMode: 'fixed' +}); + + +function BasicView(element, calendar, viewName) { + var t = this; + + + // exports + t.renderBasic = renderBasic; + t.setHeight = setHeight; + t.setWidth = setWidth; + t.renderDayOverlay = renderDayOverlay; + t.defaultSelectionEnd = defaultSelectionEnd; + t.renderSelection = renderSelection; + t.clearSelection = clearSelection; + t.reportDayClick = reportDayClick; // for selection (kinda hacky) + t.dragStart = dragStart; + t.dragStop = dragStop; + t.getHoverListener = function() { return hoverListener; }; + t.colLeft = colLeft; + t.colRight = colRight; + t.colContentLeft = colContentLeft; + t.colContentRight = colContentRight; + t.getIsCellAllDay = function() { return true; }; + t.allDayRow = allDayRow; + t.getRowCnt = function() { return rowCnt; }; + t.getColCnt = function() { return colCnt; }; + t.getColWidth = function() { return colWidth; }; + t.getDaySegmentContainer = function() { return daySegmentContainer; }; + + + // imports + View.call(t, element, calendar, viewName); + OverlayManager.call(t); + SelectionManager.call(t); + BasicEventRenderer.call(t); + var opt = t.opt; + var trigger = t.trigger; + var renderOverlay = t.renderOverlay; + var clearOverlays = t.clearOverlays; + var daySelectionMousedown = t.daySelectionMousedown; + var cellToDate = t.cellToDate; + var dateToCell = t.dateToCell; + var rangeToSegments = t.rangeToSegments; + var formatDate = calendar.formatDate; + var calculateWeekNumber = calendar.calculateWeekNumber; + + + // locals + + var table; + var head; + var headCells; + var body; + var bodyRows; + var bodyCells; + var bodyFirstCells; + var firstRowCellInners; + var firstRowCellContentInners; + var daySegmentContainer; + + var viewWidth; + var viewHeight; + var colWidth; + var weekNumberWidth; + + var rowCnt, colCnt; + var showNumbers; + var coordinateGrid; + var hoverListener; + var colPositions; + var colContentPositions; + + var tm; + var colFormat; + var showWeekNumbers; + + + + /* Rendering + ------------------------------------------------------------*/ + + + disableTextSelection(element.addClass('fc-grid')); + + + function renderBasic(_rowCnt, _colCnt, _showNumbers) { + rowCnt = _rowCnt; + colCnt = _colCnt; + showNumbers = _showNumbers; + updateOptions(); + + if (!body) { + buildEventContainer(); + } + + buildTable(); + } + + + function updateOptions() { + tm = opt('theme') ? 'ui' : 'fc'; + colFormat = opt('columnFormat'); + showWeekNumbers = opt('weekNumbers'); + } + + + function buildEventContainer() { + daySegmentContainer = + $("
") + .appendTo(element); + } + + + function buildTable() { + var html = buildTableHTML(); + + if (table) { + table.remove(); + } + table = $(html).appendTo(element); + + head = table.find('thead'); + headCells = head.find('.fc-day-header'); + body = table.find('tbody'); + bodyRows = body.find('tr'); + bodyCells = body.find('.fc-day'); + bodyFirstCells = bodyRows.find('td:first-child'); + + firstRowCellInners = bodyRows.eq(0).find('.fc-day > div'); + firstRowCellContentInners = bodyRows.eq(0).find('.fc-day-content > div'); + + markFirstLast(head.add(head.find('tr'))); // marks first+last tr/th's + markFirstLast(bodyRows); // marks first+last td's + bodyRows.eq(0).addClass('fc-first'); + bodyRows.filter(':last').addClass('fc-last'); + + bodyCells.each(function(i, _cell) { + var date = cellToDate( + Math.floor(i / colCnt), + i % colCnt + ); + trigger('dayRender', t, date, $(_cell)); + }); + + dayBind(bodyCells); + } + + + + /* HTML Building + -----------------------------------------------------------*/ + + + function buildTableHTML() { + var html = + "" + + buildHeadHTML() + + buildBodyHTML() + + "
"; + + return html; + } + + + function buildHeadHTML() { + var headerClass = tm + "-widget-header"; + var html = ''; + var col; + var date; + + html += "
" + + htmlEscape(opt('weekNumberTitle')) + + "
" + + "" + + "" + + "" + + "" + + "" + + "
" + + ( + opt('allDayHTML') || + htmlEscape(opt('allDayText')) + ) + + "" + + "
" + + "
 
"; + allDayTable = $(s).appendTo(slotLayer); + allDayRow = allDayTable.find('tr'); + + dayBind(allDayRow.find('td')); + + slotLayer.append( + "
" + + "
" + + "
" + ); + + }else{ + + daySegmentContainer = $([]); // in jQuery 1.4, we can just do $() + + } + + slotScroller = + $("
") + .appendTo(slotLayer); + + slotContainer = + $("
") + .appendTo(slotScroller); + + slotSegmentContainer = + $("
") + .appendTo(slotContainer); + + s = + "" + + ""; + + slotTime = moment.duration(+minTime); // i wish there was .clone() for durations + slotCnt = 0; + while (slotTime < maxTime) { + slotDate = t.start.clone().time(slotTime); // will be in UTC but that's good. to avoid DST issues + minutes = slotDate.minutes(); + s += + "" + + "" + + "" + + ""; + slotTime.add(slotDuration); + slotCnt++; + } + + s += + "" + + "
" + + ((!slotNormal || !minutes) ? + htmlEscape(formatDate(slotDate, opt('axisFormat'))) : + ' ' + ) + + "" + + "
 
" + + "
"; + + slotTable = $(s).appendTo(slotContainer); + + slotBind(slotTable.find('td')); + } + + + + /* Build Day Table + -----------------------------------------------------------------------*/ + + + function buildDayTable() { + var html = buildDayTableHTML(); + + if (dayTable) { + dayTable.remove(); + } + dayTable = $(html).appendTo(element); + + dayHead = dayTable.find('thead'); + dayHeadCells = dayHead.find('th').slice(1, -1); // exclude gutter + dayBody = dayTable.find('tbody'); + dayBodyCells = dayBody.find('td').slice(0, -1); // exclude gutter + dayBodyCellInners = dayBodyCells.find('> div'); + dayBodyCellContentInners = dayBodyCells.find('.fc-day-content > div'); + + dayBodyFirstCell = dayBodyCells.eq(0); + dayBodyFirstCellStretcher = dayBodyCellInners.eq(0); + + markFirstLast(dayHead.add(dayHead.find('tr'))); + markFirstLast(dayBody.add(dayBody.find('tr'))); + + // TODO: now that we rebuild the cells every time, we should call dayRender + } + + + function buildDayTableHTML() { + var html = + "" + + buildDayTableHeadHTML() + + buildDayTableBodyHTML() + + "
"; + + return html; + } + + + function buildDayTableHeadHTML() { + var headerClass = tm + "-widget-header"; + var date; + var html = ''; + var weekText; + var col; + + html += + "" + + ""; + + if (opt('weekNumbers')) { + date = cellToDate(0, 0); + weekText = calculateWeekNumber(date); + if (rtl) { + weekText += opt('weekNumberTitle'); + } + else { + weekText = opt('weekNumberTitle') + weekText; + } + html += + "" + + htmlEscape(weekText) + + ""; + } + else { + html += " "; + } + + for (col=0; col" + + htmlEscape(formatDate(date, colFormat)) + + ""; + } + + html += + " " + + "" + + ""; + + return html; + } + + + function buildDayTableBodyHTML() { + var headerClass = tm + "-widget-header"; // TODO: make these when updateOptions() called + var contentClass = tm + "-widget-content"; + var date; + var today = calendar.getNow().stripTime(); + var col; + var cellsHTML; + var cellHTML; + var classNames; + var html = ''; + + html += + "" + + "" + + " "; + + cellsHTML = ''; + + for (col=0; col" + + "
" + + "
" + + "
 
" + + "
" + + "
" + + ""; + + cellsHTML += cellHTML; + } + + html += cellsHTML; + html += + " " + + "" + + ""; + + return html; + } + + + // TODO: data-date on the cells + + + + /* Dimensions + -----------------------------------------------------------------------*/ + + + function setHeight(height) { + if (height === undefined) { + height = viewHeight; + } + viewHeight = height; + slotTopCache = {}; + + var headHeight = dayBody.position().top; + var allDayHeight = slotScroller.position().top; // including divider + var bodyHeight = Math.min( // total body height, including borders + height - headHeight, // when scrollbars + slotTable.height() + allDayHeight + 1 // when no scrollbars. +1 for bottom border + ); + + dayBodyFirstCellStretcher + .height(bodyHeight - vsides(dayBodyFirstCell)); + + slotLayer.css('top', headHeight); + + slotScroller.height(bodyHeight - allDayHeight - 1); + + // the stylesheet guarantees that the first row has no border. + // this allows .height() to work well cross-browser. + var slotHeight0 = slotTable.find('tr:first').height() + 1; // +1 for bottom border + var slotHeight1 = slotTable.find('tr:eq(1)').height(); + // HACK: i forget why we do this, but i think a cross-browser issue + slotHeight = (slotHeight0 + slotHeight1) / 2; + + snapRatio = slotDuration / snapDuration; + snapHeight = slotHeight / snapRatio; + } + + + function setWidth(width) { + viewWidth = width; + colPositions.clear(); + colContentPositions.clear(); + + var axisFirstCells = dayHead.find('th:first'); + if (allDayTable) { + axisFirstCells = axisFirstCells.add(allDayTable.find('th:first')); + } + axisFirstCells = axisFirstCells.add(slotTable.find('th:first')); + + axisWidth = 0; + setOuterWidth( + axisFirstCells + .width('') + .each(function(i, _cell) { + axisWidth = Math.max(axisWidth, $(_cell).outerWidth()); + }), + axisWidth + ); + + var gutterCells = dayTable.find('.fc-agenda-gutter'); + if (allDayTable) { + gutterCells = gutterCells.add(allDayTable.find('th.fc-agenda-gutter')); + } + + var slotTableWidth = slotScroller[0].clientWidth; // needs to be done after axisWidth (for IE7) + + gutterWidth = slotScroller.width() - slotTableWidth; + if (gutterWidth) { + setOuterWidth(gutterCells, gutterWidth); + gutterCells + .show() + .prev() + .removeClass('fc-last'); + }else{ + gutterCells + .hide() + .prev() + .addClass('fc-last'); + } + + colWidth = Math.floor((slotTableWidth - axisWidth) / colCnt); + setOuterWidth(dayHeadCells.slice(0, -1), colWidth); + } + + + + /* Scrolling + -----------------------------------------------------------------------*/ + + + function resetScroll() { + var top = computeTimeTop( + moment.duration(opt('scrollTime')) + ) + 1; // +1 for the border + + function scroll() { + slotScroller.scrollTop(top); + } + + scroll(); + setTimeout(scroll, 0); // overrides any previous scroll state made by the browser + } + + + function afterRender() { // after the view has been freshly rendered and sized + resetScroll(); + } + + + + /* Slot/Day clicking and binding + -----------------------------------------------------------------------*/ + + + function dayBind(cells) { + cells.click(slotClick) + .mousedown(daySelectionMousedown); + } + + + function slotBind(cells) { + cells.click(slotClick) + .mousedown(slotSelectionMousedown); + } + + + function slotClick(ev) { + if (!opt('selectable')) { // if selectable, SelectionManager will worry about dayClick + var col = Math.min(colCnt-1, Math.floor((ev.pageX - dayTable.offset().left - axisWidth) / colWidth)); + var date = cellToDate(0, col); + var match = this.parentNode.className.match(/fc-slot(\d+)/); // TODO: maybe use data + if (match) { + var slotIndex = parseInt(match[1], 10); + date.add(minTime + slotIndex * slotDuration); + date = calendar.rezoneDate(date); + trigger( + 'dayClick', + dayBodyCells[col], + date, + ev + ); + }else{ + trigger( + 'dayClick', + dayBodyCells[col], + date, + ev + ); + } + } + } + + + + /* Semi-transparent Overlay Helpers + -----------------------------------------------------*/ + // TODO: should be consolidated with BasicView's methods + + + function renderDayOverlay(overlayStart, overlayEnd, refreshCoordinateGrid) { // overlayEnd is exclusive + + if (refreshCoordinateGrid) { + coordinateGrid.build(); + } + + var segments = rangeToSegments(overlayStart, overlayEnd); + + for (var i=0; i= 0) { + date.time(moment.duration(minTime + snapIndex * snapDuration)); + date = calendar.rezoneDate(date); + } + + return date; + } + + + function computeDateTop(date, startOfDayDate) { + return computeTimeTop( + moment.duration( + date.clone().stripZone() - startOfDayDate.clone().stripTime() + ) + ); + } + + + function computeTimeTop(time) { // time is a duration + + if (time < minTime) { + return 0; + } + if (time >= maxTime) { + return slotTable.height(); + } + + var slots = (time - minTime) / slotDuration; + var slotIndex = Math.floor(slots); + var slotPartial = slots - slotIndex; + var slotTop = slotTopCache[slotIndex]; + + // find the position of the corresponding + // need to use this tecnhique because not all rows are rendered at same height sometimes. + if (slotTop === undefined) { + slotTop = slotTopCache[slotIndex] = + slotTable.find('tr').eq(slotIndex).find('td div')[0].offsetTop; + // .eq() is faster than ":eq()" selector + // [0].offsetTop is faster than .position().top (do we really need this optimization?) + // a better optimization would be to cache all these divs + } + + var top = + slotTop - 1 + // because first row doesn't have a top border + slotPartial * slotHeight; // part-way through the row + + top = Math.max(top, 0); + + return top; + } + + + + /* Selection + ---------------------------------------------------------------------------------*/ + + + function defaultSelectionEnd(start) { + if (start.hasTime()) { + return start.clone().add(slotDuration); + } + else { + return start.clone().add(1, 'days'); + } + } + + + function renderSelection(start, end) { + if (start.hasTime() || end.hasTime()) { + renderSlotSelection(start, end); + } + else if (opt('allDaySlot')) { + renderDayOverlay(start, end, true); // true for refreshing coordinate grid + } + } + + + function renderSlotSelection(startDate, endDate) { + var helperOption = opt('selectHelper'); + coordinateGrid.build(); + if (helperOption) { + var col = dateToCell(startDate).col; + if (col >= 0 && col < colCnt) { // only works when times are on same day + var rect = coordinateGrid.rect(0, col, 0, col, slotContainer); // only for horizontal coords + var top = computeDateTop(startDate, startDate); + var bottom = computeDateTop(endDate, startDate); + if (bottom > top) { // protect against selections that are entirely before or after visible range + rect.top = top; + rect.height = bottom - top; + rect.left += 2; + rect.width -= 5; + if ($.isFunction(helperOption)) { + var helperRes = helperOption(startDate, endDate); + if (helperRes) { + rect.position = 'absolute'; + selectionHelper = $(helperRes) + .css(rect) + .appendTo(slotContainer); + } + }else{ + rect.isStart = true; // conside rect a "seg" now + rect.isEnd = true; // + selectionHelper = $(slotSegHtml( + { + title: '', + start: startDate, + end: endDate, + className: ['fc-select-helper'], + editable: false + }, + rect + )); + selectionHelper.css('opacity', opt('dragOpacity')); + } + if (selectionHelper) { + slotBind(selectionHelper); + slotContainer.append(selectionHelper); + setOuterWidth(selectionHelper, rect.width, true); // needs to be after appended + setOuterHeight(selectionHelper, rect.height, true); + } + } + } + }else{ + renderSlotOverlay(startDate, endDate); + } + } + + + function clearSelection() { + clearOverlays(); + if (selectionHelper) { + selectionHelper.remove(); + selectionHelper = null; + } + } + + + function slotSelectionMousedown(ev) { + if (ev.which == 1 && opt('selectable')) { // ev.which==1 means left mouse button + unselect(ev); + var dates; + hoverListener.start(function(cell, origCell) { + clearSelection(); + if (cell && cell.col == origCell.col && !getIsCellAllDay(cell)) { + var d1 = realCellToDate(origCell); + var d2 = realCellToDate(cell); + dates = [ + d1, + d1.clone().add(snapDuration), // calculate minutes depending on selection slot minutes + d2, + d2.clone().add(snapDuration) + ].sort(dateCompare); + renderSlotSelection(dates[0], dates[3]); + }else{ + dates = null; + } + }, ev); + $(document).one('mouseup', function(ev) { + hoverListener.stop(); + if (dates) { + if (+dates[0] == +dates[1]) { + reportDayClick(dates[0], ev); + } + reportSelection(dates[0], dates[3], ev); + } + }); + } + } + + + function reportDayClick(date, ev) { + trigger('dayClick', dayBodyCells[dateToCell(date).col], date, ev); + } + + + + /* External Dragging + --------------------------------------------------------------------------------*/ + + + function dragStart(_dragElement, ev, ui) { + hoverListener.start(function(cell) { + clearOverlays(); + if (cell) { + var d1 = realCellToDate(cell); + var d2 = d1.clone(); + if (d1.hasTime()) { + d2.add(calendar.defaultTimedEventDuration); + renderSlotOverlay(d1, d2); + } + else { + d2.add(calendar.defaultAllDayEventDuration); + renderDayOverlay(d1, d2); + } + } + }, ev); + } + + + function dragStop(_dragElement, ev, ui) { + var cell = hoverListener.stop(); + clearOverlays(); + if (cell) { + trigger( + 'drop', + _dragElement, + realCellToDate(cell), + ev, + ui + ); + } + } + + +} + +;; + +function AgendaEventRenderer() { + var t = this; + + + // exports + t.renderEvents = renderEvents; + t.clearEvents = clearEvents; + t.slotSegHtml = slotSegHtml; + + + // imports + DayEventRenderer.call(t); + var opt = t.opt; + var trigger = t.trigger; + var isEventDraggable = t.isEventDraggable; + var isEventResizable = t.isEventResizable; + var eventElementHandlers = t.eventElementHandlers; + var setHeight = t.setHeight; + var getDaySegmentContainer = t.getDaySegmentContainer; + var getSlotSegmentContainer = t.getSlotSegmentContainer; + var getHoverListener = t.getHoverListener; + var computeDateTop = t.computeDateTop; + var getIsCellAllDay = t.getIsCellAllDay; + var colContentLeft = t.colContentLeft; + var colContentRight = t.colContentRight; + var cellToDate = t.cellToDate; + var getColCnt = t.getColCnt; + var getColWidth = t.getColWidth; + var getSnapHeight = t.getSnapHeight; + var getSnapDuration = t.getSnapDuration; + var getSlotHeight = t.getSlotHeight; + var getSlotDuration = t.getSlotDuration; + var getSlotContainer = t.getSlotContainer; + var reportEventElement = t.reportEventElement; + var showEvents = t.showEvents; + var hideEvents = t.hideEvents; + var eventDrop = t.eventDrop; + var eventResize = t.eventResize; + var renderDayOverlay = t.renderDayOverlay; + var clearOverlays = t.clearOverlays; + var renderDayEvents = t.renderDayEvents; + var getMinTime = t.getMinTime; + var getMaxTime = t.getMaxTime; + var calendar = t.calendar; + var formatDate = calendar.formatDate; + var getEventEnd = calendar.getEventEnd; + + + // overrides + t.draggableDayEvent = draggableDayEvent; + + + + /* Rendering + ----------------------------------------------------------------------------*/ + + + function renderEvents(events, modifiedEventId) { + var i, len=events.length, + dayEvents=[], + slotEvents=[]; + for (i=0; i rangeStart && eventStart < rangeEnd) { + + if (eventStart < rangeStart) { + segStart = rangeStart.clone(); + isStart = false; + } + else { + segStart = eventStart; + isStart = true; + } + + if (eventEnd > rangeEnd) { + segEnd = rangeEnd.clone(); + isEnd = false; + } + else { + segEnd = eventEnd; + isEnd = true; + } + + segs.push({ + event: event, + start: segStart, + end: segEnd, + isStart: isStart, + isEnd: isEnd + }); + } + } + + return segs.sort(compareSlotSegs); + } + + + // renders events in the 'time slots' at the bottom + // TODO: when we refactor this, when user returns `false` eventRender, don't have empty space + // TODO: refactor will include using pixels to detect collisions instead of dates (handy for seg cmp) + + function renderSlotSegs(segs, modifiedEventId) { + + var i, segCnt=segs.length, seg, + event, + top, + bottom, + columnLeft, + columnRight, + columnWidth, + width, + left, + right, + html = '', + eventElements, + eventElement, + triggerRes, + titleElement, + height, + slotSegmentContainer = getSlotSegmentContainer(), + isRTL = opt('isRTL'); + + // calculate position/dimensions, create html + for (i=0; i" + + "
" + + "
" + + htmlEscape(t.getEventTimeText(event)) + + "
" + + "
" + + htmlEscape(event.title || '') + + "
" + + "
" + + "
"; + + if (seg.isEnd && isEventResizable(event)) { + html += + "
=
"; + } + html += + ""; + return html; + } + + + function bindSlotSeg(event, eventElement, seg) { + var timeElement = eventElement.find('div.fc-event-time'); + if (isEventDraggable(event)) { + draggableSlotEvent(event, eventElement, timeElement); + } + if (seg.isEnd && isEventResizable(event)) { + resizableSlotEvent(event, eventElement, timeElement); + } + eventElementHandlers(event, eventElement); + } + + + + /* Dragging + -----------------------------------------------------------------------------------*/ + + + // when event starts out FULL-DAY + // overrides DayEventRenderer's version because it needs to account for dragging elements + // to and from the slot area. + + function draggableDayEvent(event, eventElement, seg) { + var isStart = seg.isStart; + var origWidth; + var revert; + var allDay = true; + var dayDelta; + + var hoverListener = getHoverListener(); + var colWidth = getColWidth(); + var minTime = getMinTime(); + var slotDuration = getSlotDuration(); + var slotHeight = getSlotHeight(); + var snapDuration = getSnapDuration(); + var snapHeight = getSnapHeight(); + + eventElement.draggable({ + opacity: opt('dragOpacity', 'month'), // use whatever the month view was using + revertDuration: opt('dragRevertDuration'), + start: function(ev, ui) { + + trigger('eventDragStart', eventElement[0], event, ev, ui); + hideEvents(event, eventElement); + origWidth = eventElement.width(); + + hoverListener.start(function(cell, origCell) { + clearOverlays(); + if (cell) { + revert = false; + + var origDate = cellToDate(0, origCell.col); + var date = cellToDate(0, cell.col); + dayDelta = date.diff(origDate, 'days'); + + if (!cell.row) { // on full-days + + renderDayOverlay( + event.start.clone().add(dayDelta, 'days'), + getEventEnd(event).add(dayDelta, 'days') + ); + + resetElement(); + } + else { // mouse is over bottom slots + + if (isStart) { + if (allDay) { + // convert event to temporary slot-event + eventElement.width(colWidth - 10); // don't use entire width + setOuterHeight(eventElement, calendar.defaultTimedEventDuration / slotDuration * slotHeight); // the default height + eventElement.draggable('option', 'grid', [ colWidth, 1 ]); + allDay = false; + } + } + else { + revert = true; + } + } + + revert = revert || (allDay && !dayDelta); + } + else { + resetElement(); + revert = true; + } + + eventElement.draggable('option', 'revert', revert); + + }, ev, 'drag'); + }, + stop: function(ev, ui) { + hoverListener.stop(); + clearOverlays(); + trigger('eventDragStop', eventElement[0], event, ev, ui); + + if (revert) { // hasn't moved or is out of bounds (draggable has already reverted) + + resetElement(); + eventElement.css('filter', ''); // clear IE opacity side-effects + showEvents(event, eventElement); + } + else { // changed! + + var eventStart = event.start.clone().add(dayDelta, 'days'); // already assumed to have a stripped time + var snapTime; + var snapIndex; + if (!allDay) { + snapIndex = Math.round((eventElement.offset().top - getSlotContainer().offset().top) / snapHeight); // why not use ui.offset.top? + snapTime = moment.duration(minTime + snapIndex * snapDuration); + eventStart = calendar.rezoneDate(eventStart.clone().time(snapTime)); + } + + eventDrop( + eventElement[0], + event, + eventStart, + ev, + ui + ); + } + } + }); + function resetElement() { + if (!allDay) { + eventElement + .width(origWidth) + .height('') + .draggable('option', 'grid', null); + allDay = true; + } + } + } + + + // when event starts out IN TIMESLOTS + + function draggableSlotEvent(event, eventElement, timeElement) { + var coordinateGrid = t.getCoordinateGrid(); + var colCnt = getColCnt(); + var colWidth = getColWidth(); + var snapHeight = getSnapHeight(); + var snapDuration = getSnapDuration(); + + // states + var origPosition; // original position of the element, not the mouse + var origCell; + var isInBounds, prevIsInBounds; + var isAllDay, prevIsAllDay; + var colDelta, prevColDelta; + var dayDelta; // derived from colDelta + var snapDelta, prevSnapDelta; // the number of snaps away from the original position + + // newly computed + var eventStart, eventEnd; + + eventElement.draggable({ + scroll: false, + grid: [ colWidth, snapHeight ], + axis: colCnt==1 ? 'y' : false, + opacity: opt('dragOpacity'), + revertDuration: opt('dragRevertDuration'), + start: function(ev, ui) { + + trigger('eventDragStart', eventElement[0], event, ev, ui); + hideEvents(event, eventElement); + + coordinateGrid.build(); + + // initialize states + origPosition = eventElement.position(); + origCell = coordinateGrid.cell(ev.pageX, ev.pageY); + isInBounds = prevIsInBounds = true; + isAllDay = prevIsAllDay = getIsCellAllDay(origCell); + colDelta = prevColDelta = 0; + dayDelta = 0; + snapDelta = prevSnapDelta = 0; + + eventStart = null; + eventEnd = null; + }, + drag: function(ev, ui) { + + // NOTE: this `cell` value is only useful for determining in-bounds and all-day. + // Bad for anything else due to the discrepancy between the mouse position and the + // element position while snapping. (problem revealed in PR #55) + // + // PS- the problem exists for draggableDayEvent() when dragging an all-day event to a slot event. + // We should overhaul the dragging system and stop relying on jQuery UI. + var cell = coordinateGrid.cell(ev.pageX, ev.pageY); + + // update states + isInBounds = !!cell; + if (isInBounds) { + isAllDay = getIsCellAllDay(cell); + + // calculate column delta + colDelta = Math.round((ui.position.left - origPosition.left) / colWidth); + if (colDelta != prevColDelta) { + // calculate the day delta based off of the original clicked column and the column delta + var origDate = cellToDate(0, origCell.col); + var col = origCell.col + colDelta; + col = Math.max(0, col); + col = Math.min(colCnt-1, col); + var date = cellToDate(0, col); + dayDelta = date.diff(origDate, 'days'); + } + + // calculate minute delta (only if over slots) + if (!isAllDay) { + snapDelta = Math.round((ui.position.top - origPosition.top) / snapHeight); + } + } + + // any state changes? + if ( + isInBounds != prevIsInBounds || + isAllDay != prevIsAllDay || + colDelta != prevColDelta || + snapDelta != prevSnapDelta + ) { + + // compute new dates + if (isAllDay) { + eventStart = event.start.clone().stripTime().add(dayDelta, 'days'); + eventEnd = eventStart.clone().add(calendar.defaultAllDayEventDuration); + } + else { + eventStart = event.start.clone().add(snapDelta * snapDuration).add(dayDelta, 'days'); + eventEnd = getEventEnd(event).add(snapDelta * snapDuration).add(dayDelta, 'days'); + } + + updateUI(); + + // update previous states for next time + prevIsInBounds = isInBounds; + prevIsAllDay = isAllDay; + prevColDelta = colDelta; + prevSnapDelta = snapDelta; + } + + // if out-of-bounds, revert when done, and vice versa. + eventElement.draggable('option', 'revert', !isInBounds); + + }, + stop: function(ev, ui) { + + clearOverlays(); + trigger('eventDragStop', eventElement[0], event, ev, ui); + + if (isInBounds && (isAllDay || dayDelta || snapDelta)) { // changed! + eventDrop( + eventElement[0], + event, + eventStart, + ev, + ui + ); + } + else { // either no change or out-of-bounds (draggable has already reverted) + + // reset states for next time, and for updateUI() + isInBounds = true; + isAllDay = false; + colDelta = 0; + dayDelta = 0; + snapDelta = 0; + + updateUI(); + eventElement.css('filter', ''); // clear IE opacity side-effects + + // sometimes fast drags make event revert to wrong position, so reset. + // also, if we dragged the element out of the area because of snapping, + // but the *mouse* is still in bounds, we need to reset the position. + eventElement.css(origPosition); + + showEvents(event, eventElement); + } + } + }); + + function updateUI() { + clearOverlays(); + if (isInBounds) { + if (isAllDay) { + timeElement.hide(); + eventElement.draggable('option', 'grid', null); // disable grid snapping + renderDayOverlay(eventStart, eventEnd); + } + else { + updateTimeText(); + timeElement.css('display', ''); // show() was causing display=inline + eventElement.draggable('option', 'grid', [colWidth, snapHeight]); // re-enable grid snapping + } + } + } + + function updateTimeText() { + if (eventStart) { // must of had a state change + timeElement.text( + t.getEventTimeText(eventStart, event.end ? eventEnd : null) + // ^ + // only display the new end if there was an old end + ); + } + } + + } + + + + /* Resizing + --------------------------------------------------------------------------------------*/ + + + function resizableSlotEvent(event, eventElement, timeElement) { + var snapDelta, prevSnapDelta; + var snapHeight = getSnapHeight(); + var snapDuration = getSnapDuration(); + var eventEnd; + + eventElement.resizable({ + handles: { + s: '.ui-resizable-handle' + }, + grid: snapHeight, + start: function(ev, ui) { + snapDelta = prevSnapDelta = 0; + hideEvents(event, eventElement); + trigger('eventResizeStart', eventElement[0], event, ev, ui); + }, + resize: function(ev, ui) { + // don't rely on ui.size.height, doesn't take grid into account + snapDelta = Math.round((Math.max(snapHeight, eventElement.height()) - ui.originalSize.height) / snapHeight); + if (snapDelta != prevSnapDelta) { + eventEnd = getEventEnd(event).add(snapDuration * snapDelta); + var text; + if (snapDelta) { // has there been a change? + text = t.getEventTimeText(event.start, eventEnd); + } + else { + text = t.getEventTimeText(event); // the original time text + } + timeElement.text(text); + prevSnapDelta = snapDelta; + } + }, + stop: function(ev, ui) { + trigger('eventResizeStop', eventElement[0], event, ev, ui); + if (snapDelta) { + eventResize( + eventElement[0], + event, + eventEnd, + ev, + ui + ); + } + else { + showEvents(event, eventElement); + // BUG: if event was really short, need to put title back in span + } + } + }); + } + + +} + + + +/* Agenda Event Segment Utilities +-----------------------------------------------------------------------------*/ + + +// Sets the seg.backwardCoord and seg.forwardCoord on each segment and returns a new +// list in the order they should be placed into the DOM (an implicit z-index). +function placeSlotSegs(segs) { + var levels = buildSlotSegLevels(segs); + var level0 = levels[0]; + var i; + + computeForwardSlotSegs(levels); + + if (level0) { + + for (i=0; i seg2.start && seg1.start < seg2.end; +} + + +// A cmp function for determining which forward segment to rely on more when computing coordinates. +function compareForwardSlotSegs(seg1, seg2) { + // put higher-pressure first + return seg2.forwardPressure - seg1.forwardPressure || + // put segments that are closer to initial edge first (and favor ones with no coords yet) + (seg1.backwardCoord || 0) - (seg2.backwardCoord || 0) || + // do normal sorting... + compareSlotSegs(seg1, seg2); +} + + +// A cmp function for determining which segment should be closer to the initial edge +// (the left edge on a left-to-right calendar). +function compareSlotSegs(seg1, seg2) { + return seg1.start - seg2.start || // earlier start time goes first + (seg2.end - seg2.start) - (seg1.end - seg1.start) || // tie? longer-duration goes first + (seg1.event.title || '').localeCompare(seg2.event.title); // tie? alphabetically by title +} + + +;; + +fcViews.resourceDay = ResourceDayView; + +function ResourceDayView(element, calendar) { // TODO: make a DayView mixin + var t = this; + + + // exports + t.incrementDate = incrementDate; + t.render = render; + + // imports + ResourceView.call(t, element, calendar, 'resourceDay'); + var getResources = t.getResources; + + function incrementDate(date, delta) { + var out = date.clone().stripTime().add(delta, 'days'); + out = t.skipHiddenDays(out, delta < 0 ? -1 : 1); + return out; + } + + + function render(date) { + + t.start = t.intervalStart = date.clone().stripTime(); + t.end = t.intervalEnd = t.start.clone().add(1, 'days'); + + t.title = calendar.formatDate(t.start, t.opt('titleFormat')); + + t.renderResource(getResources().length); + } + + +} +;; + +setDefaults({ + allDaySlot: true, + allDayText: 'all-day', + + scrollTime: '06:00:00', + + slotDuration: '00:30:00', + + axisFormat: generateAgendaAxisFormat, + timeFormat: { + resource: generateAgendaTimeFormat + }, + + dragOpacity: { + resource: .5 + }, + minTime: '00:00:00', + maxTime: '24:00:00', + slotEventOverlap: true +}); + + +// TODO: make it work in quirks mode (event corners, all-day height) +// TODO: test liquid width, especially in IE6 + +function ResourceView(element, calendar, viewName) { + var t = this; + + + // exports + t.renderResource = renderResource; + t.setWidth = setWidth; + t.setHeight = setHeight; + t.afterRender = afterRender; + t.computeDateTop = computeDateTop; + t.getIsCellAllDay = getIsCellAllDay; + t.allDayRow = function() { return allDayRow; }; // badly named + t.getCoordinateGrid = function() { return coordinateGrid; }; // specifically for AgendaEventRenderer + t.getHoverListener = function() { return hoverListener; }; + t.colLeft = colLeft; + t.colRight = colRight; + t.colContentLeft = colContentLeft; + t.colContentRight = colContentRight; + t.getDaySegmentContainer = function() { return daySegmentContainer; }; + t.getSlotSegmentContainer = function() { return slotSegmentContainer; }; + t.getSlotContainer = function() { return slotContainer; }; + t.getRowCnt = function() { return 1; }; + t.getColCnt = function() { return 1; }; + t.getColWidth = function() { return colWidth; }; + t.getSnapHeight = function() { return snapHeight; }; + t.getSnapDuration = function() { return snapDuration; }; + t.getSlotHeight = function() { return slotHeight; }; + t.getSlotDuration = function() { return slotDuration; }; + t.getMinTime = function() { return minTime; }; + t.getMaxTime = function() { return maxTime; }; + t.defaultSelectionEnd = defaultSelectionEnd; + t.renderDayOverlay = renderDayOverlay; + t.renderSelection = renderSelection; + t.clearSelection = clearSelection; + t.reportDayClick = reportDayClick; // selection mousedown hack + t.dragStart = dragStart; + t.dragStop = dragStop; + t.getResources = calendar.fetchResources; + + // imports + View.call(t, element, calendar, viewName); + t.eventDrop = eventDrop; + t.eventResize = eventResize; + + OverlayManager.call(t); + SelectionManager.call(t); + ResourceEventRenderer.call(t); + var opt = t.opt; + var trigger = t.trigger; + var renderOverlay = t.renderOverlay; + var clearOverlays = t.clearOverlays; + var reportSelection = t.reportSelection; + var unselect = t.unselect; + var slotSegHtml = t.slotSegHtml; + var cellToDate = t.cellToDate; + var dateToCell = t.dateToCell; + var rangeToSegments = t.rangeToSegments; + var formatDate = calendar.formatDate; + var calculateWeekNumber = calendar.calculateWeekNumber; + var reportEventChange = calendar.reportEventChange; + + // locals + + var dayTable; + var dayHead; + var dayHeadCells; + var dayBody; + var dayBodyCells; + var dayBodyCellInners; + var dayBodyCellContentInners; + var dayBodyFirstCell; + var dayBodyFirstCellStretcher; + var slotLayer; + var daySegmentContainer; + var allDayTable; + var allDayRow; + var slotScroller; + var slotContainer; + var slotSegmentContainer; + var slotTable; + var selectionHelper; + + var viewWidth; + var viewHeight; + var axisWidth; + var colWidth; + var gutterWidth; + + var slotDuration; + var slotHeight; // TODO: what if slotHeight changes? (see issue 650) + + var snapDuration; + var snapRatio; // ratio of number of "selection" slots to normal slots. (ex: 1, 2, 4) + var snapHeight; // holds the pixel hight of a "selection" slot + + var colCnt; + var slotCnt; + var coordinateGrid; + var hoverListener; + var colPositions; + var colContentPositions; + var slotTopCache = {}; + + var tm; + var rtl; + var minTime; + var maxTime; + var colFormat; + var resources = t.getResources; + + /* Rendering + -----------------------------------------------------------------------------*/ + + + disableTextSelection(element.addClass('fc-agenda')); + + + function renderResource(resourceColumnsCnt) { + colCnt = resourceColumnsCnt; + updateOptions(); + + if (!dayTable) { // first time rendering? + buildSkeleton(); // builds day table, slot area, events containers + } + else { + buildDayTable(); // rebuilds day table + } + } + + function updateOptions() { + + tm = opt('theme') ? 'ui' : 'fc'; + rtl = opt('isRTL'); + colFormat = opt('columnFormat'); + + minTime = moment.duration(opt('minTime')); + maxTime = moment.duration(opt('maxTime')); + + slotDuration = moment.duration(opt('slotDuration')); + snapDuration = opt('snapDuration'); + snapDuration = snapDuration ? moment.duration(snapDuration) : slotDuration; + } + + + + /* Build DOM + -----------------------------------------------------------------------*/ + + + function buildSkeleton() { + var s; + var headerClass = tm + "-widget-header"; + var contentClass = tm + "-widget-content"; + var slotTime; + var slotDate; + var minutes; + var slotNormal = slotDuration.asMinutes() % 15 === 0; + + buildDayTable(); + + slotLayer = + $("
") + .appendTo(element); + + if (opt('allDaySlot')) { + + daySegmentContainer = + $("
") + .appendTo(slotLayer); + + s = + "" + + "" + + "" + + "" + + "" + + "" + + "
" + + ( + opt('allDayHTML') || + htmlEscape(opt('allDayText')) + ) + + "" + + "
" + + "
 
"; + allDayTable = $(s).appendTo(slotLayer); + allDayRow = allDayTable.find('tr'); + + dayBind(allDayRow.find('td')); + + slotLayer.append( + "
" + + "
" + + "
" + ); + + }else{ + + daySegmentContainer = $([]); // in jQuery 1.4, we can just do $() + + } + + slotScroller = + $("
") + .appendTo(slotLayer); + + slotContainer = + $("
") + .appendTo(slotScroller); + + slotSegmentContainer = + $("
") + .appendTo(slotContainer); + + s = + "" + + ""; + + slotTime = moment.duration(+minTime); // i wish there was .clone() for durations + slotCnt = 0; + while (slotTime < maxTime) { + slotDate = t.start.clone().time(slotTime); // will be in UTC but that's good. to avoid DST issues + minutes = slotDate.minutes(); + s += + "" + + "" + + "" + + ""; + slotTime.add(slotDuration); + slotCnt++; + } + + s += + "" + + "
" + + ((!slotNormal || !minutes) ? + htmlEscape(formatDate(slotDate, opt('axisFormat'))) : + ' ' + ) + + "" + + "
 
" + + "
"; + + slotTable = $(s).appendTo(slotContainer); + + slotBind(slotTable.find('td')); + } + + + + /* Build Day Table + -----------------------------------------------------------------------*/ + + + function buildDayTable() { + var html = buildDayTableHTML(); + + if (dayTable) { + dayTable.remove(); + } + dayTable = $(html).appendTo(element); + + dayHead = dayTable.find('thead'); + dayHeadCells = dayHead.find('th').slice(1, -1); // exclude gutter + dayBody = dayTable.find('tbody'); + dayBodyCells = dayBody.find('td').slice(0, -1); // exclude gutter + dayBodyCellInners = dayBodyCells.find('> div'); + dayBodyCellContentInners = dayBodyCells.find('.fc-day-content > div'); + + dayBodyFirstCell = dayBodyCells.eq(0); + dayBodyFirstCellStretcher = dayBodyCellInners.eq(0); + + markFirstLast(dayHead.add(dayHead.find('tr'))); + markFirstLast(dayBody.add(dayBody.find('tr'))); + + // TODO: now that we rebuild the cells every time, we should call dayRender + } + + + function buildDayTableHTML() { + var html = + "" + + buildDayTableHeadHTML() + + buildDayTableBodyHTML() + + "
"; + + return html; + } + + + function buildDayTableHeadHTML() { + var headerClass = tm + "-widget-header"; + var date; + var html = ''; + var weekText; + var col; + + html += + "" + + ""; + + if (opt('weekNumbers')) { + date = cellToDate(0, 0); + weekText = calculateWeekNumber(date); + if (rtl) { + weekText += opt('weekNumberTitle'); + } + else { + weekText = opt('weekNumberTitle') + weekText; + } + html += + "" + + htmlEscape(weekText) + + ""; + } + else { + html += " "; + } + + for (col=0; col" + + htmlEscape(resource.name) + + ""; + } + + html += + " " + + "" + + ""; + + return html; + } + + + function buildDayTableBodyHTML() { + var headerClass = tm + "-widget-header"; // TODO: make these when updateOptions() called + var contentClass = tm + "-widget-content"; + var date; + var today = calendar.getNow().stripTime(); + var col; + var cellsHTML; + var cellHTML; + var classNames; + var html = ''; + + html += + "" + + "" + + " "; + + cellsHTML = ''; + + for (col=0; col<(colCnt||1); col++) { + var resource = resources()[col]; + date = t.intervalStart.clone(); + + classNames = [ + 'fc-col' + col, + 'fc-' + dayIDs[date.day()], + contentClass + ]; + if (resource && resource.className) { + classNames.push(resource.className); + } + if (date.isSame(today, 'day')) { + classNames.push( + tm + '-state-highlight', + 'fc-today' + ); + } + else if (date < today) { + classNames.push('fc-past'); + } + else { + classNames.push('fc-future'); + } + + cellHTML = + "" + + "
" + + "
" + + "
 
" + + "
" + + "
" + + ""; + + cellsHTML += cellHTML; + } + + html += cellsHTML; + html += + " " + + "" + + ""; + + return html; + } + + + // TODO: data-date on the cells + + + + /* Dimensions + -----------------------------------------------------------------------*/ + + + function setHeight(height) { + if (height === undefined) { + height = viewHeight; + } + viewHeight = height; + slotTopCache = {}; + + var headHeight = dayBody.position().top; + var allDayHeight = slotScroller.position().top; // including divider + var bodyHeight = Math.min( // total body height, including borders + height - headHeight, // when scrollbars + slotTable.height() + allDayHeight + 1 // when no scrollbars. +1 for bottom border + ); + + dayBodyFirstCellStretcher + .height(bodyHeight - vsides(dayBodyFirstCell)); + + slotLayer.css('top', headHeight); + + slotScroller.height(bodyHeight - allDayHeight - 1); + + // the stylesheet guarantees that the first row has no border. + // this allows .height() to work well cross-browser. + var slotHeight0 = slotTable.find('tr:first').height() + 1; // +1 for bottom border + var slotHeight1 = slotTable.find('tr:eq(1)').height(); + // HACK: i forget why we do this, but i think a cross-browser issue + slotHeight = (slotHeight0 + slotHeight1) / 2; + + snapRatio = slotDuration / snapDuration; + snapHeight = slotHeight / snapRatio; + } + + + function setWidth(width) { + viewWidth = width; + colPositions.clear(); + colContentPositions.clear(); + + var axisFirstCells = dayHead.find('th:first'); + if (allDayTable) { + axisFirstCells = axisFirstCells.add(allDayTable.find('th:first')); + } + axisFirstCells = axisFirstCells.add(slotTable.find('th:first')); + + axisWidth = 0; + setOuterWidth( + axisFirstCells + .width('') + .each(function(i, _cell) { + axisWidth = Math.max(axisWidth, $(_cell).outerWidth()); + }), + axisWidth + ); + + var gutterCells = dayTable.find('.fc-agenda-gutter'); + if (allDayTable) { + gutterCells = gutterCells.add(allDayTable.find('th.fc-agenda-gutter')); + } + + var slotTableWidth = slotScroller[0].clientWidth; // needs to be done after axisWidth (for IE7) + + gutterWidth = slotScroller.width() - slotTableWidth; + if (gutterWidth) { + setOuterWidth(gutterCells, gutterWidth); + gutterCells + .show() + .prev() + .removeClass('fc-last'); + }else{ + gutterCells + .hide() + .prev() + .addClass('fc-last'); + } + + colWidth = Math.floor((slotTableWidth - axisWidth) / colCnt); + setOuterWidth(dayHeadCells.slice(0, -1), colWidth); + } + + + + /* Scrolling + -----------------------------------------------------------------------*/ + + + function resetScroll() { + var top = computeTimeTop( + moment.duration(opt('scrollTime')) + ) + 1; // +1 for the border + + function scroll() { + slotScroller.scrollTop(top); + } + + scroll(); + setTimeout(scroll, 0); // overrides any previous scroll state made by the browser + } + + + function afterRender() { // after the view has been freshly rendered and sized + resetScroll(); + } + + + + /* Slot/Day clicking and binding + -----------------------------------------------------------------------*/ + + + function dayBind(cells) { + cells.click(slotClick) + .mousedown(daySelectionMousedown); + } + + + function slotBind(cells) { + cells.click(slotClick) + .mousedown(slotSelectionMousedown); + } + + + function slotClick(ev) { + if (!opt('selectable')) { // if selectable, SelectionManager will worry about dayClick + + var col = Math.min(colCnt-1, Math.floor((ev.pageX - dayTable.offset().left - axisWidth) / colWidth)); + var date = cellToDate(0, 0); + var match = this.parentNode.className.match(/fc-slot(\d+)/); // TODO: maybe use data + + ev.data = resources()[col]; // added + + if (match) { + var slotIndex = parseInt(match[1], 10); + date.add(minTime + slotIndex * slotDuration); + date = calendar.rezoneDate(date); + trigger( + 'dayClick', + dayBodyCells[col], + date, + ev + ); + }else{ + trigger( + 'dayClick', + dayBodyCells[col], + date, + ev + ); + } + } + } + + + + /* Semi-transparent Overlay Helpers + -----------------------------------------------------*/ + // TODO: should be consolidated with BasicView's methods + + + function renderDayOverlay(overlayStart, overlayEnd, refreshCoordinateGrid, col) { // overlayEnd is exclusive + if (refreshCoordinateGrid) { + coordinateGrid.build(); + } + + var segments = rangeToSegments(overlayStart, overlayEnd); + + for (var i=0; i= 0) { + date.time(moment.duration(minTime + snapIndex * snapDuration)); + date = calendar.rezoneDate(date); + } + + return date; + } + + + function computeDateTop(date, startOfDayDate) { + return computeTimeTop( + moment.duration( + date.clone().stripZone() - startOfDayDate.clone().stripTime() + ) + ); + } + + + function computeTimeTop(time) { // time is a duration + + if (time < minTime) { + return 0; + } + if (time >= maxTime) { + return slotTable.height(); + } + + var slots = (time - minTime) / slotDuration; + var slotIndex = Math.floor(slots); + var slotPartial = slots - slotIndex; + var slotTop = slotTopCache[slotIndex]; + + // find the position of the corresponding + // need to use this tecnhique because not all rows are rendered at same height sometimes. + if (slotTop === undefined) { + slotTop = slotTopCache[slotIndex] = + slotTable.find('tr').eq(slotIndex).find('td div')[0].offsetTop; + // .eq() is faster than ":eq()" selector + // [0].offsetTop is faster than .position().top (do we really need this optimization?) + // a better optimization would be to cache all these divs + } + + var top = + slotTop - 1 + // because first row doesn't have a top border + slotPartial * slotHeight; // part-way through the row + + top = Math.max(top, 0); + + return top; + } + + + + /* Selection + ---------------------------------------------------------------------------------*/ + + + function defaultSelectionEnd(start) { + if (start.hasTime()) { + return start.clone().add(slotDuration); + } + else { + return start.clone().add(1, 'days'); + } + } + + + function renderSelection(start, end, col) { + if (start.hasTime() || end.hasTime()) { + renderSlotSelection(start, end); //, col); + } + else if (opt('allDaySlot')) { + renderDayOverlay(start, end, true, col); // true for refreshing coordinate grid + } + } + + + function renderSlotSelection(startDate, endDate, col) { + var helperOption = opt('selectHelper'); + coordinateGrid.build(); + if (helperOption) { + col = col || dateToCell(startDate).col; + if (col >= 0 && col < colCnt) { // only works when times are on same day + var rect = coordinateGrid.rect(0, col, 0, col, slotContainer); // only for horizontal coords + var top = computeDateTop(startDate, startDate); + var bottom = computeDateTop(endDate, startDate); + if (bottom > top) { // protect against selections that are entirely before or after visible range + rect.top = top; + rect.height = bottom - top; + rect.left += 2; + rect.width -= 5; + if ($.isFunction(helperOption)) { + var helperRes = helperOption(startDate, endDate); + if (helperRes) { + rect.position = 'absolute'; + selectionHelper = $(helperRes) + .css(rect) + .appendTo(slotContainer); + } + }else{ + rect.isStart = true; // conside rect a "seg" now + rect.isEnd = true; // + selectionHelper = $(slotSegHtml( + { + title: '', + start: startDate, + end: endDate, + className: ['fc-select-helper'], + editable: false + }, + rect + )); + selectionHelper.css('opacity', opt('dragOpacity')); + } + if (selectionHelper) { + slotBind(selectionHelper); + slotContainer.append(selectionHelper); + setOuterWidth(selectionHelper, rect.width, true); // needs to be after appended + setOuterHeight(selectionHelper, rect.height, true); + } + } + } + }else{ + renderSlotOverlay(startDate, endDate, col); + } + } + + + function clearSelection() { + clearOverlays(); + if (selectionHelper) { + selectionHelper.remove(); + selectionHelper = null; + } + } + + + function slotSelectionMousedown(ev) { + if (ev.which == 1 && opt('selectable')) { // ev.which==1 means left mouse button + unselect(ev); + var dates; + var col; + hoverListener.start(function(cell, origCell) { + clearSelection(); + if (cell && cell.col == origCell.col && !getIsCellAllDay(cell)) { + col = cell.col; + var d1 = realCellToDate(origCell); + var d2 = realCellToDate(cell); + dates = [ + d1, + d1.clone().add(snapDuration), // calculate minutes depending on selection slot minutes + d2, + d2.clone().add(snapDuration) + ].sort(dateCompare); + renderSlotSelection(dates[0], dates[3], cell.col); // updated + }else{ + dates = null; + } + }, ev); + $(document).one('mouseup', function(ev) { + hoverListener.stop(); + if (dates) { + if (+dates[0] == +dates[1]) { + reportDayClick(dates[0], ev); + } + ev.data = resources()[col]; // added + reportSelection(dates[0], dates[3], ev); + } + }); + } + } + + + function reportDayClick(date, ev) { + trigger('dayClick', dayBodyCells[dateToCell(date).col], date, ev); + } + + /* Event Modification Reporting + ---------------------------------------------------------------------------------*/ + + + function eventDrop(el, event, newResources, newStart, ev, ui) { + var mutateResult = calendar.mutateResourceEvent(event, newResources, newStart, null); + + trigger( + 'eventDrop', + el, + event, + mutateResult.dateDelta, + function() { + mutateResult.undo(); + reportEventChange(event._id); + }, + ev, + ui + ); + + reportEventChange(event._id); + } + + + function eventResize(el, event, newEnd, ev, ui) { + var mutateResult = calendar.mutateResourceEvent(event, event.resources, null, newEnd); + + trigger( + 'eventResize', + el, + event, + mutateResult.durationDelta, + function() { + mutateResult.undo(); + reportEventChange(event._id); + }, + ev, + ui + ); + + reportEventChange(event._id); + } + + + /* External Dragging + --------------------------------------------------------------------------------*/ + + + function dragStart(_dragElement, ev, ui) { + hoverListener.start(function(cell) { + clearOverlays(); + if (cell) { + var d1 = realCellToDate(cell); + var d2 = d1.clone(); + if (d1.hasTime()) { + d2.add(calendar.defaultTimedEventDuration); + renderSlotOverlay(d1, d2, cell.col); + } + else { + d2.add(calendar.defaultAllDayEventDuration); + renderDayOverlay(d1, d2, true, cell.col); + } + } + }, ev); + } + + + function dragStop(_dragElement, ev, ui) { + var cell = hoverListener.stop(); + clearOverlays(); + if (cell) { + ev.data = resources()[cell.col]; + trigger( + 'drop', + _dragElement, + realCellToDate(cell), + ev, + ui + ); + } + } + + /* OVERRIDES */ + function daySelectionMousedown(ev) { + var getIsCellAllDay = t.getIsCellAllDay; + var hoverListener = t.getHoverListener(); + var reportDayClick = t.reportDayClick; // this is hacky and sort of weird + var col; + if (ev.which == 1 && opt('selectable')) { // which==1 means left mouse button + unselect(ev); + var dates; + hoverListener.start(function(cell, origCell) { // TODO: maybe put cellToDate/getIsCellAllDay info in cell + clearSelection(); + if (cell && getIsCellAllDay(cell)) { + col = cell.col; + dates = [ realCellToDate(origCell), realCellToDate(cell) ].sort(dateCompare); + renderSelection(dates[0], dates[1], col); + }else{ + dates = null; + } + }, ev); + $(document).one('mouseup', function(ev) { + hoverListener.stop(); + if (dates) { + if (+dates[0] == +dates[1]) { + reportDayClick(dates[0], true, ev); + } + ev.data = resources()[col]; + reportSelection(dates[0], dates[1], ev); + } + }); + } + } + +} +;; + +function ResourceEventRenderer() { + var t = this; + + + // exports + t.renderEvents = renderEvents; + t.clearEvents = clearEvents; + t.slotSegHtml = slotSegHtml; + + + // imports + DayEventRenderer.call(t); + var opt = t.opt; + var trigger = t.trigger; + var isEventDraggable = t.isEventDraggable; + var isEventResizable = t.isEventResizable; + var eventElementHandlers = t.eventElementHandlers; + var setHeight = t.setHeight; + var getDaySegmentContainer = t.getDaySegmentContainer; + var getSlotSegmentContainer = t.getSlotSegmentContainer; + var getHoverListener = t.getHoverListener; + var computeDateTop = t.computeDateTop; + var getIsCellAllDay = t.getIsCellAllDay; + var colContentLeft = t.colContentLeft; + var colContentRight = t.colContentRight; + var cellToDate = t.cellToDate; + var getColCnt = function() { return getResources().length; }; + var getColWidth = t.getColWidth; + var getSnapHeight = t.getSnapHeight; + var getSnapDuration = t.getSnapDuration; + var getSlotHeight = t.getSlotHeight; + var getSlotDuration = t.getSlotDuration; + var getSlotContainer = t.getSlotContainer; + var reportEventElement = t.reportEventElement; + var showEvents = t.showEvents; + var hideEvents = t.hideEvents; + var eventDrop = t.eventDrop; + var eventResize = t.eventResize; + var renderDayOverlay = t.renderDayOverlay; + var clearOverlays = t.clearOverlays; + var renderDayEvents = t.renderDayEvents; + var getMinTime = t.getMinTime; + var getMaxTime = t.getMaxTime; + var calendar = t.calendar; + var formatDate = calendar.formatDate; + var getEventEnd = calendar.getEventEnd; + var getResources = t.getResources; + + + // overrides + t.draggableDayEvent = draggableDayEvent; + + + + /* Rendering + ----------------------------------------------------------------------------*/ + + + function renderEvents(events, modifiedEventId) { + var i, len=events.length, + dayEvents=[], + slotEvents=[]; + for (i=0; i rangeStart && eventStart < rangeEnd) { + + if (eventStart < rangeStart) { + segStart = rangeStart.clone(); + isStart = false; + } + else { + segStart = eventStart; + isStart = true; + } + + if (eventEnd > rangeEnd) { + segEnd = rangeEnd.clone(); + isEnd = false; + } + else { + segEnd = eventEnd; + isEnd = true; + } + + segs.push({ + event: event, + start: segStart, + end: segEnd, + isStart: isStart, + isEnd: isEnd + }); + } + } + + return segs.sort(compareSlotSegs); + } + + function eventsForResource(resource, events) { + var resourceEvents = []; + var hasResource = function(event) { + return event.resources && $.grep(event.resources, function(id) { + return id == resource.id; + }).length; + }; + + for (var i = 0; i < events.length; i++) { + if (hasResource(events[i])) { + resourceEvents.push(events[i]); + } + } + return resourceEvents; + } + + + // renders events in the 'time slots' at the bottom + // TODO: when we refactor this, when user returns `false` eventRender, don't have empty space + // TODO: refactor will include using pixels to detect collisions instead of dates (handy for seg cmp) + + function renderSlotSegs(segs, modifiedEventId) { + + var i, segCnt=segs.length, seg, + event, + top, + bottom, + columnLeft, + columnRight, + columnWidth, + width, + left, + right, + html = '', + eventElements, + eventElement, + triggerRes, + titleElement, + height, + slotSegmentContainer = getSlotSegmentContainer(), + isRTL = opt('isRTL'); + + // calculate position/dimensions, create html + for (i=0; i" + + "
" + + "
" + + htmlEscape(t.getEventTimeText(event)) + + "
" + + "
" + + htmlEscape(event.title || '') + + "
" + + "
" + + "
"; + + if (seg.isEnd && isEventResizable(event)) { + html += + "
=
"; + } + html += + ""; + return html; + } + + + function bindSlotSeg(event, eventElement, seg) { + var timeElement = eventElement.find('div.fc-event-time'); + if (isEventDraggable(event)) { + draggableSlotEvent(event, eventElement, timeElement); + } + if (seg.isEnd && isEventResizable(event)) { + resizableSlotEvent(event, eventElement, timeElement); + } + eventElementHandlers(event, eventElement); + } + + + + /* Dragging + -----------------------------------------------------------------------------------*/ + + + // when event starts out FULL-DAY + // overrides DayEventRenderer's version because it needs to account for dragging elements + // to and from the slot area. + + function draggableDayEvent(event, eventElement, seg) { + var isStart = seg.isStart; + var origWidth; + var revert; + var allDay = true; + var dayDelta; + var origCol; + + var hoverListener = getHoverListener(); + var colWidth = getColWidth(); + var minTime = getMinTime(); + var slotDuration = getSlotDuration(); + var slotHeight = getSlotHeight(); + var snapDuration = getSnapDuration(); + var snapHeight = getSnapHeight(); + + eventElement.draggable({ + opacity: opt('dragOpacity', 'month'), // use whatever the month view was using + revertDuration: opt('dragRevertDuration'), + start: function(ev, ui) { + + trigger('eventDragStart', eventElement[0], event, ev, ui); + hideEvents(event, eventElement); + origWidth = eventElement.width(); + + hoverListener.start(function(cell, origCell) { + clearOverlays(); + if (cell) { + revert = false; + origCol = origCell.col; + var origDate = cellToDate(0, origCell.col); + var date = cellToDate(0, cell.col); + dayDelta = date.diff(origDate, 'days'); + + if (!cell.row) { // on full-days + + renderDayOverlay( + event.start.clone().add(dayDelta, 'days'), + getEventEnd(event).add(dayDelta, 'days'), + true, + 1 + ); + + resetElement(); + } + else { // mouse is over bottom slots + + if (isStart) { + if (allDay) { + // convert event to temporary slot-event + eventElement.width(colWidth - 10); // don't use entire width + setOuterHeight(eventElement, calendar.defaultTimedEventDuration / slotDuration * slotHeight); // the default height + eventElement.draggable('option', 'grid', [ colWidth, 1 ]); + allDay = false; + } + } + else { + revert = true; + } + } + + revert = revert || (allDay && !dayDelta); + } + else { + resetElement(); + revert = true; + } + + eventElement.draggable('option', 'revert', revert); + + }, ev, 'drag'); + }, + stop: function(ev, ui) { + hoverListener.stop(); + clearOverlays(); + trigger('eventDragStop', eventElement[0], event, ev, ui); + + if (revert) { // hasn't moved or is out of bounds (draggable has already reverted) + + resetElement(); + eventElement.css('filter', ''); // clear IE opacity side-effects + showEvents(event, eventElement); + } + else { // changed! + // calculate column delta + var newCol = Math.round((eventElement.offset().left - getSlotContainer().offset().left) / colWidth); + // if (newCol !== origCol){ + // event.resources = [ resources()[newCol].id ]; + // } + var resources = event.resources; + if (newCol !== origCol){ + resources = [ getResources()[newCol].id ]; + } + + var eventStart = event.start.clone(); // already assumed to have a stripped time + var snapTime; + var snapIndex; + if (!allDay) { + snapIndex = Math.round((eventElement.offset().top - getSlotContainer().offset().top) / snapHeight); // why not use ui.offset.top? + snapTime = moment.duration(minTime + snapIndex * snapDuration); + eventStart = calendar.rezoneDate(eventStart.clone().time(snapTime)); + } + + eventDrop( + eventElement[0], + event, + resources, + eventStart, + ev, + ui + ); + } + } + }); + function resetElement() { + if (!allDay) { + eventElement + .width(origWidth) + .height('') + .draggable('option', 'grid', null); + allDay = true; + } + } + } + + + // when event starts out IN TIMESLOTS + + function draggableSlotEvent(event, eventElement, timeElement) { + var coordinateGrid = t.getCoordinateGrid(); + var colCnt = getColCnt(); + var colWidth = getColWidth(); + var snapHeight = getSnapHeight(); + var snapDuration = getSnapDuration(); + + // states + var origPosition; // original position of the element, not the mouse + var origCell; + var isInBounds, prevIsInBounds; + var isAllDay, prevIsAllDay; + var colDelta, prevColDelta; + var dayDelta; // derived from colDelta + var resourceDelta; // derived from colDelta + var snapDelta, prevSnapDelta; // the number of snaps away from the original position + + // newly computed + var eventStart, eventEnd; + + eventElement.draggable({ + scroll: false, + grid: [ colWidth, snapHeight ], + axis: colCnt==1 ? 'y' : false, + opacity: opt('dragOpacity'), + revertDuration: opt('dragRevertDuration'), + start: function(ev, ui) { + + trigger('eventDragStart', eventElement[0], event, ev, ui); + hideEvents(event, eventElement); + + coordinateGrid.build(); + + // initialize states + origPosition = eventElement.position(); + origCell = coordinateGrid.cell(ev.pageX, ev.pageY); + isInBounds = prevIsInBounds = true; + isAllDay = prevIsAllDay = getIsCellAllDay(origCell); + colDelta = prevColDelta = 0; + dayDelta = 0; + resourceDelta = 0; + snapDelta = prevSnapDelta = 0; + + eventStart = null; + eventEnd = null; + }, + drag: function(ev, ui) { + + // NOTE: this `cell` value is only useful for determining in-bounds and all-day. + // Bad for anything else due to the discrepancy between the mouse position and the + // element position while snapping. (problem revealed in PR #55) + // + // PS- the problem exists for draggableDayEvent() when dragging an all-day event to a slot event. + // We should overhaul the dragging system and stop relying on jQuery UI. + var cell = coordinateGrid.cell(ev.pageX, ev.pageY); + + // update states + isInBounds = !!cell; + if (isInBounds) { + isAllDay = getIsCellAllDay(cell); + + // calculate column delta + colDelta = Math.round((ui.position.left - origPosition.left) / colWidth); + if (colDelta != prevColDelta) { + // calculate the day delta based off of the original clicked column and the column delta + resourceDelta = colDelta; + } + + // calculate minute delta (only if over slots) + if (!isAllDay) { + snapDelta = Math.round((ui.position.top - origPosition.top) / snapHeight); + } + } + + // any state changes? + if ( + isInBounds != prevIsInBounds || + isAllDay != prevIsAllDay || + colDelta != prevColDelta || + snapDelta != prevSnapDelta + ) { + + // compute new dates + if (isAllDay) { + eventStart = event.start.clone().stripTime().add(dayDelta, 'days'); + eventEnd = eventStart.clone().add(calendar.defaultAllDayEventDuration); + } + else { + eventStart = event.start.clone().add(snapDelta * snapDuration).add(dayDelta, 'days'); + eventEnd = getEventEnd(event).add(snapDelta * snapDuration).add(dayDelta, 'days'); + } + + updateUI(); + + // update previous states for next time + prevIsInBounds = isInBounds; + prevIsAllDay = isAllDay; + prevColDelta = colDelta; + prevSnapDelta = snapDelta; + } + + // if out-of-bounds, revert when done, and vice versa. + eventElement.draggable('option', 'revert', !isInBounds); + + }, + stop: function(ev, ui) { + + clearOverlays(); + + trigger('eventDragStop', eventElement[0], event, ev, ui); + + if (isInBounds && (isAllDay || resourceDelta || snapDelta)) { // changed! + var resources = event.resources; + if (resourceDelta){ + resources = [ getResources()[origCell.col + resourceDelta].id ]; + } + eventDrop( + eventElement[0], + event, + resources, + eventStart, + ev, + ui + ); + } + else { // either no change or out-of-bounds (draggable has already reverted) + + // reset states for next time, and for updateUI() + isInBounds = true; + isAllDay = false; + colDelta = 0; + dayDelta = 0; + snapDelta = 0; + + updateUI(); + eventElement.css('filter', ''); // clear IE opacity side-effects + + // sometimes fast drags make event revert to wrong position, so reset. + // also, if we dragged the element out of the area because of snapping, + // but the *mouse* is still in bounds, we need to reset the position. + eventElement.css(origPosition); + + showEvents(event, eventElement); + } + } + }); + + function updateUI() { + clearOverlays(); + if (isInBounds) { + if (isAllDay) { + timeElement.hide(); + eventElement.draggable('option', 'grid', null); // disable grid snapping + renderDayOverlay(eventStart, eventEnd, false, origCell.col + colDelta); + } + else { + updateTimeText(); + timeElement.css('display', ''); // show() was causing display=inline + eventElement.draggable('option', 'grid', [colWidth, snapHeight]); // re-enable grid snapping + } + } + } + + function updateTimeText() { + if (eventStart) { // must of had a state change + timeElement.text( + t.getEventTimeText(eventStart, event.end ? eventEnd : null) + // ^ + // only display the new end if there was an old end + ); + } + } + + } + + + + /* Resizing + --------------------------------------------------------------------------------------*/ + + + function resizableSlotEvent(event, eventElement, timeElement) { + var snapDelta, prevSnapDelta; + var snapHeight = getSnapHeight(); + var snapDuration = getSnapDuration(); + var eventEnd; + + eventElement.resizable({ + handles: { + s: '.ui-resizable-handle' + }, + grid: snapHeight, + start: function(ev, ui) { + snapDelta = prevSnapDelta = 0; + hideEvents(event, eventElement); + trigger('eventResizeStart', eventElement[0], event, ev, ui); + }, + resize: function(ev, ui) { + // don't rely on ui.size.height, doesn't take grid into account + snapDelta = Math.round((Math.max(snapHeight, eventElement.height()) - ui.originalSize.height) / snapHeight); + if (snapDelta != prevSnapDelta) { + eventEnd = getEventEnd(event).add(snapDuration * snapDelta); + var text; + if (snapDelta) { // has there been a change? + text = t.getEventTimeText(event.start, eventEnd); + } + else { + text = t.getEventTimeText(event); // the original time text + } + timeElement.text(text); + prevSnapDelta = snapDelta; + } + }, + stop: function(ev, ui) { + trigger('eventResizeStop', eventElement[0], event, ev, ui); + if (snapDelta) { + eventResize( + eventElement[0], + event, + eventEnd, + ev, + ui + ); + } + else { + showEvents(event, eventElement); + // BUG: if event was really short, need to put title back in span + } + } + }); + } + + +} +;; + + +function View(element, calendar, viewName) { + var t = this; + + + // exports + t.element = element; + t.calendar = calendar; + t.name = viewName; + t.opt = opt; + t.trigger = trigger; + t.isEventDraggable = isEventDraggable; + t.isEventResizable = isEventResizable; + t.clearEventData = clearEventData; + t.reportEventElement = reportEventElement; + t.triggerEventDestroy = triggerEventDestroy; + t.eventElementHandlers = eventElementHandlers; + t.showEvents = showEvents; + t.hideEvents = hideEvents; + t.eventDrop = eventDrop; + t.eventResize = eventResize; + // t.start, t.end // moments with ambiguous-time + // t.intervalStart, t.intervalEnd // moments with ambiguous-time + + + // imports + var reportEventChange = calendar.reportEventChange; + + + // locals + var eventElementsByID = {}; // eventID mapped to array of jQuery elements + var eventElementCouples = []; // array of objects, { event, element } // TODO: unify with segment system + var options = calendar.options; + var nextDayThreshold = moment.duration(options.nextDayThreshold); + + + + + function opt(name, viewNameOverride) { + var v = options[name]; + if ($.isPlainObject(v) && !isForcedAtomicOption(name)) { + return smartProperty(v, viewNameOverride || viewName); + } + return v; + } + + + function trigger(name, thisObj) { + return calendar.trigger.apply( + calendar, + [name, thisObj || t].concat(Array.prototype.slice.call(arguments, 2), [t]) + ); + } + + + + /* Event Editable Boolean Calculations + ------------------------------------------------------------------------------*/ + + + function isEventDraggable(event) { + var source = event.source || {}; + return firstDefined( + event.startEditable, + source.startEditable, + opt('eventStartEditable'), + event.editable, + source.editable, + opt('editable') + ); + } + + + function isEventResizable(event) { // but also need to make sure the seg.isEnd == true + var source = event.source || {}; + return firstDefined( + event.durationEditable, + source.durationEditable, + opt('eventDurationEditable'), + event.editable, + source.editable, + opt('editable') + ); + } + + + + /* Event Data + ------------------------------------------------------------------------------*/ + + + function clearEventData() { + eventElementsByID = {}; + eventElementCouples = []; + } + + + + /* Event Elements + ------------------------------------------------------------------------------*/ + + + // report when view creates an element for an event + function reportEventElement(event, element) { + eventElementCouples.push({ event: event, element: element }); + if (eventElementsByID[event._id]) { + eventElementsByID[event._id].push(element); + }else{ + eventElementsByID[event._id] = [element]; + } + } + + + function triggerEventDestroy() { + $.each(eventElementCouples, function(i, couple) { + t.trigger('eventDestroy', couple.event, couple.event, couple.element); + }); + } + + + // attaches eventClick, eventMouseover, eventMouseout + function eventElementHandlers(event, eventElement) { + eventElement + .click(function(ev) { + if (!eventElement.hasClass('ui-draggable-dragging') && + !eventElement.hasClass('ui-resizable-resizing')) { + return trigger('eventClick', this, event, ev); + } + }) + .hover( + function(ev) { + trigger('eventMouseover', this, event, ev); + }, + function(ev) { + trigger('eventMouseout', this, event, ev); + } + ); + // TODO: don't fire eventMouseover/eventMouseout *while* dragging is occuring (on subject element) + // TODO: same for resizing + } + + + function showEvents(event, exceptElement) { + eachEventElement(event, exceptElement, 'show'); + } + + + function hideEvents(event, exceptElement) { + eachEventElement(event, exceptElement, 'hide'); + } + + + function eachEventElement(event, exceptElement, funcName) { + // NOTE: there may be multiple events per ID (repeating events) + // and multiple segments per event + var elements = eventElementsByID[event._id], + i, len = elements.length; + for (i=0; i