# HG changeset patch # User Julien Cristau # Date 1389947753 -3600 # Node ID 60322cb8636c0402cdac025d3297626c41583023 # Parent c132eab34980d28ba2b71dfdbc1133edfff53a0d [web/data] Update fullcalendar to version 1.6.4 Fixes compat with jquery 1.10 (closes #3445689) diff -r c132eab34980 -r 60322cb8636c web/data/cubicweb.calendar.css --- a/web/data/cubicweb.calendar.css Thu Jan 16 15:10:54 2014 +0100 +++ b/web/data/cubicweb.calendar.css Fri Jan 17 09:35:53 2014 +0100 @@ -342,21 +342,21 @@ /* ------------------------- */ /* tooltips for fullcalendar */ -div.calevent div.tooltip { +a.calevent div.tooltip { display: none; /* tooltip hidden */ } -div.calevent:hover { +a.calevent:hover { z-index: auto !important; /* in order that the tooltip from the above .calevent div can be put over this div*/ } -div.calevent a{ +a.calevent { display: inline; font-size: none; font-weight: bold; } -div.calevent:hover div.tooltip{ +a.calevent:hover div.tooltip{ display: block; position: absolute; z-index: 10; @@ -382,4 +382,4 @@ div.fc-view{ overflow: visible; -} \ No newline at end of file +} diff -r c132eab34980 -r 60322cb8636c web/data/fullcalendar.css --- a/web/data/fullcalendar.css Thu Jan 16 15:10:54 2014 +0100 +++ b/web/data/fullcalendar.css Fri Jan 17 09:35:53 2014 +0100 @@ -1,24 +1,10 @@ -/* - * FullCalendar v1.4.8 Stylesheet - * - * Feel free to edit this file to customize the look of FullCalendar. - * When upgrading to newer versions, please upgrade this file as well, - * porting over any customizations afterwards. - * - * Date: Sat Oct 16 17:10:03 2010 -0700 - * +/*! + * FullCalendar v1.6.4 Stylesheet + * Docs & License: http://arshaw.com/fullcalendar/ + * (c) 2013 Adam Shaw */ - - -/* TODO: make font sizes look the same in all doctypes */ -.fc, -.fc .fc-header, -.fc .fc-content { - font-size: 1em; - } - .fc { direction: ltr; text-align: left; @@ -29,7 +15,13 @@ border-spacing: 0; } -.fc td, .fc th { +html .fc, +.fc table { + font-size: 1em; + } + +.fc td, +.fc th { padding: 0; vertical-align: top; } @@ -38,242 +30,262 @@ /* Header ------------------------------------------------------------------------*/ - -table.fc-header { - width: 100%; + +.fc-header td { + white-space: nowrap; } - + .fc-header-left { width: 25%; - } - -.fc-header-left table { - float: left; + text-align: left; } .fc-header-center { - width: 50%; text-align: center; } -.fc-header-center table { - margin: 0 auto; - } - .fc-header-right { width: 25%; - } - -.fc-header-right table { - float: right; + text-align: right; } .fc-header-title { + display: inline-block; + vertical-align: top; + } + +.fc-header-title h2 { margin-top: 0; white-space: nowrap; } -.fc-header-space { +.fc .fc-header-space { padding-left: 10px; } -/* right-to-left */ +.fc-header .fc-button { + margin-bottom: 1em; + vertical-align: top; + } + +/* buttons edges butting together */ -.fc-rtl .fc-header-title { - direction: rtl; +.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 { + clear: both; + zoom: 1; /* for IE7, gives accurate coordinates for [un]freezeContentHeight */ + } + +.fc-view { + 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-header .fc-state-default, -.fc-header .ui-state-default { - margin-bottom: 1em; +.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-header .fc-state-default { - border-width: 1px 0; - padding: 0 1px; - } - -.fc-header .fc-state-default, -.fc-header .fc-state-default a { - border-style: solid; +.fc-state-default { /* non-theme */ + border: 1px solid; } - -.fc-header .fc-state-default a { - display: block; - border-width: 0 1px; - margin: 0 -1px; - width: 100%; - text-decoration: none; - } - -.fc-header .fc-state-default span { - display: block; - border-style: solid; - border-width: 1px 0 1px 1px; - padding: 3px 5px; - } - -.fc-header .ui-state-default { - padding: 4px 6px; - } - -.fc-header .fc-state-default span, -.fc-header .ui-state-default span { - white-space: nowrap; + +.fc-state-default.fc-corner-left { /* non-theme */ + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; } - -/* for adjacent buttons */ - -.fc-header .fc-no-right { - padding-right: 0; - } - -.fc-header .fc-no-right a { - margin-right: 0; - border-right: 0; - } - -.fc-header .ui-no-right { - border-right: 0; + +.fc-state-default.fc-corner-right { /* non-theme */ + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; } - -/* for fake rounded corners */ - -.fc-header .fc-corner-left { - margin-left: 1px; - padding-left: 0; - } - -.fc-header .fc-corner-right { - margin-right: 1px; - padding-right: 0; - } - -/* DEFAULT button COLORS */ - -.fc-header .fc-state-default, -.fc-header .fc-state-default a { - border-color: #777; /* outer border */ - color: #333; + +/* + Our default prev/next buttons use HTML entities like ‹ › « » + and we'll try to make them look good cross-browser. +*/ + +.fc-text-arrow { + margin: 0 .1em; + font-size: 2em; + font-family: "Courier New", Courier, monospace; + vertical-align: baseline; /* for IE7 */ } -.fc-header .fc-state-default span { - border-color: #fff #fff #d1d1d1; /* inner border */ - background: #e8e8e8; - } - -/* PRESSED button COLORS (down and active) */ - -.fc-header .fc-state-active a { - color: #fff; +.fc-button-prev .fc-text-arrow, +.fc-button-next .fc-text-arrow { /* for ‹ › */ + font-weight: bold; } -.fc-header .fc-state-down span, -.fc-header .fc-state-active span { - background: #888; - border-color: #808080 #808080 #909090; /* inner border */ +/* icon (for jquery ui) */ + +.fc-button .fc-icon-wrap { + position: relative; + float: left; + top: 50%; } -/* DISABLED button COLORS */ - -.fc-header .fc-state-disabled a { - color: #999; - } - -.fc-header .fc-state-disabled, -.fc-header .fc-state-disabled a { - border-color: #ccc; /* outer border */ - } - -.fc-header .fc-state-disabled span { - border-color: #fff #fff #f0f0f0; /* inner border */ - background: #f0f0f0; +.fc-button .ui-icon { + position: relative; + float: left; + margin-top: -50%; + *margin-top: 0; + *top: -50%; } - - -/* Content Area & Global Cell Styles -------------------------------------------------------------------------*/ - -.fc-widget-content { - border: 1px solid #ccc; /* outer border color */ +/* + 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-content { - clear: both; + +.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-content .fc-state-default { - border-style: solid; - border-color: #ccc; /* inner border color */ + +.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; + } + -.fc-content .fc-state-highlight { /* today */ - background: #ffc; - } - -.fc-content .fc-not-today { /* override jq-ui highlight (TODO: ui-widget-content) */ - background: none; - } - -.fc-cell-overlay { /* semi-transparent rectangle while dragging */ - background: #9cf; - opacity: .2; - filter: alpha(opacity=20); /* for IE */ - } - -.fc-view { /* prevents dragging outside of widget */ - width: 100%; - overflow: hidden; - } - - - - /* Global Event Styles ------------------------------------------------------------------------*/ -.fc-event, -.fc-agenda .fc-event-time, -.fc-event a { - border-style: solid; - border-color: #36c; /* default BORDER color (probably the same as background-color) */ - background-color: #36c; /* default BACKGROUND color */ - color: #fff; /* default TEXT color */ +.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; } - /* Use the 'className' CalEvent property and the following - * example CSS to change event color on a per-event basis: - * - * .myclass, - * .fc-agenda .myclass .fc-event-time, - * .myclass a { - * background-color: black; - * border-color: black; - * color: red; - * } - */ - -.fc-event { - text-align: left; - } - -.fc-event a { - overflow: hidden; - font-size: .85em; - text-decoration: none; +a.fc-event, +.fc-event-draggable { cursor: pointer; } -.fc-event-editable { - cursor: pointer; +.fc-rtl .fc-event { + text-align: right; + } + +.fc-event-inner { + width: 100%; + height: 100%; + overflow: hidden; } .fc-event-time, @@ -281,29 +293,13 @@ padding: 0 1px; } -/* for fake rounded corners */ - -.fc-event a { - display: block; - position: relative; - width: 100%; - height: 100%; - } - -/* right-to-left */ - -.fc-rtl .fc-event a { - text-align: right; - } - -/* resizable */ - .fc .ui-resizable-handle { display: block; position: absolute; z-index: 99999; - border: 0 !important; /* important overrides pre jquery ui 1.7 styles */ - background: url(data:image/gif;base64,AAAA) !important; /* hover fix for IE */ + overflow: hidden; /* hacky spaces (IE6/7) */ + font-size: 300%; /* */ + line-height: 50%; /* */ } @@ -315,29 +311,19 @@ border-width: 1px 0; margin-bottom: 1px; } - -.fc-event-hori a { - border-width: 0; - } - -/* for fake rounded corners */ - -.fc-content .fc-corner-left { - margin-left: 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-content .fc-corner-left a { - margin-left: -1px; - border-left-width: 1px; - } - -.fc-content .fc-corner-right { - margin-right: 1px; - } - -.fc-content .fc-corner-right a { - margin-right: -1px; + +.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 */ @@ -364,26 +350,49 @@ +/* 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 table { - width: 100%; - } - -.fc .fc-grid th { - border-width: 0 0 0 1px; +.fc-grid th { text-align: center; } - -.fc .fc-grid td { - border-width: 1px 0 0 1px; + +.fc .fc-week-number { + width: 22px; + text-align: center; } - -.fc-grid th.fc-leftmost, -.fc-grid td.fc-leftmost { - border-left: 0; + +.fc .fc-week-number div { + padding: 0 2px; } .fc-grid .fc-day-number { @@ -401,7 +410,7 @@ .fc-grid .fc-day-content { clear: both; - padding: 2px 2px 0; /* distance between events and day edges */ + padding: 2px 2px 1px; /* distance between events and day edges */ } /* event styles */ @@ -411,10 +420,6 @@ } /* right-to-left */ - -.fc-rtl .fc-grid { - direction: rtl; - } .fc-rtl .fc-grid .fc-day-number { float: left; @@ -424,85 +429,95 @@ float: right; } + + /* Agenda Week View, Agenda Day View ------------------------------------------------------------------------*/ -.fc .fc-agenda th, -.fc .fc-agenda td { - border-width: 1px 0 0 1px; - } - -.fc .fc-agenda .fc-leftmost { - border-left: 0; +.fc-agenda table { + border-collapse: separate; } -.fc-agenda tr.fc-first th, -.fc-agenda tr.fc-first td { - border-top: 0; - } - -.fc-agenda-head tr.fc-last th { - border-bottom-width: 1px; - } - -.fc .fc-agenda-head td, -.fc .fc-agenda-body td { - background: none; - } - -.fc-agenda-head th { +.fc-agenda-days th { text-align: center; } -/* the time axis running down the left side */ - -.fc-agenda .fc-axis { +.fc-agenda .fc-agenda-axis { width: 50px; padding: 0 4px; vertical-align: middle; + text-align: right; white-space: nowrap; - text-align: right; font-weight: normal; } + +.fc-agenda .fc-week-number { + font-weight: bold; + } + +.fc-agenda .fc-day-content { + padding: 2px 2px 1px; + } -/* all-day event cells at top */ +/* make axis border take precedence */ -.fc-agenda-head tr.fc-all-day th { - height: 35px; +.fc-agenda-days .fc-agenda-axis { + border-right-width: 1px; + } + +.fc-agenda-days .fc-col0 { + border-left-width: 0; } -.fc-agenda-head td { - padding-bottom: 10px; +/* 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; } -.fc .fc-divider div { - font-size: 1px; /* for IE6/7 */ +/* divider (between all-day and slots) */ + +.fc-agenda-divider-inner { height: 2px; + overflow: hidden; + } + +.fc-widget-header .fc-agenda-divider-inner { + background: #eee; } -.fc .fc-divider .fc-state-default { - background: #eee; /* color for divider between all-day and time-slot events */ +/* 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; } -/* body styles */ - -.fc .fc-agenda-body td div { - height: 20px; /* slot height */ - } - -.fc .fc-agenda-body tr.fc-minor th, -.fc .fc-agenda-body tr.fc-minor td { +.fc-agenda-slots tr.fc-minor th, +.fc-agenda-slots tr.fc-minor td { border-top-style: dotted; } -.fc-agenda .fc-day-content { - padding: 2px 2px 0; /* distance between events and day edges */ - } - -/* vertical background columns */ - -.fc .fc-agenda-bg .ui-state-highlight { - background-image: none; /* tall column, don't want repeating background image */ +.fc-agenda-slots tr.fc-minor th.ui-widget-header { + *border-top-style: solid; /* doesn't work with background in IE6/7 */ } @@ -513,52 +528,30 @@ .fc-event-vert { border-width: 0 1px; } - -.fc-event-vert a { - border-width: 0; + +.fc-event-vert.fc-event-start { + border-top-width: 1px; + border-top-left-radius: 3px; + border-top-right-radius: 3px; } - -/* for fake rounded corners */ - -.fc-content .fc-corner-top { - margin-top: 1px; + +.fc-event-vert.fc-event-end { + border-bottom-width: 1px; + border-bottom-left-radius: 3px; + border-bottom-right-radius: 3px; } -.fc-content .fc-corner-top a { - margin-top: -1px; - border-top-width: 1px; - } - -.fc-content .fc-corner-bottom { - margin-bottom: 1px; +.fc-event-vert .fc-event-time { + white-space: nowrap; + font-size: 10px; } - -.fc-content .fc-corner-bottom a { - margin-bottom: -1px; - border-bottom-width: 1px; - } - -/* event content */ - -.fc-event-vert span { - display: block; + +.fc-event-vert .fc-event-inner { position: relative; z-index: 2; } -.fc-event-vert span.fc-event-time { - white-space: nowrap; - _white-space: normal; - overflow: hidden; - border: 0; - font-size: 10px; - } - -.fc-event-vert span.fc-event-title { - line-height: 13px; - } - -.fc-event-vert span.fc-event-bg { /* makes the event lighter w/ a semi-transparent overlay */ +.fc-event-vert .fc-event-bg { /* makes the event lighter w/ a semi-transparent overlay */ position: absolute; z-index: 1; top: 0; @@ -566,8 +559,13 @@ width: 100%; height: 100%; background: #fff; - opacity: .3; - filter: alpha(opacity=30); /* for IE */ + 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 */ @@ -576,6 +574,7 @@ 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; @@ -583,4 +582,8 @@ cursor: s-resize; } +.fc-agenda .ui-resizable-resizing { /* TODO: better selector */ + _overflow: hidden; + } + diff -r c132eab34980 -r 60322cb8636c web/data/fullcalendar.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/data/fullcalendar.js Fri Jan 17 09:35:53 2014 +0100 @@ -0,0 +1,6110 @@ +/*! + * FullCalendar v1.6.4 + * Docs & License: http://arshaw.com/fullcalendar/ + * (c) 2013 Adam Shaw + */ + +/* + * Use fullcalendar.css for basic styling. + * For event drag & drop, requires jQuery UI draggable. + * For event resizing, requires jQuery UI resizable. + */ + +(function($, undefined) { + + +;; + +var defaults = { + + // display + defaultView: 'month', + aspectRatio: 1.35, + header: { + left: 'title', + center: '', + right: 'today prev,next' + }, + weekends: true, + weekNumbers: false, + weekNumberCalculation: 'iso', + weekNumberTitle: 'W', + + // editing + //editable: false, + //disableDragging: false, + //disableResizing: false, + + allDayDefault: true, + ignoreTimezone: true, + + // event ajax + lazyFetching: true, + startParam: 'start', + endParam: 'end', + + // time formats + titleFormat: { + month: 'MMMM yyyy', + week: "MMM d[ yyyy]{ '—'[ MMM] d yyyy}", + day: 'dddd, MMM d, yyyy' + }, + columnFormat: { + month: 'ddd', + week: 'ddd M/d', + day: 'dddd M/d' + }, + timeFormat: { // for event elements + '': 'h(:mm)t' // default + }, + + // locale + isRTL: false, + firstDay: 0, + monthNames: ['January','February','March','April','May','June','July','August','September','October','November','December'], + monthNamesShort: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'], + dayNames: ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'], + dayNamesShort: ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'], + buttonText: { + prev: "", + next: "", + prevYear: "«", + nextYear: "»", + today: 'today', + month: 'month', + week: 'week', + day: 'day' + }, + + // jquery-ui theming + theme: false, + buttonIcons: { + prev: 'circle-triangle-w', + next: 'circle-triangle-e' + }, + + //selectable: false, + unselectAuto: true, + + dropAccept: '*', + + handleWindowResize: true + +}; + +// right-to-left defaults +var rtlDefaults = { + header: { + left: 'next,prev today', + center: '', + right: 'title' + }, + buttonText: { + prev: "", + next: "", + prevYear: "»", + nextYear: "«" + }, + buttonIcons: { + prev: 'circle-triangle-e', + next: 'circle-triangle-w' + } +}; + + + +;; + +var fc = $.fullCalendar = { version: "1.6.4" }; +var fcViews = fc.views = {}; + + +$.fn.fullCalendar = function(options) { + + + // method calling + if (typeof options == 'string') { + var args = Array.prototype.slice.call(arguments, 1); + var res; + this.each(function() { + var calendar = $.data(this, 'fullCalendar'); + if (calendar && $.isFunction(calendar[options])) { + var r = calendar[options].apply(calendar, args); + if (res === undefined) { + res = r; + } + if (options == 'destroy') { + $.removeData(this, 'fullCalendar'); + } + } + }); + if (res !== undefined) { + return res; + } + return this; + } + + options = options || {}; + + // would like to have this logic in EventManager, but needs to happen before options are recursively extended + var eventSources = options.eventSources || []; + delete options.eventSources; + if (options.events) { + eventSources.push(options.events); + delete options.events; + } + + + options = $.extend(true, {}, + defaults, + (options.isRTL || options.isRTL===undefined && defaults.isRTL) ? rtlDefaults : {}, + options + ); + + + this.each(function(i, _element) { + var element = $(_element); + var calendar = new Calendar(element, options, eventSources); + element.data('fullCalendar', calendar); // TODO: look into memory leak implications + calendar.render(); + }); + + + return this; + +}; + + +// function for adding/overriding defaults +function setDefaults(d) { + $.extend(true, defaults, d); +} + + + +;; + + +function Calendar(element, options, eventSources) { + var t = this; + + + // exports + t.options = options; + t.render = render; + t.destroy = destroy; + t.refetchEvents = refetchEvents; + t.reportEvents = reportEvents; + t.reportEventChange = reportEventChange; + t.rerenderEvents = rerenderEvents; + t.changeView = changeView; + t.select = select; + t.unselect = unselect; + t.prev = prev; + t.next = next; + t.prevYear = prevYear; + t.nextYear = nextYear; + t.today = today; + t.gotoDate = gotoDate; + t.incrementDate = incrementDate; + t.formatDate = function(format, date) { return formatDate(format, date, options) }; + t.formatDates = function(format, date1, date2) { return formatDates(format, date1, date2, options) }; + t.getDate = getDate; + t.getView = getView; + t.option = option; + t.trigger = trigger; + + + // imports + EventManager.call(t, options, eventSources); + var isFetchNeeded = t.isFetchNeeded; + var fetchEvents = t.fetchEvents; + + + // locals + var _element = element[0]; + var header; + var headerElement; + var content; + var tm; // for making theme classes + var currentView; + var elementOuterWidth; + var suggestedViewHeight; + var resizeUID = 0; + var ignoreWindowResize = 0; + var date = new Date(); + var events = []; + var _dragElement; + + + + /* Main Rendering + -----------------------------------------------------------------------------*/ + + + setYMD(date, options.year, options.month, options.date); + + + function render(inc) { + if (!content) { + initialRender(); + } + else if (elementVisible()) { + // mainly for the public API + calcSize(); + _renderView(inc); + } + } + + + function initialRender() { + tm = options.theme ? 'ui' : 'fc'; + element.addClass('fc'); + if (options.isRTL) { + element.addClass('fc-rtl'); + } + else { + element.addClass('fc-ltr'); + } + if (options.theme) { + element.addClass('ui-widget'); + } + + content = $("
") + .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); + + header.destroy(); + content.remove(); + element.removeClass('fc 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 || date < currentView.start || date >= currentView.end // or new date range + ) { + 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(); + currentView.render(date, inc || 0); // the view's render method ONLY renders the skeleton, nothing else + setSize(); + unfreezeContentHeight(); + (currentView.afterRender || noop)(); + + updateTitle(); + updateTodayButton(); + + trigger('viewRender', currentView, currentView, currentView.element); + currentView.trigger('viewDisplay', _element); // deprecated + + 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() { + if (!ignoreWindowResize) { + 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--; + } + } + }, 200); + }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.setEventData(events); // for View.js, TODO: unify with renderEvents + 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.visStart, currentView.visEnd)) { + fetchAndRenderEvents(); + } + else { + renderEvents(); + } + } + + + function fetchAndRenderEvents() { + fetchEvents(currentView.visStart, currentView.visEnd); + // ... 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 today = new Date(); + if (today >= currentView.start && today < currentView.end) { + header.disableButton('today'); + } + else { + header.enableButton('today'); + } + } + + + + /* Selection + -----------------------------------------------------------------------------*/ + + + function select(start, end, allDay) { + currentView.select(start, end, allDay===undefined ? true : allDay); + } + + + function unselect() { // safe to be called before renderView + if (currentView) { + currentView.unselect(); + } + } + + + + /* Date + -----------------------------------------------------------------------------*/ + + + function prev() { + renderView(-1); + } + + + function next() { + renderView(1); + } + + + function prevYear() { + addYears(date, -1); + renderView(); + } + + + function nextYear() { + addYears(date, 1); + renderView(); + } + + + function today() { + date = new Date(); + renderView(); + } + + + function gotoDate(year, month, dateOfMonth) { + if (year instanceof Date) { + date = cloneDate(year); // provided 1 argument, a Date + }else{ + setYMD(date, year, month, dateOfMonth); + } + renderView(); + } + + + function incrementDate(years, months, days) { + if (years !== undefined) { + addYears(date, years); + } + if (months !== undefined) { + addMonths(date, months); + } + if (days !== undefined) { + addDays(date, days); + } + renderView(); + } + + + function getDate() { + return cloneDate(date); + } + + + + /* Height "Freezing" + -----------------------------------------------------------------------------*/ + + + function freezeContentHeight() { + content.css({ + width: '100%', + height: content.height(), + overflow: 'hidden' + }); + } + + + function unfreezeContentHeight() { + content.css({ + width: '', + height: '', + overflow: '' + }); + } + + + + /* Misc + -----------------------------------------------------------------------------*/ + + + 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) { + $(document) + .bind('dragstart', function(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); + } + } + }) + .bind('dragstop', function(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(formatDate(date, weekNumberFormat)) + + "
" + + ""; + } + + for (col=0; col" + + "
"; + + if (showNumbers) { + html += "
" + date.getDate() + "
"; + } + + html += + "
" + + "
 
" + + "
" + + "
" + + ""; + + return html; + } + + + + /* Dimensions + -----------------------------------------------------------*/ + + + function setHeight(height) { + viewHeight = height; + + var bodyHeight = viewHeight - head.height(); + 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 = parseISO8601($(this).data('date')); + trigger('dayClick', this, date, true, 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) { + var icon = options.theme ? smartProperty(options.buttonIcons, buttonName) : null; // why are we using smartProperty here? + var text = smartProperty(options.buttonText, buttonName); // why are we using smartProperty here? + var button = $( + "" + + (icon ? + "" + + "" + + "" : + text + ) + + "" + ) + .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, _sources) { + 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.normalizeEvent = normalizeEvent; + + + // imports + var trigger = t.trigger; + var getView = t.getView; + var reportEvents = t.reportEvents; + + + // locals + var stickySource = { events: [] }; + var sources = [ stickySource ]; + var rangeStart, rangeEnd; + var currentFetchID = 0; + var pendingSourceCnt = 0; + var loadingLevel = 0; + var cache = []; + + + for (var i=0; i<_sources.length; i++) { + _addEventSource(_sources[i]); + } + + + + /* Fetching + -----------------------------------------------------------------------------*/ + + + function isFetchNeeded(start, end) { + return !rangeStart || start < rangeStart || end > rangeEnd; + } + + + function fetchEvents(start, end) { + rangeStart = start; + rangeEnd = end; + cache = []; + var fetchID = ++currentFetchID; + var len = sources.length; + pendingSourceCnt = len; + for (var i=0; i)), return null instead + return null; +} + + +function parseISO8601(s, ignoreTimezone) { // ignoreTimezone defaults to false + // derived from http://delete.me.uk/2005/03/iso8601.html + // TODO: for a know glitch/feature, read tests/issue_206_parseDate_dst.html + var m = s.match(/^([0-9]{4})(-([0-9]{2})(-([0-9]{2})([T ]([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?(Z|(([-+])([0-9]{2})(:?([0-9]{2}))?))?)?)?)?$/); + if (!m) { + return null; + } + var date = new Date(m[1], 0, 1); + if (ignoreTimezone || !m[13]) { + var check = new Date(m[1], 0, 1, 9, 0); + if (m[3]) { + date.setMonth(m[3] - 1); + check.setMonth(m[3] - 1); + } + if (m[5]) { + date.setDate(m[5]); + check.setDate(m[5]); + } + fixDate(date, check); + if (m[7]) { + date.setHours(m[7]); + } + if (m[8]) { + date.setMinutes(m[8]); + } + if (m[10]) { + date.setSeconds(m[10]); + } + if (m[12]) { + date.setMilliseconds(Number("0." + m[12]) * 1000); + } + fixDate(date, check); + }else{ + date.setUTCFullYear( + m[1], + m[3] ? m[3] - 1 : 0, + m[5] || 1 + ); + date.setUTCHours( + m[7] || 0, + m[8] || 0, + m[10] || 0, + m[12] ? Number("0." + m[12]) * 1000 : 0 + ); + if (m[14]) { + var offset = Number(m[16]) * 60 + (m[18] ? Number(m[18]) : 0); + offset *= m[15] == '-' ? 1 : -1; + date = new Date(+date + (offset * 60 * 1000)); + } + } + return date; +} + + +function parseTime(s) { // returns minutes since start of day + if (typeof s == 'number') { // an hour + return s * 60; + } + if (typeof s == 'object') { // a Date object + return s.getHours() * 60 + s.getMinutes(); + } + var m = s.match(/(\d+)(?::(\d+))?\s*(\w+)?/); + if (m) { + var h = parseInt(m[1], 10); + if (m[3]) { + h %= 12; + if (m[3].toLowerCase().charAt(0) == 'p') { + h += 12; + } + } + return h * 60 + (m[2] ? parseInt(m[2], 10) : 0); + } +} + + + +/* Date Formatting +-----------------------------------------------------------------------------*/ +// TODO: use same function formatDate(date, [date2], format, [options]) + + +function formatDate(date, format, options) { + return formatDates(date, null, format, options); +} + + +function formatDates(date1, date2, format, options) { + options = options || defaults; + var date = date1, + otherDate = date2, + i, len = format.length, c, + i2, formatter, + res = ''; + for (i=0; ii; i2--) { + if (formatter = dateFormatters[format.substring(i, i2)]) { + if (date) { + res += formatter(date, options); + } + i = i2 - 1; + break; + } + } + if (i2 == i) { + if (date) { + res += c; + } + } + } + } + return res; +}; + + +var dateFormatters = { + s : function(d) { return d.getSeconds() }, + ss : function(d) { return zeroPad(d.getSeconds()) }, + m : function(d) { return d.getMinutes() }, + mm : function(d) { return zeroPad(d.getMinutes()) }, + h : function(d) { return d.getHours() % 12 || 12 }, + hh : function(d) { return zeroPad(d.getHours() % 12 || 12) }, + H : function(d) { return d.getHours() }, + HH : function(d) { return zeroPad(d.getHours()) }, + d : function(d) { return d.getDate() }, + dd : function(d) { return zeroPad(d.getDate()) }, + ddd : function(d,o) { return o.dayNamesShort[d.getDay()] }, + dddd: function(d,o) { return o.dayNames[d.getDay()] }, + M : function(d) { return d.getMonth() + 1 }, + MM : function(d) { return zeroPad(d.getMonth() + 1) }, + MMM : function(d,o) { return o.monthNamesShort[d.getMonth()] }, + MMMM: function(d,o) { return o.monthNames[d.getMonth()] }, + yy : function(d) { return (d.getFullYear()+'').substring(2) }, + yyyy: function(d) { return d.getFullYear() }, + t : function(d) { return d.getHours() < 12 ? 'a' : 'p' }, + tt : function(d) { return d.getHours() < 12 ? 'am' : 'pm' }, + T : function(d) { return d.getHours() < 12 ? 'A' : 'P' }, + TT : function(d) { return d.getHours() < 12 ? 'AM' : 'PM' }, + u : function(d) { return formatDate(d, "yyyy-MM-dd'T'HH:mm:ss'Z'") }, + S : function(d) { + var date = d.getDate(); + if (date > 10 && date < 20) { + return 'th'; + } + return ['st', 'nd', 'rd'][date%10-1] || 'th'; + }, + w : function(d, o) { // local + return o.weekNumberCalculation(d); + }, + W : function(d) { // ISO + return iso8601Week(d); + } +}; +fc.dateFormatters = dateFormatters; + + +/* thanks jQuery UI (https://github.com/jquery/jquery-ui/blob/master/ui/jquery.ui.datepicker.js) + * + * Set as calculateWeek to determine the week of the year based on the ISO 8601 definition. + * `date` - the date to get the week for + * `number` - the number of the week within the year that contains this date + */ +function iso8601Week(date) { + var time; + var checkDate = new Date(date.getTime()); + + // Find Thursday of this week starting on Monday + checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); + + time = checkDate.getTime(); + checkDate.setMonth(0); // Compare with Jan 1 + checkDate.setDate(1); + return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1; +} + + +;; + +fc.applyAll = applyAll; + + +/* Event Date Math +-----------------------------------------------------------------------------*/ + + +function exclEndDay(event) { + if (event.end) { + return _exclEndDay(event.end, event.allDay); + }else{ + return addDays(cloneDate(event.start), 1); + } +} + + +function _exclEndDay(end, allDay) { + end = cloneDate(end); + return allDay || end.getHours() || end.getMinutes() ? addDays(end, 1) : clearTime(end); + // why don't we check for seconds/ms too? +} + + + +/* Event Element Binding +-----------------------------------------------------------------------------*/ + + +function lazySegBind(container, segs, bindHandlers) { + container.unbind('mouseover').mouseover(function(ev) { + var parent=ev.target, e, + i, seg; + while (parent != this) { + e = parent; + parent = parent.parentNode; + } + if ((i = e._fci) !== undefined) { + e._fci = undefined; + seg = segs[i]; + bindHandlers(seg.event, seg.element, seg); + $(ev.target).trigger(ev); + } + ev.stopPropagation(); + }); +} + + + +/* Element Dimensions +-----------------------------------------------------------------------------*/ + + +function setOuterWidth(element, width, includeMargins) { + for (var i=0, e; i=0; i--) { + res = obj[parts[i].toLowerCase()]; + if (res !== undefined) { + return res; + } + } + return obj['']; +} + + +function htmlEscape(s) { + return s.replace(/&/g, '&') + .replace(//g, '>') + .replace(/'/g, ''') + .replace(/"/g, '"') + .replace(/\n/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) { + e.children() + .removeClass('fc-first fc-last') + .filter(':first-child') + .addClass('fc-first') + .end() + .filter(':last-child') + .addClass('fc-last'); +} + + +function setDayID(cell, date) { + cell.each(function(i, _cell) { + _cell.className = _cell.className.replace(/^fc-\w*/, 'fc-' + dayIDs[date.getDay()]); + // TODO: make a way that doesn't rely on order of classes + }); +} + + +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") + .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(weekNumberTitle) + + "
" + + "" + + "" + + "" + + "" + + "" + + "
" + opt('allDayText') + "" + + "
" + + "
 
"; + allDayTable = $(s).appendTo(slotLayer); + allDayRow = allDayTable.find('tr'); + + dayBind(allDayRow.find('td')); + + slotLayer.append( + "
" + + "
" + + "
" + ); + + }else{ + + daySegmentContainer = $([]); // in jQuery 1.4, we can just do $() + + } + + slotScroller = + $("
") + .appendTo(slotLayer); + + slotContainer = + $("
") + .appendTo(slotScroller); + + slotSegmentContainer = + $("
") + .appendTo(slotContainer); + + s = + "" + + ""; + d = zeroDate(); + maxd = addMinutes(cloneDate(d), maxMinute); + addMinutes(d, minMinute); + slotCnt = 0; + for (i=0; d < maxd; i++) { + minutes = d.getMinutes(); + s += + "" + + "" + + "" + + ""; + addMinutes(d, opt('slotMinutes')); + slotCnt++; + } + s += + "" + + "
" + + ((!slotNormal || !minutes) ? formatDate(d, opt('axisFormat')) : ' ') + + "" + + "
 
" + + "
"; + slotTable = $(s).appendTo(slotContainer); + + slotBind(slotTable.find('td')); + } + + + + /* Build Day Table + -----------------------------------------------------------------------*/ + + + function buildDayTable() { + var html = buildDayTableHTML(); + + if (dayTable) { + dayTable.remove(); + } + dayTable = $(html).appendTo(element); + + dayHead = dayTable.find('thead'); + dayHeadCells = dayHead.find('th').slice(1, -1); // exclude gutter + dayBody = dayTable.find('tbody'); + dayBodyCells = dayBody.find('td').slice(0, -1); // exclude gutter + dayBodyCellInners = dayBodyCells.find('> div'); + dayBodyCellContentInners = dayBodyCells.find('.fc-day-content > div'); + + dayBodyFirstCell = dayBodyCells.eq(0); + dayBodyFirstCellStretcher = dayBodyCellInners.eq(0); + + markFirstLast(dayHead.add(dayHead.find('tr'))); + markFirstLast(dayBody.add(dayBody.find('tr'))); + + // TODO: now that we rebuild the cells every time, we should call dayRender + } + + + function buildDayTableHTML() { + var html = + "" + + buildDayTableHeadHTML() + + buildDayTableBodyHTML() + + "
"; + + return html; + } + + + function buildDayTableHeadHTML() { + var headerClass = tm + "-widget-header"; + var date; + var html = ''; + var weekText; + var col; + + html += + "" + + ""; + + if (showWeekNumbers) { + date = cellToDate(0, 0); + weekText = formatDate(date, weekNumberFormat); + if (rtl) { + weekText += weekNumberTitle; + } + else { + weekText = 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 = clearTime(new Date()); + var col; + var cellsHTML; + var cellHTML; + var classNames; + var html = ''; + + html += + "" + + "" + + " "; + + cellsHTML = ''; + + for (col=0; col" + + "
" + + "
" + + "
 
" + + "
" + + "
" + + ""; + + cellsHTML += cellHTML; + } + + html += cellsHTML; + html += + " " + + "" + + ""; + + return html; + } + + + // TODO: data-date on the cells + + + + /* Dimensions + -----------------------------------------------------------------------*/ + + + function setHeight(height) { + if (height === undefined) { + height = viewHeight; + } + viewHeight = height; + slotTopCache = {}; + + var headHeight = dayBody.position().top; + var allDayHeight = slotScroller.position().top; // including divider + var bodyHeight = Math.min( // total body height, including borders + height - headHeight, // when scrollbars + slotTable.height() + allDayHeight + 1 // when no scrollbars. +1 for bottom border + ); + + dayBodyFirstCellStretcher + .height(bodyHeight - vsides(dayBodyFirstCell)); + + slotLayer.css('top', headHeight); + + slotScroller.height(bodyHeight - allDayHeight - 1); + + // the stylesheet guarantees that the first row has no border. + // this allows .height() to work well cross-browser. + slotHeight = slotTable.find('tr:first').height() + 1; // +1 for bottom border + + snapRatio = opt('slotMinutes') / snapMinutes; + snapHeight = slotHeight / snapRatio; + } + + + function setWidth(width) { + viewWidth = width; + colPositions.clear(); + colContentPositions.clear(); + + var axisFirstCells = dayHead.find('th:first'); + if (allDayTable) { + axisFirstCells = axisFirstCells.add(allDayTable.find('th:first')); + } + axisFirstCells = axisFirstCells.add(slotTable.find('th:first')); + + axisWidth = 0; + setOuterWidth( + axisFirstCells + .width('') + .each(function(i, _cell) { + axisWidth = Math.max(axisWidth, $(_cell).outerWidth()); + }), + axisWidth + ); + + var gutterCells = dayTable.find('.fc-agenda-gutter'); + if (allDayTable) { + gutterCells = gutterCells.add(allDayTable.find('th.fc-agenda-gutter')); + } + + var slotTableWidth = slotScroller[0].clientWidth; // needs to be done after axisWidth (for IE7) + + gutterWidth = slotScroller.width() - slotTableWidth; + if (gutterWidth) { + setOuterWidth(gutterCells, gutterWidth); + gutterCells + .show() + .prev() + .removeClass('fc-last'); + }else{ + gutterCells + .hide() + .prev() + .addClass('fc-last'); + } + + colWidth = Math.floor((slotTableWidth - axisWidth) / colCnt); + setOuterWidth(dayHeadCells.slice(0, -1), colWidth); + } + + + + /* Scrolling + -----------------------------------------------------------------------*/ + + + function resetScroll() { + var d0 = zeroDate(); + var scrollDate = cloneDate(d0); + scrollDate.setHours(opt('firstHour')); + var top = timePosition(d0, scrollDate) + 1; // +1 for the border + function scroll() { + slotScroller.scrollTop(top); + } + scroll(); + setTimeout(scroll, 0); // overrides any previous scroll state made by the browser + } + + + function afterRender() { // after the view has been freshly rendered and sized + resetScroll(); + } + + + + /* Slot/Day clicking and binding + -----------------------------------------------------------------------*/ + + + function dayBind(cells) { + cells.click(slotClick) + .mousedown(daySelectionMousedown); + } + + + function slotBind(cells) { + cells.click(slotClick) + .mousedown(slotSelectionMousedown); + } + + + function slotClick(ev) { + if (!opt('selectable')) { // if selectable, SelectionManager will worry about dayClick + var col = Math.min(colCnt-1, Math.floor((ev.pageX - dayTable.offset().left - axisWidth) / colWidth)); + var date = cellToDate(0, col); + var rowMatch = this.parentNode.className.match(/fc-slot(\d+)/); // TODO: maybe use data + if (rowMatch) { + var mins = parseInt(rowMatch[1]) * opt('slotMinutes'); + var hours = Math.floor(mins/60); + date.setHours(hours); + date.setMinutes(mins%60 + minMinute); + trigger('dayClick', dayBodyCells[col], date, false, ev); + }else{ + trigger('dayClick', dayBodyCells[col], date, true, ev); + } + } + } + + + + /* Semi-transparent Overlay Helpers + -----------------------------------------------------*/ + // TODO: should be consolidated with BasicView's methods + + + function renderDayOverlay(overlayStart, overlayEnd, refreshCoordinateGrid) { // overlayEnd is exclusive + + if (refreshCoordinateGrid) { + coordinateGrid.build(); + } + + var segments = rangeToSegments(overlayStart, overlayEnd); + + for (var i=0; i= 0) { + addMinutes(d, minMinute + slotIndex * snapMinutes); + } + return d; + } + + + // get the Y coordinate of the given time on the given day (both Date objects) + function timePosition(day, time) { // both date objects. day holds 00:00 of current day + day = cloneDate(day, true); + if (time < addMinutes(cloneDate(day), minMinute)) { + return 0; + } + if (time >= addMinutes(cloneDate(day), maxMinute)) { + return slotTable.height(); + } + var slotMinutes = opt('slotMinutes'), + minutes = time.getHours()*60 + time.getMinutes() - minMinute, + slotI = Math.floor(minutes / slotMinutes), + slotTop = slotTopCache[slotI]; + if (slotTop === undefined) { + slotTop = slotTopCache[slotI] = + slotTable.find('tr').eq(slotI).find('td div')[0].offsetTop; + // .eq() is faster than ":eq()" selector + // [0].offsetTop is faster than .position().top (do we really need this optimization?) + // a better optimization would be to cache all these divs + } + return Math.max(0, Math.round( + slotTop - 1 + slotHeight * ((minutes % slotMinutes) / slotMinutes) + )); + } + + + function getAllDayRow(index) { + return allDayRow; + } + + + function defaultEventEnd(event) { + var start = cloneDate(event.start); + if (event.allDay) { + return start; + } + return addMinutes(start, opt('defaultEventMinutes')); + } + + + + /* Selection + ---------------------------------------------------------------------------------*/ + + + function defaultSelectionEnd(startDate, allDay) { + if (allDay) { + return cloneDate(startDate); + } + return addMinutes(cloneDate(startDate), opt('slotMinutes')); + } + + + function renderSelection(startDate, endDate, allDay) { // only for all-day + if (allDay) { + if (opt('allDaySlot')) { + renderDayOverlay(startDate, addDays(cloneDate(endDate), 1), true); + } + }else{ + renderSlotSelection(startDate, endDate); + } + } + + + function renderSlotSelection(startDate, endDate) { + var helperOption = opt('selectHelper'); + coordinateGrid.build(); + if (helperOption) { + var col = dateToCell(startDate).col; + if (col >= 0 && col < colCnt) { // only works when times are on same day + var rect = coordinateGrid.rect(0, col, 0, col, slotContainer); // only for horizontal coords + var top = timePosition(startDate, startDate); + var bottom = timePosition(startDate, endDate); + if (bottom > top) { // protect against selections that are entirely before or after visible range + rect.top = top; + rect.height = bottom - top; + rect.left += 2; + rect.width -= 5; + if ($.isFunction(helperOption)) { + var helperRes = helperOption(startDate, endDate); + if (helperRes) { + rect.position = 'absolute'; + selectionHelper = $(helperRes) + .css(rect) + .appendTo(slotContainer); + } + }else{ + rect.isStart = true; // conside rect a "seg" now + rect.isEnd = true; // + selectionHelper = $(slotSegHtml( + { + title: '', + start: startDate, + end: endDate, + className: ['fc-select-helper'], + editable: false + }, + rect + )); + selectionHelper.css('opacity', opt('dragOpacity')); + } + if (selectionHelper) { + slotBind(selectionHelper); + slotContainer.append(selectionHelper); + setOuterWidth(selectionHelper, rect.width, true); // needs to be after appended + setOuterHeight(selectionHelper, rect.height, true); + } + } + } + }else{ + renderSlotOverlay(startDate, endDate); + } + } + + + function clearSelection() { + clearOverlays(); + if (selectionHelper) { + selectionHelper.remove(); + selectionHelper = null; + } + } + + + function slotSelectionMousedown(ev) { + if (ev.which == 1 && opt('selectable')) { // ev.which==1 means left mouse button + unselect(ev); + var dates; + hoverListener.start(function(cell, origCell) { + clearSelection(); + if (cell && cell.col == origCell.col && !getIsCellAllDay(cell)) { + var d1 = realCellToDate(origCell); + var d2 = realCellToDate(cell); + dates = [ + d1, + addMinutes(cloneDate(d1), snapMinutes), // calculate minutes depending on selection slot minutes + d2, + addMinutes(cloneDate(d2), snapMinutes) + ].sort(dateCompare); + renderSlotSelection(dates[0], dates[3]); + }else{ + dates = null; + } + }, ev); + $(document).one('mouseup', function(ev) { + hoverListener.stop(); + if (dates) { + if (+dates[0] == +dates[1]) { + reportDayClick(dates[0], false, ev); + } + reportSelection(dates[0], dates[3], false, ev); + } + }); + } + } + + + function reportDayClick(date, allDay, ev) { + trigger('dayClick', dayBodyCells[dateToCell(date).col], date, allDay, ev); + } + + + + /* External Dragging + --------------------------------------------------------------------------------*/ + + + function dragStart(_dragElement, ev, ui) { + hoverListener.start(function(cell) { + clearOverlays(); + if (cell) { + if (getIsCellAllDay(cell)) { + renderCellOverlay(cell.row, cell.col, cell.row, cell.col); + }else{ + var d1 = realCellToDate(cell); + var d2 = addMinutes(cloneDate(d1), opt('defaultEventMinutes')); + renderSlotOverlay(d1, d2); + } + } + }, ev); + } + + + function dragStop(_dragElement, ev, ui) { + var cell = hoverListener.stop(); + clearOverlays(); + if (cell) { + trigger('drop', _dragElement, realCellToDate(cell), getIsCellAllDay(cell), ev, ui); + } + } + + +} + +;; + +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 eventEnd = t.eventEnd; + var eventElementHandlers = t.eventElementHandlers; + var setHeight = t.setHeight; + var getDaySegmentContainer = t.getDaySegmentContainer; + var getSlotSegmentContainer = t.getSlotSegmentContainer; + var getHoverListener = t.getHoverListener; + var getMaxMinute = t.getMaxMinute; + var getMinMinute = t.getMinMinute; + var timePosition = t.timePosition; + var 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 getSnapMinutes = t.getSnapMinutes; + 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 calendar = t.calendar; + var formatDate = calendar.formatDate; + var formatDates = calendar.formatDates; + + + // overrides + t.draggableDayEvent = draggableDayEvent; + + + + /* Rendering + ----------------------------------------------------------------------------*/ + + + function renderEvents(events, modifiedEventId) { + var i, len=events.length, + dayEvents=[], + slotEvents=[]; + for (i=0; i start && eventStart < end) { + if (eventStart < start) { + segStart = cloneDate(start); + isStart = false; + }else{ + segStart = eventStart; + isStart = true; + } + if (eventEnd > end) { + segEnd = cloneDate(end); + isEnd = false; + }else{ + segEnd = eventEnd; + isEnd = true; + } + segs.push({ + event: event, + start: segStart, + end: segEnd, + isStart: isStart, + isEnd: isEnd + }); + } + } + return segs.sort(compareSlotSegs); + } + + + function slotEventEnd(event) { + if (event.end) { + return cloneDate(event.end); + }else{ + return addMinutes(cloneDate(event.start), opt('defaultEventMinutes')); + } + } + + + // renders events in the 'time slots' at the bottom + // 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(formatDates(event.start, event.end, opt('timeFormat'))) + + "
" + + "
" + + 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 snapHeight = getSnapHeight(); + var snapMinutes = getSnapMinutes(); + var minMinute = getMinMinute(); + eventElement.draggable({ + opacity: opt('dragOpacity', 'month'), // use whatever the month view was using + revertDuration: opt('dragRevertDuration'), + start: function(ev, ui) { + trigger('eventDragStart', eventElement, event, ev, ui); + hideEvents(event, eventElement); + origWidth = eventElement.width(); + hoverListener.start(function(cell, origCell) { + clearOverlays(); + if (cell) { + revert = false; + var origDate = cellToDate(0, origCell.col); + var date = cellToDate(0, cell.col); + dayDelta = dayDiff(date, origDate); + if (!cell.row) { + // on full-days + renderDayOverlay( + addDays(cloneDate(event.start), dayDelta), + addDays(exclEndDay(event), dayDelta) + ); + 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, + snapHeight * Math.round( + (event.end ? ((event.end - event.start) / MINUTE_MS) : opt('defaultEventMinutes')) / + snapMinutes + ) + ); + 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, 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 minuteDelta = 0; + if (!allDay) { + minuteDelta = Math.round((eventElement.offset().top - getSlotContainer().offset().top) / snapHeight) + * snapMinutes + + minMinute + - (event.start.getHours() * 60 + event.start.getMinutes()); + } + eventDrop(this, event, dayDelta, minuteDelta, allDay, 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 snapMinutes = getSnapMinutes(); + + // 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 minuteDelta, prevMinuteDelta; + + 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, 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; + minuteDelta = prevMinuteDelta = 0; + + }, + 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 = dayDiff(date, origDate); + } + + // calculate minute delta (only if over slots) + if (!isAllDay) { + minuteDelta = Math.round((ui.position.top - origPosition.top) / snapHeight) * snapMinutes; + } + } + + // any state changes? + if ( + isInBounds != prevIsInBounds || + isAllDay != prevIsAllDay || + colDelta != prevColDelta || + minuteDelta != prevMinuteDelta + ) { + + updateUI(); + + // update previous states for next time + prevIsInBounds = isInBounds; + prevIsAllDay = isAllDay; + prevColDelta = colDelta; + prevMinuteDelta = minuteDelta; + } + + // if out-of-bounds, revert when done, and vice versa. + eventElement.draggable('option', 'revert', !isInBounds); + + }, + stop: function(ev, ui) { + + clearOverlays(); + trigger('eventDragStop', eventElement, event, ev, ui); + + if (isInBounds && (isAllDay || dayDelta || minuteDelta)) { // changed! + eventDrop(this, event, dayDelta, isAllDay ? 0 : minuteDelta, isAllDay, 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; + minuteDelta = 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( + addDays(cloneDate(event.start), dayDelta), + addDays(exclEndDay(event), dayDelta) + ); + } + else { + updateTimeText(minuteDelta); + timeElement.css('display', ''); // show() was causing display=inline + eventElement.draggable('option', 'grid', [colWidth, snapHeight]); // re-enable grid snapping + } + } + } + + function updateTimeText(minuteDelta) { + var newStart = addMinutes(cloneDate(event.start), minuteDelta); + var newEnd; + if (event.end) { + newEnd = addMinutes(cloneDate(event.end), minuteDelta); + } + timeElement.text(formatDates(newStart, newEnd, opt('timeFormat'))); + } + + } + + + + /* Resizing + --------------------------------------------------------------------------------------*/ + + + function resizableSlotEvent(event, eventElement, timeElement) { + var snapDelta, prevSnapDelta; + var snapHeight = getSnapHeight(); + var snapMinutes = getSnapMinutes(); + eventElement.resizable({ + handles: { + s: '.ui-resizable-handle' + }, + grid: snapHeight, + start: function(ev, ui) { + snapDelta = prevSnapDelta = 0; + hideEvents(event, eventElement); + trigger('eventResizeStart', this, 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) { + timeElement.text( + formatDates( + event.start, + (!snapDelta && !event.end) ? null : // no change, so don't display time range + addMinutes(eventEnd(event), snapMinutes*snapDelta), + opt('timeFormat') + ) + ); + prevSnapDelta = snapDelta; + } + }, + stop: function(ev, ui) { + trigger('eventResizeStop', this, event, ev, ui); + if (snapDelta) { + eventResize(this, event, 0, snapMinutes*snapDelta, 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 +} + + +;; + + +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.setEventData = setEventData; + t.clearEventData = clearEventData; + t.eventEnd = eventEnd; + t.reportEventElement = reportEventElement; + t.triggerEventDestroy = triggerEventDestroy; + t.eventElementHandlers = eventElementHandlers; + t.showEvents = showEvents; + t.hideEvents = hideEvents; + t.eventDrop = eventDrop; + t.eventResize = eventResize; + // t.title + // t.start, t.end + // t.visStart, t.visEnd + + + // imports + var defaultEventEnd = t.defaultEventEnd; + var normalizeEvent = calendar.normalizeEvent; // in EventManager + var reportEventChange = calendar.reportEventChange; + + + // locals + var eventsByID = {}; // eventID mapped to array of events (there can be multiple b/c of repeating events) + 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; + + + + function opt(name, viewNameOverride) { + var v = options[name]; + if ($.isPlainObject(v)) { + 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') + ) + && !opt('disableDragging'); // deprecated + } + + + 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') + ) + && !opt('disableResizing'); // deprecated + } + + + + /* Event Data + ------------------------------------------------------------------------------*/ + + + function setEventData(events) { // events are already normalized at this point + eventsByID = {}; + var i, len=events.length, event; + for (i=0; i