Commit 03cf7c38882c8b034e49719d3360dd5556ca8bc3
1 parent
22787c6693
Exists in
master
minor changes due to ui updation and integrated schedular
Showing
9 changed files
with
10869 additions
and
102 deletions
Show diff stats
app/css/custom.css
... | ... | @@ -185,7 +185,6 @@ font-size: 10px !important; |
185 | 185 | } |
186 | 186 | .customAccordianHeader > span{ |
187 | 187 | font-size: 15px; |
188 | - margin-right: 20px; | |
189 | 188 | line-height: 22px; |
190 | 189 | float: left; |
191 | 190 | } |
... | ... | @@ -200,7 +199,6 @@ font-size: 10px !important; |
200 | 199 | width: 150px; |
201 | 200 | height: 26px; |
202 | 201 | padding: 0 6px; |
203 | - margin-right: 20px; | |
204 | 202 | float: left; |
205 | 203 | } |
206 | 204 | .customAccordianHeader > select.form-control[disabled]{ |
... | ... | @@ -213,7 +211,7 @@ font-size: 10px !important; |
213 | 211 | width: auto; |
214 | 212 | } |
215 | 213 | .customAccordianHeader > input.form-control{ |
216 | - width: 80px; | |
214 | + width: 70px; | |
217 | 215 | height: 26px; |
218 | 216 | padding: 0 6px; |
219 | 217 | float: left; | ... | ... |
app/index.html
... | ... | @@ -54,7 +54,7 @@ |
54 | 54 | <script src='https://fullcalendar.io/js/fullcalendar-3.4.0/lib/moment.min.js'></script> |
55 | 55 | <script src='https://fullcalendar.io/js/fullcalendar-3.4.0/lib/jquery.min.js'></script> |
56 | 56 | <script src='https://fullcalendar.io/js/fullcalendar-3.4.0/lib/jquery-ui.min.js'></script> --> |
57 | - | |
57 | + <link rel="stylesheet" href="bower_components/fullcalendar/dist/fullcalendar.css"/> | |
58 | 58 | |
59 | 59 | </head> |
60 | 60 | <body> |
... | ... | @@ -94,9 +94,12 @@ |
94 | 94 | <script src="bower_components/select2/select2.js"></script> |
95 | 95 | <script src="bower_components/angular-ui-select2/src/select2.js"></script> |
96 | 96 | <script src="bower_components/angular-ckeditor/angular-ckeditor.js"></script> |
97 | + <script src="bower_components/angular-dragdrop/src/angular-dragdrop.min.js"></script> | |
97 | 98 | |
98 | 99 | <!-- <script src="https://unpkg.com/ng-table@2.0.2/bundles/ng-table.min.js"></script> --> |
99 | - | |
100 | + <script type="text/javascript" src="bower_components/angular-ui-calendar/src/calendar.js"></script> | |
101 | + <!-- <script type="text/javascript" src="bower_components/fullcalendar/dist/fullcalendar.min.js"></script> --> | |
102 | + | |
100 | 103 | |
101 | 104 | <!-- |
102 | 105 | <script src="scripts/inspinia.js"></script> |
... | ... | @@ -197,7 +200,9 @@ |
197 | 200 | <script src="partials/enterFuelOrder/enterFuelOrder.service.js"></script> |
198 | 201 | |
199 | 202 | <script src="partials/main/main.service.js"></script> |
200 | - <script src='https://fullcalendar.io/js/fullcalendar-3.4.0/fullcalendar.min.js'></script> | |
203 | + <!-- <script src='https://fullcalendar.io/js/fullcalendar-3.4.0/fullcalendar.min.js'></script> --> | |
204 | + <script type="text/javascript" src="bower_components/fullcalendar/dist/fullcalendar.min.js"></script> | |
205 | + <script type="text/javascript" src="bower_components/fullcalendar/dist/gcal.js"></script> | |
201 | 206 | |
202 | 207 | </body> |
203 | 208 | </html> |
204 | 209 | \ No newline at end of file | ... | ... |
app/js/app.js
1 | 1 | 'use strict'; |
2 | 2 | |
3 | 3 | |
4 | - angular.module('acufuel', ['ngCookies', 'ngResource', 'ui.router', 'ngAnimate', 'ui.bootstrap', 'xeditable', 'ui.toggle', 'ngTable', 'ui.select2', 'ckeditor']) | |
4 | + angular.module('acufuel', ['ngCookies', 'ngResource', 'ui.router', 'ngAnimate', 'ui.bootstrap', 'xeditable', 'ui.toggle', 'ngTable', 'ui.select2', 'ckeditor', 'ui.calendar', 'ngDragDrop']) | |
5 | 5 | |
6 | 6 | .config(['$httpProvider', function($httpProvider) { |
7 | 7 | $httpProvider.defaults.withCredentials = true; | ... | ... |
app/js/fullcalender.js
Changes suppressed. Click to show
1 | 1 | /*! |
2 | - * FullCalendar v3.4.0 | |
3 | - * Docs & License: https://fullcalendar.io/ | |
4 | - * (c) 2017 Adam Shaw | |
2 | + * FullCalendar v2.3.1 | |
3 | + * Docs & License: http://fullcalendar.io/ | |
4 | + * (c) 2015 Adam Shaw | |
5 | 5 | */ |
6 | -!function(t){"function"==typeof define&&define.amd?define(["jquery","moment"],t):"object"==typeof exports?module.exports=t(require("jquery"),require("moment")):t(jQuery,moment)}(function(t,e){function n(t){return it(t,Qt)}function i(t,e){e.left&&t.css({"border-left-width":1,"margin-left":e.left-1}),e.right&&t.css({"border-right-width":1,"margin-right":e.right-1})}function r(t){t.css({"margin-left":"","margin-right":"","border-left-width":"","border-right-width":""})}function s(){t("body").addClass("fc-not-allowed")}function o(){t("body").removeClass("fc-not-allowed")}function a(e,n,i){var r=Math.floor(n/e.length),s=Math.floor(n-r*(e.length-1)),o=[],a=[],u=[],h=0;l(e),e.each(function(n,i){var l=n===e.length-1?s:r,c=t(i).outerHeight(!0);c<l?(o.push(i),a.push(c),u.push(t(i).height())):h+=c}),i&&(n-=h,r=Math.floor(n/o.length),s=Math.floor(n-r*(o.length-1))),t(o).each(function(e,n){var i=e===o.length-1?s:r,l=a[e],h=u[e],c=i-(l-h);l<i&&t(n).height(c)})}function l(t){t.height("")}function u(e){var n=0;return e.find("> *").each(function(e,i){var r=t(i).outerWidth();r>n&&(n=r)}),n++,e.width(n),n}function h(t,e){var n,i=t.add(e);return i.css({position:"relative",left:-1}),n=t.outerHeight()-e.outerHeight(),i.css({position:"",left:""}),n}function c(e){var n=e.css("position"),i=e.parents().filter(function(){var e=t(this);return/(auto|scroll)/.test(e.css("overflow")+e.css("overflow-y")+e.css("overflow-x"))}).eq(0);return"fixed"!==n&&i.length?i:t(e[0].ownerDocument||document)}function d(t,e){var n=t.offset(),i=n.left-(e?e.left:0),r=n.top-(e?e.top:0);return{left:i,right:i+t.outerWidth(),top:r,bottom:r+t.outerHeight()}}function f(t,e){var n=t.offset(),i=p(t),r=n.left+w(t,"border-left-width")+i.left-(e?e.left:0),s=n.top+w(t,"border-top-width")+i.top-(e?e.top:0);return{left:r,right:r+t[0].clientWidth,top:s,bottom:s+t[0].clientHeight}}function g(t,e){var n=t.offset(),i=n.left+w(t,"border-left-width")+w(t,"padding-left")-(e?e.left:0),r=n.top+w(t,"border-top-width")+w(t,"padding-top")-(e?e.top:0);return{left:i,right:i+t.width(),top:r,bottom:r+t.height()}}function p(t){var e,n=t[0].offsetWidth-t[0].clientWidth,i=t[0].offsetHeight-t[0].clientHeight;return n=v(n),i=v(i),e={left:0,right:0,top:0,bottom:i},m()&&"rtl"==t.css("direction")?e.left=n:e.right=n,e}function v(t){return t=Math.max(0,t),t=Math.round(t)}function m(){return null===Xt&&(Xt=y()),Xt}function y(){var e=t("<div><div/></div>").css({position:"absolute",top:-1e3,left:0,border:0,padding:0,overflow:"scroll",direction:"rtl"}).appendTo("body"),n=e.children(),i=n.offset().left>e.offset().left;return e.remove(),i}function w(t,e){return parseFloat(t.css(e))||0}function S(t){return 1==t.which&&!t.ctrlKey}function b(t){var e=t.originalEvent.touches;return e&&e.length?e[0].pageX:t.pageX}function E(t){var e=t.originalEvent.touches;return e&&e.length?e[0].pageY:t.pageY}function D(t){return/^touch/.test(t.type)}function T(t){t.addClass("fc-unselectable").on("selectstart",H)}function C(t){t.removeClass("fc-unselectable").off("selectstart",H)}function H(t){t.preventDefault()}function R(t,e){var n={left:Math.max(t.left,e.left),right:Math.min(t.right,e.right),top:Math.max(t.top,e.top),bottom:Math.min(t.bottom,e.bottom)};return n.left<n.right&&n.top<n.bottom&&n}function x(t,e){return{left:Math.min(Math.max(t.left,e.left),e.right),top:Math.min(Math.max(t.top,e.top),e.bottom)}}function I(t){return{left:(t.left+t.right)/2,top:(t.top+t.bottom)/2}}function k(t,e){return{left:t.left-e.left,top:t.top-e.top}}function M(e){var n,i,r=[],s=[];for("string"==typeof e?s=e.split(/\s*,\s*/):"function"==typeof e?s=[e]:t.isArray(e)&&(s=e),n=0;n<s.length;n++)i=s[n],"string"==typeof i?r.push("-"==i.charAt(0)?{field:i.substring(1),order:-1}:{field:i,order:1}):"function"==typeof i&&r.push({func:i});return r}function B(t,e,n){var i,r;for(i=0;i<n.length;i++)if(r=L(t,e,n[i]))return r;return 0}function L(t,e,n){return n.func?n.func(t,e):N(t[n.field],e[n.field])*(n.order||1)}function N(e,n){return e||n?null==n?-1:null==e?1:"string"===t.type(e)||"string"===t.type(n)?String(e).localeCompare(String(n)):e-n:0}function z(t,e){var n,i,r,s,o=t.start,a=t.end,l=e.start,u=e.end;if(a>l&&o<u)return o>=l?(n=o.clone(),r=!0):(n=l.clone(),r=!1),a<=u?(i=a.clone(),s=!0):(i=u.clone(),s=!1),{start:n,end:i,isStart:r,isEnd:s}}function F(t,n){return e.duration({days:t.clone().stripTime().diff(n.clone().stripTime(),"days"),ms:t.time()-n.time()})}function A(t,n){return e.duration({days:t.clone().stripTime().diff(n.clone().stripTime(),"days")})}function G(t,n,i){return e.duration(Math.round(t.diff(n,i,!0)),i)}function V(t,e){var n,i,r;for(n=0;n<Jt.length&&(i=Jt[n],!((r=P(i,t,e))>=1&&vt(r)));n++);return i}function O(t,e){var n=V(t);return"week"===n&&"object"==typeof e&&e.days&&(n="day"),n}function P(t,n,i){return null!=i?i.diff(n,t,!0):e.isDuration(n)?n.as(t):n.end.diff(n.start,t,!0)}function _(t,e,n){var i;return tt(n)?(e-t)/n:(i=n.asMonths(),Math.abs(i)>=1&&vt(i)?e.diff(t,"months",!0)/i:e.diff(t,"days",!0)/n.asDays())}function W(t,e){var n,i;return tt(t)||tt(e)?t/e:(n=t.asMonths(),i=e.asMonths(),Math.abs(n)>=1&&vt(n)&&Math.abs(i)>=1&&vt(i)?n/i:t.asDays()/e.asDays())}function Y(t,n){var i;return tt(t)?e.duration(t*n):(i=t.asMonths(),Math.abs(i)>=1&&vt(i)?e.duration({months:i*n}):e.duration({days:t.asDays()*n}))}function q(t){return{start:t.start.clone(),end:t.end.clone()}}function U(t,e){return t=q(t),e.start&&(t.start=j(t.start,e)),e.end&&(t.end=K(t.end,e.end)),t}function j(t,e){return t=t.clone(),e.start&&(t=J(t,e.start)),e.end&&t>=e.end&&(t=e.end.clone().subtract(1)),t}function Z(t,e){return(!e.start||t>=e.start)&&(!e.end||t<e.end)}function $(t,e){return(!e.start||t.end>=e.start)&&(!e.end||t.start<e.end)}function Q(t,e){return(!e.start||t.start>=e.start)&&(!e.end||t.end<=e.end)}function X(t,e){return(t.start&&e.start&&t.start.isSame(e.start)||!t.start&&!e.start)&&(t.end&&e.end&&t.end.isSame(e.end)||!t.end&&!e.end)}function K(t,e){return(t.isBefore(e)?t:e).clone()}function J(t,e){return(t.isAfter(e)?t:e).clone()}function tt(t){return Boolean(t.hours()||t.minutes()||t.seconds()||t.milliseconds())}function et(t){return"[object Date]"===Object.prototype.toString.call(t)||t instanceof Date}function nt(t){return/^\d+\:\d+(?:\:\d+\.?(?:\d{3})?)?$/.test(t)}function it(t,e){var n,i,r,s,o,a,l={};if(e)for(n=0;n<e.length;n++){for(i=e[n],r=[],s=t.length-1;s>=0;s--)if("object"==typeof(o=t[s][i]))r.unshift(o);else if(void 0!==o){l[i]=o;break}r.length&&(l[i]=it(r))}for(n=t.length-1;n>=0;n--){a=t[n];for(i in a)i in l||(l[i]=a[i])}return l}function rt(t){var e=function(){};return e.prototype=t,new e}function st(t,e){for(var n in t)ot(t,n)&&(e[n]=t[n])}function ot(t,e){return te.call(t,e)}function at(e){return/undefined|null|boolean|number|string/.test(t.type(e))}function lt(e,n,i){if(t.isFunction(e)&&(e=[e]),e){var r,s;for(r=0;r<e.length;r++)s=e[r].apply(n,i)||s;return s}}function ut(){for(var t=0;t<arguments.length;t++)if(void 0!==arguments[t])return arguments[t]}function ht(t){return(t+"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/'/g,"'").replace(/"/g,""").replace(/\n/g,"<br />")}function ct(t){return t.replace(/&.*?;/g,"")}function dt(e){var n=[];return t.each(e,function(t,e){null!=e&&n.push(t+":"+e)}),n.join(";")}function ft(e){var n=[];return t.each(e,function(t,e){null!=e&&n.push(t+'="'+ht(e)+'"')}),n.join(" ")}function gt(t){return t.charAt(0).toUpperCase()+t.slice(1)}function pt(t,e){return t-e}function vt(t){return t%1==0}function mt(t,e){var n=t[e];return function(){return n.apply(t,arguments)}}function yt(t,e,n){var i,r,s,o,a,l=function(){var u=+new Date-o;u<e?i=setTimeout(l,e-u):(i=null,n||(a=t.apply(s,r),s=r=null))};return function(){s=this,r=arguments,o=+new Date;var u=n&&!i;return i||(i=setTimeout(l,e)),u&&(a=t.apply(s,r),s=r=null),a}}function wt(n,i,r){var s,o,a,l,u=n[0],h=1==n.length&&"string"==typeof u;return e.isMoment(u)||et(u)||void 0===u?l=e.apply(null,n):(s=!1,o=!1,h?ee.test(u)?(u+="-01",n=[u],s=!0,o=!0):(a=ne.exec(u))&&(s=!a[5],o=!0):t.isArray(u)&&(o=!0),l=i||s?e.utc.apply(e,n):e.apply(null,n),s?(l._ambigTime=!0,l._ambigZone=!0):r&&(o?l._ambigZone=!0:h&&l.utcOffset(u))),l._fullCalendar=!0,l}function St(t){return"en"!==t.locale()?t.clone().locale("en"):t}function bt(){}function Et(t,e){var n;return ot(e,"constructor")&&(n=e.constructor),"function"!=typeof n&&(n=e.constructor=function(){t.apply(this,arguments)}),n.prototype=rt(t.prototype),st(e,n.prototype),st(t,n),n}function Dt(t,e){st(e,t.prototype)}function Tt(t,e){t.then=function(n){return"function"==typeof n&&n(e),t}}function Ct(t){t.then=function(e,n){return"function"==typeof n&&n(),t}}function Ht(t,e){return!t&&!e||!(!t||!e)&&(t.component===e.component&&Rt(t,e)&&Rt(e,t))}function Rt(t,e){for(var n in t)if(!/^(component|left|right|top|bottom)$/.test(n)&&t[n]!==e[n])return!1;return!0}function xt(t){return{start:t.start.clone(),end:t.end?t.end.clone():null,allDay:t.allDay}}function It(t){var e=Mt(t);return"background"===e||"inverse-background"===e}function kt(t){return"inverse-background"===Mt(t)}function Mt(t){return ut((t.source||{}).rendering,t.rendering)}function Bt(t){var e,n,i={};for(e=0;e<t.length;e++)n=t[e],(i[n._id]||(i[n._id]=[])).push(n);return i}function Lt(t,e){return t.start-e.start}function Nt(n){var i,r,s,o,a=Zt.dataAttrPrefix;return a&&(a+="-"),i=n.data(a+"event")||null,i&&(i="object"==typeof i?t.extend({},i):{},r=i.start,null==r&&(r=i.time),s=i.duration,o=i.stick,delete i.start,delete i.time,delete i.duration,delete i.stick),null==r&&(r=n.data(a+"start")),null==r&&(r=n.data(a+"time")),null==s&&(s=n.data(a+"duration")),null==o&&(o=n.data(a+"stick")),r=null!=r?e.duration(r):null,s=null!=s?e.duration(s):null,o=Boolean(o),{eventProps:i,startTime:r,duration:s,stick:o}}function zt(t,e){var n,i;for(n=0;n<e.length;n++)if(i=e[n],i.leftCol<=t.rightCol&&i.rightCol>=t.leftCol)return!0;return!1}function Ft(t,e){return t.leftCol-e.leftCol}function At(t){var e,n,i,r=[];for(e=0;e<t.length;e++){for(n=t[e],i=0;i<r.length&&Ot(n,r[i]).length;i++);n.level=i,(r[i]||(r[i]=[])).push(n)}return r}function Gt(t){var e,n,i,r,s;for(e=0;e<t.length;e++)for(n=t[e],i=0;i<n.length;i++)for(r=n[i],r.forwardSegs=[],s=e+1;s<t.length;s++)Ot(r,t[s],r.forwardSegs)}function Vt(t){var e,n,i=t.forwardSegs,r=0;if(void 0===t.forwardPressure){for(e=0;e<i.length;e++)n=i[e],Vt(n),r=Math.max(r,1+n.forwardPressure);t.forwardPressure=r}}function Ot(t,e,n){n=n||[];for(var i=0;i<e.length;i++)Pt(t,e[i])&&n.push(e[i]);return n}function Pt(t,e){return t.bottom>e.top&&t.top<e.bottom}function _t(t){this.items=t||[]}function Wt(e,n){function i(t){n=t}function r(){var i=n.layout;p=e.opt("theme")?"ui":"fc",i?(g?g.empty():g=this.el=t("<div class='fc-toolbar "+n.extraClasses+"'/>"),g.append(o("left")).append(o("right")).append(o("center")).append('<div class="fc-clear"/>')):s()}function s(){g&&(g.remove(),g=f.el=null)}function o(i){var r=t('<div class="fc-'+i+'"/>'),s=n.layout[i],o=e.opt("customButtons")||{},a=e.opt("buttonText")||{};return s&&t.each(s.split(" "),function(n){var i,s=t(),l=!0;t.each(this.split(","),function(n,i){var r,u,h,c,d,f,g,m,y,w;"title"==i?(s=s.add(t("<h2> </h2>")),l=!1):((r=o[i])?(h=function(t){r.click&&r.click.call(w[0],t)},c="",d=r.text):(u=e.getViewSpec(i))?(h=function(){e.changeView(i)},v.push(i),c=u.buttonTextOverride,d=u.buttonTextDefault):e[i]&&(h=function(){e[i]()},c=(e.overrides.buttonText||{})[i],d=a[i]),h&&(f=r?r.themeIcon:e.opt("themeButtonIcons")[i],g=r?r.icon:e.opt("buttonIcons")[i],m=c?ht(c):f&&e.opt("theme")?"<span class='ui-icon ui-icon-"+f+"'></span>":g&&!e.opt("theme")?"<span class='fc-icon fc-icon-"+g+"'></span>":ht(d),y=["fc-"+i+"-button",p+"-button",p+"-state-default"],w=t('<button type="button" class="'+y.join(" ")+'">'+m+"</button>").click(function(t){w.hasClass(p+"-state-disabled")||(h(t),(w.hasClass(p+"-state-active")||w.hasClass(p+"-state-disabled"))&&w.removeClass(p+"-state-hover"))}).mousedown(function(){w.not("."+p+"-state-active").not("."+p+"-state-disabled").addClass(p+"-state-down")}).mouseup(function(){w.removeClass(p+"-state-down")}).hover(function(){w.not("."+p+"-state-active").not("."+p+"-state-disabled").addClass(p+"-state-hover")},function(){w.removeClass(p+"-state-hover").removeClass(p+"-state-down")}),s=s.add(w)))}),l&&s.first().addClass(p+"-corner-left").end().last().addClass(p+"-corner-right").end(),s.length>1?(i=t("<div/>"),l&&i.addClass("fc-button-group"),i.append(s),r.append(i)):r.append(s)}),r}function a(t){g&&g.find("h2").text(t)}function l(t){g&&g.find(".fc-"+t+"-button").addClass(p+"-state-active")}function u(t){g&&g.find(".fc-"+t+"-button").removeClass(p+"-state-active")}function h(t){g&&g.find(".fc-"+t+"-button").prop("disabled",!0).addClass(p+"-state-disabled")}function c(t){g&&g.find(".fc-"+t+"-button").prop("disabled",!1).removeClass(p+"-state-disabled")}function d(){return v}var f=this;f.setToolbarOptions=i,f.render=r,f.removeElement=s,f.updateTitle=a,f.activateButton=l,f.deactivateButton=u,f.disableButton=h,f.enableButton=c,f.getViewsWithButtons=d,f.el=null;var g,p,v=[]}function Yt(e){t.each(Me,function(t,n){null==e[t]&&(e[t]=n(e))})}function qt(t){return e.localeData(t)||e.localeData("en")}function Ut(){function n(t,e){return!q.opt("lazyFetching")||s(t,e)?o(t,e):he.resolve(Z)}function i(){Z=r(K),q.trigger("eventsReset",Z)}function r(t){var e,n,i=[];for(e=0;e<t.length;e++)n=t[e],n.start.clone().stripZone()<j&&q.getEventEnd(n).stripZone()>U&&i.push(n);return i}function s(t,e){return!U||t<U||e>j}function o(t,e){return U=t,j=e,a()}function a(){return u(Q,"reset")}function l(t){return u(b(t))}function u(t,e){var n,i;for("reset"===e?K=[]:"add"!==e&&(K=C(K,t)),n=0;n<t.length;n++)i=t[n],"pending"!==i._status&&X++,i._fetchId=(i._fetchId||0)+1,i._status="pending";for(n=0;n<t.length;n++)i=t[n],h(i,i._fetchId);return X?he.construct(function(t){q.one("eventsReceived",t)}):he.resolve(Z)}function h(e,n){f(e,function(i){var r,s,o,a=t.isArray(e.events);if(n===e._fetchId&&"rejected"!==e._status){if(e._status="resolved",i)for(r=0;r<i.length;r++)s=i[r],(o=a?s:z(s,e))&&K.push.apply(K,_(o));d()}})}function c(t){var e="pending"===t._status;t._status="rejected",e&&d()}function d(){--X||(i(K),q.trigger("eventsReceived",Z))}function f(e,n){var i,r,s=Zt.sourceFetchers;for(i=0;i<s.length;i++){if(!0===(r=s[i].call(q,e,U.clone(),j.clone(),q.opt("timezone"),n)))return;if("object"==typeof r)return void f(r,n)}var o=e.events;if(o)t.isFunction(o)?(q.pushLoading(),o.call(q,U.clone(),j.clone(),q.opt("timezone"),function(t){n(t),q.popLoading()})):t.isArray(o)?n(o):n();else{if(e.url){var a,l=e.success,u=e.error,h=e.complete;a=t.isFunction(e.data)?e.data():e.data;var c=t.extend({},a||{}),d=ut(e.startParam,q.opt("startParam")),g=ut(e.endParam,q.opt("endParam")),p=ut(e.timezoneParam,q.opt("timezoneParam"));d&&(c[d]=U.format()),g&&(c[g]=j.format()),q.opt("timezone")&&"local"!=q.opt("timezone")&&(c[p]=q.opt("timezone")),q.pushLoading(),t.ajax(t.extend({},Be,e,{data:c,success:function(e){e=e||[];var i=lt(l,this,arguments);t.isArray(i)&&(e=i),n(e)},error:function(){lt(u,this,arguments),n()},complete:function(){lt(h,this,arguments),q.popLoading()}}))}else n()}}function g(t){var e=p(t);e&&(Q.push(e),u([e],"add"))}function p(e){var n,i,r=Zt.sourceNormalizers;if(t.isFunction(e)||t.isArray(e)?n={events:e}:"string"==typeof e?n={url:e}:"object"==typeof e&&(n=t.extend({},e)),n){for(n.className?"string"==typeof n.className&&(n.className=n.className.split(/\s+/)):n.className=[],t.isArray(n.events)&&(n.origArray=n.events,n.events=t.map(n.events,function(t){return z(t,n)})),i=0;i<r.length;i++)r[i].call(q,n);return n}}function v(t){y(E(t))}function m(t){null==t?y(Q,!0):y(b(t))}function y(e,n){var r;for(r=0;r<e.length;r++)c(e[r]);n?(Q=[],K=[]):(Q=t.grep(Q,function(t){for(r=0;r<e.length;r++)if(t===e[r])return!1;return!0}),K=C(K,e)),i()}function w(){return Q.slice(1)}function S(e){return t.grep(Q,function(t){return t.id&&t.id===e})[0]}function b(e){e?t.isArray(e)||(e=[e]):e=[];var n,i=[];for(n=0;n<e.length;n++)i.push.apply(i,E(e[n]));return i}function E(e){var n,i;for(n=0;n<Q.length;n++)if((i=Q[n])===e)return[i];return i=S(e),i?[i]:t.grep(Q,function(t){return D(e,t)})}function D(t,e){return t&&e&&T(t)==T(e)}function T(t){return("object"==typeof t?t.origArray||t.googleCalendarId||t.url||t.events:null)||t}function C(e,n){return t.grep(e,function(t){for(var e=0;e<n.length;e++)if(t.source===n[e])return!1;return!0})}function H(t){R([t])}function R(t){var e,n;for(e=0;e<t.length;e++)n=t[e],n.start=q.moment(n.start),n.end?n.end=q.moment(n.end):n.end=null,W(n,x(n));i()}function x(e){var n={};return t.each(e,function(t,e){I(t)&&void 0!==e&&at(e)&&(n[t]=e)}),n}function I(t){return!/^_|^(id|allDay|start|end)$/.test(t)}function k(t,e){return M([t],e)}function M(t,e){var n,r,s,o,a,l=[];for(s=0;s<t.length;s++)if(r=z(t[s])){for(n=_(r),o=0;o<n.length;o++)a=n[o],a.source||(e&&($.events.push(a),a.source=$),K.push(a));l=l.concat(n)}return l.length&&i(),l}function B(e){var n,r;for(null==e?e=function(){return!0}:t.isFunction(e)||(n=e+"",e=function(t){return t._id==n}),K=t.grep(K,e,!0),r=0;r<Q.length;r++)t.isArray(Q[r].events)&&(Q[r].events=t.grep(Q[r].events,e,!0));i()}function L(e){return t.isFunction(e)?t.grep(K,e):null!=e?(e+="",t.grep(K,function(t){return t._id==e})):K}function N(t){t.start=q.moment(t.start),t.end&&(t.end=q.moment(t.end)),jt(t)}function z(n,i){var r,s,o,a=q.opt("eventDataTransform"),l={};if(a&&(n=a(n)),i&&i.eventDataTransform&&(n=i.eventDataTransform(n)),t.extend(l,n),i&&(l.source=i),l._id=n._id||(void 0===n.id?"_fc"+Le++:n.id+""),n.className?"string"==typeof n.className?l.className=n.className.split(/\s+/):l.className=n.className:l.className=[],r=n.start||n.date,s=n.end,nt(r)&&(r=e.duration(r)),nt(s)&&(s=e.duration(s)),n.dow||e.isDuration(r)||e.isDuration(s))l.start=r?e.duration(r):null,l.end=s?e.duration(s):null,l._recurring=!0;else{if(r&&(r=q.moment(r),!r.isValid()))return!1;s&&(s=q.moment(s),s.isValid()||(s=null)),o=n.allDay,void 0===o&&(o=ut(i?i.allDayDefault:void 0,q.opt("allDayDefault"))),V(r,s,o,l)}return q.normalizeEvent(l),l}function V(t,e,n,i){i.start=t,i.end=e,i.allDay=n,O(i),jt(i)}function O(t){P(t),t.end&&!t.end.isAfter(t.start)&&(t.end=null),t.end||(q.opt("forceEventDuration")?t.end=q.getDefaultEventEnd(t.allDay,t.start):t.end=null)}function P(t){null==t.allDay&&(t.allDay=!(t.start.hasTime()||t.end&&t.end.hasTime())),t.allDay?(t.start.stripTime(),t.end&&t.end.stripTime()):(t.start.hasTime()||(t.start=q.applyTimezone(t.start.time(0))),t.end&&!t.end.hasTime()&&(t.end=q.applyTimezone(t.end.time(0))))}function _(e,n,i){var r,s,o,a,l,u,h,c,d,f=[];if(n=n||U,i=i||j,e)if(e._recurring){if(s=e.dow)for(r={},o=0;o<s.length;o++)r[s[o]]=!0;for(a=n.clone().stripTime();a.isBefore(i);)r&&!r[a.day()]||(l=e.start,u=e.end,h=a.clone(),c=null,l&&(h=h.time(l)),u&&(c=a.clone().time(u)),d=t.extend({},e),V(h,c,!l&&!u,d),f.push(d)),a.add(1,"days")}else f.push(e);return f}function W(e,n,i){function r(t,e){return i?G(t,e,i):n.allDay?A(t,e):F(t,e)}var s,o,a,l,u,h,c={};return n=n||{},n.start||(n.start=e.start.clone()),void 0===n.end&&(n.end=e.end?e.end.clone():null),null==n.allDay&&(n.allDay=e.allDay),O(n),s={start:e._start.clone(),end:e._end?e._end.clone():q.getDefaultEventEnd(e._allDay,e._start),allDay:n.allDay},O(s),o=null!==e._end&&null===n.end,a=r(n.start,s.start),n.end?(l=r(n.end,s.end),u=l.subtract(a)):u=null,t.each(n,function(t,e){I(t)&&void 0!==e&&(c[t]=e)}),h=Y(L(e._id),o,n.allDay,a,u,c),{dateDelta:a,durationDelta:u,undo:h}}function Y(e,n,i,r,s,o){var a=q.getIsAmbigTimezone(),l=[];return r&&!r.valueOf()&&(r=null),s&&!s.valueOf()&&(s=null),t.each(e,function(e,u){var h,c;h={start:u.start.clone(),end:u.end?u.end.clone():null,allDay:u.allDay},t.each(o,function(t){h[t]=u[t]}),c={start:u._start,end:u._end,allDay:i},O(c),n?c.end=null:s&&!c.end&&(c.end=q.getDefaultEventEnd(c.allDay,c.start)),r&&(c.start.add(r),c.end&&c.end.add(r)),s&&c.end.add(s),a&&!c.allDay&&(r||s)&&(c.start.stripZone(),c.end&&c.end.stripZone()),t.extend(u,o,c),jt(u),l.push(function(){t.extend(u,h),jt(u)})}),function(){for(var t=0;t<l.length;t++)l[t]()}}var q=this;q.requestEvents=n,q.reportEventChange=i,q.isFetchNeeded=s,q.fetchEvents=o,q.fetchEventSources=u,q.refetchEvents=a,q.refetchEventSources=l,q.getEventSources=w,q.getEventSourceById=S,q.addEventSource=g,q.removeEventSource=v,q.removeEventSources=m,q.updateEvent=H,q.updateEvents=R,q.renderEvent=k,q.renderEvents=M,q.removeEvents=B,q.clientEvents=L,q.mutateEvent=W,q.normalizeEventDates=O,q.normalizeEventTimes=P;var U,j,Z,$={events:[]},Q=[$],X=0,K=[];t.each((q.opt("events")?[q.opt("events")]:[]).concat(q.opt("eventSources")||[]),function(t,e){var n=p(e);n&&Q.push(n)}),q.getEventCache=function(){return K},q.rezoneArrayEventSources=function(){var e,n,i;for(e=0;e<Q.length;e++)if(n=Q[e].events,t.isArray(n))for(i=0;i<n.length;i++)N(n[i])},q.buildEventFromInput=z,q.expandEvent=_}function jt(t){t._allDay=t.allDay,t._start=t.start.clone(),t._end=t.end?t.end.clone():null}var Zt=t.fullCalendar={version:"3.4.0",internalApiVersion:9},$t=Zt.views={};t.fn.fullCalendar=function(e){var n=Array.prototype.slice.call(arguments,1),i=this;return this.each(function(r,s){var o,a=t(s),l=a.data("fullCalendar");"string"==typeof e?l&&t.isFunction(l[e])&&(o=l[e].apply(l,n),r||(i=o),"destroy"===e&&a.removeData("fullCalendar")):l||(l=new Re(a,e),a.data("fullCalendar",l),l.render())}),i};var Qt=["header","footer","buttonText","buttonIcons","themeButtonIcons"];Zt.intersectRanges=z,Zt.applyAll=lt,Zt.debounce=yt,Zt.isInt=vt,Zt.htmlEscape=ht,Zt.cssToStr=dt,Zt.proxy=mt,Zt.capitaliseFirstLetter=gt,Zt.getOuterRect=d,Zt.getClientRect=f,Zt.getContentRect=g,Zt.getScrollbarWidths=p;var Xt=null;Zt.preventDefault=H,Zt.intersectRects=R,Zt.parseFieldSpecs=M,Zt.compareByFieldSpecs=B,Zt.compareByFieldSpec=L,Zt.flexibleCompare=N,Zt.computeGreatestUnit=V,Zt.divideRangeByDuration=_,Zt.divideDurationByDuration=W,Zt.multiplyDuration=Y,Zt.durationHasTime=tt;var Kt=["sun","mon","tue","wed","thu","fri","sat"],Jt=["year","month","week","day","hour","minute","second","millisecond"];Zt.log=function(){var t=window.console;if(t&&t.log)return t.log.apply(t,arguments)},Zt.warn=function(){var t=window.console;return t&&t.warn?t.warn.apply(t,arguments):Zt.log.apply(Zt,arguments)};var te={}.hasOwnProperty;Zt.createObject=rt;var ee=/^\s*\d{4}-\d\d$/,ne=/^\s*\d{4}-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?)?$/,ie=e.fn,re=t.extend({},ie),se=e.momentProperties;se.push("_fullCalendar"),se.push("_ambigTime"),se.push("_ambigZone"),Zt.moment=function(){return wt(arguments)},Zt.moment.utc=function(){var t=wt(arguments,!0);return t.hasTime()&&t.utc(),t},Zt.moment.parseZone=function(){return wt(arguments,!0,!0)},ie.week=ie.weeks=function(t){var e=this._locale._fullCalendar_weekCalc;return null==t&&"function"==typeof e?e(this):"ISO"===e?re.isoWeek.apply(this,arguments):re.week.apply(this,arguments)},ie.time=function(t){if(!this._fullCalendar)return re.time.apply(this,arguments);if(null==t)return e.duration({hours:this.hours(),minutes:this.minutes(),seconds:this.seconds(),milliseconds:this.milliseconds()});this._ambigTime=!1,e.isDuration(t)||e.isMoment(t)||(t=e.duration(t));var n=0;return e.isDuration(t)&&(n=24*Math.floor(t.asDays())),this.hours(n+t.hours()).minutes(t.minutes()).seconds(t.seconds()).milliseconds(t.milliseconds())},ie.stripTime=function(){return this._ambigTime||(this.utc(!0),this.set({hours:0,minutes:0,seconds:0,ms:0}),this._ambigTime=!0,this._ambigZone=!0),this},ie.hasTime=function(){return!this._ambigTime},ie.stripZone=function(){var t;return this._ambigZone||(t=this._ambigTime,this.utc(!0),this._ambigTime=t||!1,this._ambigZone=!0),this},ie.hasZone=function(){return!this._ambigZone},ie.local=function(t){return re.local.call(this,this._ambigZone||t),this._ambigTime=!1,this._ambigZone=!1,this},ie.utc=function(t){return re.utc.call(this,t),this._ambigTime=!1,this._ambigZone=!1,this},ie.utcOffset=function(t){return null!=t&&(this._ambigTime=!1,this._ambigZone=!1),re.utcOffset.apply(this,arguments)},ie.format=function(){return this._fullCalendar&&arguments[0]?oe(this,arguments[0]):this._ambigTime?le(St(this),"YYYY-MM-DD"):this._ambigZone?le(St(this),"YYYY-MM-DD[T]HH:mm:ss"):this._fullCalendar?le(St(this)):re.format.apply(this,arguments)},ie.toISOString=function(){return this._ambigTime?le(St(this),"YYYY-MM-DD"):this._ambigZone?le(St(this),"YYYY-MM-DD[T]HH:mm:ss"):this._fullCalendar?re.toISOString.apply(St(this),arguments):re.toISOString.apply(this,arguments)},function(){function t(t,e){return h(r(e).fakeFormatString,t)}function e(t,e){return re.format.call(t,e)}function n(t,e,n,s,o){var a;return t=Zt.moment.parseZone(t),e=Zt.moment.parseZone(e),a=t.localeData(),n=a.longDateFormat(n)||n,i(r(n),t,e,s||" - ",o)}function i(t,e,n,i,r){var s,o,a,l=t.sameUnits,u=e.clone().stripZone(),h=n.clone().stripZone(),f=c(t.fakeFormatString,e),g=c(t.fakeFormatString,n),p="",v="",m="",y="",w="";for(s=0;s<l.length&&(!l[s]||u.isSame(h,l[s]));s++)p+=f[s];for(o=l.length-1;o>s&&(!l[o]||u.isSame(h,l[o]))&&(o-1!==s||"."!==f[o]);o--)v=f[o]+v;for(a=s;a<=o;a++)m+=f[a],y+=g[a];return(m||y)&&(w=r?y+i+m:m+i+y),d(p+w+v)}function r(t){return S[t]||(S[t]=s(t))}function s(t){var e=o(t);return{fakeFormatString:l(e),sameUnits:u(e)}}function o(t){for(var e,n=[],i=/\[([^\]]*)\]|\(([^\)]*)\)|(LTS|LT|(\w)\4*o?)|([^\w\[\(]+)/g;e=i.exec(t);)e[1]?n.push.apply(n,a(e[1])):e[2]?n.push({maybe:o(e[2])}):e[3]?n.push({token:e[3]}):e[5]&&n.push.apply(n,a(e[5]));return n}function a(t){return". "===t?["."," "]:[t]}function l(t){var e,n,i=[];for(e=0;e<t.length;e++)n=t[e],"string"==typeof n?i.push("["+n+"]"):n.token?n.token in y?i.push(p+"["+n.token+"]"):i.push(n.token):n.maybe&&i.push(v+l(n.maybe)+v);return i.join(g)}function u(t){var e,n,i,r=[];for(e=0;e<t.length;e++)n=t[e],n.token?(i=w[n.token.charAt(0)],r.push(i?i.unit:"second")):n.maybe?r.push.apply(r,u(n.maybe)):r.push(null);return r}function h(t,e){return d(c(t,e).join(""))}function c(t,n){var i,r,s=[],o=e(n,t),a=o.split(g);for(i=0;i<a.length;i++)r=a[i],r.charAt(0)===p?s.push(y[r.substring(1)](n)):s.push(r);return s}function d(t){return t.replace(m,function(t,e){return e.match(/[1-9]/)?e:""})}function f(t){var e,n,i,r,s=o(t);for(e=0;e<s.length;e++)n=s[e],n.token&&(i=w[n.token.charAt(0)])&&(!r||i.value>r.value)&&(r=i);return r?r.unit:null}Zt.formatDate=t,Zt.formatRange=n,Zt.oldMomentFormat=e,Zt.queryMostGranularFormatUnit=f;var g="\v",p="",v="",m=new RegExp(v+"([^"+v+"]*)"+v,"g"),y={t:function(t){return e(t,"a").charAt(0)},T:function(t){return e(t,"A").charAt(0)}},w={Y:{value:1,unit:"year"},M:{value:2,unit:"month"},W:{value:3,unit:"week"},w:{value:3,unit:"week"},D:{value:4,unit:"day"},d:{value:4,unit:"day"}},S={}}();var oe=Zt.formatDate,ae=Zt.formatRange,le=Zt.oldMomentFormat;Zt.Class=bt,bt.extend=function(){var t,e,n=arguments.length;for(t=0;t<n;t++)e=arguments[t],t<n-1&&Dt(this,e);return Et(this,e||{})},bt.mixin=function(t){Dt(this,t)};var ue=bt.extend(fe,ge,{_props:null,_watchers:null,_globalWatchArgs:null,constructor:function(){this._watchers={},this._props={},this.applyGlobalWatchers()},applyGlobalWatchers:function(){var t,e=this._globalWatchArgs||[];for(t=0;t<e.length;t++)this.watch.apply(this,e[t])},has:function(t){return t in this._props},get:function(t){return void 0===t?this._props:this._props[t]},set:function(t,e){var n;"string"==typeof t?(n={},n[t]=void 0===e?null:e):n=t,this.setProps(n)},reset:function(t){var e,n=this._props,i={};for(e in n)i[e]=void 0;for(e in t)i[e]=t[e];this.setProps(i)},unset:function(t){var e,n,i={};for(e="string"==typeof t?[t]:t,n=0;n<e.length;n++)i[e[n]]=void 0;this.setProps(i)},setProps:function(t){var e,n,i={},r=0;for(e in t)"object"!=typeof(n=t[e])&&n===this._props[e]||(i[e]=n,r++);if(r){this.trigger("before:batchChange",i);for(e in i)n=i[e],this.trigger("before:change",e,n),this.trigger("before:change:"+e,n);for(e in i)n=i[e],void 0===n?delete this._props[e]:this._props[e]=n,this.trigger("change:"+e,n),this.trigger("change",e,n);this.trigger("batchChange",i)}},watch:function(t,e,n,i){var r=this;this.unwatch(t),this._watchers[t]=this._watchDeps(e,function(e){var i=n.call(r,e);i&&i.then?(r.unset(t),i.then(function(e){r.set(t,e)})):r.set(t,i)},function(){r.unset(t),i&&i.call(r)})},unwatch:function(t){var e=this._watchers[t];e&&(delete this._watchers[t],e.teardown())},_watchDeps:function(t,e,n){function i(t,e,i){1===++a&&u===l&&(d=!0,n(),d=!1)}function r(t,n,i){void 0===n?(i||void 0===h[t]||u--,delete h[t]):(i||void 0!==h[t]||u++,h[t]=n),--a||u===l&&(d||e(h))}function s(t,e){o.on(t,e),c.push([t,e])}var o=this,a=0,l=t.length,u=0,h={},c=[],d=!1;return t.forEach(function(t){var e=!1;"?"===t.charAt(0)&&(t=t.substring(1),e=!0),s("before:change:"+t,function(n){i(t,n,e)}),s("change:"+t,function(n){r(t,n,e)})}),t.forEach(function(t){var e=!1;"?"===t.charAt(0)&&(t=t.substring(1),e=!0),o.has(t)?(h[t]=o.get(t),u++):e&&u++}),u===l&&e(h),{teardown:function(){for(var t=0;t<c.length;t++)o.off(c[t][0],c[t][1]);c=null,u===l&&n()},flash:function(){u===l&&(n(),e(h))}}},flash:function(t){var e=this._watchers[t];e&&e.flash()}});ue.watch=function(){var t=this.prototype;t._globalWatchArgs||(t._globalWatchArgs=[]),t._globalWatchArgs.push(arguments)},Zt.Model=ue;var he={construct:function(e){var n=t.Deferred(),i=n.promise();return"function"==typeof e&&e(function(t){n.resolve(t),Tt(i,t)},function(){n.reject(),Ct(i)}),i},resolve:function(e){var n=t.Deferred().resolve(e),i=n.promise();return Tt(i,e),i},reject:function(){var e=t.Deferred().reject(),n=e.promise();return Ct(n),n}};Zt.Promise=he;var ce=bt.extend(fe,{q:null,isPaused:!1,isRunning:!1,constructor:function(){this.q=[]},queue:function(){this.q.push.apply(this.q,arguments),this.tryStart()},pause:function(){this.isPaused=!0},resume:function(){this.isPaused=!1,this.tryStart()},tryStart:function(){!this.isRunning&&this.canRunNext()&&(this.isRunning=!0,this.trigger("start"),this.runNext())},canRunNext:function(){return!this.isPaused&&this.q.length},runNext:function(){this.runTask(this.q.shift())},runTask:function(t){this.runTaskFunc(t)},runTaskFunc:function(t){function e(){n.canRunNext()?n.runNext():(n.isRunning=!1,n.trigger("stop"))}var n=this,i=t();i&&i.then?i.then(e):e()}});Zt.TaskQueue=ce;var de=ce.extend({waitsByNamespace:null,waitNamespace:null,waitId:null,constructor:function(t){ce.call(this),this.waitsByNamespace=t||{}},queue:function(t,e,n){var i,r={func:t,namespace:e,type:n};e&&(i=this.waitsByNamespace[e]),this.waitNamespace&&(e===this.waitNamespace&&null!=i?this.delayWait(i):(this.clearWait(),this.tryStart())),this.compoundTask(r)&&(this.waitNamespace||null==i?this.tryStart():this.startWait(e,i))},startWait:function(t,e){this.waitNamespace=t,this.spawnWait(e)},delayWait:function(t){clearTimeout(this.waitId),this.spawnWait(t)},spawnWait:function(t){var e=this;this.waitId=setTimeout(function(){e.waitNamespace=null,e.tryStart()},t)},clearWait:function(){this.waitNamespace&&(clearTimeout(this.waitId),this.waitId=null,this.waitNamespace=null)},canRunNext:function(){if(!ce.prototype.canRunNext.apply(this,arguments))return!1;if(this.waitNamespace){for(var t=this.q,e=0;e<t.length;e++)if(t[e].namespace!==this.waitNamespace)return!0;return!1}return!0},runTask:function(t){this.runTaskFunc(t.func)},compoundTask:function(t){var e,n,i=this.q,r=!0;if(t.namespace&&("destroy"===t.type||"init"===t.type)){for(e=i.length-1;e>=0;e--)n=i[e],n.namespace!==t.namespace||"add"!==n.type&&"remove"!==n.type||i.splice(e,1);"destroy"===t.type?i.length&&(n=i[i.length-1],n.namespace===t.namespace&&("init"===n.type?(r=!1,i.pop()):"destroy"===n.type&&(r=!1))):"init"===t.type&&i.length&&(n=i[i.length-1],n.namespace===t.namespace&&"init"===n.type&&i.pop())}return r&&i.push(t),r}});Zt.RenderQueue=de;var fe=Zt.EmitterMixin={on:function(e,n){return t(this).on(e,this._prepareIntercept(n)),this},one:function(e,n){return t(this).one(e,this._prepareIntercept(n)),this},_prepareIntercept:function(e){var n=function(t,n){return e.apply(n.context||this,n.args||[])};return e.guid||(e.guid=t.guid++),n.guid=e.guid,n}, | |
7 | -off:function(e,n){return t(this).off(e,n),this},trigger:function(e){var n=Array.prototype.slice.call(arguments,1);return t(this).triggerHandler(e,{args:n}),this},triggerWith:function(e,n,i){return t(this).triggerHandler(e,{context:n,args:i}),this}},ge=Zt.ListenerMixin=function(){var e=0;return{listenerId:null,listenTo:function(e,n,i){if("object"==typeof n)for(var r in n)n.hasOwnProperty(r)&&this.listenTo(e,r,n[r]);else"string"==typeof n&&e.on(n+"."+this.getListenerNamespace(),t.proxy(i,this))},stopListeningTo:function(t,e){t.off((e||"")+"."+this.getListenerNamespace())},getListenerNamespace:function(){return null==this.listenerId&&(this.listenerId=e++),"_listener"+this.listenerId}}}(),pe=bt.extend(ge,{isHidden:!0,options:null,el:null,margin:10,constructor:function(t){this.options=t||{}},show:function(){this.isHidden&&(this.el||this.render(),this.el.show(),this.position(),this.isHidden=!1,this.trigger("show"))},hide:function(){this.isHidden||(this.el.hide(),this.isHidden=!0,this.trigger("hide"))},render:function(){var e=this,n=this.options;this.el=t('<div class="fc-popover"/>').addClass(n.className||"").css({top:0,left:0}).append(n.content).appendTo(n.parentEl),this.el.on("click",".fc-close",function(){e.hide()}),n.autoHide&&this.listenTo(t(document),"mousedown",this.documentMousedown)},documentMousedown:function(e){this.el&&!t(e.target).closest(this.el).length&&this.hide()},removeElement:function(){this.hide(),this.el&&(this.el.remove(),this.el=null),this.stopListeningTo(t(document),"mousedown")},position:function(){var e,n,i,r,s,o=this.options,a=this.el.offsetParent().offset(),l=this.el.outerWidth(),u=this.el.outerHeight(),h=t(window),d=c(this.el);r=o.top||0,s=void 0!==o.left?o.left:void 0!==o.right?o.right-l:0,d.is(window)||d.is(document)?(d=h,e=0,n=0):(i=d.offset(),e=i.top,n=i.left),e+=h.scrollTop(),n+=h.scrollLeft(),!1!==o.viewportConstrain&&(r=Math.min(r,e+d.outerHeight()-u-this.margin),r=Math.max(r,e+this.margin),s=Math.min(s,n+d.outerWidth()-l-this.margin),s=Math.max(s,n+this.margin)),this.el.css({top:r-a.top,left:s-a.left})},trigger:function(t){this.options[t]&&this.options[t].apply(this,Array.prototype.slice.call(arguments,1))}}),ve=Zt.CoordCache=bt.extend({els:null,forcedOffsetParentEl:null,origin:null,boundingRect:null,isHorizontal:!1,isVertical:!1,lefts:null,rights:null,tops:null,bottoms:null,constructor:function(e){this.els=t(e.els),this.isHorizontal=e.isHorizontal,this.isVertical=e.isVertical,this.forcedOffsetParentEl=e.offsetParent?t(e.offsetParent):null},build:function(){var t=this.forcedOffsetParentEl;!t&&this.els.length>0&&(t=this.els.eq(0).offsetParent()),this.origin=t?t.offset():null,this.boundingRect=this.queryBoundingRect(),this.isHorizontal&&this.buildElHorizontals(),this.isVertical&&this.buildElVerticals()},clear:function(){this.origin=null,this.boundingRect=null,this.lefts=null,this.rights=null,this.tops=null,this.bottoms=null},ensureBuilt:function(){this.origin||this.build()},buildElHorizontals:function(){var e=[],n=[];this.els.each(function(i,r){var s=t(r),o=s.offset().left,a=s.outerWidth();e.push(o),n.push(o+a)}),this.lefts=e,this.rights=n},buildElVerticals:function(){var e=[],n=[];this.els.each(function(i,r){var s=t(r),o=s.offset().top,a=s.outerHeight();e.push(o),n.push(o+a)}),this.tops=e,this.bottoms=n},getHorizontalIndex:function(t){this.ensureBuilt();var e,n=this.lefts,i=this.rights,r=n.length;for(e=0;e<r;e++)if(t>=n[e]&&t<i[e])return e},getVerticalIndex:function(t){this.ensureBuilt();var e,n=this.tops,i=this.bottoms,r=n.length;for(e=0;e<r;e++)if(t>=n[e]&&t<i[e])return e},getLeftOffset:function(t){return this.ensureBuilt(),this.lefts[t]},getLeftPosition:function(t){return this.ensureBuilt(),this.lefts[t]-this.origin.left},getRightOffset:function(t){return this.ensureBuilt(),this.rights[t]},getRightPosition:function(t){return this.ensureBuilt(),this.rights[t]-this.origin.left},getWidth:function(t){return this.ensureBuilt(),this.rights[t]-this.lefts[t]},getTopOffset:function(t){return this.ensureBuilt(),this.tops[t]},getTopPosition:function(t){return this.ensureBuilt(),this.tops[t]-this.origin.top},getBottomOffset:function(t){return this.ensureBuilt(),this.bottoms[t]},getBottomPosition:function(t){return this.ensureBuilt(),this.bottoms[t]-this.origin.top},getHeight:function(t){return this.ensureBuilt(),this.bottoms[t]-this.tops[t]},queryBoundingRect:function(){var t;return this.els.length>0&&(t=c(this.els.eq(0)),!t.is(document))?f(t):null},isPointInBounds:function(t,e){return this.isLeftInBounds(t)&&this.isTopInBounds(e)},isLeftInBounds:function(t){return!this.boundingRect||t>=this.boundingRect.left&&t<this.boundingRect.right},isTopInBounds:function(t){return!this.boundingRect||t>=this.boundingRect.top&&t<this.boundingRect.bottom}}),me=Zt.DragListener=bt.extend(ge,{options:null,subjectEl:null,originX:null,originY:null,scrollEl:null,isInteracting:!1,isDistanceSurpassed:!1,isDelayEnded:!1,isDragging:!1,isTouch:!1,isGeneric:!1,delay:null,delayTimeoutId:null,minDistance:null,shouldCancelTouchScroll:!0,scrollAlwaysKills:!1,constructor:function(t){this.options=t||{}},startInteraction:function(e,n){if("mousedown"===e.type){if(we.get().shouldIgnoreMouse())return;if(!S(e))return;e.preventDefault()}this.isInteracting||(n=n||{},this.delay=ut(n.delay,this.options.delay,0),this.minDistance=ut(n.distance,this.options.distance,0),this.subjectEl=this.options.subjectEl,T(t("body")),this.isInteracting=!0,this.isTouch=D(e),this.isGeneric="dragstart"===e.type,this.isDelayEnded=!1,this.isDistanceSurpassed=!1,this.originX=b(e),this.originY=E(e),this.scrollEl=c(t(e.target)),this.bindHandlers(),this.initAutoScroll(),this.handleInteractionStart(e),this.startDelay(e),this.minDistance||this.handleDistanceSurpassed(e))},handleInteractionStart:function(t){this.trigger("interactionStart",t)},endInteraction:function(e,n){this.isInteracting&&(this.endDrag(e),this.delayTimeoutId&&(clearTimeout(this.delayTimeoutId),this.delayTimeoutId=null),this.destroyAutoScroll(),this.unbindHandlers(),this.isInteracting=!1,this.handleInteractionEnd(e,n),C(t("body")))},handleInteractionEnd:function(t,e){this.trigger("interactionEnd",t,e||!1)},bindHandlers:function(){var e=we.get();this.isGeneric?this.listenTo(t(document),{drag:this.handleMove,dragstop:this.endInteraction}):this.isTouch?this.listenTo(e,{touchmove:this.handleTouchMove,touchend:this.endInteraction,scroll:this.handleTouchScroll}):this.listenTo(e,{mousemove:this.handleMouseMove,mouseup:this.endInteraction}),this.listenTo(e,{selectstart:H,contextmenu:H})},unbindHandlers:function(){this.stopListeningTo(we.get()),this.stopListeningTo(t(document))},startDrag:function(t,e){this.startInteraction(t,e),this.isDragging||(this.isDragging=!0,this.handleDragStart(t))},handleDragStart:function(t){this.trigger("dragStart",t)},handleMove:function(t){var e=b(t)-this.originX,n=E(t)-this.originY,i=this.minDistance;this.isDistanceSurpassed||e*e+n*n>=i*i&&this.handleDistanceSurpassed(t),this.isDragging&&this.handleDrag(e,n,t)},handleDrag:function(t,e,n){this.trigger("drag",t,e,n),this.updateAutoScroll(n)},endDrag:function(t){this.isDragging&&(this.isDragging=!1,this.handleDragEnd(t))},handleDragEnd:function(t){this.trigger("dragEnd",t)},startDelay:function(t){var e=this;this.delay?this.delayTimeoutId=setTimeout(function(){e.handleDelayEnd(t)},this.delay):this.handleDelayEnd(t)},handleDelayEnd:function(t){this.isDelayEnded=!0,this.isDistanceSurpassed&&this.startDrag(t)},handleDistanceSurpassed:function(t){this.isDistanceSurpassed=!0,this.isDelayEnded&&this.startDrag(t)},handleTouchMove:function(t){this.isDragging&&this.shouldCancelTouchScroll&&t.preventDefault(),this.handleMove(t)},handleMouseMove:function(t){this.handleMove(t)},handleTouchScroll:function(t){this.isDragging&&!this.scrollAlwaysKills||this.endInteraction(t,!0)},trigger:function(t){this.options[t]&&this.options[t].apply(this,Array.prototype.slice.call(arguments,1)),this["_"+t]&&this["_"+t].apply(this,Array.prototype.slice.call(arguments,1))}});me.mixin({isAutoScroll:!1,scrollBounds:null,scrollTopVel:null,scrollLeftVel:null,scrollIntervalId:null,scrollSensitivity:30,scrollSpeed:200,scrollIntervalMs:50,initAutoScroll:function(){var t=this.scrollEl;this.isAutoScroll=this.options.scroll&&t&&!t.is(window)&&!t.is(document),this.isAutoScroll&&this.listenTo(t,"scroll",yt(this.handleDebouncedScroll,100))},destroyAutoScroll:function(){this.endAutoScroll(),this.isAutoScroll&&this.stopListeningTo(this.scrollEl,"scroll")},computeScrollBounds:function(){this.isAutoScroll&&(this.scrollBounds=d(this.scrollEl))},updateAutoScroll:function(t){var e,n,i,r,s=this.scrollSensitivity,o=this.scrollBounds,a=0,l=0;o&&(e=(s-(E(t)-o.top))/s,n=(s-(o.bottom-E(t)))/s,i=(s-(b(t)-o.left))/s,r=(s-(o.right-b(t)))/s,e>=0&&e<=1?a=e*this.scrollSpeed*-1:n>=0&&n<=1&&(a=n*this.scrollSpeed),i>=0&&i<=1?l=i*this.scrollSpeed*-1:r>=0&&r<=1&&(l=r*this.scrollSpeed)),this.setScrollVel(a,l)},setScrollVel:function(t,e){this.scrollTopVel=t,this.scrollLeftVel=e,this.constrainScrollVel(),!this.scrollTopVel&&!this.scrollLeftVel||this.scrollIntervalId||(this.scrollIntervalId=setInterval(mt(this,"scrollIntervalFunc"),this.scrollIntervalMs))},constrainScrollVel:function(){var t=this.scrollEl;this.scrollTopVel<0?t.scrollTop()<=0&&(this.scrollTopVel=0):this.scrollTopVel>0&&t.scrollTop()+t[0].clientHeight>=t[0].scrollHeight&&(this.scrollTopVel=0),this.scrollLeftVel<0?t.scrollLeft()<=0&&(this.scrollLeftVel=0):this.scrollLeftVel>0&&t.scrollLeft()+t[0].clientWidth>=t[0].scrollWidth&&(this.scrollLeftVel=0)},scrollIntervalFunc:function(){var t=this.scrollEl,e=this.scrollIntervalMs/1e3;this.scrollTopVel&&t.scrollTop(t.scrollTop()+this.scrollTopVel*e),this.scrollLeftVel&&t.scrollLeft(t.scrollLeft()+this.scrollLeftVel*e),this.constrainScrollVel(),this.scrollTopVel||this.scrollLeftVel||this.endAutoScroll()},endAutoScroll:function(){this.scrollIntervalId&&(clearInterval(this.scrollIntervalId),this.scrollIntervalId=null,this.handleScrollEnd())},handleDebouncedScroll:function(){this.scrollIntervalId||this.handleScrollEnd()},handleScrollEnd:function(){}});var ye=me.extend({component:null,origHit:null,hit:null,coordAdjust:null,constructor:function(t,e){me.call(this,e),this.component=t},handleInteractionStart:function(t){var e,n,i,r=this.subjectEl;this.component.hitsNeeded(),this.computeScrollBounds(),t?(n={left:b(t),top:E(t)},i=n,r&&(e=d(r),i=x(i,e)),this.origHit=this.queryHit(i.left,i.top),r&&this.options.subjectCenter&&(this.origHit&&(e=R(this.origHit,e)||e),i=I(e)),this.coordAdjust=k(i,n)):(this.origHit=null,this.coordAdjust=null),me.prototype.handleInteractionStart.apply(this,arguments)},handleDragStart:function(t){var e;me.prototype.handleDragStart.apply(this,arguments),(e=this.queryHit(b(t),E(t)))&&this.handleHitOver(e)},handleDrag:function(t,e,n){var i;me.prototype.handleDrag.apply(this,arguments),i=this.queryHit(b(n),E(n)),Ht(i,this.hit)||(this.hit&&this.handleHitOut(),i&&this.handleHitOver(i))},handleDragEnd:function(){this.handleHitDone(),me.prototype.handleDragEnd.apply(this,arguments)},handleHitOver:function(t){var e=Ht(t,this.origHit);this.hit=t,this.trigger("hitOver",this.hit,e,this.origHit)},handleHitOut:function(){this.hit&&(this.trigger("hitOut",this.hit),this.handleHitDone(),this.hit=null)},handleHitDone:function(){this.hit&&this.trigger("hitDone",this.hit)},handleInteractionEnd:function(){me.prototype.handleInteractionEnd.apply(this,arguments),this.origHit=null,this.hit=null,this.component.hitsNotNeeded()},handleScrollEnd:function(){me.prototype.handleScrollEnd.apply(this,arguments),this.isDragging&&(this.component.releaseHits(),this.component.prepareHits())},queryHit:function(t,e){return this.coordAdjust&&(t+=this.coordAdjust.left,e+=this.coordAdjust.top),this.component.queryHit(t,e)}});Zt.touchMouseIgnoreWait=500;var we=bt.extend(ge,fe,{isTouching:!1,mouseIgnoreDepth:0,handleScrollProxy:null,bind:function(){var e=this;this.listenTo(t(document),{touchstart:this.handleTouchStart,touchcancel:this.handleTouchCancel,touchend:this.handleTouchEnd,mousedown:this.handleMouseDown,mousemove:this.handleMouseMove,mouseup:this.handleMouseUp,click:this.handleClick,selectstart:this.handleSelectStart,contextmenu:this.handleContextMenu}),window.addEventListener("touchmove",this.handleTouchMoveProxy=function(n){e.handleTouchMove(t.Event(n))},{passive:!1}),window.addEventListener("scroll",this.handleScrollProxy=function(n){e.handleScroll(t.Event(n))},!0)},unbind:function(){this.stopListeningTo(t(document)),window.removeEventListener("touchmove",this.handleTouchMoveProxy),window.removeEventListener("scroll",this.handleScrollProxy,!0)},handleTouchStart:function(t){this.stopTouch(t,!0),this.isTouching=!0,this.trigger("touchstart",t)},handleTouchMove:function(t){this.isTouching&&this.trigger("touchmove",t)},handleTouchCancel:function(t){this.isTouching&&(this.trigger("touchcancel",t),this.stopTouch(t))},handleTouchEnd:function(t){this.stopTouch(t)},handleMouseDown:function(t){this.shouldIgnoreMouse()||this.trigger("mousedown",t)},handleMouseMove:function(t){this.shouldIgnoreMouse()||this.trigger("mousemove",t)},handleMouseUp:function(t){this.shouldIgnoreMouse()||this.trigger("mouseup",t)},handleClick:function(t){this.shouldIgnoreMouse()||this.trigger("click",t)},handleSelectStart:function(t){this.trigger("selectstart",t)},handleContextMenu:function(t){this.trigger("contextmenu",t)},handleScroll:function(t){this.trigger("scroll",t)},stopTouch:function(t,e){this.isTouching&&(this.isTouching=!1,this.trigger("touchend",t),e||this.startTouchMouseIgnore())},startTouchMouseIgnore:function(){var t=this,e=Zt.touchMouseIgnoreWait;e&&(this.mouseIgnoreDepth++,setTimeout(function(){t.mouseIgnoreDepth--},e))},shouldIgnoreMouse:function(){return this.isTouching||Boolean(this.mouseIgnoreDepth)}});!function(){var t=null,e=0;we.get=function(){return t||(t=new we,t.bind()),t},we.needed=function(){we.get(),e++},we.unneeded=function(){--e||(t.unbind(),t=null)}}();var Se=bt.extend(ge,{options:null,sourceEl:null,el:null,parentEl:null,top0:null,left0:null,y0:null,x0:null,topDelta:null,leftDelta:null,isFollowing:!1,isHidden:!1,isAnimating:!1,constructor:function(e,n){this.options=n=n||{},this.sourceEl=e,this.parentEl=n.parentEl?t(n.parentEl):e.parent()},start:function(e){this.isFollowing||(this.isFollowing=!0,this.y0=E(e),this.x0=b(e),this.topDelta=0,this.leftDelta=0,this.isHidden||this.updatePosition(),D(e)?this.listenTo(t(document),"touchmove",this.handleMove):this.listenTo(t(document),"mousemove",this.handleMove))},stop:function(e,n){function i(){r.isAnimating=!1,r.removeElement(),r.top0=r.left0=null,n&&n()}var r=this,s=this.options.revertDuration;this.isFollowing&&!this.isAnimating&&(this.isFollowing=!1,this.stopListeningTo(t(document)),e&&s&&!this.isHidden?(this.isAnimating=!0,this.el.animate({top:this.top0,left:this.left0},{duration:s,complete:i})):i())},getEl:function(){var t=this.el;return t||(t=this.el=this.sourceEl.clone().addClass(this.options.additionalClass||"").css({position:"absolute",visibility:"",display:this.isHidden?"none":"",margin:0,right:"auto",bottom:"auto",width:this.sourceEl.width(),height:this.sourceEl.height(),opacity:this.options.opacity||"",zIndex:this.options.zIndex}),t.addClass("fc-unselectable"),t.appendTo(this.parentEl)),t},removeElement:function(){this.el&&(this.el.remove(),this.el=null)},updatePosition:function(){var t,e;this.getEl(),null===this.top0&&(t=this.sourceEl.offset(),e=this.el.offsetParent().offset(),this.top0=t.top-e.top,this.left0=t.left-e.left),this.el.css({top:this.top0+this.topDelta,left:this.left0+this.leftDelta})},handleMove:function(t){this.topDelta=E(t)-this.y0,this.leftDelta=b(t)-this.x0,this.isHidden||this.updatePosition()},hide:function(){this.isHidden||(this.isHidden=!0,this.el&&this.el.hide())},show:function(){this.isHidden&&(this.isHidden=!1,this.updatePosition(),this.getEl().show())}}),be=Zt.Grid=bt.extend(ge,{hasDayInteractions:!0,view:null,isRTL:null,start:null,end:null,el:null,elsByFill:null,eventTimeFormat:null,displayEventTime:null,displayEventEnd:null,minResizeDuration:null,largeUnit:null,dayClickListener:null,daySelectListener:null,segDragListener:null,segResizeListener:null,externalDragListener:null,constructor:function(t){this.view=t,this.isRTL=t.opt("isRTL"),this.elsByFill={},this.dayClickListener=this.buildDayClickListener(),this.daySelectListener=this.buildDaySelectListener()},computeEventTimeFormat:function(){return this.view.opt("smallTimeFormat")},computeDisplayEventTime:function(){return!0},computeDisplayEventEnd:function(){return!0},setRange:function(t){this.start=t.start.clone(),this.end=t.end.clone(),this.rangeUpdated(),this.processRangeOptions()},rangeUpdated:function(){},processRangeOptions:function(){var t,e,n=this.view;this.eventTimeFormat=n.opt("eventTimeFormat")||n.opt("timeFormat")||this.computeEventTimeFormat(),t=n.opt("displayEventTime"),null==t&&(t=this.computeDisplayEventTime()),e=n.opt("displayEventEnd"),null==e&&(e=this.computeDisplayEventEnd()),this.displayEventTime=t,this.displayEventEnd=e},spanToSegs:function(t){},diffDates:function(t,e){return this.largeUnit?G(t,e,this.largeUnit):F(t,e)},hitsNeededDepth:0,hitsNeeded:function(){this.hitsNeededDepth++||this.prepareHits()},hitsNotNeeded:function(){this.hitsNeededDepth&&!--this.hitsNeededDepth&&this.releaseHits()},prepareHits:function(){},releaseHits:function(){},queryHit:function(t,e){},getSafeHitSpan:function(t){var e=this.getHitSpan(t);return Q(e,this.view.activeRange)?e:null},getHitSpan:function(t){},getHitEl:function(t){},setElement:function(t){this.el=t,this.hasDayInteractions&&(T(t),this.bindDayHandler("touchstart",this.dayTouchStart),this.bindDayHandler("mousedown",this.dayMousedown)),this.bindSegHandlers(),this.bindGlobalHandlers()},bindDayHandler:function(e,n){var i=this;this.el.on(e,function(e){if(!t(e.target).is(i.segSelector+","+i.segSelector+" *,.fc-more,a[data-goto]"))return n.call(i,e)})},removeElement:function(){this.unbindGlobalHandlers(),this.clearDragListeners(),this.el.remove()},renderSkeleton:function(){},renderDates:function(){},unrenderDates:function(){},bindGlobalHandlers:function(){this.listenTo(t(document),{dragstart:this.externalDragStart,sortstart:this.externalDragStart})},unbindGlobalHandlers:function(){this.stopListeningTo(t(document))},dayMousedown:function(t){var e=this.view;we.get().shouldIgnoreMouse()||(this.dayClickListener.startInteraction(t),e.opt("selectable")&&this.daySelectListener.startInteraction(t,{distance:e.opt("selectMinDistance")}))},dayTouchStart:function(t){var e,n=this.view;n.isSelected||n.selectedEvent||(e=n.opt("selectLongPressDelay"),null==e&&(e=n.opt("longPressDelay")),this.dayClickListener.startInteraction(t),n.opt("selectable")&&this.daySelectListener.startInteraction(t,{delay:e}))},buildDayClickListener:function(){var t,e=this,n=this.view,i=new ye(this,{scroll:n.opt("dragScroll"),interactionStart:function(){t=i.origHit},hitOver:function(e,n,i){n||(t=null)},hitOut:function(){t=null},interactionEnd:function(i,r){var s;!r&&t&&(s=e.getSafeHitSpan(t))&&n.triggerDayClick(s,e.getHitEl(t),i)}});return i.shouldCancelTouchScroll=!1,i.scrollAlwaysKills=!0,i},buildDaySelectListener:function(){var t,e=this,n=this.view;return new ye(this,{scroll:n.opt("dragScroll"),interactionStart:function(){t=null},dragStart:function(){n.unselect()},hitOver:function(n,i,r){var o,a;r&&(o=e.getSafeHitSpan(r),a=e.getSafeHitSpan(n),t=o&&a?e.computeSelection(o,a):null,t?e.renderSelection(t):!1===t&&s())},hitOut:function(){t=null,e.unrenderSelection()},hitDone:function(){o()},interactionEnd:function(e,i){!i&&t&&n.reportSelection(t,e)}})},clearDragListeners:function(){this.dayClickListener.endInteraction(),this.daySelectListener.endInteraction(),this.segDragListener&&this.segDragListener.endInteraction(),this.segResizeListener&&this.segResizeListener.endInteraction(),this.externalDragListener&&this.externalDragListener.endInteraction()},renderEventLocationHelper:function(t,e){var n=this.fabricateHelperEvent(t,e);return this.renderHelper(n,e)},fabricateHelperEvent:function(t,e){var n=e?rt(e.event):{};return n.start=t.start.clone(),n.end=t.end?t.end.clone():null,n.allDay=null,this.view.calendar.normalizeEventDates(n),n.className=(n.className||[]).concat("fc-helper"),e||(n.editable=!1),n},renderHelper:function(t,e){},unrenderHelper:function(){},renderSelection:function(t){this.renderHighlight(t)},unrenderSelection:function(){this.unrenderHighlight()},computeSelection:function(t,e){var n=this.computeSelectionSpan(t,e);return!(n&&!this.view.calendar.isSelectionSpanAllowed(n))&&n},computeSelectionSpan:function(t,e){var n=[t.start,t.end,e.start,e.end];return n.sort(pt),{start:n[0].clone(),end:n[3].clone()}},renderHighlight:function(t){this.renderFill("highlight",this.spanToSegs(t))},unrenderHighlight:function(){this.unrenderFill("highlight")},highlightSegClasses:function(){return["fc-highlight"]},renderBusinessHours:function(){},unrenderBusinessHours:function(){},getNowIndicatorUnit:function(){},renderNowIndicator:function(t){},unrenderNowIndicator:function(){},renderFill:function(t,e){},unrenderFill:function(t){var e=this.elsByFill[t];e&&(e.remove(),delete this.elsByFill[t])},renderFillSegEls:function(e,n){var i,r=this,s=this[e+"SegEl"],o="",a=[];if(n.length){for(i=0;i<n.length;i++)o+=this.fillSegHtml(e,n[i]);t(o).each(function(e,i){var o=n[e],l=t(i);s&&(l=s.call(r,o,l)),l&&(l=t(l),l.is(r.fillSegTag)&&(o.el=l,a.push(o)))})}return a},fillSegTag:"div",fillSegHtml:function(t,e){var n=this[t+"SegClasses"],i=this[t+"SegCss"],r=n?n.call(this,e):[],s=dt(i?i.call(this,e):{});return"<"+this.fillSegTag+(r.length?' class="'+r.join(" ")+'"':"")+(s?' style="'+s+'"':"")+" />"},getDayClasses:function(t,e){var n,i=this.view,r=[];return Z(t,i.activeRange)?(r.push("fc-"+Kt[t.day()]),1==i.currentRangeAs("months")&&t.month()!=i.currentRange.start.month()&&r.push("fc-other-month"),n=i.calendar.getNow(),t.isSame(n,"day")?(r.push("fc-today"),!0!==e&&r.push(i.highlightStateClass)):t<n?r.push("fc-past"):r.push("fc-future")):r.push("fc-disabled-day"),r}});be.mixin({segSelector:".fc-event-container > *",mousedOverSeg:null,isDraggingSeg:!1,isResizingSeg:!1,isDraggingExternal:!1,segs:null,renderEvents:function(t){var e,n=[],i=[];for(e=0;e<t.length;e++)(It(t[e])?n:i).push(t[e]);this.segs=[].concat(this.renderBgEvents(n),this.renderFgEvents(i))},renderBgEvents:function(t){var e=this.eventsToSegs(t);return this.renderBgSegs(e)||e},renderFgEvents:function(t){var e=this.eventsToSegs(t);return this.renderFgSegs(e)||e},unrenderEvents:function(){this.handleSegMouseout(),this.clearDragListeners(),this.unrenderFgSegs(),this.unrenderBgSegs(),this.segs=null},getEventSegs:function(){return this.segs||[]},renderFgSegs:function(t){},unrenderFgSegs:function(){},renderFgSegEls:function(e,n){var i,r=this.view,s="",o=[];if(e.length){for(i=0;i<e.length;i++)s+=this.fgSegHtml(e[i],n);t(s).each(function(n,i){var s=e[n],a=r.resolveEventEl(s.event,t(i));a&&(a.data("fc-seg",s),s.el=a,o.push(s))})}return o},fgSegHtml:function(t,e){},renderBgSegs:function(t){return this.renderFill("bgEvent",t)},unrenderBgSegs:function(){this.unrenderFill("bgEvent")},bgEventSegEl:function(t,e){return this.view.resolveEventEl(t.event,e)},bgEventSegClasses:function(t){var e=t.event,n=e.source||{};return["fc-bgevent"].concat(e.className,n.className||[])},bgEventSegCss:function(t){return{"background-color":this.getSegSkinCss(t)["background-color"]}},businessHoursSegClasses:function(t){return["fc-nonbusiness","fc-bgevent"]},buildBusinessHourSegs:function(t,e){return this.eventsToSegs(this.buildBusinessHourEvents(t,e))},buildBusinessHourEvents:function(e,n){var i,r=this.view.calendar;return null==n&&(n=r.opt("businessHours")),i=r.computeBusinessHourEvents(e,n),!i.length&&n&&(i=[t.extend({},Ne,{start:this.view.activeRange.end,end:this.view.activeRange.end,dow:null})]),i},bindSegHandlers:function(){this.bindSegHandlersToEl(this.el)},bindSegHandlersToEl:function(t){this.bindSegHandlerToEl(t,"touchstart",this.handleSegTouchStart),this.bindSegHandlerToEl(t,"mouseenter",this.handleSegMouseover),this.bindSegHandlerToEl(t,"mouseleave",this.handleSegMouseout),this.bindSegHandlerToEl(t,"mousedown",this.handleSegMousedown),this.bindSegHandlerToEl(t,"click",this.handleSegClick)},bindSegHandlerToEl:function(e,n,i){var r=this;e.on(n,this.segSelector,function(e){var n=t(this).data("fc-seg");if(n&&!r.isDraggingSeg&&!r.isResizingSeg)return i.call(r,n,e)})},handleSegClick:function(t,e){!1===this.view.publiclyTrigger("eventClick",t.el[0],t.event,e)&&e.preventDefault()},handleSegMouseover:function(t,e){we.get().shouldIgnoreMouse()||this.mousedOverSeg||(this.mousedOverSeg=t,this.view.isEventResizable(t.event)&&t.el.addClass("fc-allow-mouse-resize"),this.view.publiclyTrigger("eventMouseover",t.el[0],t.event,e))},handleSegMouseout:function(t,e){e=e||{},this.mousedOverSeg&&(t=t||this.mousedOverSeg,this.mousedOverSeg=null,this.view.isEventResizable(t.event)&&t.el.removeClass("fc-allow-mouse-resize"),this.view.publiclyTrigger("eventMouseout",t.el[0],t.event,e))},handleSegMousedown:function(t,e){!this.startSegResize(t,e,{distance:5})&&this.view.isEventDraggable(t.event)&&this.buildSegDragListener(t).startInteraction(e,{distance:5})},handleSegTouchStart:function(t,e){var n,i,r=this.view,s=t.event,o=r.isEventSelected(s),a=r.isEventDraggable(s),l=r.isEventResizable(s),u=!1;o&&l&&(u=this.startSegResize(t,e)),u||!a&&!l||(i=r.opt("eventLongPressDelay"),null==i&&(i=r.opt("longPressDelay")),n=a?this.buildSegDragListener(t):this.buildSegSelectListener(t),n.startInteraction(e,{delay:o?0:i}))},startSegResize:function(e,n,i){return!!t(n.target).is(".fc-resizer")&&(this.buildSegResizeListener(e,t(n.target).is(".fc-start-resizer")).startInteraction(n,i),!0)},buildSegDragListener:function(t){var e,n,i,r=this,a=this.view,l=t.el,u=t.event;if(this.segDragListener)return this.segDragListener;var h=this.segDragListener=new ye(a,{scroll:a.opt("dragScroll"),subjectEl:l,subjectCenter:!0,interactionStart:function(i){t.component=r,e=!1,n=new Se(t.el,{additionalClass:"fc-dragging",parentEl:a.el,opacity:h.isTouch?null:a.opt("dragOpacity"),revertDuration:a.opt("dragRevertDuration"),zIndex:2}),n.hide(),n.start(i)},dragStart:function(n){h.isTouch&&!a.isEventSelected(u)&&a.selectEvent(u),e=!0,r.handleSegMouseout(t,n),r.segDragStart(t,n),a.hideEvent(u)},hitOver:function(e,o,l){var c,d,f,g=!0;t.hit&&(l=t.hit),c=l.component.getSafeHitSpan(l),d=e.component.getSafeHitSpan(e),c&&d?(i=r.computeEventDrop(c,d,u),g=i&&r.isEventLocationAllowed(i,u)):g=!1,g||(i=null,s()),i&&(f=a.renderDrag(i,t))?(f.addClass("fc-dragging"),h.isTouch||r.applyDragOpacity(f),n.hide()):n.show(),o&&(i=null)},hitOut:function(){a.unrenderDrag(),n.show(),i=null},hitDone:function(){o()},interactionEnd:function(s){delete t.component,n.stop(!i,function(){e&&(a.unrenderDrag(),r.segDragStop(t,s)),i?a.reportSegDrop(t,i,r.largeUnit,l,s):a.showEvent(u)}),r.segDragListener=null}});return h},buildSegSelectListener:function(t){var e=this,n=this.view,i=t.event;if(this.segDragListener)return this.segDragListener;var r=this.segDragListener=new me({dragStart:function(t){r.isTouch&&!n.isEventSelected(i)&&n.selectEvent(i)},interactionEnd:function(t){e.segDragListener=null}});return r},segDragStart:function(t,e){this.isDraggingSeg=!0,this.view.publiclyTrigger("eventDragStart",t.el[0],t.event,e,{})},segDragStop:function(t,e){this.isDraggingSeg=!1,this.view.publiclyTrigger("eventDragStop",t.el[0],t.event,e,{})},computeEventDrop:function(t,e,n){var i,r,s=this.view.calendar,o=t.start,a=e.start;return o.hasTime()===a.hasTime()?(i=this.diffDates(a,o),n.allDay&&tt(i)?(r={start:n.start.clone(),end:s.getEventEnd(n),allDay:!1},s.normalizeEventTimes(r)):r=xt(n),r.start.add(i),r.end&&r.end.add(i)):r={start:a.clone(),end:null,allDay:!a.hasTime()},r},applyDragOpacity:function(t){var e=this.view.opt("dragOpacity");null!=e&&t.css("opacity",e)},externalDragStart:function(e,n){var i,r,s=this.view;s.opt("droppable")&&(i=t((n?n.item:null)||e.target),r=s.opt("dropAccept"),(t.isFunction(r)?r.call(i[0],i):i.is(r))&&(this.isDraggingExternal||this.listenToExternalDrag(i,e,n)))},listenToExternalDrag:function(t,e,n){var i,r=this,a=this.view,l=Nt(t);(r.externalDragListener=new ye(this,{interactionStart:function(){r.isDraggingExternal=!0},hitOver:function(t){var e=!0,n=t.component.getSafeHitSpan(t);n?(i=r.computeExternalDrop(n,l),e=i&&r.isExternalLocationAllowed(i,l.eventProps)):e=!1,e||(i=null,s()),i&&r.renderDrag(i)},hitOut:function(){i=null},hitDone:function(){o(),r.unrenderDrag()},interactionEnd:function(e){i&&a.reportExternalDrop(l,i,t,e,n),r.isDraggingExternal=!1,r.externalDragListener=null}})).startDrag(e)},computeExternalDrop:function(t,e){var n=this.view.calendar,i={start:n.applyTimezone(t.start),end:null};return e.startTime&&!i.start.hasTime()&&i.start.time(e.startTime),e.duration&&(i.end=i.start.clone().add(e.duration)),i},renderDrag:function(t,e){},unrenderDrag:function(){},buildSegResizeListener:function(t,e){var n,i,r=this,a=this.view,l=a.calendar,u=t.el,h=t.event,c=l.getEventEnd(h);return this.segResizeListener=new ye(this,{scroll:a.opt("dragScroll"),subjectEl:u,interactionStart:function(){n=!1},dragStart:function(e){n=!0,r.handleSegMouseout(t,e),r.segResizeStart(t,e)},hitOver:function(n,o,l){var u=!0,d=r.getSafeHitSpan(l),f=r.getSafeHitSpan(n);d&&f?(i=e?r.computeEventStartResize(d,f,h):r.computeEventEndResize(d,f,h),u=i&&r.isEventLocationAllowed(i,h)):u=!1,u?i.start.isSame(h.start.clone().stripZone())&&i.end.isSame(c.clone().stripZone())&&(i=null):(i=null,s()),i&&(a.hideEvent(h),r.renderEventResize(i,t))},hitOut:function(){i=null,a.showEvent(h)},hitDone:function(){r.unrenderEventResize(),o()},interactionEnd:function(e){n&&r.segResizeStop(t,e),i?a.reportSegResize(t,i,r.largeUnit,u,e):a.showEvent(h),r.segResizeListener=null}})},segResizeStart:function(t,e){this.isResizingSeg=!0,this.view.publiclyTrigger("eventResizeStart",t.el[0],t.event,e,{})},segResizeStop:function(t,e){this.isResizingSeg=!1,this.view.publiclyTrigger("eventResizeStop",t.el[0],t.event,e,{})},computeEventStartResize:function(t,e,n){return this.computeEventResize("start",t,e,n)},computeEventEndResize:function(t,e,n){return this.computeEventResize("end",t,e,n)},computeEventResize:function(t,e,n,i){var r,s,o=this.view.calendar,a=this.diffDates(n[t],e[t]);return r={start:i.start.clone(),end:o.getEventEnd(i),allDay:i.allDay},r.allDay&&tt(a)&&(r.allDay=!1,o.normalizeEventTimes(r)),r[t].add(a),r.start.isBefore(r.end)||(s=this.minResizeDuration||(i.allDay?o.defaultAllDayEventDuration:o.defaultTimedEventDuration),"start"==t?r.start=r.end.clone().subtract(s):r.end=r.start.clone().add(s)),r},renderEventResize:function(t,e){},unrenderEventResize:function(){},getEventTimeText:function(t,e,n){return null==e&&(e=this.eventTimeFormat),null==n&&(n=this.displayEventEnd),this.displayEventTime&&t.start.hasTime()?n&&t.end?this.view.formatRange(t,e):t.start.format(e):""},getSegClasses:function(t,e,n){var i=this.view,r=["fc-event",t.isStart?"fc-start":"fc-not-start",t.isEnd?"fc-end":"fc-not-end"].concat(this.getSegCustomClasses(t));return e&&r.push("fc-draggable"),n&&r.push("fc-resizable"),i.isEventSelected(t.event)&&r.push("fc-selected"),r},getSegCustomClasses:function(t){var e=t.event;return[].concat(e.className,e.source?e.source.className:[])},getSegSkinCss:function(t){return{"background-color":this.getSegBackgroundColor(t),"border-color":this.getSegBorderColor(t),color:this.getSegTextColor(t)}},getSegBackgroundColor:function(t){return t.event.backgroundColor||t.event.color||this.getSegDefaultBackgroundColor(t)},getSegDefaultBackgroundColor:function(t){var e=t.event.source||{};return e.backgroundColor||e.color||this.view.opt("eventBackgroundColor")||this.view.opt("eventColor")},getSegBorderColor:function(t){return t.event.borderColor||t.event.color||this.getSegDefaultBorderColor(t)},getSegDefaultBorderColor:function(t){var e=t.event.source||{};return e.borderColor||e.color||this.view.opt("eventBorderColor")||this.view.opt("eventColor")},getSegTextColor:function(t){ | |
8 | -return t.event.textColor||this.getSegDefaultTextColor(t)},getSegDefaultTextColor:function(t){return(t.event.source||{}).textColor||this.view.opt("eventTextColor")},isEventLocationAllowed:function(t,e){if(this.isEventLocationInRange(t)){var n,i=this.view.calendar,r=this.eventToSpans(t);if(r.length){for(n=0;n<r.length;n++)if(!i.isEventSpanAllowed(r[n],e))return!1;return!0}}return!1},isExternalLocationAllowed:function(t,e){if(this.isEventLocationInRange(t)){var n,i=this.view.calendar,r=this.eventToSpans(t);if(r.length){for(n=0;n<r.length;n++)if(!i.isExternalSpanAllowed(r[n],t,e))return!1;return!0}}return!1},isEventLocationInRange:function(t){return Q(this.eventToRawRange(t),this.view.validRange)},eventToSegs:function(t){return this.eventsToSegs([t])},eventToSpans:function(t){var e=this.eventToRange(t);return e?this.eventRangeToSpans(e,t):[]},eventsToSegs:function(e,n){var i=this,r=Bt(e),s=[];return t.each(r,function(t,e){var r,o,a=[],l=[];for(o=0;o<e.length;o++)(r=i.eventToRange(e[o]))&&(l.push(r),a.push(e[o]));if(kt(e[0]))for(l=i.invertRanges(l),o=0;o<l.length;o++)s.push.apply(s,i.eventRangeToSegs(l[o],e[0],n));else for(o=0;o<l.length;o++)s.push.apply(s,i.eventRangeToSegs(l[o],a[o],n))}),s},eventToRange:function(t){return this.refineRawEventRange(this.eventToRawRange(t))},refineRawEventRange:function(t){var e=this.view,n=e.calendar,i=z(t,e.activeRange);if(i)return n.localizeMoment(i.start),n.localizeMoment(i.end),i},eventToRawRange:function(t){var e=this.view.calendar;return{start:t.start.clone().stripZone(),end:(t.end?t.end.clone():e.getDefaultEventEnd(null!=t.allDay?t.allDay:!t.start.hasTime(),t.start)).stripZone()}},eventRangeToSegs:function(t,e,n){var i,r=this.eventRangeToSpans(t,e),s=[];for(i=0;i<r.length;i++)s.push.apply(s,this.eventSpanToSegs(r[i],e,n));return s},eventRangeToSpans:function(e,n){return[t.extend({},e)]},eventSpanToSegs:function(t,e,n){var i,r,s=n?n(t):this.spanToSegs(t);for(i=0;i<s.length;i++)r=s[i],t.isStart||(r.isStart=!1),t.isEnd||(r.isEnd=!1),r.event=e,r.eventStartMS=+t.start,r.eventDurationMS=t.end-t.start;return s},invertRanges:function(t){var e,n,i=this.view,r=i.activeRange.start.clone(),s=i.activeRange.end.clone(),o=[],a=r;for(t.sort(Lt),e=0;e<t.length;e++)n=t[e],n.start>a&&o.push({start:a,end:n.start}),n.end>a&&(a=n.end);return a<s&&o.push({start:a,end:s}),o},sortEventSegs:function(t){t.sort(mt(this,"compareEventSegs"))},compareEventSegs:function(t,e){return t.eventStartMS-e.eventStartMS||e.eventDurationMS-t.eventDurationMS||e.event.allDay-t.event.allDay||B(t.event,e.event,this.view.eventOrderSpecs)}}),Zt.pluckEventDateProps=xt,Zt.isBgEvent=It,Zt.dataAttrPrefix="";var Ee=Zt.DayTableMixin={breakOnWeeks:!1,dayDates:null,dayIndices:null,daysPerRow:null,rowCnt:null,colCnt:null,colHeadFormat:null,updateDayTable:function(){for(var t,e,n,i=this.view,r=this.start.clone(),s=-1,o=[],a=[];r.isBefore(this.end);)i.isHiddenDay(r)?o.push(s+.5):(s++,o.push(s),a.push(r.clone())),r.add(1,"days");if(this.breakOnWeeks){for(e=a[0].day(),t=1;t<a.length&&a[t].day()!=e;t++);n=Math.ceil(a.length/t)}else n=1,t=a.length;this.dayDates=a,this.dayIndices=o,this.daysPerRow=t,this.rowCnt=n,this.updateDayTableCols()},updateDayTableCols:function(){this.colCnt=this.computeColCnt(),this.colHeadFormat=this.view.opt("columnFormat")||this.computeColHeadFormat()},computeColCnt:function(){return this.daysPerRow},getCellDate:function(t,e){return this.dayDates[this.getCellDayIndex(t,e)].clone()},getCellRange:function(t,e){var n=this.getCellDate(t,e);return{start:n,end:n.clone().add(1,"days")}},getCellDayIndex:function(t,e){return t*this.daysPerRow+this.getColDayIndex(e)},getColDayIndex:function(t){return this.isRTL?this.colCnt-1-t:t},getDateDayIndex:function(t){var e=this.dayIndices,n=t.diff(this.start,"days");return n<0?e[0]-1:n>=e.length?e[e.length-1]+1:e[n]},computeColHeadFormat:function(){return this.rowCnt>1||this.colCnt>10?"ddd":this.colCnt>1?this.view.opt("dayOfMonthFormat"):"dddd"},sliceRangeByRow:function(t){var e,n,i,r,s,o=this.daysPerRow,a=this.view.computeDayRange(t),l=this.getDateDayIndex(a.start),u=this.getDateDayIndex(a.end.clone().subtract(1,"days")),h=[];for(e=0;e<this.rowCnt;e++)n=e*o,i=n+o-1,r=Math.max(l,n),s=Math.min(u,i),r=Math.ceil(r),s=Math.floor(s),r<=s&&h.push({row:e,firstRowDayIndex:r-n,lastRowDayIndex:s-n,isStart:r===l,isEnd:s===u});return h},sliceRangeByDay:function(t){var e,n,i,r,s,o,a=this.daysPerRow,l=this.view.computeDayRange(t),u=this.getDateDayIndex(l.start),h=this.getDateDayIndex(l.end.clone().subtract(1,"days")),c=[];for(e=0;e<this.rowCnt;e++)for(n=e*a,i=n+a-1,r=n;r<=i;r++)s=Math.max(u,r),o=Math.min(h,r),s=Math.ceil(s),o=Math.floor(o),s<=o&&c.push({row:e,firstRowDayIndex:s-n,lastRowDayIndex:o-n,isStart:s===u,isEnd:o===h});return c},renderHeadHtml:function(){return'<div class="fc-row '+this.view.widgetHeaderClass+'"><table><thead>'+this.renderHeadTrHtml()+"</thead></table></div>"},renderHeadIntroHtml:function(){return this.renderIntroHtml()},renderHeadTrHtml:function(){return"<tr>"+(this.isRTL?"":this.renderHeadIntroHtml())+this.renderHeadDateCellsHtml()+(this.isRTL?this.renderHeadIntroHtml():"")+"</tr>"},renderHeadDateCellsHtml:function(){var t,e,n=[];for(t=0;t<this.colCnt;t++)e=this.getCellDate(0,t),n.push(this.renderHeadDateCellHtml(e));return n.join("")},renderHeadDateCellHtml:function(t,e,n){var i=this.view,r=Z(t,i.activeRange),s=["fc-day-header",i.widgetHeaderClass],o=ht(t.format(this.colHeadFormat));return 1===this.rowCnt?s=s.concat(this.getDayClasses(t,!0)):s.push("fc-"+Kt[t.day()]),'<th class="'+s.join(" ")+'"'+(1===(r&&this.rowCnt)?' data-date="'+t.format("YYYY-MM-DD")+'"':"")+(e>1?' colspan="'+e+'"':"")+(n?" "+n:"")+">"+(r?i.buildGotoAnchorHtml({date:t,forceOff:this.rowCnt>1||1===this.colCnt},o):o)+"</th>"},renderBgTrHtml:function(t){return"<tr>"+(this.isRTL?"":this.renderBgIntroHtml(t))+this.renderBgCellsHtml(t)+(this.isRTL?this.renderBgIntroHtml(t):"")+"</tr>"},renderBgIntroHtml:function(t){return this.renderIntroHtml()},renderBgCellsHtml:function(t){var e,n,i=[];for(e=0;e<this.colCnt;e++)n=this.getCellDate(t,e),i.push(this.renderBgCellHtml(n));return i.join("")},renderBgCellHtml:function(t,e){var n=this.view,i=Z(t,n.activeRange),r=this.getDayClasses(t);return r.unshift("fc-day",n.widgetContentClass),'<td class="'+r.join(" ")+'"'+(i?' data-date="'+t.format("YYYY-MM-DD")+'"':"")+(e?" "+e:"")+"></td>"},renderIntroHtml:function(){},bookendCells:function(t){var e=this.renderIntroHtml();e&&(this.isRTL?t.append(e):t.prepend(e))}},De=Zt.DayGrid=be.extend(Ee,{numbersVisible:!1,bottomCoordPadding:0,rowEls:null,cellEls:null,helperEls:null,rowCoordCache:null,colCoordCache:null,renderDates:function(t){var e,n,i=this.view,r=this.rowCnt,s=this.colCnt,o="";for(e=0;e<r;e++)o+=this.renderDayRowHtml(e,t);for(this.el.html(o),this.rowEls=this.el.find(".fc-row"),this.cellEls=this.el.find(".fc-day, .fc-disabled-day"),this.rowCoordCache=new ve({els:this.rowEls,isVertical:!0}),this.colCoordCache=new ve({els:this.cellEls.slice(0,this.colCnt),isHorizontal:!0}),e=0;e<r;e++)for(n=0;n<s;n++)i.publiclyTrigger("dayRender",null,this.getCellDate(e,n),this.getCellEl(e,n))},unrenderDates:function(){this.removeSegPopover()},renderBusinessHours:function(){var t=this.buildBusinessHourSegs(!0);this.renderFill("businessHours",t,"bgevent")},unrenderBusinessHours:function(){this.unrenderFill("businessHours")},renderDayRowHtml:function(t,e){var n=this.view,i=["fc-row","fc-week",n.widgetContentClass];return e&&i.push("fc-rigid"),'<div class="'+i.join(" ")+'"><div class="fc-bg"><table>'+this.renderBgTrHtml(t)+'</table></div><div class="fc-content-skeleton"><table>'+(this.numbersVisible?"<thead>"+this.renderNumberTrHtml(t)+"</thead>":"")+"</table></div></div>"},renderNumberTrHtml:function(t){return"<tr>"+(this.isRTL?"":this.renderNumberIntroHtml(t))+this.renderNumberCellsHtml(t)+(this.isRTL?this.renderNumberIntroHtml(t):"")+"</tr>"},renderNumberIntroHtml:function(t){return this.renderIntroHtml()},renderNumberCellsHtml:function(t){var e,n,i=[];for(e=0;e<this.colCnt;e++)n=this.getCellDate(t,e),i.push(this.renderNumberCellHtml(n));return i.join("")},renderNumberCellHtml:function(t){var e,n,i=this.view,r="",s=Z(t,i.activeRange),o=i.dayNumbersVisible&&s;return o||i.cellWeekNumbersVisible?(e=this.getDayClasses(t),e.unshift("fc-day-top"),i.cellWeekNumbersVisible&&(n="ISO"===t._locale._fullCalendar_weekCalc?1:t._locale.firstDayOfWeek()),r+='<td class="'+e.join(" ")+'"'+(s?' data-date="'+t.format()+'"':"")+">",i.cellWeekNumbersVisible&&t.day()==n&&(r+=i.buildGotoAnchorHtml({date:t,type:"week"},{class:"fc-week-number"},t.format("w"))),o&&(r+=i.buildGotoAnchorHtml(t,{class:"fc-day-number"},t.date())),r+="</td>"):"<td/>"},computeEventTimeFormat:function(){return this.view.opt("extraSmallTimeFormat")},computeDisplayEventEnd:function(){return 1==this.colCnt},rangeUpdated:function(){this.updateDayTable()},spanToSegs:function(t){var e,n,i=this.sliceRangeByRow(t);for(e=0;e<i.length;e++)n=i[e],this.isRTL?(n.leftCol=this.daysPerRow-1-n.lastRowDayIndex,n.rightCol=this.daysPerRow-1-n.firstRowDayIndex):(n.leftCol=n.firstRowDayIndex,n.rightCol=n.lastRowDayIndex);return i},prepareHits:function(){this.colCoordCache.build(),this.rowCoordCache.build(),this.rowCoordCache.bottoms[this.rowCnt-1]+=this.bottomCoordPadding},releaseHits:function(){this.colCoordCache.clear(),this.rowCoordCache.clear()},queryHit:function(t,e){if(this.colCoordCache.isLeftInBounds(t)&&this.rowCoordCache.isTopInBounds(e)){var n=this.colCoordCache.getHorizontalIndex(t),i=this.rowCoordCache.getVerticalIndex(e);if(null!=i&&null!=n)return this.getCellHit(i,n)}},getHitSpan:function(t){return this.getCellRange(t.row,t.col)},getHitEl:function(t){return this.getCellEl(t.row,t.col)},getCellHit:function(t,e){return{row:t,col:e,component:this,left:this.colCoordCache.getLeftOffset(e),right:this.colCoordCache.getRightOffset(e),top:this.rowCoordCache.getTopOffset(t),bottom:this.rowCoordCache.getBottomOffset(t)}},getCellEl:function(t,e){return this.cellEls.eq(t*this.colCnt+e)},renderDrag:function(t,e){var n,i=this.eventToSpans(t);for(n=0;n<i.length;n++)this.renderHighlight(i[n]);if(e&&e.component!==this)return this.renderEventLocationHelper(t,e)},unrenderDrag:function(){this.unrenderHighlight(),this.unrenderHelper()},renderEventResize:function(t,e){var n,i=this.eventToSpans(t);for(n=0;n<i.length;n++)this.renderHighlight(i[n]);return this.renderEventLocationHelper(t,e)},unrenderEventResize:function(){this.unrenderHighlight(),this.unrenderHelper()},renderHelper:function(e,n){var i,r=[],s=this.eventToSegs(e);return s=this.renderFgSegEls(s),i=this.renderSegRows(s),this.rowEls.each(function(e,s){var o,a=t(s),l=t('<div class="fc-helper-skeleton"><table/></div>');o=n&&n.row===e?n.el.position().top:a.find(".fc-content-skeleton tbody").position().top,l.css("top",o).find("table").append(i[e].tbodyEl),a.append(l),r.push(l[0])}),this.helperEls=t(r)},unrenderHelper:function(){this.helperEls&&(this.helperEls.remove(),this.helperEls=null)},fillSegTag:"td",renderFill:function(e,n,i){var r,s,o,a=[];for(n=this.renderFillSegEls(e,n),r=0;r<n.length;r++)s=n[r],o=this.renderFillRow(e,s,i),this.rowEls.eq(s.row).append(o),a.push(o[0]);return this.elsByFill[e]=t(a),n},renderFillRow:function(e,n,i){var r,s,o=this.colCnt,a=n.leftCol,l=n.rightCol+1;return i=i||e.toLowerCase(),r=t('<div class="fc-'+i+'-skeleton"><table><tr/></table></div>'),s=r.find("tr"),a>0&&s.append('<td colspan="'+a+'"/>'),s.append(n.el.attr("colspan",l-a)),l<o&&s.append('<td colspan="'+(o-l)+'"/>'),this.bookendCells(s),r}});De.mixin({rowStructs:null,unrenderEvents:function(){this.removeSegPopover(),be.prototype.unrenderEvents.apply(this,arguments)},getEventSegs:function(){return be.prototype.getEventSegs.call(this).concat(this.popoverSegs||[])},renderBgSegs:function(e){var n=t.grep(e,function(t){return t.event.allDay});return be.prototype.renderBgSegs.call(this,n)},renderFgSegs:function(e){var n;return e=this.renderFgSegEls(e),n=this.rowStructs=this.renderSegRows(e),this.rowEls.each(function(e,i){t(i).find(".fc-content-skeleton > table").append(n[e].tbodyEl)}),e},unrenderFgSegs:function(){for(var t,e=this.rowStructs||[];t=e.pop();)t.tbodyEl.remove();this.rowStructs=null},renderSegRows:function(t){var e,n,i=[];for(e=this.groupSegRows(t),n=0;n<e.length;n++)i.push(this.renderSegRow(n,e[n]));return i},fgSegHtml:function(t,e){var n,i,r=this.view,s=t.event,o=r.isEventDraggable(s),a=!e&&s.allDay&&t.isStart&&r.isEventResizableFromStart(s),l=!e&&s.allDay&&t.isEnd&&r.isEventResizableFromEnd(s),u=this.getSegClasses(t,o,a||l),h=dt(this.getSegSkinCss(t)),c="";return u.unshift("fc-day-grid-event","fc-h-event"),t.isStart&&(n=this.getEventTimeText(s))&&(c='<span class="fc-time">'+ht(n)+"</span>"),i='<span class="fc-title">'+(ht(s.title||"")||" ")+"</span>",'<a class="'+u.join(" ")+'"'+(s.url?' href="'+ht(s.url)+'"':"")+(h?' style="'+h+'"':"")+'><div class="fc-content">'+(this.isRTL?i+" "+c:c+" "+i)+"</div>"+(a?'<div class="fc-resizer fc-start-resizer" />':"")+(l?'<div class="fc-resizer fc-end-resizer" />':"")+"</a>"},renderSegRow:function(e,n){function i(e){for(;o<e;)h=(m[r-1]||[])[o],h?h.attr("rowspan",parseInt(h.attr("rowspan")||1,10)+1):(h=t("<td/>"),a.append(h)),v[r][o]=h,m[r][o]=h,o++}var r,s,o,a,l,u,h,c=this.colCnt,d=this.buildSegLevels(n),f=Math.max(1,d.length),g=t("<tbody/>"),p=[],v=[],m=[];for(r=0;r<f;r++){if(s=d[r],o=0,a=t("<tr/>"),p.push([]),v.push([]),m.push([]),s)for(l=0;l<s.length;l++){for(u=s[l],i(u.leftCol),h=t('<td class="fc-event-container"/>').append(u.el),u.leftCol!=u.rightCol?h.attr("colspan",u.rightCol-u.leftCol+1):m[r][o]=h;o<=u.rightCol;)v[r][o]=h,p[r][o]=u,o++;a.append(h)}i(c),this.bookendCells(a),g.append(a)}return{row:e,tbodyEl:g,cellMatrix:v,segMatrix:p,segLevels:d,segs:n}},buildSegLevels:function(t){var e,n,i,r=[];for(this.sortEventSegs(t),e=0;e<t.length;e++){for(n=t[e],i=0;i<r.length&&zt(n,r[i]);i++);n.level=i,(r[i]||(r[i]=[])).push(n)}for(i=0;i<r.length;i++)r[i].sort(Ft);return r},groupSegRows:function(t){var e,n=[];for(e=0;e<this.rowCnt;e++)n.push([]);for(e=0;e<t.length;e++)n[t[e].row].push(t[e]);return n}}),De.mixin({segPopover:null,popoverSegs:null,removeSegPopover:function(){this.segPopover&&this.segPopover.hide()},limitRows:function(t){var e,n,i=this.rowStructs||[];for(e=0;e<i.length;e++)this.unlimitRow(e),!1!==(n=!!t&&("number"==typeof t?t:this.computeRowLevelLimit(e)))&&this.limitRow(e,n)},computeRowLevelLimit:function(e){function n(e,n){s=Math.max(s,t(n).outerHeight())}var i,r,s,o=this.rowEls.eq(e),a=o.height(),l=this.rowStructs[e].tbodyEl.children();for(i=0;i<l.length;i++)if(r=l.eq(i).removeClass("fc-limited"),s=0,r.find("> td > :first-child").each(n),r.position().top+s>a)return i;return!1},limitRow:function(e,n){function i(i){for(;E<i;)u=w.getCellSegs(e,E,n),u.length&&(d=s[n-1][E],y=w.renderMoreLink(e,E,u),m=t("<div/>").append(y),d.append(m),b.push(m[0])),E++}var r,s,o,a,l,u,h,c,d,f,g,p,v,m,y,w=this,S=this.rowStructs[e],b=[],E=0;if(n&&n<S.segLevels.length){for(r=S.segLevels[n-1],s=S.cellMatrix,o=S.tbodyEl.children().slice(n).addClass("fc-limited").get(),a=0;a<r.length;a++){for(l=r[a],i(l.leftCol),c=[],h=0;E<=l.rightCol;)u=this.getCellSegs(e,E,n),c.push(u),h+=u.length,E++;if(h){for(d=s[n-1][l.leftCol],f=d.attr("rowspan")||1,g=[],p=0;p<c.length;p++)v=t('<td class="fc-more-cell"/>').attr("rowspan",f),u=c[p],y=this.renderMoreLink(e,l.leftCol+p,[l].concat(u)),m=t("<div/>").append(y),v.append(m),g.push(v[0]),b.push(v[0]);d.addClass("fc-limited").after(t(g)),o.push(d[0])}}i(this.colCnt),S.moreEls=t(b),S.limitedEls=t(o)}},unlimitRow:function(t){var e=this.rowStructs[t];e.moreEls&&(e.moreEls.remove(),e.moreEls=null),e.limitedEls&&(e.limitedEls.removeClass("fc-limited"),e.limitedEls=null)},renderMoreLink:function(e,n,i){var r=this,s=this.view;return t('<a class="fc-more"/>').text(this.getMoreLinkText(i.length)).on("click",function(o){var a=s.opt("eventLimitClick"),l=r.getCellDate(e,n),u=t(this),h=r.getCellEl(e,n),c=r.getCellSegs(e,n),d=r.resliceDaySegs(c,l),f=r.resliceDaySegs(i,l);"function"==typeof a&&(a=s.publiclyTrigger("eventLimitClick",null,{date:l,dayEl:h,moreEl:u,segs:d,hiddenSegs:f},o)),"popover"===a?r.showSegPopover(e,n,u,d):"string"==typeof a&&s.calendar.zoomTo(l,a)})},showSegPopover:function(t,e,n,i){var r,s,o=this,a=this.view,l=n.parent();r=1==this.rowCnt?a.el:this.rowEls.eq(t),s={className:"fc-more-popover",content:this.renderSegPopoverContent(t,e,i),parentEl:this.view.el,top:r.offset().top,autoHide:!0,viewportConstrain:a.opt("popoverViewportConstrain"),hide:function(){if(o.popoverSegs)for(var t,e=0;e<o.popoverSegs.length;++e)t=o.popoverSegs[e],a.publiclyTrigger("eventDestroy",t.event,t.event,t.el);o.segPopover.removeElement(),o.segPopover=null,o.popoverSegs=null}},this.isRTL?s.right=l.offset().left+l.outerWidth()+1:s.left=l.offset().left-1,this.segPopover=new pe(s),this.segPopover.show(),this.bindSegHandlersToEl(this.segPopover.el)},renderSegPopoverContent:function(e,n,i){var r,s=this.view,o=s.opt("theme"),a=this.getCellDate(e,n).format(s.opt("dayPopoverFormat")),l=t('<div class="fc-header '+s.widgetHeaderClass+'"><span class="fc-close '+(o?"ui-icon ui-icon-closethick":"fc-icon fc-icon-x")+'"></span><span class="fc-title">'+ht(a)+'</span><div class="fc-clear"/></div><div class="fc-body '+s.widgetContentClass+'"><div class="fc-event-container"></div></div>'),u=l.find(".fc-event-container");for(i=this.renderFgSegEls(i,!0),this.popoverSegs=i,r=0;r<i.length;r++)this.hitsNeeded(),i[r].hit=this.getCellHit(e,n),this.hitsNotNeeded(),u.append(i[r].el);return l},resliceDaySegs:function(e,n){var i=t.map(e,function(t){return t.event}),r=n.clone(),s=r.clone().add(1,"days"),o={start:r,end:s};return e=this.eventsToSegs(i,function(t){var e=z(t,o);return e?[e]:[]}),this.sortEventSegs(e),e},getMoreLinkText:function(t){var e=this.view.opt("eventLimitText");return"function"==typeof e?e(t):"+"+t+" "+e},getCellSegs:function(t,e,n){for(var i,r=this.rowStructs[t].segMatrix,s=n||0,o=[];s<r.length;)i=r[s][e],i&&o.push(i),s++;return o}});var Te=Zt.TimeGrid=be.extend(Ee,{slotDuration:null,snapDuration:null,snapsPerSlot:null,labelFormat:null,labelInterval:null,colEls:null,slatContainerEl:null,slatEls:null,nowIndicatorEls:null,colCoordCache:null,slatCoordCache:null,constructor:function(){be.apply(this,arguments),this.processOptions()},renderDates:function(){this.el.html(this.renderHtml()),this.colEls=this.el.find(".fc-day, .fc-disabled-day"),this.slatContainerEl=this.el.find(".fc-slats"),this.slatEls=this.slatContainerEl.find("tr"),this.colCoordCache=new ve({els:this.colEls,isHorizontal:!0}),this.slatCoordCache=new ve({els:this.slatEls,isVertical:!0}),this.renderContentSkeleton()},renderHtml:function(){return'<div class="fc-bg"><table>'+this.renderBgTrHtml(0)+'</table></div><div class="fc-slats"><table>'+this.renderSlatRowHtml()+"</table></div>"},renderSlatRowHtml:function(){for(var t,n,i,r=this.view,s=this.isRTL,o="",a=e.duration(+this.view.minTime);a<this.view.maxTime;)t=this.start.clone().time(a),n=vt(W(a,this.labelInterval)),i='<td class="fc-axis fc-time '+r.widgetContentClass+'" '+r.axisStyleAttr()+">"+(n?"<span>"+ht(t.format(this.labelFormat))+"</span>":"")+"</td>",o+='<tr data-time="'+t.format("HH:mm:ss")+'"'+(n?"":' class="fc-minor"')+">"+(s?"":i)+'<td class="'+r.widgetContentClass+'"/>'+(s?i:"")+"</tr>",a.add(this.slotDuration);return o},processOptions:function(){var n,i=this.view,r=i.opt("slotDuration"),s=i.opt("snapDuration");r=e.duration(r),s=s?e.duration(s):r,this.slotDuration=r,this.snapDuration=s,this.snapsPerSlot=r/s,this.minResizeDuration=s,n=i.opt("slotLabelFormat"),t.isArray(n)&&(n=n[n.length-1]),this.labelFormat=n||i.opt("smallTimeFormat"),n=i.opt("slotLabelInterval"),this.labelInterval=n?e.duration(n):this.computeLabelInterval(r)},computeLabelInterval:function(t){var n,i,r;for(n=_e.length-1;n>=0;n--)if(i=e.duration(_e[n]),r=W(i,t),vt(r)&&r>1)return i;return e.duration(t)},computeEventTimeFormat:function(){return this.view.opt("noMeridiemTimeFormat")},computeDisplayEventEnd:function(){return!0},prepareHits:function(){this.colCoordCache.build(),this.slatCoordCache.build()},releaseHits:function(){this.colCoordCache.clear()},queryHit:function(t,e){var n=this.snapsPerSlot,i=this.colCoordCache,r=this.slatCoordCache;if(i.isLeftInBounds(t)&&r.isTopInBounds(e)){var s=i.getHorizontalIndex(t),o=r.getVerticalIndex(e);if(null!=s&&null!=o){var a=r.getTopOffset(o),l=r.getHeight(o),u=(e-a)/l,h=Math.floor(u*n),c=o*n+h,d=a+h/n*l,f=a+(h+1)/n*l;return{col:s,snap:c,component:this,left:i.getLeftOffset(s),right:i.getRightOffset(s),top:d,bottom:f}}}},getHitSpan:function(t){var e,n=this.getCellDate(0,t.col),i=this.computeSnapTime(t.snap);return n.time(i),e=n.clone().add(this.snapDuration),{start:n,end:e}},getHitEl:function(t){return this.colEls.eq(t.col)},rangeUpdated:function(){this.updateDayTable()},computeSnapTime:function(t){return e.duration(this.view.minTime+this.snapDuration*t)},spanToSegs:function(t){var e,n=this.sliceRangeByTimes(t);for(e=0;e<n.length;e++)this.isRTL?n[e].col=this.daysPerRow-1-n[e].dayIndex:n[e].col=n[e].dayIndex;return n},sliceRangeByTimes:function(t){var e,n,i,r,s=[];for(n=0;n<this.daysPerRow;n++)i=this.dayDates[n].clone().time(0),r={start:i.clone().add(this.view.minTime),end:i.clone().add(this.view.maxTime)},(e=z(t,r))&&(e.dayIndex=n,s.push(e));return s},updateSize:function(t){this.slatCoordCache.build(),t&&this.updateSegVerticals([].concat(this.fgSegs||[],this.bgSegs||[],this.businessSegs||[]))},getTotalSlatHeight:function(){return this.slatContainerEl.outerHeight()},computeDateTop:function(t,n){return this.computeTimeTop(e.duration(t-n.clone().stripTime()))},computeTimeTop:function(t){var e,n,i=this.slatEls.length,r=(t-this.view.minTime)/this.slotDuration;return r=Math.max(0,r),r=Math.min(i,r),e=Math.floor(r),e=Math.min(e,i-1),n=r-e,this.slatCoordCache.getTopPosition(e)+this.slatCoordCache.getHeight(e)*n},renderDrag:function(t,e){var n,i;if(e)return this.renderEventLocationHelper(t,e);for(n=this.eventToSpans(t),i=0;i<n.length;i++)this.renderHighlight(n[i])},unrenderDrag:function(){this.unrenderHelper(),this.unrenderHighlight()},renderEventResize:function(t,e){return this.renderEventLocationHelper(t,e)},unrenderEventResize:function(){this.unrenderHelper()},renderHelper:function(t,e){return this.renderHelperSegs(this.eventToSegs(t),e)},unrenderHelper:function(){this.unrenderHelperSegs()},renderBusinessHours:function(){this.renderBusinessSegs(this.buildBusinessHourSegs())},unrenderBusinessHours:function(){this.unrenderBusinessSegs()},getNowIndicatorUnit:function(){return"minute"},renderNowIndicator:function(e){var n,i=this.spanToSegs({start:e,end:e}),r=this.computeDateTop(e,e),s=[];for(n=0;n<i.length;n++)s.push(t('<div class="fc-now-indicator fc-now-indicator-line"></div>').css("top",r).appendTo(this.colContainerEls.eq(i[n].col))[0]);i.length>0&&s.push(t('<div class="fc-now-indicator fc-now-indicator-arrow"></div>').css("top",r).appendTo(this.el.find(".fc-content-skeleton"))[0]),this.nowIndicatorEls=t(s)},unrenderNowIndicator:function(){this.nowIndicatorEls&&(this.nowIndicatorEls.remove(),this.nowIndicatorEls=null)},renderSelection:function(t){this.view.opt("selectHelper")?this.renderEventLocationHelper(t):this.renderHighlight(t)},unrenderSelection:function(){this.unrenderHelper(),this.unrenderHighlight()},renderHighlight:function(t){this.renderHighlightSegs(this.spanToSegs(t))},unrenderHighlight:function(){this.unrenderHighlightSegs()}});Te.mixin({colContainerEls:null,fgContainerEls:null,bgContainerEls:null,helperContainerEls:null,highlightContainerEls:null,businessContainerEls:null,fgSegs:null,bgSegs:null,helperSegs:null,highlightSegs:null,businessSegs:null,renderContentSkeleton:function(){var e,n,i="";for(e=0;e<this.colCnt;e++)i+='<td><div class="fc-content-col"><div class="fc-event-container fc-helper-container"></div><div class="fc-event-container"></div><div class="fc-highlight-container"></div><div class="fc-bgevent-container"></div><div class="fc-business-container"></div></div></td>';n=t('<div class="fc-content-skeleton"><table><tr>'+i+"</tr></table></div>"),this.colContainerEls=n.find(".fc-content-col"),this.helperContainerEls=n.find(".fc-helper-container"),this.fgContainerEls=n.find(".fc-event-container:not(.fc-helper-container)"),this.bgContainerEls=n.find(".fc-bgevent-container"),this.highlightContainerEls=n.find(".fc-highlight-container"),this.businessContainerEls=n.find(".fc-business-container"),this.bookendCells(n.find("tr")),this.el.append(n)},renderFgSegs:function(t){return t=this.renderFgSegsIntoContainers(t,this.fgContainerEls),this.fgSegs=t,t},unrenderFgSegs:function(){this.unrenderNamedSegs("fgSegs")},renderHelperSegs:function(e,n){var i,r,s,o=[];for(e=this.renderFgSegsIntoContainers(e,this.helperContainerEls),i=0;i<e.length;i++)r=e[i],n&&n.col===r.col&&(s=n.el,r.el.css({left:s.css("left"),right:s.css("right"),"margin-left":s.css("margin-left"),"margin-right":s.css("margin-right")})),o.push(r.el[0]);return this.helperSegs=e,t(o)},unrenderHelperSegs:function(){this.unrenderNamedSegs("helperSegs")},renderBgSegs:function(t){return t=this.renderFillSegEls("bgEvent",t),this.updateSegVerticals(t),this.attachSegsByCol(this.groupSegsByCol(t),this.bgContainerEls),this.bgSegs=t,t},unrenderBgSegs:function(){this.unrenderNamedSegs("bgSegs")},renderHighlightSegs:function(t){t=this.renderFillSegEls("highlight",t),this.updateSegVerticals(t),this.attachSegsByCol(this.groupSegsByCol(t),this.highlightContainerEls),this.highlightSegs=t},unrenderHighlightSegs:function(){this.unrenderNamedSegs("highlightSegs")},renderBusinessSegs:function(t){t=this.renderFillSegEls("businessHours",t),this.updateSegVerticals(t),this.attachSegsByCol(this.groupSegsByCol(t),this.businessContainerEls),this.businessSegs=t},unrenderBusinessSegs:function(){this.unrenderNamedSegs("businessSegs")},groupSegsByCol:function(t){var e,n=[];for(e=0;e<this.colCnt;e++)n.push([]);for(e=0;e<t.length;e++)n[t[e].col].push(t[e]);return n},attachSegsByCol:function(t,e){var n,i,r;for(n=0;n<this.colCnt;n++)for(i=t[n],r=0;r<i.length;r++)e.eq(n).append(i[r].el)},unrenderNamedSegs:function(t){var e,n=this[t];if(n){for(e=0;e<n.length;e++)n[e].el.remove();this[t]=null}},renderFgSegsIntoContainers:function(t,e){var n,i;for(t=this.renderFgSegEls(t),n=this.groupSegsByCol(t),i=0;i<this.colCnt;i++)this.updateFgSegCoords(n[i]);return this.attachSegsByCol(n,e),t},fgSegHtml:function(t,e){var n,i,r,s=this.view,o=t.event,a=s.isEventDraggable(o),l=!e&&t.isStart&&s.isEventResizableFromStart(o),u=!e&&t.isEnd&&s.isEventResizableFromEnd(o),h=this.getSegClasses(t,a,l||u),c=dt(this.getSegSkinCss(t));return h.unshift("fc-time-grid-event","fc-v-event"),s.isMultiDayEvent(o)?(t.isStart||t.isEnd)&&(n=this.getEventTimeText(t),i=this.getEventTimeText(t,"LT"),r=this.getEventTimeText(t,null,!1)):(n=this.getEventTimeText(o),i=this.getEventTimeText(o,"LT"),r=this.getEventTimeText(o,null,!1)),'<a class="'+h.join(" ")+'"'+(o.url?' href="'+ht(o.url)+'"':"")+(c?' style="'+c+'"':"")+'><div class="fc-content">'+(n?'<div class="fc-time" data-start="'+ht(r)+'" data-full="'+ht(i)+'"><span>'+ht(n)+"</span></div>":"")+(o.title?'<div class="fc-title">'+ht(o.title)+"</div>":"")+'</div><div class="fc-bg"/>'+(u?'<div class="fc-resizer fc-end-resizer" />':"")+"</a>"},updateSegVerticals:function(t){this.computeSegVerticals(t),this.assignSegVerticals(t)},computeSegVerticals:function(t){var e,n,i;for(e=0;e<t.length;e++)n=t[e],i=this.dayDates[n.dayIndex],n.top=this.computeDateTop(n.start,i),n.bottom=this.computeDateTop(n.end,i)},assignSegVerticals:function(t){var e,n;for(e=0;e<t.length;e++)n=t[e],n.el.css(this.generateSegVerticalCss(n))},generateSegVerticalCss:function(t){return{top:t.top,bottom:-t.bottom}},updateFgSegCoords:function(t){this.computeSegVerticals(t),this.computeFgSegHorizontals(t),this.assignSegVerticals(t),this.assignFgSegHorizontals(t)},computeFgSegHorizontals:function(t){var e,n,i;if(this.sortEventSegs(t),e=At(t),Gt(e),n=e[0]){for(i=0;i<n.length;i++)Vt(n[i]);for(i=0;i<n.length;i++)this.computeFgSegForwardBack(n[i],0,0)}},computeFgSegForwardBack:function(t,e,n){var i,r=t.forwardSegs;if(void 0===t.forwardCoord)for(r.length?(this.sortForwardSegs(r),this.computeFgSegForwardBack(r[0],e+1,n),t.forwardCoord=r[0].backwardCoord):t.forwardCoord=1,t.backwardCoord=t.forwardCoord-(t.forwardCoord-n)/(e+1),i=0;i<r.length;i++)this.computeFgSegForwardBack(r[i],0,t.forwardCoord)},sortForwardSegs:function(t){t.sort(mt(this,"compareForwardSegs"))},compareForwardSegs:function(t,e){return e.forwardPressure-t.forwardPressure||(t.backwardCoord||0)-(e.backwardCoord||0)||this.compareEventSegs(t,e)},assignFgSegHorizontals:function(t){var e,n;for(e=0;e<t.length;e++)n=t[e],n.el.css(this.generateFgSegHorizontalCss(n)),n.bottom-n.top<30&&n.el.addClass("fc-short")},generateFgSegHorizontalCss:function(t){var e,n,i=this.view.opt("slotEventOverlap"),r=t.backwardCoord,s=t.forwardCoord,o=this.generateSegVerticalCss(t);return i&&(s=Math.min(1,r+2*(s-r))),this.isRTL?(e=1-s,n=r):(e=r,n=1-s),o.zIndex=t.level+1,o.left=100*e+"%",o.right=100*n+"%",i&&t.forwardPressure&&(o[this.isRTL?"marginLeft":"marginRight"]=20),o}});var Ce=Zt.View=ue.extend({type:null,name:null,title:null,calendar:null,viewSpec:null,options:null,el:null,renderQueue:null,batchRenderDepth:0,isDatesRendered:!1,isEventsRendered:!1,isBaseRendered:!1,queuedScroll:null,isRTL:!1,isSelected:!1,selectedEvent:null,eventOrderSpecs:null,widgetHeaderClass:null,widgetContentClass:null,highlightStateClass:null,nextDayThreshold:null,isHiddenDayHash:null,isNowIndicatorRendered:null,initialNowDate:null,initialNowQueriedMs:null,nowIndicatorTimeoutID:null,nowIndicatorIntervalID:null,constructor:function(t,n){ue.prototype.constructor.call(this),this.calendar=t,this.viewSpec=n,this.type=n.type,this.options=n.options,this.name=this.type,this.nextDayThreshold=e.duration(this.opt("nextDayThreshold")),this.initThemingProps(),this.initHiddenDays(),this.isRTL=this.opt("isRTL"),this.eventOrderSpecs=M(this.opt("eventOrder")),this.renderQueue=this.buildRenderQueue(),this.initAutoBatchRender(),this.initialize()},buildRenderQueue:function(){var t=this,e=new de({event:this.opt("eventRenderWait")});return e.on("start",function(){t.freezeHeight(),t.addScroll(t.queryScroll())}),e.on("stop",function(){t.thawHeight(),t.popScroll()}),e},initAutoBatchRender:function(){var t=this;this.on("before:change",function(){t.startBatchRender()}),this.on("change",function(){t.stopBatchRender()})},startBatchRender:function(){this.batchRenderDepth++||this.renderQueue.pause()},stopBatchRender:function(){--this.batchRenderDepth||this.renderQueue.resume()},initialize:function(){},opt:function(t){return this.options[t]},publiclyTrigger:function(t,e){var n=this.calendar;return n.publiclyTrigger.apply(n,[t,e||this].concat(Array.prototype.slice.call(arguments,2),[this]))},updateTitle:function(){this.title=this.computeTitle(),this.calendar.setToolbarsTitle(this.title)},computeTitle:function(){var t;return t=/^(year|month)$/.test(this.currentRangeUnit)?this.currentRange:this.activeRange,this.formatRange({start:this.calendar.applyTimezone(t.start),end:this.calendar.applyTimezone(t.end)},this.opt("titleFormat")||this.computeTitleFormat(),this.opt("titleRangeSeparator"))},computeTitleFormat:function(){return"year"==this.currentRangeUnit?"YYYY":"month"==this.currentRangeUnit?this.opt("monthYearFormat"):this.currentRangeAs("days")>1?"ll":"LL"},formatRange:function(t,e,n){var i=t.end;return i.hasTime()||(i=i.clone().subtract(1)),ae(t.start,i,e,n,this.opt("isRTL"))},getAllDayHtml:function(){return this.opt("allDayHtml")||ht(this.opt("allDayText"))},buildGotoAnchorHtml:function(e,n,i){var r,s,o,a;return t.isPlainObject(e)?(r=e.date,s=e.type,o=e.forceOff):r=e,r=Zt.moment(r),a={date:r.format("YYYY-MM-DD"),type:s||"day"},"string"==typeof n&&(i=n,n=null),n=n?" "+ft(n):"",i=i||"",!o&&this.opt("navLinks")?"<a"+n+' data-goto="'+ht(JSON.stringify(a))+'">'+i+"</a>":"<span"+n+">"+i+"</span>"},setElement:function(t){this.el=t,this.bindGlobalHandlers(),this.bindBaseRenderHandlers(),this.renderSkeleton()}, | |
9 | -removeElement:function(){this.unsetDate(),this.unrenderSkeleton(),this.unbindGlobalHandlers(),this.unbindBaseRenderHandlers(),this.el.remove()},renderSkeleton:function(){},unrenderSkeleton:function(){},setDate:function(t){var e=this.get("dateProfile"),n=this.buildDateProfile(t,null,!0);return e&&X(e.activeRange,n.activeRange)||this.set("dateProfile",n),n.date},unsetDate:function(){this.unset("dateProfile")},requestDateRender:function(t){var e=this;this.renderQueue.queue(function(){e.executeDateRender(t)},"date","init")},requestDateUnrender:function(){var t=this;this.renderQueue.queue(function(){t.executeDateUnrender()},"date","destroy")},fetchInitialEvents:function(t){return this.calendar.requestEvents(t.activeRange.start,t.activeRange.end)},bindEventChanges:function(){this.listenTo(this.calendar,"eventsReset",this.resetEvents)},unbindEventChanges:function(){this.stopListeningTo(this.calendar,"eventsReset")},setEvents:function(t){this.set("currentEvents",t),this.set("hasEvents",!0)},unsetEvents:function(){this.unset("currentEvents"),this.unset("hasEvents")},resetEvents:function(t){this.startBatchRender(),this.unsetEvents(),this.setEvents(t),this.stopBatchRender()},requestEventsRender:function(t){var e=this;this.renderQueue.queue(function(){e.executeEventsRender(t)},"event","init")},requestEventsUnrender:function(){var t=this;this.renderQueue.queue(function(){t.executeEventsUnrender()},"event","destroy")},executeDateRender:function(t,e){this.setDateProfileForRendering(t),this.updateTitle(),this.calendar.updateToolbarButtons(),this.render&&this.render(),this.renderDates(),this.updateSize(),this.renderBusinessHours(),this.startNowIndicator(),e||this.addScroll(this.computeInitialDateScroll()),this.isDatesRendered=!0,this.trigger("datesRendered")},executeDateUnrender:function(){this.unselect(),this.stopNowIndicator(),this.trigger("before:datesUnrendered"),this.unrenderBusinessHours(),this.unrenderDates(),this.destroy&&this.destroy(),this.isDatesRendered=!1},renderDates:function(){},unrenderDates:function(){},bindBaseRenderHandlers:function(){var t=this;this.on("datesRendered.baseHandler",function(){t.onBaseRender()}),this.on("before:datesUnrendered.baseHandler",function(){t.onBeforeBaseUnrender()})},unbindBaseRenderHandlers:function(){this.off(".baseHandler")},onBaseRender:function(){this.applyScreenState(),this.publiclyTrigger("viewRender",this,this,this.el)},onBeforeBaseUnrender:function(){this.applyScreenState(),this.publiclyTrigger("viewDestroy",this,this,this.el)},bindGlobalHandlers:function(){this.listenTo(we.get(),{touchstart:this.processUnselect,mousedown:this.handleDocumentMousedown})},unbindGlobalHandlers:function(){this.stopListeningTo(we.get())},initThemingProps:function(){var t=this.opt("theme")?"ui":"fc";this.widgetHeaderClass=t+"-widget-header",this.widgetContentClass=t+"-widget-content",this.highlightStateClass=t+"-state-highlight"},renderBusinessHours:function(){},unrenderBusinessHours:function(){},startNowIndicator:function(){var t,n,i,r=this;this.opt("nowIndicator")&&(t=this.getNowIndicatorUnit())&&(n=mt(this,"updateNowIndicator"),this.initialNowDate=this.calendar.getNow(),this.initialNowQueriedMs=+new Date,this.renderNowIndicator(this.initialNowDate),this.isNowIndicatorRendered=!0,i=this.initialNowDate.clone().startOf(t).add(1,t)-this.initialNowDate,this.nowIndicatorTimeoutID=setTimeout(function(){r.nowIndicatorTimeoutID=null,n(),i=+e.duration(1,t),i=Math.max(100,i),r.nowIndicatorIntervalID=setInterval(n,i)},i))},updateNowIndicator:function(){this.isNowIndicatorRendered&&(this.unrenderNowIndicator(),this.renderNowIndicator(this.initialNowDate.clone().add(new Date-this.initialNowQueriedMs)))},stopNowIndicator:function(){this.isNowIndicatorRendered&&(this.nowIndicatorTimeoutID&&(clearTimeout(this.nowIndicatorTimeoutID),this.nowIndicatorTimeoutID=null),this.nowIndicatorIntervalID&&(clearTimeout(this.nowIndicatorIntervalID),this.nowIndicatorIntervalID=null),this.unrenderNowIndicator(),this.isNowIndicatorRendered=!1)},getNowIndicatorUnit:function(){},renderNowIndicator:function(t){},unrenderNowIndicator:function(){},updateSize:function(t){var e;t&&(e=this.queryScroll()),this.updateHeight(t),this.updateWidth(t),this.updateNowIndicator(),t&&this.applyScroll(e)},updateWidth:function(t){},updateHeight:function(t){var e=this.calendar;this.setHeight(e.getSuggestedViewHeight(),e.isHeightAuto())},setHeight:function(t,e){},addForcedScroll:function(e){this.addScroll(t.extend(e,{isForced:!0}))},addScroll:function(e){var n=this.queuedScroll||(this.queuedScroll={});n.isForced||t.extend(n,e)},popScroll:function(){this.applyQueuedScroll(),this.queuedScroll=null},applyQueuedScroll:function(){this.queuedScroll&&this.applyScroll(this.queuedScroll)},queryScroll:function(){var e={};return this.isDatesRendered&&t.extend(e,this.queryDateScroll()),e},applyScroll:function(t){this.isDatesRendered&&this.applyDateScroll(t)},computeInitialDateScroll:function(){return{}},queryDateScroll:function(){return{}},applyDateScroll:function(t){},freezeHeight:function(){this.calendar.freezeContentHeight()},thawHeight:function(){this.calendar.thawContentHeight()},executeEventsRender:function(t){this.renderEvents(t),this.isEventsRendered=!0,this.onEventsRender()},executeEventsUnrender:function(){this.onBeforeEventsUnrender(),this.destroyEvents&&this.destroyEvents(),this.unrenderEvents(),this.isEventsRendered=!1},onEventsRender:function(){this.applyScreenState(),this.renderedEventSegEach(function(t){this.publiclyTrigger("eventAfterRender",t.event,t.event,t.el)}),this.publiclyTrigger("eventAfterAllRender")},onBeforeEventsUnrender:function(){this.applyScreenState(),this.renderedEventSegEach(function(t){this.publiclyTrigger("eventDestroy",t.event,t.event,t.el)})},applyScreenState:function(){this.thawHeight(),this.freezeHeight(),this.applyQueuedScroll()},renderEvents:function(t){},unrenderEvents:function(){},resolveEventEl:function(e,n){var i=this.publiclyTrigger("eventRender",e,e,n);return!1===i?n=null:i&&!0!==i&&(n=t(i)),n},showEvent:function(t){this.renderedEventSegEach(function(t){t.el.css("visibility","")},t)},hideEvent:function(t){this.renderedEventSegEach(function(t){t.el.css("visibility","hidden")},t)},renderedEventSegEach:function(t,e){var n,i=this.getEventSegs();for(n=0;n<i.length;n++)e&&i[n].event._id!==e._id||i[n].el&&t.call(this,i[n])},getEventSegs:function(){return[]},isEventDraggable:function(t){return this.isEventStartEditable(t)},isEventStartEditable:function(t){return ut(t.startEditable,(t.source||{}).startEditable,this.opt("eventStartEditable"),this.isEventGenerallyEditable(t))},isEventGenerallyEditable:function(t){return ut(t.editable,(t.source||{}).editable,this.opt("editable"))},reportSegDrop:function(t,e,n,i,r){var s=this.calendar,o=s.mutateSeg(t,e,n),a=function(){o.undo(),s.reportEventChange()};this.triggerEventDrop(t.event,o.dateDelta,a,i,r),s.reportEventChange()},triggerEventDrop:function(t,e,n,i,r){this.publiclyTrigger("eventDrop",i[0],t,e,n,r,{})},reportExternalDrop:function(e,n,i,r,s){var o,a,l=e.eventProps;l&&(o=t.extend({},l,n),a=this.calendar.renderEvent(o,e.stick)[0]),this.triggerExternalDrop(a,n,i,r,s)},triggerExternalDrop:function(t,e,n,i,r){this.publiclyTrigger("drop",n[0],e.start,i,r),t&&this.publiclyTrigger("eventReceive",null,t)},renderDrag:function(t,e){},unrenderDrag:function(){},isEventResizableFromStart:function(t){return this.opt("eventResizableFromStart")&&this.isEventResizable(t)},isEventResizableFromEnd:function(t){return this.isEventResizable(t)},isEventResizable:function(t){var e=t.source||{};return ut(t.durationEditable,e.durationEditable,this.opt("eventDurationEditable"),t.editable,e.editable,this.opt("editable"))},reportSegResize:function(t,e,n,i,r){var s=this.calendar,o=s.mutateSeg(t,e,n),a=function(){o.undo(),s.reportEventChange()};this.triggerEventResize(t.event,o.durationDelta,a,i,r),s.reportEventChange()},triggerEventResize:function(t,e,n,i,r){this.publiclyTrigger("eventResize",i[0],t,e,n,r,{})},select:function(t,e){this.unselect(e),this.renderSelection(t),this.reportSelection(t,e)},renderSelection:function(t){},reportSelection:function(t,e){this.isSelected=!0,this.triggerSelect(t,e)},triggerSelect:function(t,e){this.publiclyTrigger("select",null,this.calendar.applyTimezone(t.start),this.calendar.applyTimezone(t.end),e)},unselect:function(t){this.isSelected&&(this.isSelected=!1,this.destroySelection&&this.destroySelection(),this.unrenderSelection(),this.publiclyTrigger("unselect",null,t))},unrenderSelection:function(){},selectEvent:function(t){this.selectedEvent&&this.selectedEvent===t||(this.unselectEvent(),this.renderedEventSegEach(function(t){t.el.addClass("fc-selected")},t),this.selectedEvent=t)},unselectEvent:function(){this.selectedEvent&&(this.renderedEventSegEach(function(t){t.el.removeClass("fc-selected")},this.selectedEvent),this.selectedEvent=null)},isEventSelected:function(t){return this.selectedEvent&&this.selectedEvent._id===t._id},handleDocumentMousedown:function(t){S(t)&&this.processUnselect(t)},processUnselect:function(t){this.processRangeUnselect(t),this.processEventUnselect(t)},processRangeUnselect:function(e){var n;this.isSelected&&this.opt("unselectAuto")&&((n=this.opt("unselectCancel"))&&t(e.target).closest(n).length||this.unselect(e))},processEventUnselect:function(e){this.selectedEvent&&(t(e.target).closest(".fc-selected").length||this.unselectEvent())},triggerDayClick:function(t,e,n){this.publiclyTrigger("dayClick",e,this.calendar.applyTimezone(t.start),n)},computeDayRange:function(t){var e,n=t.start.clone().stripTime(),i=t.end,r=null;return i&&(r=i.clone().stripTime(),(e=+i.time())&&e>=this.nextDayThreshold&&r.add(1,"days")),(!i||r<=n)&&(r=n.clone().add(1,"days")),{start:n,end:r}},isMultiDayEvent:function(t){var e=this.computeDayRange(t);return e.end.diff(e.start,"days")>1}});Ce.watch("displayingDates",["dateProfile"],function(t){this.requestDateRender(t.dateProfile)},function(){this.requestDateUnrender()}),Ce.watch("initialEvents",["dateProfile"],function(t){return this.fetchInitialEvents(t.dateProfile)}),Ce.watch("bindingEvents",["initialEvents"],function(t){this.setEvents(t.initialEvents),this.bindEventChanges()},function(){this.unbindEventChanges(),this.unsetEvents()}),Ce.watch("displayingEvents",["displayingDates","hasEvents"],function(){this.requestEventsRender(this.get("currentEvents"))},function(){this.requestEventsUnrender()}),Ce.mixin({currentRange:null,currentRangeUnit:null,renderRange:null,activeRange:null,validRange:null,dateIncrement:null,minTime:null,maxTime:null,usesMinMaxTime:!1,start:null,end:null,intervalStart:null,intervalEnd:null,setDateProfileForRendering:function(t){this.currentRange=t.currentRange,this.currentRangeUnit=t.currentRangeUnit,this.renderRange=t.renderRange,this.activeRange=t.activeRange,this.validRange=t.validRange,this.dateIncrement=t.dateIncrement,this.minTime=t.minTime,this.maxTime=t.maxTime,this.start=t.activeRange.start,this.end=t.activeRange.end,this.intervalStart=t.currentRange.start,this.intervalEnd=t.currentRange.end},buildPrevDateProfile:function(t){var e=t.clone().startOf(this.currentRangeUnit).subtract(this.dateIncrement);return this.buildDateProfile(e,-1)},buildNextDateProfile:function(t){var e=t.clone().startOf(this.currentRangeUnit).add(this.dateIncrement);return this.buildDateProfile(e,1)},buildDateProfile:function(t,n,i){var r,s,o,a,l=this.buildValidRange(),u=null,h=null;return i&&(t=j(t,l)),r=this.buildCurrentRangeInfo(t,n),s=this.buildRenderRange(r.range,r.unit),o=q(s),this.opt("showNonCurrentDates")||(o=U(o,r.range)),u=e.duration(this.opt("minTime")),h=e.duration(this.opt("maxTime")),this.adjustActiveRange(o,u,h),o=U(o,l),t=j(t,o),a=$(r.range,l),{validRange:l,currentRange:r.range,currentRangeUnit:r.unit,activeRange:o,renderRange:s,minTime:u,maxTime:h,isValid:a,date:t,dateIncrement:this.buildDateIncrement(r.duration)}},buildValidRange:function(){return this.getRangeOption("validRange",this.calendar.getNow())||{}},buildCurrentRangeInfo:function(t,e){var n,i=null,r=null,s=null;return this.viewSpec.duration?(i=this.viewSpec.duration,r=this.viewSpec.durationUnit,s=this.buildRangeFromDuration(t,e,i,r)):(n=this.opt("dayCount"))?(r="day",s=this.buildRangeFromDayCount(t,e,n)):(s=this.buildCustomVisibleRange(t))?r=V(s.start,s.end):(i=this.getFallbackDuration(),r=V(i),s=this.buildRangeFromDuration(t,e,i,r)),this.normalizeCurrentRange(s,r),{duration:i,unit:r,range:s}},getFallbackDuration:function(){return e.duration({days:1})},normalizeCurrentRange:function(t,e){/^(year|month|week|day)$/.test(e)?(t.start.stripTime(),t.end.stripTime()):(t.start.hasTime()||t.start.time(0),t.end.hasTime()||t.end.time(0))},adjustActiveRange:function(t,e,n){var i=!1;this.usesMinMaxTime&&(e<0&&(t.start.time(0).add(e),i=!0),n>864e5&&(t.end.time(n-864e5),i=!0),i&&(t.start.hasTime()||t.start.time(0),t.end.hasTime()||t.end.time(0)))},buildRangeFromDuration:function(t,n,i,r){var s,o,a,l=this.opt("dateAlignment"),u=t.clone();return i.as("days")<=1&&this.isHiddenDay(u)&&(u=this.skipHiddenDays(u,n),u.startOf("day")),l||(o=this.opt("dateIncrement"),o?(a=e.duration(o),l=a<i?O(a,o):r):l=r),u.startOf(l),s=u.clone().add(i),{start:u,end:s}},buildRangeFromDayCount:function(t,e,n){var i,r=this.opt("dateAlignment"),s=0,o=t.clone();r&&o.startOf(r),o.startOf("day"),o=this.skipHiddenDays(o,e),i=o.clone();do{i.add(1,"day"),this.isHiddenDay(i)||s++}while(s<n);return{start:o,end:i}},buildCustomVisibleRange:function(t){var e=this.getRangeOption("visibleRange",this.calendar.moment(t));return!e||e.start&&e.end?e:null},buildRenderRange:function(t,e){return this.trimHiddenDays(t)},buildDateIncrement:function(t){var n,i=this.opt("dateIncrement");return i?e.duration(i):(n=this.opt("dateAlignment"))?e.duration(1,n):t||e.duration({days:1})},trimHiddenDays:function(t){return{start:this.skipHiddenDays(t.start),end:this.skipHiddenDays(t.end,-1,!0)}},currentRangeAs:function(t){var e=this.currentRange;return e.end.diff(e.start,t,!0)},getRangeOption:function(t){var e=this.opt(t);if("function"==typeof e&&(e=e.apply(null,Array.prototype.slice.call(arguments,1))),e)return this.calendar.parseRange(e)},initHiddenDays:function(){var e,n=this.opt("hiddenDays")||[],i=[],r=0;for(!1===this.opt("weekends")&&n.push(0,6),e=0;e<7;e++)(i[e]=-1!==t.inArray(e,n))||r++;if(!r)throw"invalid hiddenDays";this.isHiddenDayHash=i},isHiddenDay:function(t){return e.isMoment(t)&&(t=t.day()),this.isHiddenDayHash[t]},skipHiddenDays:function(t,e,n){var i=t.clone();for(e=e||1;this.isHiddenDayHash[(i.day()+(n?e:0)+7)%7];)i.add(e,"days");return i}});var He=Zt.Scroller=bt.extend({el:null,scrollEl:null,overflowX:null,overflowY:null,constructor:function(t){t=t||{},this.overflowX=t.overflowX||t.overflow||"auto",this.overflowY=t.overflowY||t.overflow||"auto"},render:function(){this.el=this.renderEl(),this.applyOverflow()},renderEl:function(){return this.scrollEl=t('<div class="fc-scroller"></div>')},clear:function(){this.setHeight("auto"),this.applyOverflow()},destroy:function(){this.el.remove()},applyOverflow:function(){this.scrollEl.css({"overflow-x":this.overflowX,"overflow-y":this.overflowY})},lockOverflow:function(t){var e=this.overflowX,n=this.overflowY;t=t||this.getScrollbarWidths(),"auto"===e&&(e=t.top||t.bottom||this.scrollEl[0].scrollWidth-1>this.scrollEl[0].clientWidth?"scroll":"hidden"),"auto"===n&&(n=t.left||t.right||this.scrollEl[0].scrollHeight-1>this.scrollEl[0].clientHeight?"scroll":"hidden"),this.scrollEl.css({"overflow-x":e,"overflow-y":n})},setHeight:function(t){this.scrollEl.height(t)},getScrollTop:function(){return this.scrollEl.scrollTop()},setScrollTop:function(t){this.scrollEl.scrollTop(t)},getClientWidth:function(){return this.scrollEl[0].clientWidth},getClientHeight:function(){return this.scrollEl[0].clientHeight},getScrollbarWidths:function(){return p(this.scrollEl)}});_t.prototype.proxyCall=function(t){var e=Array.prototype.slice.call(arguments,1),n=[];return this.items.forEach(function(i){n.push(i[t].apply(i,e))}),n};var Re=Zt.Calendar=bt.extend(fe,{view:null,viewsByType:null,currentDate:null,loadingLevel:0,constructor:function(t,e){we.needed(),this.el=t,this.viewsByType={},this.viewSpecCache={},this.initOptionsInternals(e),this.initMomentInternals(),this.initCurrentDate(),Ut.call(this),this.initialize()},initialize:function(){},getCalendar:function(){return this},getView:function(){return this.view},publiclyTrigger:function(t,e){var n=Array.prototype.slice.call(arguments,2),i=this.opt(t);if(e=e||this.el[0],this.triggerWith(t,e,n),i)return i.apply(e,n)},instantiateView:function(t){var e=this.getViewSpec(t);return new e.class(this,e)},isValidViewType:function(t){return Boolean(this.getViewSpec(t))},changeView:function(t,e){e&&(e.start&&e.end?this.recordOptionOverrides({visibleRange:e}):this.currentDate=this.moment(e).stripZone()),this.renderView(t)},zoomTo:function(t,e){var n;e=e||"day",n=this.getViewSpec(e)||this.getUnitViewSpec(e),this.currentDate=t.clone(),this.renderView(n?n.type:null)},initCurrentDate:function(){var t=this.opt("defaultDate");this.currentDate=null!=t?this.moment(t).stripZone():this.getNow()},prev:function(){var t=this.view.buildPrevDateProfile(this.currentDate);t.isValid&&(this.currentDate=t.date,this.renderView())},next:function(){var t=this.view.buildNextDateProfile(this.currentDate);t.isValid&&(this.currentDate=t.date,this.renderView())},prevYear:function(){this.currentDate.add(-1,"years"),this.renderView()},nextYear:function(){this.currentDate.add(1,"years"),this.renderView()},today:function(){this.currentDate=this.getNow(),this.renderView()},gotoDate:function(t){this.currentDate=this.moment(t).stripZone(),this.renderView()},incrementDate:function(t){this.currentDate.add(e.duration(t)),this.renderView()},getDate:function(){return this.applyTimezone(this.currentDate)},pushLoading:function(){this.loadingLevel++||this.publiclyTrigger("loading",null,!0,this.view)},popLoading:function(){--this.loadingLevel||this.publiclyTrigger("loading",null,!1,this.view)},select:function(t,e){this.view.select(this.buildSelectSpan.apply(this,arguments))},unselect:function(){this.view&&this.view.unselect()},buildSelectSpan:function(t,e){var n,i=this.moment(t).stripZone();return n=e?this.moment(e).stripZone():i.hasTime()?i.clone().add(this.defaultTimedEventDuration):i.clone().add(this.defaultAllDayEventDuration),{start:i,end:n}},parseRange:function(t){var e=null,n=null;return t.start&&(e=this.moment(t.start).stripZone()),t.end&&(n=this.moment(t.end).stripZone()),e||n?e&&n&&n.isBefore(e)?null:{start:e,end:n}:null},rerenderEvents:function(){this.elementVisible()&&this.reportEventChange()}});Re.mixin({dirDefaults:null,localeDefaults:null,overrides:null,dynamicOverrides:null,optionsModel:null,initOptionsInternals:function(e){this.overrides=t.extend({},e),this.dynamicOverrides={},this.optionsModel=new ue,this.populateOptionsHash()},option:function(t,e){var n;if("string"==typeof t){if(void 0===e)return this.optionsModel.get(t);n={},n[t]=e,this.setOptions(n)}else"object"==typeof t&&this.setOptions(t)},opt:function(t){return this.optionsModel.get(t)},setOptions:function(t){var e,n=0;this.recordOptionOverrides(t);for(e in t)n++;if(1===n){if("height"===e||"contentHeight"===e||"aspectRatio"===e)return void this.updateSize(!0);if("defaultDate"===e)return;if("businessHours"===e)return void(this.view&&(this.view.unrenderBusinessHours(),this.view.renderBusinessHours()));if("timezone"===e)return this.rezoneArrayEventSources(),void this.refetchEvents()}this.renderHeader(),this.renderFooter(),this.viewsByType={},this.reinitView()},populateOptionsHash:function(){var t,e,i,r,s;t=ut(this.dynamicOverrides.locale,this.overrides.locale),e=xe[t],e||(t=Re.defaults.locale,e=xe[t]||{}),i=ut(this.dynamicOverrides.isRTL,this.overrides.isRTL,e.isRTL,Re.defaults.isRTL),r=i?Re.rtlDefaults:{},this.dirDefaults=r,this.localeDefaults=e,s=n([Re.defaults,r,e,this.overrides,this.dynamicOverrides]),Yt(s),this.optionsModel.reset(s)},recordOptionOverrides:function(t){var e;for(e in t)this.dynamicOverrides[e]=t[e];this.viewSpecCache={},this.populateOptionsHash()}}),Re.mixin({defaultAllDayEventDuration:null,defaultTimedEventDuration:null,localeData:null,initMomentInternals:function(){var t=this;this.defaultAllDayEventDuration=e.duration(this.opt("defaultAllDayEventDuration")),this.defaultTimedEventDuration=e.duration(this.opt("defaultTimedEventDuration")),this.optionsModel.watch("buildingMomentLocale",["?locale","?monthNames","?monthNamesShort","?dayNames","?dayNamesShort","?firstDay","?weekNumberCalculation"],function(e){var n,i=e.weekNumberCalculation,r=e.firstDay;"iso"===i&&(i="ISO");var s=rt(qt(e.locale));e.monthNames&&(s._months=e.monthNames),e.monthNamesShort&&(s._monthsShort=e.monthNamesShort),e.dayNames&&(s._weekdays=e.dayNames),e.dayNamesShort&&(s._weekdaysShort=e.dayNamesShort),null==r&&"ISO"===i&&(r=1),null!=r&&(n=rt(s._week),n.dow=r,s._week=n),"ISO"!==i&&"local"!==i&&"function"!=typeof i||(s._fullCalendar_weekCalc=i),t.localeData=s,t.currentDate&&t.localizeMoment(t.currentDate)})},moment:function(){var t;return"local"===this.opt("timezone")?(t=Zt.moment.apply(null,arguments),t.hasTime()&&t.local()):t="UTC"===this.opt("timezone")?Zt.moment.utc.apply(null,arguments):Zt.moment.parseZone.apply(null,arguments),this.localizeMoment(t),t},localizeMoment:function(t){t._locale=this.localeData},getIsAmbigTimezone:function(){return"local"!==this.opt("timezone")&&"UTC"!==this.opt("timezone")},applyTimezone:function(t){if(!t.hasTime())return t.clone();var e,n=this.moment(t.toArray()),i=t.time()-n.time();return i&&(e=n.clone().add(i),t.time()-e.time()==0&&(n=e)),n},getNow:function(){var t=this.opt("now");return"function"==typeof t&&(t=t()),this.moment(t).stripZone()},humanizeDuration:function(t){return t.locale(this.opt("locale")).humanize()},getEventEnd:function(t){return t.end?t.end.clone():this.getDefaultEventEnd(t.allDay,t.start)},getDefaultEventEnd:function(t,e){var n=e.clone();return t?n.stripTime().add(this.defaultAllDayEventDuration):n.add(this.defaultTimedEventDuration),this.getIsAmbigTimezone()&&n.stripZone(),n}}),Re.mixin({viewSpecCache:null,getViewSpec:function(t){var e=this.viewSpecCache;return e[t]||(e[t]=this.buildViewSpec(t))},getUnitViewSpec:function(e){var n,i,r;if(-1!=t.inArray(e,Jt))for(n=this.header.getViewsWithButtons(),t.each(Zt.views,function(t){n.push(t)}),i=0;i<n.length;i++)if((r=this.getViewSpec(n[i]))&&r.singleUnit==e)return r},buildViewSpec:function(t){for(var i,r,s,o,a,l=this.overrides.views||{},u=[],h=[],c=[],d=t;d;)i=$t[d],r=l[d],d=null,"function"==typeof i&&(i={class:i}),i&&(u.unshift(i),h.unshift(i.defaults||{}),s=s||i.duration,d=d||i.type),r&&(c.unshift(r),s=s||r.duration,d=d||r.type);return i=it(u),i.type=t,!!i.class&&(s=s||this.dynamicOverrides.duration||this.overrides.duration,s&&(o=e.duration(s),o.valueOf()&&(a=O(o,s),i.duration=o,i.durationUnit=a,1===o.as(a)&&(i.singleUnit=a,c.unshift(l[a]||{})))),i.defaults=n(h),i.overrides=n(c),this.buildViewSpecOptions(i),this.buildViewSpecButtonText(i,t),i)},buildViewSpecOptions:function(t){t.options=n([Re.defaults,t.defaults,this.dirDefaults,this.localeDefaults,this.overrides,t.overrides,this.dynamicOverrides]),Yt(t.options)},buildViewSpecButtonText:function(t,e){function n(n){var i=n.buttonText||{};return i[e]||(t.buttonTextKey?i[t.buttonTextKey]:null)||(t.singleUnit?i[t.singleUnit]:null)}t.buttonTextOverride=n(this.dynamicOverrides)||n(this.overrides)||t.overrides.buttonText,t.buttonTextDefault=n(this.localeDefaults)||n(this.dirDefaults)||t.defaults.buttonText||n(Re.defaults)||(t.duration?this.humanizeDuration(t.duration):null)||e}}),Re.mixin({el:null,contentEl:null,suggestedViewHeight:null,windowResizeProxy:null,ignoreWindowResize:0,render:function(){this.contentEl?this.elementVisible()&&(this.calcSize(),this.renderView()):this.initialRender()},initialRender:function(){var e=this,n=this.el;n.addClass("fc"),n.on("click.fc","a[data-goto]",function(n){var i=t(this),r=i.data("goto"),s=e.moment(r.date),o=r.type,a=e.view.opt("navLink"+gt(o)+"Click");"function"==typeof a?a(s,n):("string"==typeof a&&(o=a),e.zoomTo(s,o))}),this.optionsModel.watch("applyingThemeClasses",["?theme"],function(t){n.toggleClass("ui-widget",t.theme),n.toggleClass("fc-unthemed",!t.theme)}),this.optionsModel.watch("applyingDirClasses",["?isRTL","?locale"],function(t){n.toggleClass("fc-ltr",!t.isRTL),n.toggleClass("fc-rtl",t.isRTL)}),this.contentEl=t("<div class='fc-view-container'/>").prependTo(n),this.initToolbars(),this.renderHeader(),this.renderFooter(),this.renderView(this.opt("defaultView")),this.opt("handleWindowResize")&&t(window).resize(this.windowResizeProxy=yt(this.windowResize.bind(this),this.opt("windowResizeDelay")))},destroy:function(){this.view&&this.view.removeElement(),this.toolbarsManager.proxyCall("removeElement"),this.contentEl.remove(),this.el.removeClass("fc fc-ltr fc-rtl fc-unthemed ui-widget"),this.el.off(".fc"),this.windowResizeProxy&&(t(window).unbind("resize",this.windowResizeProxy),this.windowResizeProxy=null),we.unneeded()},elementVisible:function(){return this.el.is(":visible")},renderView:function(e,n){this.ignoreWindowResize++;var i=this.view&&e&&this.view.type!==e;i&&(this.freezeContentHeight(),this.clearView()),!this.view&&e&&(this.view=this.viewsByType[e]||(this.viewsByType[e]=this.instantiateView(e)),this.view.setElement(t("<div class='fc-view fc-"+e+"-view' />").appendTo(this.contentEl)),this.toolbarsManager.proxyCall("activateButton",e)),this.view&&(n&&this.view.addForcedScroll(n),this.elementVisible()&&(this.currentDate=this.view.setDate(this.currentDate))),i&&this.thawContentHeight(),this.ignoreWindowResize--},clearView:function(){this.toolbarsManager.proxyCall("deactivateButton",this.view.type),this.view.removeElement(),this.view=null},reinitView:function(){this.ignoreWindowResize++,this.freezeContentHeight();var t=this.view.type,e=this.view.queryScroll();this.clearView(),this.calcSize(),this.renderView(t,e),this.thawContentHeight(),this.ignoreWindowResize--},getSuggestedViewHeight:function(){return null===this.suggestedViewHeight&&this.calcSize(),this.suggestedViewHeight},isHeightAuto:function(){return"auto"===this.opt("contentHeight")||"auto"===this.opt("height")},updateSize:function(t){if(this.elementVisible())return t&&this._calcSize(),this.ignoreWindowResize++,this.view.updateSize(!0),this.ignoreWindowResize--,!0},calcSize:function(){this.elementVisible()&&this._calcSize()},_calcSize:function(){var t=this.opt("contentHeight"),e=this.opt("height");this.suggestedViewHeight="number"==typeof t?t:"function"==typeof t?t():"number"==typeof e?e-this.queryToolbarsHeight():"function"==typeof e?e()-this.queryToolbarsHeight():"parent"===e?this.el.parent().height()-this.queryToolbarsHeight():Math.round(this.contentEl.width()/Math.max(this.opt("aspectRatio"),.5))},windowResize:function(t){!this.ignoreWindowResize&&t.target===window&&this.view.renderRange&&this.updateSize(!0)&&this.view.publiclyTrigger("windowResize",this.el[0])},freezeContentHeight:function(){this.contentEl.css({width:"100%",height:this.contentEl.height(),overflow:"hidden"})},thawContentHeight:function(){this.contentEl.css({width:"",height:"",overflow:""})}}),Re.mixin({header:null,footer:null,toolbarsManager:null,initToolbars:function(){this.header=new Wt(this,this.computeHeaderOptions()),this.footer=new Wt(this,this.computeFooterOptions()),this.toolbarsManager=new _t([this.header,this.footer])},computeHeaderOptions:function(){return{extraClasses:"fc-header-toolbar",layout:this.opt("header")}},computeFooterOptions:function(){return{extraClasses:"fc-footer-toolbar",layout:this.opt("footer")}},renderHeader:function(){var t=this.header;t.setToolbarOptions(this.computeHeaderOptions()),t.render(),t.el&&this.el.prepend(t.el)},renderFooter:function(){var t=this.footer;t.setToolbarOptions(this.computeFooterOptions()),t.render(),t.el&&this.el.append(t.el)},setToolbarsTitle:function(t){this.toolbarsManager.proxyCall("updateTitle",t)},updateToolbarButtons:function(){var t=this.getNow(),e=this.view,n=e.buildDateProfile(t),i=e.buildPrevDateProfile(this.currentDate),r=e.buildNextDateProfile(this.currentDate);this.toolbarsManager.proxyCall(n.isValid&&!Z(t,e.currentRange)?"enableButton":"disableButton","today"),this.toolbarsManager.proxyCall(i.isValid?"enableButton":"disableButton","prev"),this.toolbarsManager.proxyCall(r.isValid?"enableButton":"disableButton","next")},queryToolbarsHeight:function(){return this.toolbarsManager.items.reduce(function(t,e){return t+(e.el?e.el.outerHeight(!0):0)},0)}}),Re.defaults={titleRangeSeparator:" รขโฌโ ",monthYearFormat:"MMMM YYYY",defaultTimedEventDuration:"02:00:00",defaultAllDayEventDuration:{days:1},forceEventDuration:!1,nextDayThreshold:"09:00:00",defaultView:"month",aspectRatio:1.35,header:{left:"title",center:"",right:"today prev,next"},weekends:!0,weekNumbers:!1,weekNumberTitle:"W",weekNumberCalculation:"local",scrollTime:"06:00:00",minTime:"00:00:00",maxTime:"24:00:00",showNonCurrentDates:!0,lazyFetching:!0,startParam:"start",endParam:"end",timezoneParam:"timezone",timezone:!1,isRTL:!1,buttonText:{prev:"prev",next:"next",prevYear:"prev year",nextYear:"next year",year:"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"},allDayText:"all-day",theme:!1,themeButtonIcons:{prev:"circle-triangle-w",next:"circle-triangle-e",prevYear:"seek-prev",nextYear:"seek-next"},dragOpacity:.75,dragRevertDuration:500,dragScroll:!0,unselectAuto:!0,dropAccept:"*",eventOrder:"title",eventLimit:!1,eventLimitText:"more",eventLimitClick:"popover",dayPopoverFormat:"LL",handleWindowResize:!0,windowResizeDelay:100,longPressDelay:1e3},Re.englishDefaults={dayPopoverFormat:"dddd, MMMM D"},Re.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 xe=Zt.locales={};Zt.datepickerLocale=function(e,n,i){var r=xe[e]||(xe[e]={});r.isRTL=i.isRTL,r.weekNumberTitle=i.weekHeader,t.each(Ie,function(t,e){r[t]=e(i)}),t.datepicker&&(t.datepicker.regional[n]=t.datepicker.regional[e]=i,t.datepicker.regional.en=t.datepicker.regional[""],t.datepicker.setDefaults(i))},Zt.locale=function(e,i){var r,s;r=xe[e]||(xe[e]={}),i&&(r=xe[e]=n([r,i])),s=qt(e),t.each(ke,function(t,e){null==r[t]&&(r[t]=e(s,r))}),Re.defaults.locale=e};var Ie={buttonText:function(t){return{prev:ct(t.prevText),next:ct(t.nextText),today:ct(t.currentText)}},monthYearFormat:function(t){return t.showMonthAfterYear?"YYYY["+t.yearSuffix+"] MMMM":"MMMM YYYY["+t.yearSuffix+"]"}},ke={dayOfMonthFormat:function(t,e){var n=t.longDateFormat("l");return n=n.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g,""),e.isRTL?n+=" ddd":n="ddd "+n,n},mediumTimeFormat:function(t){return t.longDateFormat("LT").replace(/\s*a$/i,"a")},smallTimeFormat:function(t){return t.longDateFormat("LT").replace(":mm","(:mm)").replace(/(\Wmm)$/,"($1)").replace(/\s*a$/i,"a")},extraSmallTimeFormat:function(t){return t.longDateFormat("LT").replace(":mm","(:mm)").replace(/(\Wmm)$/,"($1)").replace(/\s*a$/i,"t")},hourFormat:function(t){return t.longDateFormat("LT").replace(":mm","").replace(/(\Wmm)$/,"").replace(/\s*a$/i,"a")},noMeridiemTimeFormat:function(t){return t.longDateFormat("LT").replace(/\s*a$/i,"")}},Me={smallDayDateFormat:function(t){return t.isRTL?"D dd":"dd D"},weekFormat:function(t){return t.isRTL?"w[ "+t.weekNumberTitle+"]":"["+t.weekNumberTitle+" ]w"},smallWeekFormat:function(t){return t.isRTL?"w["+t.weekNumberTitle+"]":"["+t.weekNumberTitle+"]w"}};Zt.locale("en",Re.englishDefaults),Zt.sourceNormalizers=[],Zt.sourceFetchers=[];var Be={dataType:"json",cache:!1},Le=1;Re.prototype.mutateSeg=function(t,e){return this.mutateEvent(t.event,e)},Re.prototype.normalizeEvent=function(t){},Re.prototype.spanContainsSpan=function(t,e){var n=t.start.clone().stripZone(),i=this.getEventEnd(t).stripZone() | |
10 | -;return e.start>=n&&e.end<=i},Re.prototype.getPeerEvents=function(t,e){var n,i,r=this.getEventCache(),s=[];for(n=0;n<r.length;n++)i=r[n],e&&e._id===i._id||s.push(i);return s},Re.prototype.isEventSpanAllowed=function(t,e){var n=e.source||{},i=this.opt("eventAllow"),r=ut(e.constraint,n.constraint,this.opt("eventConstraint")),s=ut(e.overlap,n.overlap,this.opt("eventOverlap"));return this.isSpanAllowed(t,r,s,e)&&(!i||!1!==i(t,e))},Re.prototype.isExternalSpanAllowed=function(e,n,i){var r,s;return i&&(r=t.extend({},i,n),s=this.expandEvent(this.buildEventFromInput(r))[0]),s?this.isEventSpanAllowed(e,s):this.isSelectionSpanAllowed(e)},Re.prototype.isSelectionSpanAllowed=function(t){var e=this.opt("selectAllow");return this.isSpanAllowed(t,this.opt("selectConstraint"),this.opt("selectOverlap"))&&(!e||!1!==e(t))},Re.prototype.isSpanAllowed=function(t,e,n,i){var r,s,o,a,l,u;if(null!=e&&(r=this.constraintToEvents(e))){for(s=!1,a=0;a<r.length;a++)if(this.spanContainsSpan(r[a],t)){s=!0;break}if(!s)return!1}for(o=this.getPeerEvents(t,i),a=0;a<o.length;a++)if(l=o[a],this.eventIntersectsRange(l,t)){if(!1===n)return!1;if("function"==typeof n&&!n(l,i))return!1;if(i){if(!1===(u=ut(l.overlap,(l.source||{}).overlap)))return!1;if("function"==typeof u&&!u(i,l))return!1}}return!0},Re.prototype.constraintToEvents=function(t){return"businessHours"===t?this.getCurrentBusinessHourEvents():"object"==typeof t?null!=t.start?this.expandEvent(this.buildEventFromInput(t)):null:this.clientEvents(t)},Re.prototype.eventIntersectsRange=function(t,e){var n=t.start.clone().stripZone(),i=this.getEventEnd(t).stripZone();return e.start<i&&e.end>n};var Ne={id:"_fcBusinessHours",start:"09:00",end:"17:00",dow:[1,2,3,4,5],rendering:"inverse-background"};Re.prototype.getCurrentBusinessHourEvents=function(t){return this.computeBusinessHourEvents(t,this.opt("businessHours"))},Re.prototype.computeBusinessHourEvents=function(e,n){return!0===n?this.expandBusinessHourEvents(e,[{}]):t.isPlainObject(n)?this.expandBusinessHourEvents(e,[n]):t.isArray(n)?this.expandBusinessHourEvents(e,n,!0):[]},Re.prototype.expandBusinessHourEvents=function(e,n,i){var r,s,o=this.getView(),a=[];for(r=0;r<n.length;r++)s=n[r],i&&!s.dow||(s=t.extend({},Ne,s),e&&(s.start=null,s.end=null),a.push.apply(a,this.expandEvent(this.buildEventFromInput(s),o.activeRange.start,o.activeRange.end)));return a};var ze=Zt.BasicView=Ce.extend({scroller:null,dayGridClass:De,dayGrid:null,dayNumbersVisible:!1,colWeekNumbersVisible:!1,cellWeekNumbersVisible:!1,weekNumberWidth:null,headContainerEl:null,headRowEl:null,initialize:function(){this.dayGrid=this.instantiateDayGrid(),this.scroller=new He({overflowX:"hidden",overflowY:"auto"})},instantiateDayGrid:function(){return new(this.dayGridClass.extend(Fe))(this)},buildRenderRange:function(t,e){var n=Ce.prototype.buildRenderRange.apply(this,arguments);return/^(year|month)$/.test(e)&&(n.start.startOf("week"),n.end.weekday()&&n.end.add(1,"week").startOf("week")),this.trimHiddenDays(n)},renderDates:function(){this.dayGrid.breakOnWeeks=/year|month|week/.test(this.currentRangeUnit),this.dayGrid.setRange(this.renderRange),this.dayNumbersVisible=this.dayGrid.rowCnt>1,this.opt("weekNumbers")&&(this.opt("weekNumbersWithinDays")?(this.cellWeekNumbersVisible=!0,this.colWeekNumbersVisible=!1):(this.cellWeekNumbersVisible=!1,this.colWeekNumbersVisible=!0)),this.dayGrid.numbersVisible=this.dayNumbersVisible||this.cellWeekNumbersVisible||this.colWeekNumbersVisible,this.el.addClass("fc-basic-view").html(this.renderSkeletonHtml()),this.renderHead(),this.scroller.render();var e=this.scroller.el.addClass("fc-day-grid-container"),n=t('<div class="fc-day-grid" />').appendTo(e);this.el.find(".fc-body > tr > td").append(e),this.dayGrid.setElement(n),this.dayGrid.renderDates(this.hasRigidRows())},renderHead:function(){this.headContainerEl=this.el.find(".fc-head-container").html(this.dayGrid.renderHeadHtml()),this.headRowEl=this.headContainerEl.find(".fc-row")},unrenderDates:function(){this.dayGrid.unrenderDates(),this.dayGrid.removeElement(),this.scroller.destroy()},renderBusinessHours:function(){this.dayGrid.renderBusinessHours()},unrenderBusinessHours:function(){this.dayGrid.unrenderBusinessHours()},renderSkeletonHtml:function(){return'<table><thead class="fc-head"><tr><td class="fc-head-container '+this.widgetHeaderClass+'"></td></tr></thead><tbody class="fc-body"><tr><td class="'+this.widgetContentClass+'"></td></tr></tbody></table>'},weekNumberStyleAttr:function(){return null!==this.weekNumberWidth?'style="width:'+this.weekNumberWidth+'px"':""},hasRigidRows:function(){var t=this.opt("eventLimit");return t&&"number"!=typeof t},updateWidth:function(){this.colWeekNumbersVisible&&(this.weekNumberWidth=u(this.el.find(".fc-week-number")))},setHeight:function(t,e){var n,s,o=this.opt("eventLimit");this.scroller.clear(),r(this.headRowEl),this.dayGrid.removeSegPopover(),o&&"number"==typeof o&&this.dayGrid.limitRows(o),n=this.computeScrollerHeight(t),this.setGridHeight(n,e),o&&"number"!=typeof o&&this.dayGrid.limitRows(o),e||(this.scroller.setHeight(n),s=this.scroller.getScrollbarWidths(),(s.left||s.right)&&(i(this.headRowEl,s),n=this.computeScrollerHeight(t),this.scroller.setHeight(n)),this.scroller.lockOverflow(s))},computeScrollerHeight:function(t){return t-h(this.el,this.scroller.el)},setGridHeight:function(t,e){e?l(this.dayGrid.rowEls):a(this.dayGrid.rowEls,t,!0)},computeInitialDateScroll:function(){return{top:0}},queryDateScroll:function(){return{top:this.scroller.getScrollTop()}},applyDateScroll:function(t){void 0!==t.top&&this.scroller.setScrollTop(t.top)},hitsNeeded:function(){this.dayGrid.hitsNeeded()},hitsNotNeeded:function(){this.dayGrid.hitsNotNeeded()},prepareHits:function(){this.dayGrid.prepareHits()},releaseHits:function(){this.dayGrid.releaseHits()},queryHit:function(t,e){return this.dayGrid.queryHit(t,e)},getHitSpan:function(t){return this.dayGrid.getHitSpan(t)},getHitEl:function(t){return this.dayGrid.getHitEl(t)},renderEvents:function(t){this.dayGrid.renderEvents(t),this.updateHeight()},getEventSegs:function(){return this.dayGrid.getEventSegs()},unrenderEvents:function(){this.dayGrid.unrenderEvents()},renderDrag:function(t,e){return this.dayGrid.renderDrag(t,e)},unrenderDrag:function(){this.dayGrid.unrenderDrag()},renderSelection:function(t){this.dayGrid.renderSelection(t)},unrenderSelection:function(){this.dayGrid.unrenderSelection()}}),Fe={renderHeadIntroHtml:function(){var t=this.view;return t.colWeekNumbersVisible?'<th class="fc-week-number '+t.widgetHeaderClass+'" '+t.weekNumberStyleAttr()+"><span>"+ht(t.opt("weekNumberTitle"))+"</span></th>":""},renderNumberIntroHtml:function(t){var e=this.view,n=this.getCellDate(t,0);return e.colWeekNumbersVisible?'<td class="fc-week-number" '+e.weekNumberStyleAttr()+">"+e.buildGotoAnchorHtml({date:n,type:"week",forceOff:1===this.colCnt},n.format("w"))+"</td>":""},renderBgIntroHtml:function(){var t=this.view;return t.colWeekNumbersVisible?'<td class="fc-week-number '+t.widgetContentClass+'" '+t.weekNumberStyleAttr()+"></td>":""},renderIntroHtml:function(){var t=this.view;return t.colWeekNumbersVisible?'<td class="fc-week-number" '+t.weekNumberStyleAttr()+"></td>":""}},Ae=Zt.MonthView=ze.extend({buildRenderRange:function(){var t,e=ze.prototype.buildRenderRange.apply(this,arguments);return this.isFixedWeeks()&&(t=Math.ceil(e.end.diff(e.start,"weeks",!0)),e.end.add(6-t,"weeks")),e},setGridHeight:function(t,e){e&&(t*=this.rowCnt/6),a(this.dayGrid.rowEls,t,!e)},isFixedWeeks:function(){return this.opt("fixedWeekCount")}});$t.basic={class:ze},$t.basicDay={type:"basic",duration:{days:1}},$t.basicWeek={type:"basic",duration:{weeks:1}},$t.month={class:Ae,duration:{months:1},defaults:{fixedWeekCount:!0}};var Ge=Zt.AgendaView=Ce.extend({scroller:null,timeGridClass:Te,timeGrid:null,dayGridClass:De,dayGrid:null,axisWidth:null,headContainerEl:null,noScrollRowEls:null,bottomRuleEl:null,usesMinMaxTime:!0,initialize:function(){this.timeGrid=this.instantiateTimeGrid(),this.opt("allDaySlot")&&(this.dayGrid=this.instantiateDayGrid()),this.scroller=new He({overflowX:"hidden",overflowY:"auto"})},instantiateTimeGrid:function(){return new(this.timeGridClass.extend(Ve))(this)},instantiateDayGrid:function(){return new(this.dayGridClass.extend(Oe))(this)},renderDates:function(){this.timeGrid.setRange(this.renderRange),this.dayGrid&&this.dayGrid.setRange(this.renderRange),this.el.addClass("fc-agenda-view").html(this.renderSkeletonHtml()),this.renderHead(),this.scroller.render();var e=this.scroller.el.addClass("fc-time-grid-container"),n=t('<div class="fc-time-grid" />').appendTo(e);this.el.find(".fc-body > tr > td").append(e),this.timeGrid.setElement(n),this.timeGrid.renderDates(),this.bottomRuleEl=t('<hr class="fc-divider '+this.widgetHeaderClass+'"/>').appendTo(this.timeGrid.el),this.dayGrid&&(this.dayGrid.setElement(this.el.find(".fc-day-grid")),this.dayGrid.renderDates(),this.dayGrid.bottomCoordPadding=this.dayGrid.el.next("hr").outerHeight()),this.noScrollRowEls=this.el.find(".fc-row:not(.fc-scroller *)")},renderHead:function(){this.headContainerEl=this.el.find(".fc-head-container").html(this.timeGrid.renderHeadHtml())},unrenderDates:function(){this.timeGrid.unrenderDates(),this.timeGrid.removeElement(),this.dayGrid&&(this.dayGrid.unrenderDates(),this.dayGrid.removeElement()),this.scroller.destroy()},renderSkeletonHtml:function(){return'<table><thead class="fc-head"><tr><td class="fc-head-container '+this.widgetHeaderClass+'"></td></tr></thead><tbody class="fc-body"><tr><td class="'+this.widgetContentClass+'">'+(this.dayGrid?'<div class="fc-day-grid"/><hr class="fc-divider '+this.widgetHeaderClass+'"/>':"")+"</td></tr></tbody></table>"},axisStyleAttr:function(){return null!==this.axisWidth?'style="width:'+this.axisWidth+'px"':""},renderBusinessHours:function(){this.timeGrid.renderBusinessHours(),this.dayGrid&&this.dayGrid.renderBusinessHours()},unrenderBusinessHours:function(){this.timeGrid.unrenderBusinessHours(),this.dayGrid&&this.dayGrid.unrenderBusinessHours()},getNowIndicatorUnit:function(){return this.timeGrid.getNowIndicatorUnit()},renderNowIndicator:function(t){this.timeGrid.renderNowIndicator(t)},unrenderNowIndicator:function(){this.timeGrid.unrenderNowIndicator()},updateSize:function(t){this.timeGrid.updateSize(t),Ce.prototype.updateSize.call(this,t)},updateWidth:function(){this.axisWidth=u(this.el.find(".fc-axis"))},setHeight:function(t,e){var n,s,o;this.bottomRuleEl.hide(),this.scroller.clear(),r(this.noScrollRowEls),this.dayGrid&&(this.dayGrid.removeSegPopover(),n=this.opt("eventLimit"),n&&"number"!=typeof n&&(n=Pe),n&&this.dayGrid.limitRows(n)),e||(s=this.computeScrollerHeight(t),this.scroller.setHeight(s),o=this.scroller.getScrollbarWidths(),(o.left||o.right)&&(i(this.noScrollRowEls,o),s=this.computeScrollerHeight(t),this.scroller.setHeight(s)),this.scroller.lockOverflow(o),this.timeGrid.getTotalSlatHeight()<s&&this.bottomRuleEl.show())},computeScrollerHeight:function(t){return t-h(this.el,this.scroller.el)},computeInitialDateScroll:function(){var t=e.duration(this.opt("scrollTime")),n=this.timeGrid.computeTimeTop(t);return n=Math.ceil(n),n&&n++,{top:n}},queryDateScroll:function(){return{top:this.scroller.getScrollTop()}},applyDateScroll:function(t){void 0!==t.top&&this.scroller.setScrollTop(t.top)},hitsNeeded:function(){this.timeGrid.hitsNeeded(),this.dayGrid&&this.dayGrid.hitsNeeded()},hitsNotNeeded:function(){this.timeGrid.hitsNotNeeded(),this.dayGrid&&this.dayGrid.hitsNotNeeded()},prepareHits:function(){this.timeGrid.prepareHits(),this.dayGrid&&this.dayGrid.prepareHits()},releaseHits:function(){this.timeGrid.releaseHits(),this.dayGrid&&this.dayGrid.releaseHits()},queryHit:function(t,e){var n=this.timeGrid.queryHit(t,e);return!n&&this.dayGrid&&(n=this.dayGrid.queryHit(t,e)),n},getHitSpan:function(t){return t.component.getHitSpan(t)},getHitEl:function(t){return t.component.getHitEl(t)},renderEvents:function(t){var e,n=[],i=[];for(e=0;e<t.length;e++)t[e].allDay?n.push(t[e]):i.push(t[e]);this.timeGrid.renderEvents(i),this.dayGrid&&this.dayGrid.renderEvents(n),this.updateHeight()},getEventSegs:function(){return this.timeGrid.getEventSegs().concat(this.dayGrid?this.dayGrid.getEventSegs():[])},unrenderEvents:function(){this.timeGrid.unrenderEvents(),this.dayGrid&&this.dayGrid.unrenderEvents()},renderDrag:function(t,e){return t.start.hasTime()?this.timeGrid.renderDrag(t,e):this.dayGrid?this.dayGrid.renderDrag(t,e):void 0},unrenderDrag:function(){this.timeGrid.unrenderDrag(),this.dayGrid&&this.dayGrid.unrenderDrag()},renderSelection:function(t){t.start.hasTime()||t.end.hasTime()?this.timeGrid.renderSelection(t):this.dayGrid&&this.dayGrid.renderSelection(t)},unrenderSelection:function(){this.timeGrid.unrenderSelection(),this.dayGrid&&this.dayGrid.unrenderSelection()}}),Ve={renderHeadIntroHtml:function(){var t,e=this.view;return e.opt("weekNumbers")?(t=this.start.format(e.opt("smallWeekFormat")),'<th class="fc-axis fc-week-number '+e.widgetHeaderClass+'" '+e.axisStyleAttr()+">"+e.buildGotoAnchorHtml({date:this.start,type:"week",forceOff:this.colCnt>1},ht(t))+"</th>"):'<th class="fc-axis '+e.widgetHeaderClass+'" '+e.axisStyleAttr()+"></th>"},renderBgIntroHtml:function(){var t=this.view;return'<td class="fc-axis '+t.widgetContentClass+'" '+t.axisStyleAttr()+"></td>"},renderIntroHtml:function(){return'<td class="fc-axis" '+this.view.axisStyleAttr()+"></td>"}},Oe={renderBgIntroHtml:function(){var t=this.view;return'<td class="fc-axis '+t.widgetContentClass+'" '+t.axisStyleAttr()+"><span>"+t.getAllDayHtml()+"</span></td>"},renderIntroHtml:function(){return'<td class="fc-axis" '+this.view.axisStyleAttr()+"></td>"}},Pe=5,_e=[{hours:1},{minutes:30},{minutes:15},{seconds:30},{seconds:15}];$t.agenda={class:Ge,defaults:{allDaySlot:!0,slotDuration:"00:30:00",slotEventOverlap:!0}},$t.agendaDay={type:"agenda",duration:{days:1}},$t.agendaWeek={type:"agenda",duration:{weeks:1}};var We=Ce.extend({grid:null,scroller:null,initialize:function(){this.grid=new Ye(this),this.scroller=new He({overflowX:"hidden",overflowY:"auto"})},renderSkeleton:function(){this.el.addClass("fc-list-view "+this.widgetContentClass),this.scroller.render(),this.scroller.el.appendTo(this.el),this.grid.setElement(this.scroller.scrollEl)},unrenderSkeleton:function(){this.scroller.destroy()},setHeight:function(t,e){this.scroller.setHeight(this.computeScrollerHeight(t))},computeScrollerHeight:function(t){return t-h(this.el,this.scroller.el)},renderDates:function(){this.grid.setRange(this.renderRange)},renderEvents:function(t){this.grid.renderEvents(t)},unrenderEvents:function(){this.grid.unrenderEvents()},isEventResizable:function(t){return!1},isEventDraggable:function(t){return!1}}),Ye=be.extend({segSelector:".fc-list-item",hasDayInteractions:!1,spanToSegs:function(t){for(var e,n=this.view,i=n.renderRange.start.clone().time(0),r=0,s=[];i<n.renderRange.end;)if(e=z(t,{start:i,end:i.clone().add(1,"day")}),e&&(e.dayIndex=r,s.push(e)),i.add(1,"day"),r++,e&&!e.isEnd&&t.end.hasTime()&&t.end<i.clone().add(this.view.nextDayThreshold)){e.end=t.end.clone(),e.isEnd=!0;break}return s},computeEventTimeFormat:function(){return this.view.opt("mediumTimeFormat")},handleSegClick:function(e,n){var i;be.prototype.handleSegClick.apply(this,arguments),t(n.target).closest("a[href]").length||(i=e.event.url)&&!n.isDefaultPrevented()&&(window.location.href=i)},renderFgSegs:function(t){return t=this.renderFgSegEls(t),t.length?this.renderSegList(t):this.renderEmptyMessage(),t},renderEmptyMessage:function(){this.el.html('<div class="fc-list-empty-wrap2"><div class="fc-list-empty-wrap1"><div class="fc-list-empty">'+ht(this.view.opt("noEventsMessage"))+"</div></div></div>")},renderSegList:function(e){var n,i,r,s=this.groupSegsByDay(e),o=t('<table class="fc-list-table"><tbody/></table>'),a=o.find("tbody");for(n=0;n<s.length;n++)if(i=s[n])for(a.append(this.dayHeaderHtml(this.view.renderRange.start.clone().add(n,"days"))),this.sortEventSegs(i),r=0;r<i.length;r++)a.append(i[r].el);this.el.empty().append(o)},groupSegsByDay:function(t){var e,n,i=[];for(e=0;e<t.length;e++)n=t[e],(i[n.dayIndex]||(i[n.dayIndex]=[])).push(n);return i},dayHeaderHtml:function(t){var e=this.view,n=e.opt("listDayFormat"),i=e.opt("listDayAltFormat");return'<tr class="fc-list-heading" data-date="'+t.format("YYYY-MM-DD")+'"><td class="'+e.widgetHeaderClass+'" colspan="3">'+(n?e.buildGotoAnchorHtml(t,{class:"fc-list-heading-main"},ht(t.format(n))):"")+(i?e.buildGotoAnchorHtml(t,{class:"fc-list-heading-alt"},ht(t.format(i))):"")+"</td></tr>"},fgSegHtml:function(t){var e,n=this.view,i=["fc-list-item"].concat(this.getSegCustomClasses(t)),r=this.getSegBackgroundColor(t),s=t.event,o=s.url;return e=s.allDay?n.getAllDayHtml():n.isMultiDayEvent(s)?t.isStart||t.isEnd?ht(this.getEventTimeText(t)):n.getAllDayHtml():ht(this.getEventTimeText(s)),o&&i.push("fc-has-url"),'<tr class="'+i.join(" ")+'">'+(this.displayEventTime?'<td class="fc-list-item-time '+n.widgetContentClass+'">'+(e||"")+"</td>":"")+'<td class="fc-list-item-marker '+n.widgetContentClass+'"><span class="fc-event-dot"'+(r?' style="background-color:'+r+'"':"")+'></span></td><td class="fc-list-item-title '+n.widgetContentClass+'"><a'+(o?' href="'+ht(o)+'"':"")+">"+ht(t.event.title||"")+"</a></td></tr>"}});return $t.list={class:We,buttonTextKey:"list",defaults:{buttonText:"list",listDayFormat:"LL",noEventsMessage:"No events to display"}},$t.listDay={type:"list",duration:{days:1},defaults:{listDayFormat:"dddd"}},$t.listWeek={type:"list",duration:{weeks:1},defaults:{listDayFormat:"dddd",listDayAltFormat:"LL"}},$t.listMonth={type:"list",duration:{month:1},defaults:{listDayAltFormat:"dddd"}},$t.listYear={type:"list",duration:{year:1},defaults:{listDayAltFormat:"dddd"}},Zt}); | |
11 | 6 | \ No newline at end of file |
7 | + | |
8 | +(function(factory) { | |
9 | + if (typeof define === 'function' && define.amd) { | |
10 | + define([ 'jquery', 'moment' ], factory); | |
11 | + } | |
12 | + else if (typeof exports === 'object') { // Node/CommonJS | |
13 | + module.exports = factory(require('jquery'), require('moment')); | |
14 | + } | |
15 | + else { | |
16 | + factory(jQuery, moment); | |
17 | + } | |
18 | +})(function($, moment) { | |
19 | + | |
20 | +;; | |
21 | + | |
22 | +var fc = $.fullCalendar = { version: "2.3.1" }; | |
23 | +var fcViews = fc.views = {}; | |
24 | + | |
25 | + | |
26 | +$.fn.fullCalendar = function(options) { | |
27 | + var args = Array.prototype.slice.call(arguments, 1); // for a possible method call | |
28 | + var res = this; // what this function will return (this jQuery object by default) | |
29 | + | |
30 | + this.each(function(i, _element) { // loop each DOM element involved | |
31 | + var element = $(_element); | |
32 | + var calendar = element.data('fullCalendar'); // get the existing calendar object (if any) | |
33 | + var singleRes; // the returned value of this single method call | |
34 | + | |
35 | + // a method call | |
36 | + if (typeof options === 'string') { | |
37 | + if (calendar && $.isFunction(calendar[options])) { | |
38 | + singleRes = calendar[options].apply(calendar, args); | |
39 | + if (!i) { | |
40 | + res = singleRes; // record the first method call result | |
41 | + } | |
42 | + if (options === 'destroy') { // for the destroy method, must remove Calendar object data | |
43 | + element.removeData('fullCalendar'); | |
44 | + } | |
45 | + } | |
46 | + } | |
47 | + // a new calendar initialization | |
48 | + else if (!calendar) { // don't initialize twice | |
49 | + calendar = new fc.CalendarBase(element, options); | |
50 | + element.data('fullCalendar', calendar); | |
51 | + calendar.render(); | |
52 | + } | |
53 | + }); | |
54 | + | |
55 | + return res; | |
56 | +}; | |
57 | + | |
58 | + | |
59 | +var complexOptions = [ // names of options that are objects whose properties should be combined | |
60 | + 'header', | |
61 | + 'buttonText', | |
62 | + 'buttonIcons', | |
63 | + 'themeButtonIcons' | |
64 | +]; | |
65 | + | |
66 | + | |
67 | +// Recursively combines all passed-in option-hash arguments into a new single option-hash. | |
68 | +// Given option-hashes are ordered from lowest to highest priority. | |
69 | +function mergeOptions() { | |
70 | + var chain = Array.prototype.slice.call(arguments); // convert to a real array | |
71 | + var complexVals = {}; // hash for each complex option's combined values | |
72 | + var i, name; | |
73 | + var combinedVal; | |
74 | + var j; | |
75 | + var val; | |
76 | + | |
77 | + // for each complex option, loop through each option-hash and accumulate the combined values | |
78 | + for (i = 0; i < complexOptions.length; i++) { | |
79 | + name = complexOptions[i]; | |
80 | + combinedVal = null; // an object holding the merge of all the values | |
81 | + | |
82 | + for (j = 0; j < chain.length; j++) { | |
83 | + val = chain[j][name]; | |
84 | + | |
85 | + if ($.isPlainObject(val)) { | |
86 | + combinedVal = $.extend(combinedVal || {}, val); // merge new properties | |
87 | + } | |
88 | + else if (val != null) { // a non-null non-undefined atomic option | |
89 | + combinedVal = null; // signal to use the atomic value | |
90 | + } | |
91 | + } | |
92 | + | |
93 | + // if not null, the final value was a combination of other objects. record it | |
94 | + if (combinedVal !== null) { | |
95 | + complexVals[name] = combinedVal; | |
96 | + } | |
97 | + } | |
98 | + | |
99 | + chain.unshift({}); // $.extend will mutate this with the result | |
100 | + chain.push(complexVals); // computed complex values are applied last | |
101 | + return $.extend.apply($, chain); // combine | |
102 | +} | |
103 | + | |
104 | + | |
105 | +// Given options specified for the calendar's constructor, massages any legacy options into a non-legacy form. | |
106 | +// Converts View-Option-Hashes into the View-Specific-Options format. | |
107 | +function massageOverrides(input) { | |
108 | + var overrides = { views: input.views || {} }; // the output. ensure a `views` hash | |
109 | + var subObj; | |
110 | + | |
111 | + // iterate through all option override properties (except `views`) | |
112 | + $.each(input, function(name, val) { | |
113 | + if (name != 'views') { | |
114 | + | |
115 | + // could the value be a legacy View-Option-Hash? | |
116 | + if ( | |
117 | + $.isPlainObject(val) && | |
118 | + !/(time|duration|interval)$/i.test(name) && // exclude duration options. might be given as objects | |
119 | + $.inArray(name, complexOptions) == -1 // complex options aren't allowed to be View-Option-Hashes | |
120 | + ) { | |
121 | + subObj = null; | |
122 | + | |
123 | + // iterate through the properties of this possible View-Option-Hash value | |
124 | + $.each(val, function(subName, subVal) { | |
125 | + | |
126 | + // is the property targeting a view? | |
127 | + if (/^(month|week|day|default|basic(Week|Day)?|agenda(Week|Day)?)$/.test(subName)) { | |
128 | + if (!overrides.views[subName]) { // ensure the view-target entry exists | |
129 | + overrides.views[subName] = {}; | |
130 | + } | |
131 | + overrides.views[subName][name] = subVal; // record the value in the `views` object | |
132 | + } | |
133 | + else { // a non-View-Option-Hash property | |
134 | + if (!subObj) { | |
135 | + subObj = {}; | |
136 | + } | |
137 | + subObj[subName] = subVal; // accumulate these unrelated values for later | |
138 | + } | |
139 | + }); | |
140 | + | |
141 | + if (subObj) { // non-View-Option-Hash properties? transfer them as-is | |
142 | + overrides[name] = subObj; | |
143 | + } | |
144 | + } | |
145 | + else { | |
146 | + overrides[name] = val; // transfer normal options as-is | |
147 | + } | |
148 | + } | |
149 | + }); | |
150 | + | |
151 | + return overrides; | |
152 | +} | |
153 | + | |
154 | +;; | |
155 | + | |
156 | +// exports | |
157 | +fc.intersectionToSeg = intersectionToSeg; | |
158 | +fc.applyAll = applyAll; | |
159 | +fc.debounce = debounce; | |
160 | +fc.isInt = isInt; | |
161 | +fc.htmlEscape = htmlEscape; | |
162 | +fc.cssToStr = cssToStr; | |
163 | +fc.proxy = proxy; | |
164 | + | |
165 | + | |
166 | +/* FullCalendar-specific DOM Utilities | |
167 | +----------------------------------------------------------------------------------------------------------------------*/ | |
168 | + | |
169 | + | |
170 | +// Given the scrollbar widths of some other container, create borders/margins on rowEls in order to match the left | |
171 | +// and right space that was offset by the scrollbars. A 1-pixel border first, then margin beyond that. | |
172 | +function compensateScroll(rowEls, scrollbarWidths) { | |
173 | + if (scrollbarWidths.left) { | |
174 | + rowEls.css({ | |
175 | + 'border-left-width': 1, | |
176 | + 'margin-left': scrollbarWidths.left - 1 | |
177 | + }); | |
178 | + } | |
179 | + if (scrollbarWidths.right) { | |
180 | + rowEls.css({ | |
181 | + 'border-right-width': 1, | |
182 | + 'margin-right': scrollbarWidths.right - 1 | |
183 | + }); | |
184 | + } | |
185 | +} | |
186 | + | |
187 | + | |
188 | +// Undoes compensateScroll and restores all borders/margins | |
189 | +function uncompensateScroll(rowEls) { | |
190 | + rowEls.css({ | |
191 | + 'margin-left': '', | |
192 | + 'margin-right': '', | |
193 | + 'border-left-width': '', | |
194 | + 'border-right-width': '' | |
195 | + }); | |
196 | +} | |
197 | + | |
198 | + | |
199 | +// Make the mouse cursor express that an event is not allowed in the current area | |
200 | +function disableCursor() { | |
201 | + $('body').addClass('fc-not-allowed'); | |
202 | +} | |
203 | + | |
204 | + | |
205 | +// Returns the mouse cursor to its original look | |
206 | +function enableCursor() { | |
207 | + $('body').removeClass('fc-not-allowed'); | |
208 | +} | |
209 | + | |
210 | + | |
211 | +// Given a total available height to fill, have `els` (essentially child rows) expand to accomodate. | |
212 | +// By default, all elements that are shorter than the recommended height are expanded uniformly, not considering | |
213 | +// any other els that are already too tall. if `shouldRedistribute` is on, it considers these tall rows and | |
214 | +// reduces the available height. | |
215 | +function distributeHeight(els, availableHeight, shouldRedistribute) { | |
216 | + | |
217 | + // *FLOORING NOTE*: we floor in certain places because zoom can give inaccurate floating-point dimensions, | |
218 | + // and it is better to be shorter than taller, to avoid creating unnecessary scrollbars. | |
219 | + | |
220 | + var minOffset1 = Math.floor(availableHeight / els.length); // for non-last element | |
221 | + var minOffset2 = Math.floor(availableHeight - minOffset1 * (els.length - 1)); // for last element *FLOORING NOTE* | |
222 | + var flexEls = []; // elements that are allowed to expand. array of DOM nodes | |
223 | + var flexOffsets = []; // amount of vertical space it takes up | |
224 | + var flexHeights = []; // actual css height | |
225 | + var usedHeight = 0; | |
226 | + | |
227 | + undistributeHeight(els); // give all elements their natural height | |
228 | + | |
229 | + // find elements that are below the recommended height (expandable). | |
230 | + // important to query for heights in a single first pass (to avoid reflow oscillation). | |
231 | + els.each(function(i, el) { | |
232 | + var minOffset = i === els.length - 1 ? minOffset2 : minOffset1; | |
233 | + var naturalOffset = $(el).outerHeight(true); | |
234 | + | |
235 | + if (naturalOffset < minOffset) { | |
236 | + flexEls.push(el); | |
237 | + flexOffsets.push(naturalOffset); | |
238 | + flexHeights.push($(el).height()); | |
239 | + } | |
240 | + else { | |
241 | + // this element stretches past recommended height (non-expandable). mark the space as occupied. | |
242 | + usedHeight += naturalOffset; | |
243 | + } | |
244 | + }); | |
245 | + | |
246 | + // readjust the recommended height to only consider the height available to non-maxed-out rows. | |
247 | + if (shouldRedistribute) { | |
248 | + availableHeight -= usedHeight; | |
249 | + minOffset1 = Math.floor(availableHeight / flexEls.length); | |
250 | + minOffset2 = Math.floor(availableHeight - minOffset1 * (flexEls.length - 1)); // *FLOORING NOTE* | |
251 | + } | |
252 | + | |
253 | + // assign heights to all expandable elements | |
254 | + $(flexEls).each(function(i, el) { | |
255 | + var minOffset = i === flexEls.length - 1 ? minOffset2 : minOffset1; | |
256 | + var naturalOffset = flexOffsets[i]; | |
257 | + var naturalHeight = flexHeights[i]; | |
258 | + var newHeight = minOffset - (naturalOffset - naturalHeight); // subtract the margin/padding | |
259 | + | |
260 | + if (naturalOffset < minOffset) { // we check this again because redistribution might have changed things | |
261 | + $(el).height(newHeight); | |
262 | + } | |
263 | + }); | |
264 | +} | |
265 | + | |
266 | + | |
267 | +// Undoes distrubuteHeight, restoring all els to their natural height | |
268 | +function undistributeHeight(els) { | |
269 | + els.height(''); | |
270 | +} | |
271 | + | |
272 | + | |
273 | +// Given `els`, a jQuery set of <td> cells, find the cell with the largest natural width and set the widths of all the | |
274 | +// cells to be that width. | |
275 | +// PREREQUISITE: if you want a cell to take up width, it needs to have a single inner element w/ display:inline | |
276 | +function matchCellWidths(els) { | |
277 | + var maxInnerWidth = 0; | |
278 | + | |
279 | + els.find('> *').each(function(i, innerEl) { | |
280 | + var innerWidth = $(innerEl).outerWidth(); | |
281 | + if (innerWidth > maxInnerWidth) { | |
282 | + maxInnerWidth = innerWidth; | |
283 | + } | |
284 | + }); | |
285 | + | |
286 | + maxInnerWidth++; // sometimes not accurate of width the text needs to stay on one line. insurance | |
287 | + | |
288 | + els.width(maxInnerWidth); | |
289 | + | |
290 | + return maxInnerWidth; | |
291 | +} | |
292 | + | |
293 | + | |
294 | +// Turns a container element into a scroller if its contents is taller than the allotted height. | |
295 | +// Returns true if the element is now a scroller, false otherwise. | |
296 | +// NOTE: this method is best because it takes weird zooming dimensions into account | |
297 | +function setPotentialScroller(containerEl, height) { | |
298 | + containerEl.height(height).addClass('fc-scroller'); | |
299 | + | |
300 | + // are scrollbars needed? | |
301 | + if (containerEl[0].scrollHeight - 1 > containerEl[0].clientHeight) { // !!! -1 because IE is often off-by-one :( | |
302 | + return true; | |
303 | + } | |
304 | + | |
305 | + unsetScroller(containerEl); // undo | |
306 | + return false; | |
307 | +} | |
308 | + | |
309 | + | |
310 | +// Takes an element that might have been a scroller, and turns it back into a normal element. | |
311 | +function unsetScroller(containerEl) { | |
312 | + containerEl.height('').removeClass('fc-scroller'); | |
313 | +} | |
314 | + | |
315 | + | |
316 | +/* General DOM Utilities | |
317 | +----------------------------------------------------------------------------------------------------------------------*/ | |
318 | + | |
319 | +fc.getClientRect = getClientRect; | |
320 | +fc.getContentRect = getContentRect; | |
321 | +fc.getScrollbarWidths = getScrollbarWidths; | |
322 | + | |
323 | + | |
324 | +// borrowed from https://github.com/jquery/jquery-ui/blob/1.11.0/ui/core.js#L51 | |
325 | +function getScrollParent(el) { | |
326 | + var position = el.css('position'), | |
327 | + scrollParent = el.parents().filter(function() { | |
328 | + var parent = $(this); | |
329 | + return (/(auto|scroll)/).test( | |
330 | + parent.css('overflow') + parent.css('overflow-y') + parent.css('overflow-x') | |
331 | + ); | |
332 | + }).eq(0); | |
333 | + | |
334 | + return position === 'fixed' || !scrollParent.length ? $(el[0].ownerDocument || document) : scrollParent; | |
335 | +} | |
336 | + | |
337 | + | |
338 | +// Queries the outer bounding area of a jQuery element. | |
339 | +// Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive). | |
340 | +function getOuterRect(el) { | |
341 | + var offset = el.offset(); | |
342 | + | |
343 | + return { | |
344 | + left: offset.left, | |
345 | + right: offset.left + el.outerWidth(), | |
346 | + top: offset.top, | |
347 | + bottom: offset.top + el.outerHeight() | |
348 | + }; | |
349 | +} | |
350 | + | |
351 | + | |
352 | +// Queries the area within the margin/border/scrollbars of a jQuery element. Does not go within the padding. | |
353 | +// Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive). | |
354 | +// NOTE: should use clientLeft/clientTop, but very unreliable cross-browser. | |
355 | +function getClientRect(el) { | |
356 | + var offset = el.offset(); | |
357 | + var scrollbarWidths = getScrollbarWidths(el); | |
358 | + var left = offset.left + getCssFloat(el, 'border-left-width') + scrollbarWidths.left; | |
359 | + var top = offset.top + getCssFloat(el, 'border-top-width') + scrollbarWidths.top; | |
360 | + | |
361 | + return { | |
362 | + left: left, | |
363 | + right: left + el[0].clientWidth, // clientWidth includes padding but NOT scrollbars | |
364 | + top: top, | |
365 | + bottom: top + el[0].clientHeight // clientHeight includes padding but NOT scrollbars | |
366 | + }; | |
367 | +} | |
368 | + | |
369 | + | |
370 | +// Queries the area within the margin/border/padding of a jQuery element. Assumed not to have scrollbars. | |
371 | +// Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive). | |
372 | +function getContentRect(el) { | |
373 | + var offset = el.offset(); // just outside of border, margin not included | |
374 | + var left = offset.left + getCssFloat(el, 'border-left-width') + getCssFloat(el, 'padding-left'); | |
375 | + var top = offset.top + getCssFloat(el, 'border-top-width') + getCssFloat(el, 'padding-top'); | |
376 | + | |
377 | + return { | |
378 | + left: left, | |
379 | + right: left + el.width(), | |
380 | + top: top, | |
381 | + bottom: top + el.height() | |
382 | + }; | |
383 | +} | |
384 | + | |
385 | + | |
386 | +// Returns the computed left/right/top/bottom scrollbar widths for the given jQuery element. | |
387 | +// NOTE: should use clientLeft/clientTop, but very unreliable cross-browser. | |
388 | +function getScrollbarWidths(el) { | |
389 | + var leftRightWidth = el.innerWidth() - el[0].clientWidth; // the paddings cancel out, leaving the scrollbars | |
390 | + var widths = { | |
391 | + left: 0, | |
392 | + right: 0, | |
393 | + top: 0, | |
394 | + bottom: el.innerHeight() - el[0].clientHeight // the paddings cancel out, leaving the bottom scrollbar | |
395 | + }; | |
396 | + | |
397 | + if (getIsLeftRtlScrollbars() && el.css('direction') == 'rtl') { // is the scrollbar on the left side? | |
398 | + widths.left = leftRightWidth; | |
399 | + } | |
400 | + else { | |
401 | + widths.right = leftRightWidth; | |
402 | + } | |
403 | + | |
404 | + return widths; | |
405 | +} | |
406 | + | |
407 | + | |
408 | +// Logic for determining if, when the element is right-to-left, the scrollbar appears on the left side | |
409 | + | |
410 | +var _isLeftRtlScrollbars = null; | |
411 | + | |
412 | +function getIsLeftRtlScrollbars() { // responsible for caching the computation | |
413 | + if (_isLeftRtlScrollbars === null) { | |
414 | + _isLeftRtlScrollbars = computeIsLeftRtlScrollbars(); | |
415 | + } | |
416 | + return _isLeftRtlScrollbars; | |
417 | +} | |
418 | + | |
419 | +function computeIsLeftRtlScrollbars() { // creates an offscreen test element, then removes it | |
420 | + var el = $('<div><div/></div>') | |
421 | + .css({ | |
422 | + position: 'absolute', | |
423 | + top: -1000, | |
424 | + left: 0, | |
425 | + border: 0, | |
426 | + padding: 0, | |
427 | + overflow: 'scroll', | |
428 | + direction: 'rtl' | |
429 | + }) | |
430 | + .appendTo('body'); | |
431 | + var innerEl = el.children(); | |
432 | + var res = innerEl.offset().left > el.offset().left; // is the inner div shifted to accommodate a left scrollbar? | |
433 | + el.remove(); | |
434 | + return res; | |
435 | +} | |
436 | + | |
437 | + | |
438 | +// Retrieves a jQuery element's computed CSS value as a floating-point number. | |
439 | +// If the queried value is non-numeric (ex: IE can return "medium" for border width), will just return zero. | |
440 | +function getCssFloat(el, prop) { | |
441 | + return parseFloat(el.css(prop)) || 0; | |
442 | +} | |
443 | + | |
444 | + | |
445 | +// Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac) | |
446 | +function isPrimaryMouseButton(ev) { | |
447 | + return ev.which == 1 && !ev.ctrlKey; | |
448 | +} | |
449 | + | |
450 | + | |
451 | +/* Geometry | |
452 | +----------------------------------------------------------------------------------------------------------------------*/ | |
453 | + | |
454 | + | |
455 | +// Returns a new rectangle that is the intersection of the two rectangles. If they don't intersect, returns false | |
456 | +function intersectRects(rect1, rect2) { | |
457 | + var res = { | |
458 | + left: Math.max(rect1.left, rect2.left), | |
459 | + right: Math.min(rect1.right, rect2.right), | |
460 | + top: Math.max(rect1.top, rect2.top), | |
461 | + bottom: Math.min(rect1.bottom, rect2.bottom) | |
462 | + }; | |
463 | + | |
464 | + if (res.left < res.right && res.top < res.bottom) { | |
465 | + return res; | |
466 | + } | |
467 | + return false; | |
468 | +} | |
469 | + | |
470 | + | |
471 | +// Returns a new point that will have been moved to reside within the given rectangle | |
472 | +function constrainPoint(point, rect) { | |
473 | + return { | |
474 | + left: Math.min(Math.max(point.left, rect.left), rect.right), | |
475 | + top: Math.min(Math.max(point.top, rect.top), rect.bottom) | |
476 | + }; | |
477 | +} | |
478 | + | |
479 | + | |
480 | +// Returns a point that is the center of the given rectangle | |
481 | +function getRectCenter(rect) { | |
482 | + return { | |
483 | + left: (rect.left + rect.right) / 2, | |
484 | + top: (rect.top + rect.bottom) / 2 | |
485 | + }; | |
486 | +} | |
487 | + | |
488 | + | |
489 | +// Subtracts point2's coordinates from point1's coordinates, returning a delta | |
490 | +function diffPoints(point1, point2) { | |
491 | + return { | |
492 | + left: point1.left - point2.left, | |
493 | + top: point1.top - point2.top | |
494 | + }; | |
495 | +} | |
496 | + | |
497 | + | |
498 | +/* FullCalendar-specific Misc Utilities | |
499 | +----------------------------------------------------------------------------------------------------------------------*/ | |
500 | + | |
501 | + | |
502 | +// Creates a basic segment with the intersection of the two ranges. Returns undefined if no intersection. | |
503 | +// Expects all dates to be normalized to the same timezone beforehand. | |
504 | +// TODO: move to date section? | |
505 | +function intersectionToSeg(subjectRange, constraintRange) { | |
506 | + var subjectStart = subjectRange.start; | |
507 | + var subjectEnd = subjectRange.end; | |
508 | + var constraintStart = constraintRange.start; | |
509 | + var constraintEnd = constraintRange.end; | |
510 | + var segStart, segEnd; | |
511 | + var isStart, isEnd; | |
512 | + | |
513 | + if (subjectEnd > constraintStart && subjectStart < constraintEnd) { // in bounds at all? | |
514 | + | |
515 | + if (subjectStart >= constraintStart) { | |
516 | + segStart = subjectStart.clone(); | |
517 | + isStart = true; | |
518 | + } | |
519 | + else { | |
520 | + segStart = constraintStart.clone(); | |
521 | + isStart = false; | |
522 | + } | |
523 | + | |
524 | + if (subjectEnd <= constraintEnd) { | |
525 | + segEnd = subjectEnd.clone(); | |
526 | + isEnd = true; | |
527 | + } | |
528 | + else { | |
529 | + segEnd = constraintEnd.clone(); | |
530 | + isEnd = false; | |
531 | + } | |
532 | + | |
533 | + return { | |
534 | + start: segStart, | |
535 | + end: segEnd, | |
536 | + isStart: isStart, | |
537 | + isEnd: isEnd | |
538 | + }; | |
539 | + } | |
540 | +} | |
541 | + | |
542 | + | |
543 | +/* Date Utilities | |
544 | +----------------------------------------------------------------------------------------------------------------------*/ | |
545 | + | |
546 | +fc.computeIntervalUnit = computeIntervalUnit; | |
547 | +fc.durationHasTime = durationHasTime; | |
548 | + | |
549 | +var dayIDs = [ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ]; | |
550 | +var intervalUnits = [ 'year', 'month', 'week', 'day', 'hour', 'minute', 'second', 'millisecond' ]; | |
551 | + | |
552 | + | |
553 | +// Diffs the two moments into a Duration where full-days are recorded first, then the remaining time. | |
554 | +// Moments will have their timezones normalized. | |
555 | +function diffDayTime(a, b) { | |
556 | + return moment.duration({ | |
557 | + days: a.clone().stripTime().diff(b.clone().stripTime(), 'days'), | |
558 | + ms: a.time() - b.time() // time-of-day from day start. disregards timezone | |
559 | + }); | |
560 | +} | |
561 | + | |
562 | + | |
563 | +// Diffs the two moments via their start-of-day (regardless of timezone). Produces whole-day durations. | |
564 | +function diffDay(a, b) { | |
565 | + return moment.duration({ | |
566 | + days: a.clone().stripTime().diff(b.clone().stripTime(), 'days') | |
567 | + }); | |
568 | +} | |
569 | + | |
570 | + | |
571 | +// Diffs two moments, producing a duration, made of a whole-unit-increment of the given unit. Uses rounding. | |
572 | +function diffByUnit(a, b, unit) { | |
573 | + return moment.duration( | |
574 | + Math.round(a.diff(b, unit, true)), // returnFloat=true | |
575 | + unit | |
576 | + ); | |
577 | +} | |
578 | + | |
579 | + | |
580 | +// Computes the unit name of the largest whole-unit period of time. | |
581 | +// For example, 48 hours will be "days" whereas 49 hours will be "hours". | |
582 | +// Accepts start/end, a range object, or an original duration object. | |
583 | +function computeIntervalUnit(start, end) { | |
584 | + var i, unit; | |
585 | + var val; | |
586 | + | |
587 | + for (i = 0; i < intervalUnits.length; i++) { | |
588 | + unit = intervalUnits[i]; | |
589 | + val = computeRangeAs(unit, start, end); | |
590 | + | |
591 | + if (val >= 1 && isInt(val)) { | |
592 | + break; | |
593 | + } | |
594 | + } | |
595 | + | |
596 | + return unit; // will be "milliseconds" if nothing else matches | |
597 | +} | |
598 | + | |
599 | + | |
600 | +// Computes the number of units (like "hours") in the given range. | |
601 | +// Range can be a {start,end} object, separate start/end args, or a Duration. | |
602 | +// Results are based on Moment's .as() and .diff() methods, so results can depend on internal handling | |
603 | +// of month-diffing logic (which tends to vary from version to version). | |
604 | +function computeRangeAs(unit, start, end) { | |
605 | + | |
606 | + if (end != null) { // given start, end | |
607 | + return end.diff(start, unit, true); | |
608 | + } | |
609 | + else if (moment.isDuration(start)) { // given duration | |
610 | + return start.as(unit); | |
611 | + } | |
612 | + else { // given { start, end } range object | |
613 | + return start.end.diff(start.start, unit, true); | |
614 | + } | |
615 | +} | |
616 | + | |
617 | + | |
618 | +// Returns a boolean about whether the given duration has any time parts (hours/minutes/seconds/ms) | |
619 | +function durationHasTime(dur) { | |
620 | + return Boolean(dur.hours() || dur.minutes() || dur.seconds() || dur.milliseconds()); | |
621 | +} | |
622 | + | |
623 | + | |
624 | +function isNativeDate(input) { | |
625 | + return Object.prototype.toString.call(input) === '[object Date]' || input instanceof Date; | |
626 | +} | |
627 | + | |
628 | + | |
629 | +// Returns a boolean about whether the given input is a time string, like "06:40:00" or "06:00" | |
630 | +function isTimeString(str) { | |
631 | + return /^\d+\:\d+(?:\:\d+\.?(?:\d{3})?)?$/.test(str); | |
632 | +} | |
633 | + | |
634 | + | |
635 | +/* General Utilities | |
636 | +----------------------------------------------------------------------------------------------------------------------*/ | |
637 | + | |
638 | +var hasOwnPropMethod = {}.hasOwnProperty; | |
639 | + | |
640 | + | |
641 | +// Create an object that has the given prototype. Just like Object.create | |
642 | +function createObject(proto) { | |
643 | + var f = function() {}; | |
644 | + f.prototype = proto; | |
645 | + return new f(); | |
646 | +} | |
647 | + | |
648 | + | |
649 | +function copyOwnProps(src, dest) { | |
650 | + for (var name in src) { | |
651 | + if (hasOwnProp(src, name)) { | |
652 | + dest[name] = src[name]; | |
653 | + } | |
654 | + } | |
655 | +} | |
656 | + | |
657 | + | |
658 | +// Copies over certain methods with the same names as Object.prototype methods. Overcomes an IE<=8 bug: | |
659 | +// https://developer.mozilla.org/en-US/docs/ECMAScript_DontEnum_attribute#JScript_DontEnum_Bug | |
660 | +function copyNativeMethods(src, dest) { | |
661 | + var names = [ 'constructor', 'toString', 'valueOf' ]; | |
662 | + var i, name; | |
663 | + | |
664 | + for (i = 0; i < names.length; i++) { | |
665 | + name = names[i]; | |
666 | + | |
667 | + if (src[name] !== Object.prototype[name]) { | |
668 | + dest[name] = src[name]; | |
669 | + } | |
670 | + } | |
671 | +} | |
672 | + | |
673 | + | |
674 | +function hasOwnProp(obj, name) { | |
675 | + return hasOwnPropMethod.call(obj, name); | |
676 | +} | |
677 | + | |
678 | + | |
679 | +// Is the given value a non-object non-function value? | |
680 | +function isAtomic(val) { | |
681 | + return /undefined|null|boolean|number|string/.test($.type(val)); | |
682 | +} | |
683 | + | |
684 | + | |
685 | +function applyAll(functions, thisObj, args) { | |
686 | + if ($.isFunction(functions)) { | |
687 | + functions = [ functions ]; | |
688 | + } | |
689 | + if (functions) { | |
690 | + var i; | |
691 | + var ret; | |
692 | + for (i=0; i<functions.length; i++) { | |
693 | + ret = functions[i].apply(thisObj, args) || ret; | |
694 | + } | |
695 | + return ret; | |
696 | + } | |
697 | +} | |
698 | + | |
699 | + | |
700 | +function firstDefined() { | |
701 | + for (var i=0; i<arguments.length; i++) { | |
702 | + if (arguments[i] !== undefined) { | |
703 | + return arguments[i]; | |
704 | + } | |
705 | + } | |
706 | +} | |
707 | + | |
708 | + | |
709 | +function htmlEscape(s) { | |
710 | + return (s + '').replace(/&/g, '&') | |
711 | + .replace(/</g, '<') | |
712 | + .replace(/>/g, '>') | |
713 | + .replace(/'/g, ''') | |
714 | + .replace(/"/g, '"') | |
715 | + .replace(/\n/g, '<br />'); | |
716 | +} | |
717 | + | |
718 | + | |
719 | +function stripHtmlEntities(text) { | |
720 | + return text.replace(/&.*?;/g, ''); | |
721 | +} | |
722 | + | |
723 | + | |
724 | +// Given a hash of CSS properties, returns a string of CSS. | |
725 | +// Uses property names as-is (no camel-case conversion). Will not make statements for null/undefined values. | |
726 | +function cssToStr(cssProps) { | |
727 | + var statements = []; | |
728 | + | |
729 | + $.each(cssProps, function(name, val) { | |
730 | + if (val != null) { | |
731 | + statements.push(name + ':' + val); | |
732 | + } | |
733 | + }); | |
734 | + | |
735 | + return statements.join(';'); | |
736 | +} | |
737 | + | |
738 | + | |
739 | +function capitaliseFirstLetter(str) { | |
740 | + return str.charAt(0).toUpperCase() + str.slice(1); | |
741 | +} | |
742 | + | |
743 | + | |
744 | +function compareNumbers(a, b) { // for .sort() | |
745 | + return a - b; | |
746 | +} | |
747 | + | |
748 | + | |
749 | +function isInt(n) { | |
750 | + return n % 1 === 0; | |
751 | +} | |
752 | + | |
753 | + | |
754 | +// Returns a method bound to the given object context. | |
755 | +// Just like one of the jQuery.proxy signatures, but without the undesired behavior of treating the same method with | |
756 | +// different contexts as identical when binding/unbinding events. | |
757 | +function proxy(obj, methodName) { | |
758 | + var method = obj[methodName]; | |
759 | + | |
760 | + return function() { | |
761 | + return method.apply(obj, arguments); | |
762 | + }; | |
763 | +} | |
764 | + | |
765 | + | |
766 | +// Returns a function, that, as long as it continues to be invoked, will not | |
767 | +// be triggered. The function will be called after it stops being called for | |
768 | +// N milliseconds. | |
769 | +// https://github.com/jashkenas/underscore/blob/1.6.0/underscore.js#L714 | |
770 | +function debounce(func, wait) { | |
771 | + var timeoutId; | |
772 | + var args; | |
773 | + var context; | |
774 | + var timestamp; // of most recent call | |
775 | + var later = function() { | |
776 | + var last = +new Date() - timestamp; | |
777 | + if (last < wait && last > 0) { | |
778 | + timeoutId = setTimeout(later, wait - last); | |
779 | + } | |
780 | + else { | |
781 | + timeoutId = null; | |
782 | + func.apply(context, args); | |
783 | + if (!timeoutId) { | |
784 | + context = args = null; | |
785 | + } | |
786 | + } | |
787 | + }; | |
788 | + | |
789 | + return function() { | |
790 | + context = this; | |
791 | + args = arguments; | |
792 | + timestamp = +new Date(); | |
793 | + if (!timeoutId) { | |
794 | + timeoutId = setTimeout(later, wait); | |
795 | + } | |
796 | + }; | |
797 | +} | |
798 | + | |
799 | +;; | |
800 | + | |
801 | +var ambigDateOfMonthRegex = /^\s*\d{4}-\d\d$/; | |
802 | +var ambigTimeOrZoneRegex = | |
803 | + /^\s*\d{4}-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?)?$/; | |
804 | +var newMomentProto = moment.fn; // where we will attach our new methods | |
805 | +var oldMomentProto = $.extend({}, newMomentProto); // copy of original moment methods | |
806 | +var allowValueOptimization; | |
807 | +var setUTCValues; // function defined below | |
808 | +var setLocalValues; // function defined below | |
809 | + | |
810 | + | |
811 | +// Creating | |
812 | +// ------------------------------------------------------------------------------------------------- | |
813 | + | |
814 | +// Creates a new moment, similar to the vanilla moment(...) constructor, but with | |
815 | +// extra features (ambiguous time, enhanced formatting). When given an existing moment, | |
816 | +// it will function as a clone (and retain the zone of the moment). Anything else will | |
817 | +// result in a moment in the local zone. | |
818 | +fc.moment = function() { | |
819 | + return makeMoment(arguments); | |
820 | +}; | |
821 | + | |
822 | +// Sames as fc.moment, but forces the resulting moment to be in the UTC timezone. | |
823 | +fc.moment.utc = function() { | |
824 | + var mom = makeMoment(arguments, true); | |
825 | + | |
826 | + // Force it into UTC because makeMoment doesn't guarantee it | |
827 | + // (if given a pre-existing moment for example) | |
828 | + if (mom.hasTime()) { // don't give ambiguously-timed moments a UTC zone | |
829 | + mom.utc(); | |
830 | + } | |
831 | + | |
832 | + return mom; | |
833 | +}; | |
834 | + | |
835 | +// Same as fc.moment, but when given an ISO8601 string, the timezone offset is preserved. | |
836 | +// ISO8601 strings with no timezone offset will become ambiguously zoned. | |
837 | +fc.moment.parseZone = function() { | |
838 | + return makeMoment(arguments, true, true); | |
839 | +}; | |
840 | + | |
841 | +// Builds an enhanced moment from args. When given an existing moment, it clones. When given a | |
842 | +// native Date, or called with no arguments (the current time), the resulting moment will be local. | |
843 | +// Anything else needs to be "parsed" (a string or an array), and will be affected by: | |
844 | +// parseAsUTC - if there is no zone information, should we parse the input in UTC? | |
845 | +// parseZone - if there is zone information, should we force the zone of the moment? | |
846 | +function makeMoment(args, parseAsUTC, parseZone) { | |
847 | + var input = args[0]; | |
848 | + var isSingleString = args.length == 1 && typeof input === 'string'; | |
849 | + var isAmbigTime; | |
850 | + var isAmbigZone; | |
851 | + var ambigMatch; | |
852 | + var mom; | |
853 | + | |
854 | + if (moment.isMoment(input)) { | |
855 | + mom = moment.apply(null, args); // clone it | |
856 | + transferAmbigs(input, mom); // the ambig flags weren't transfered with the clone | |
857 | + } | |
858 | + else if (isNativeDate(input) || input === undefined) { | |
859 | + mom = moment.apply(null, args); // will be local | |
860 | + } | |
861 | + else { // "parsing" is required | |
862 | + isAmbigTime = false; | |
863 | + isAmbigZone = false; | |
864 | + | |
865 | + if (isSingleString) { | |
866 | + if (ambigDateOfMonthRegex.test(input)) { | |
867 | + // accept strings like '2014-05', but convert to the first of the month | |
868 | + input += '-01'; | |
869 | + args = [ input ]; // for when we pass it on to moment's constructor | |
870 | + isAmbigTime = true; | |
871 | + isAmbigZone = true; | |
872 | + } | |
873 | + else if ((ambigMatch = ambigTimeOrZoneRegex.exec(input))) { | |
874 | + isAmbigTime = !ambigMatch[5]; // no time part? | |
875 | + isAmbigZone = true; | |
876 | + } | |
877 | + } | |
878 | + else if ($.isArray(input)) { | |
879 | + // arrays have no timezone information, so assume ambiguous zone | |
880 | + isAmbigZone = true; | |
881 | + } | |
882 | + // otherwise, probably a string with a format | |
883 | + | |
884 | + if (parseAsUTC || isAmbigTime) { | |
885 | + mom = moment.utc.apply(moment, args); | |
886 | + } | |
887 | + else { | |
888 | + mom = moment.apply(null, args); | |
889 | + } | |
890 | + | |
891 | + if (isAmbigTime) { | |
892 | + mom._ambigTime = true; | |
893 | + mom._ambigZone = true; // ambiguous time always means ambiguous zone | |
894 | + } | |
895 | + else if (parseZone) { // let's record the inputted zone somehow | |
896 | + if (isAmbigZone) { | |
897 | + mom._ambigZone = true; | |
898 | + } | |
899 | + else if (isSingleString) { | |
900 | + if (mom.utcOffset) { | |
901 | + mom.utcOffset(input); // if not a valid zone, will assign UTC | |
902 | + } | |
903 | + else { | |
904 | + mom.zone(input); // for moment-pre-2.9 | |
905 | + } | |
906 | + } | |
907 | + } | |
908 | + } | |
909 | + | |
910 | + mom._fullCalendar = true; // flag for extended functionality | |
911 | + | |
912 | + return mom; | |
913 | +} | |
914 | + | |
915 | + | |
916 | +// A clone method that works with the flags related to our enhanced functionality. | |
917 | +// In the future, use moment.momentProperties | |
918 | +newMomentProto.clone = function() { | |
919 | + var mom = oldMomentProto.clone.apply(this, arguments); | |
920 | + | |
921 | + // these flags weren't transfered with the clone | |
922 | + transferAmbigs(this, mom); | |
923 | + if (this._fullCalendar) { | |
924 | + mom._fullCalendar = true; | |
925 | + } | |
926 | + | |
927 | + return mom; | |
928 | +}; | |
929 | + | |
930 | + | |
931 | +// Week Number | |
932 | +// ------------------------------------------------------------------------------------------------- | |
933 | + | |
934 | + | |
935 | +// Returns the week number, considering the locale's custom week number calcuation | |
936 | +// `weeks` is an alias for `week` | |
937 | +newMomentProto.week = newMomentProto.weeks = function(input) { | |
938 | + var weekCalc = (this._locale || this._lang) // works pre-moment-2.8 | |
939 | + ._fullCalendar_weekCalc; | |
940 | + | |
941 | + if (input == null && typeof weekCalc === 'function') { // custom function only works for getter | |
942 | + return weekCalc(this); | |
943 | + } | |
944 | + else if (weekCalc === 'ISO') { | |
945 | + return oldMomentProto.isoWeek.apply(this, arguments); // ISO getter/setter | |
946 | + } | |
947 | + | |
948 | + return oldMomentProto.week.apply(this, arguments); // local getter/setter | |
949 | +}; | |
950 | + | |
951 | + | |
952 | +// Time-of-day | |
953 | +// ------------------------------------------------------------------------------------------------- | |
954 | + | |
955 | +// GETTER | |
956 | +// Returns a Duration with the hours/minutes/seconds/ms values of the moment. | |
957 | +// If the moment has an ambiguous time, a duration of 00:00 will be returned. | |
958 | +// | |
959 | +// SETTER | |
960 | +// You can supply a Duration, a Moment, or a Duration-like argument. | |
961 | +// When setting the time, and the moment has an ambiguous time, it then becomes unambiguous. | |
962 | +newMomentProto.time = function(time) { | |
963 | + | |
964 | + // Fallback to the original method (if there is one) if this moment wasn't created via FullCalendar. | |
965 | + // `time` is a generic enough method name where this precaution is necessary to avoid collisions w/ other plugins. | |
966 | + if (!this._fullCalendar) { | |
967 | + return oldMomentProto.time.apply(this, arguments); | |
968 | + } | |
969 | + | |
970 | + if (time == null) { // getter | |
971 | + return moment.duration({ | |
972 | + hours: this.hours(), | |
973 | + minutes: this.minutes(), | |
974 | + seconds: this.seconds(), | |
975 | + milliseconds: this.milliseconds() | |
976 | + }); | |
977 | + } | |
978 | + else { // setter | |
979 | + | |
980 | + this._ambigTime = false; // mark that the moment now has a time | |
981 | + | |
982 | + if (!moment.isDuration(time) && !moment.isMoment(time)) { | |
983 | + time = moment.duration(time); | |
984 | + } | |
985 | + | |
986 | + // The day value should cause overflow (so 24 hours becomes 00:00:00 of next day). | |
987 | + // Only for Duration times, not Moment times. | |
988 | + var dayHours = 0; | |
989 | + if (moment.isDuration(time)) { | |
990 | + dayHours = Math.floor(time.asDays()) * 24; | |
991 | + } | |
992 | + | |
993 | + // We need to set the individual fields. | |
994 | + // Can't use startOf('day') then add duration. In case of DST at start of day. | |
995 | + return this.hours(dayHours + time.hours()) | |
996 | + .minutes(time.minutes()) | |
997 | + .seconds(time.seconds()) | |
998 | + .milliseconds(time.milliseconds()); | |
999 | + } | |
1000 | +}; | |
1001 | + | |
1002 | +// Converts the moment to UTC, stripping out its time-of-day and timezone offset, | |
1003 | +// but preserving its YMD. A moment with a stripped time will display no time | |
1004 | +// nor timezone offset when .format() is called. | |
1005 | +newMomentProto.stripTime = function() { | |
1006 | + var a; | |
1007 | + | |
1008 | + if (!this._ambigTime) { | |
1009 | + | |
1010 | + // get the values before any conversion happens | |
1011 | + a = this.toArray(); // array of y/m/d/h/m/s/ms | |
1012 | + | |
1013 | + // TODO: use keepLocalTime in the future | |
1014 | + this.utc(); // set the internal UTC flag (will clear the ambig flags) | |
1015 | + setUTCValues(this, a.slice(0, 3)); // set the year/month/date. time will be zero | |
1016 | + | |
1017 | + // Mark the time as ambiguous. This needs to happen after the .utc() call, which might call .utcOffset(), | |
1018 | + // which clears all ambig flags. Same with setUTCValues with moment-timezone. | |
1019 | + this._ambigTime = true; | |
1020 | + this._ambigZone = true; // if ambiguous time, also ambiguous timezone offset | |
1021 | + } | |
1022 | + | |
1023 | + return this; // for chaining | |
1024 | +}; | |
1025 | + | |
1026 | +// Returns if the moment has a non-ambiguous time (boolean) | |
1027 | +newMomentProto.hasTime = function() { | |
1028 | + return !this._ambigTime; | |
1029 | +}; | |
1030 | + | |
1031 | + | |
1032 | +// Timezone | |
1033 | +// ------------------------------------------------------------------------------------------------- | |
1034 | + | |
1035 | +// Converts the moment to UTC, stripping out its timezone offset, but preserving its | |
1036 | +// YMD and time-of-day. A moment with a stripped timezone offset will display no | |
1037 | +// timezone offset when .format() is called. | |
1038 | +// TODO: look into Moment's keepLocalTime functionality | |
1039 | +newMomentProto.stripZone = function() { | |
1040 | + var a, wasAmbigTime; | |
1041 | + | |
1042 | + if (!this._ambigZone) { | |
1043 | + | |
1044 | + // get the values before any conversion happens | |
1045 | + a = this.toArray(); // array of y/m/d/h/m/s/ms | |
1046 | + wasAmbigTime = this._ambigTime; | |
1047 | + | |
1048 | + this.utc(); // set the internal UTC flag (might clear the ambig flags, depending on Moment internals) | |
1049 | + setUTCValues(this, a); // will set the year/month/date/hours/minutes/seconds/ms | |
1050 | + | |
1051 | + // the above call to .utc()/.utcOffset() unfortunately might clear the ambig flags, so restore | |
1052 | + this._ambigTime = wasAmbigTime || false; | |
1053 | + | |
1054 | + // Mark the zone as ambiguous. This needs to happen after the .utc() call, which might call .utcOffset(), | |
1055 | + // which clears the ambig flags. Same with setUTCValues with moment-timezone. | |
1056 | + this._ambigZone = true; | |
1057 | + } | |
1058 | + | |
1059 | + return this; // for chaining | |
1060 | +}; | |
1061 | + | |
1062 | +// Returns of the moment has a non-ambiguous timezone offset (boolean) | |
1063 | +newMomentProto.hasZone = function() { | |
1064 | + return !this._ambigZone; | |
1065 | +}; | |
1066 | + | |
1067 | + | |
1068 | +// this method implicitly marks a zone | |
1069 | +newMomentProto.local = function() { | |
1070 | + var a = this.toArray(); // year,month,date,hours,minutes,seconds,ms as an array | |
1071 | + var wasAmbigZone = this._ambigZone; | |
1072 | + | |
1073 | + oldMomentProto.local.apply(this, arguments); | |
1074 | + | |
1075 | + // ensure non-ambiguous | |
1076 | + // this probably already happened via local() -> utcOffset(), but don't rely on Moment's internals | |
1077 | + this._ambigTime = false; | |
1078 | + this._ambigZone = false; | |
1079 | + | |
1080 | + if (wasAmbigZone) { | |
1081 | + // If the moment was ambiguously zoned, the date fields were stored as UTC. | |
1082 | + // We want to preserve these, but in local time. | |
1083 | + // TODO: look into Moment's keepLocalTime functionality | |
1084 | + setLocalValues(this, a); | |
1085 | + } | |
1086 | + | |
1087 | + return this; // for chaining | |
1088 | +}; | |
1089 | + | |
1090 | + | |
1091 | +// implicitly marks a zone | |
1092 | +newMomentProto.utc = function() { | |
1093 | + oldMomentProto.utc.apply(this, arguments); | |
1094 | + | |
1095 | + // ensure non-ambiguous | |
1096 | + // this probably already happened via utc() -> utcOffset(), but don't rely on Moment's internals | |
1097 | + this._ambigTime = false; | |
1098 | + this._ambigZone = false; | |
1099 | + | |
1100 | + return this; | |
1101 | +}; | |
1102 | + | |
1103 | + | |
1104 | +// methods for arbitrarily manipulating timezone offset. | |
1105 | +// should clear time/zone ambiguity when called. | |
1106 | +$.each([ | |
1107 | + 'zone', // only in moment-pre-2.9. deprecated afterwards | |
1108 | + 'utcOffset' | |
1109 | +], function(i, name) { | |
1110 | + if (oldMomentProto[name]) { // original method exists? | |
1111 | + | |
1112 | + // this method implicitly marks a zone (will probably get called upon .utc() and .local()) | |
1113 | + newMomentProto[name] = function(tzo) { | |
1114 | + | |
1115 | + if (tzo != null) { // setter | |
1116 | + // these assignments needs to happen before the original zone method is called. | |
1117 | + // I forget why, something to do with a browser crash. | |
1118 | + this._ambigTime = false; | |
1119 | + this._ambigZone = false; | |
1120 | + } | |
1121 | + | |
1122 | + return oldMomentProto[name].apply(this, arguments); | |
1123 | + }; | |
1124 | + } | |
1125 | +}); | |
1126 | + | |
1127 | + | |
1128 | +// Formatting | |
1129 | +// ------------------------------------------------------------------------------------------------- | |
1130 | + | |
1131 | +newMomentProto.format = function() { | |
1132 | + if (this._fullCalendar && arguments[0]) { // an enhanced moment? and a format string provided? | |
1133 | + return formatDate(this, arguments[0]); // our extended formatting | |
1134 | + } | |
1135 | + if (this._ambigTime) { | |
1136 | + return oldMomentFormat(this, 'YYYY-MM-DD'); | |
1137 | + } | |
1138 | + if (this._ambigZone) { | |
1139 | + return oldMomentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss'); | |
1140 | + } | |
1141 | + return oldMomentProto.format.apply(this, arguments); | |
1142 | +}; | |
1143 | + | |
1144 | +newMomentProto.toISOString = function() { | |
1145 | + if (this._ambigTime) { | |
1146 | + return oldMomentFormat(this, 'YYYY-MM-DD'); | |
1147 | + } | |
1148 | + if (this._ambigZone) { | |
1149 | + return oldMomentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss'); | |
1150 | + } | |
1151 | + return oldMomentProto.toISOString.apply(this, arguments); | |
1152 | +}; | |
1153 | + | |
1154 | + | |
1155 | +// Querying | |
1156 | +// ------------------------------------------------------------------------------------------------- | |
1157 | + | |
1158 | +// Is the moment within the specified range? `end` is exclusive. | |
1159 | +// FYI, this method is not a standard Moment method, so always do our enhanced logic. | |
1160 | +newMomentProto.isWithin = function(start, end) { | |
1161 | + var a = commonlyAmbiguate([ this, start, end ]); | |
1162 | + return a[0] >= a[1] && a[0] < a[2]; | |
1163 | +}; | |
1164 | + | |
1165 | +// When isSame is called with units, timezone ambiguity is normalized before the comparison happens. | |
1166 | +// If no units specified, the two moments must be identically the same, with matching ambig flags. | |
1167 | +newMomentProto.isSame = function(input, units) { | |
1168 | + var a; | |
1169 | + | |
1170 | + // only do custom logic if this is an enhanced moment | |
1171 | + if (!this._fullCalendar) { | |
1172 | + return oldMomentProto.isSame.apply(this, arguments); | |
1173 | + } | |
1174 | + | |
1175 | + if (units) { | |
1176 | + a = commonlyAmbiguate([ this, input ], true); // normalize timezones but don't erase times | |
1177 | + return oldMomentProto.isSame.call(a[0], a[1], units); | |
1178 | + } | |
1179 | + else { | |
1180 | + input = fc.moment.parseZone(input); // normalize input | |
1181 | + return oldMomentProto.isSame.call(this, input) && | |
1182 | + Boolean(this._ambigTime) === Boolean(input._ambigTime) && | |
1183 | + Boolean(this._ambigZone) === Boolean(input._ambigZone); | |
1184 | + } | |
1185 | +}; | |
1186 | + | |
1187 | +// Make these query methods work with ambiguous moments | |
1188 | +$.each([ | |
1189 | + 'isBefore', | |
1190 | + 'isAfter' | |
1191 | +], function(i, methodName) { | |
1192 | + newMomentProto[methodName] = function(input, units) { | |
1193 | + var a; | |
1194 | + | |
1195 | + // only do custom logic if this is an enhanced moment | |
1196 | + if (!this._fullCalendar) { | |
1197 | + return oldMomentProto[methodName].apply(this, arguments); | |
1198 | + } | |
1199 | + | |
1200 | + a = commonlyAmbiguate([ this, input ]); | |
1201 | + return oldMomentProto[methodName].call(a[0], a[1], units); | |
1202 | + }; | |
1203 | +}); | |
1204 | + | |
1205 | + | |
1206 | +// Misc Internals | |
1207 | +// ------------------------------------------------------------------------------------------------- | |
1208 | + | |
1209 | +// given an array of moment-like inputs, return a parallel array w/ moments similarly ambiguated. | |
1210 | +// for example, of one moment has ambig time, but not others, all moments will have their time stripped. | |
1211 | +// set `preserveTime` to `true` to keep times, but only normalize zone ambiguity. | |
1212 | +// returns the original moments if no modifications are necessary. | |
1213 | +function commonlyAmbiguate(inputs, preserveTime) { | |
1214 | + var anyAmbigTime = false; | |
1215 | + var anyAmbigZone = false; | |
1216 | + var len = inputs.length; | |
1217 | + var moms = []; | |
1218 | + var i, mom; | |
1219 | + | |
1220 | + // parse inputs into real moments and query their ambig flags | |
1221 | + for (i = 0; i < len; i++) { | |
1222 | + mom = inputs[i]; | |
1223 | + if (!moment.isMoment(mom)) { | |
1224 | + mom = fc.moment.parseZone(mom); | |
1225 | + } | |
1226 | + anyAmbigTime = anyAmbigTime || mom._ambigTime; | |
1227 | + anyAmbigZone = anyAmbigZone || mom._ambigZone; | |
1228 | + moms.push(mom); | |
1229 | + } | |
1230 | + | |
1231 | + // strip each moment down to lowest common ambiguity | |
1232 | + // use clones to avoid modifying the original moments | |
1233 | + for (i = 0; i < len; i++) { | |
1234 | + mom = moms[i]; | |
1235 | + if (!preserveTime && anyAmbigTime && !mom._ambigTime) { | |
1236 | + moms[i] = mom.clone().stripTime(); | |
1237 | + } | |
1238 | + else if (anyAmbigZone && !mom._ambigZone) { | |
1239 | + moms[i] = mom.clone().stripZone(); | |
1240 | + } | |
1241 | + } | |
1242 | + | |
1243 | + return moms; | |
1244 | +} | |
1245 | + | |
1246 | +// Transfers all the flags related to ambiguous time/zone from the `src` moment to the `dest` moment | |
1247 | +// TODO: look into moment.momentProperties for this. | |
1248 | +function transferAmbigs(src, dest) { | |
1249 | + if (src._ambigTime) { | |
1250 | + dest._ambigTime = true; | |
1251 | + } | |
1252 | + else if (dest._ambigTime) { | |
1253 | + dest._ambigTime = false; | |
1254 | + } | |
1255 | + | |
1256 | + if (src._ambigZone) { | |
1257 | + dest._ambigZone = true; | |
1258 | + } | |
1259 | + else if (dest._ambigZone) { | |
1260 | + dest._ambigZone = false; | |
1261 | + } | |
1262 | +} | |
1263 | + | |
1264 | + | |
1265 | +// Sets the year/month/date/etc values of the moment from the given array. | |
1266 | +// Inefficient because it calls each individual setter. | |
1267 | +function setMomentValues(mom, a) { | |
1268 | + mom.year(a[0] || 0) | |
1269 | + .month(a[1] || 0) | |
1270 | + .date(a[2] || 0) | |
1271 | + .hours(a[3] || 0) | |
1272 | + .minutes(a[4] || 0) | |
1273 | + .seconds(a[5] || 0) | |
1274 | + .milliseconds(a[6] || 0); | |
1275 | +} | |
1276 | + | |
1277 | +// Can we set the moment's internal date directly? | |
1278 | +allowValueOptimization = '_d' in moment() && 'updateOffset' in moment; | |
1279 | + | |
1280 | +// Utility function. Accepts a moment and an array of the UTC year/month/date/etc values to set. | |
1281 | +// Assumes the given moment is already in UTC mode. | |
1282 | +setUTCValues = allowValueOptimization ? function(mom, a) { | |
1283 | + // simlate what moment's accessors do | |
1284 | + mom._d.setTime(Date.UTC.apply(Date, a)); | |
1285 | + moment.updateOffset(mom, false); // keepTime=false | |
1286 | +} : setMomentValues; | |
1287 | + | |
1288 | +// Utility function. Accepts a moment and an array of the local year/month/date/etc values to set. | |
1289 | +// Assumes the given moment is already in local mode. | |
1290 | +setLocalValues = allowValueOptimization ? function(mom, a) { | |
1291 | + // simlate what moment's accessors do | |
1292 | + mom._d.setTime(+new Date( // FYI, there is now way to apply an array of args to a constructor | |
1293 | + a[0] || 0, | |
1294 | + a[1] || 0, | |
1295 | + a[2] || 0, | |
1296 | + a[3] || 0, | |
1297 | + a[4] || 0, | |
1298 | + a[5] || 0, | |
1299 | + a[6] || 0 | |
1300 | + )); | |
1301 | + moment.updateOffset(mom, false); // keepTime=false | |
1302 | +} : setMomentValues; | |
1303 | + | |
1304 | +;; | |
1305 | + | |
1306 | +// Single Date Formatting | |
1307 | +// ------------------------------------------------------------------------------------------------- | |
1308 | + | |
1309 | + | |
1310 | +// call this if you want Moment's original format method to be used | |
1311 | +function oldMomentFormat(mom, formatStr) { | |
1312 | + return oldMomentProto.format.call(mom, formatStr); // oldMomentProto defined in moment-ext.js | |
1313 | +} | |
1314 | + | |
1315 | + | |
1316 | +// Formats `date` with a Moment formatting string, but allow our non-zero areas and | |
1317 | +// additional token. | |
1318 | +function formatDate(date, formatStr) { | |
1319 | + return formatDateWithChunks(date, getFormatStringChunks(formatStr)); | |
1320 | +} | |
1321 | + | |
1322 | + | |
1323 | +function formatDateWithChunks(date, chunks) { | |
1324 | + var s = ''; | |
1325 | + var i; | |
1326 | + | |
1327 | + for (i=0; i<chunks.length; i++) { | |
1328 | + s += formatDateWithChunk(date, chunks[i]); | |
1329 | + } | |
1330 | + | |
1331 | + return s; | |
1332 | +} | |
1333 | + | |
1334 | + | |
1335 | +// addition formatting tokens we want recognized | |
1336 | +var tokenOverrides = { | |
1337 | + t: function(date) { // "a" or "p" | |
1338 | + return oldMomentFormat(date, 'a').charAt(0); | |
1339 | + }, | |
1340 | + T: function(date) { // "A" or "P" | |
1341 | + return oldMomentFormat(date, 'A').charAt(0); | |
1342 | + } | |
1343 | +}; | |
1344 | + | |
1345 | + | |
1346 | +function formatDateWithChunk(date, chunk) { | |
1347 | + var token; | |
1348 | + var maybeStr; | |
1349 | + | |
1350 | + if (typeof chunk === 'string') { // a literal string | |
1351 | + return chunk; | |
1352 | + } | |
1353 | + else if ((token = chunk.token)) { // a token, like "YYYY" | |
1354 | + if (tokenOverrides[token]) { | |
1355 | + return tokenOverrides[token](date); // use our custom token | |
1356 | + } | |
1357 | + return oldMomentFormat(date, token); | |
1358 | + } | |
1359 | + else if (chunk.maybe) { // a grouping of other chunks that must be non-zero | |
1360 | + maybeStr = formatDateWithChunks(date, chunk.maybe); | |
1361 | + if (maybeStr.match(/[1-9]/)) { | |
1362 | + return maybeStr; | |
1363 | + } | |
1364 | + } | |
1365 | + | |
1366 | + return ''; | |
1367 | +} | |
1368 | + | |
1369 | + | |
1370 | +// Date Range Formatting | |
1371 | +// ------------------------------------------------------------------------------------------------- | |
1372 | +// TODO: make it work with timezone offset | |
1373 | + | |
1374 | +// Using a formatting string meant for a single date, generate a range string, like | |
1375 | +// "Sep 2 - 9 2013", that intelligently inserts a separator where the dates differ. | |
1376 | +// If the dates are the same as far as the format string is concerned, just return a single | |
1377 | +// rendering of one date, without any separator. | |
1378 | +function formatRange(date1, date2, formatStr, separator, isRTL) { | |
1379 | + var localeData; | |
1380 | + | |
1381 | + date1 = fc.moment.parseZone(date1); | |
1382 | + date2 = fc.moment.parseZone(date2); | |
1383 | + | |
1384 | + localeData = (date1.localeData || date1.lang).call(date1); // works with moment-pre-2.8 | |
1385 | + | |
1386 | + // Expand localized format strings, like "LL" -> "MMMM D YYYY" | |
1387 | + formatStr = localeData.longDateFormat(formatStr) || formatStr; | |
1388 | + // BTW, this is not important for `formatDate` because it is impossible to put custom tokens | |
1389 | + // or non-zero areas in Moment's localized format strings. | |
1390 | + | |
1391 | + separator = separator || ' - '; | |
1392 | + | |
1393 | + return formatRangeWithChunks( | |
1394 | + date1, | |
1395 | + date2, | |
1396 | + getFormatStringChunks(formatStr), | |
1397 | + separator, | |
1398 | + isRTL | |
1399 | + ); | |
1400 | +} | |
1401 | +fc.formatRange = formatRange; // expose | |
1402 | + | |
1403 | + | |
1404 | +function formatRangeWithChunks(date1, date2, chunks, separator, isRTL) { | |
1405 | + var chunkStr; // the rendering of the chunk | |
1406 | + var leftI; | |
1407 | + var leftStr = ''; | |
1408 | + var rightI; | |
1409 | + var rightStr = ''; | |
1410 | + var middleI; | |
1411 | + var middleStr1 = ''; | |
1412 | + var middleStr2 = ''; | |
1413 | + var middleStr = ''; | |
1414 | + | |
1415 | + // Start at the leftmost side of the formatting string and continue until you hit a token | |
1416 | + // that is not the same between dates. | |
1417 | + for (leftI=0; leftI<chunks.length; leftI++) { | |
1418 | + chunkStr = formatSimilarChunk(date1, date2, chunks[leftI]); | |
1419 | + if (chunkStr === false) { | |
1420 | + break; | |
1421 | + } | |
1422 | + leftStr += chunkStr; | |
1423 | + } | |
1424 | + | |
1425 | + // Similarly, start at the rightmost side of the formatting string and move left | |
1426 | + for (rightI=chunks.length-1; rightI>leftI; rightI--) { | |
1427 | + chunkStr = formatSimilarChunk(date1, date2, chunks[rightI]); | |
1428 | + if (chunkStr === false) { | |
1429 | + break; | |
1430 | + } | |
1431 | + rightStr = chunkStr + rightStr; | |
1432 | + } | |
1433 | + | |
1434 | + // The area in the middle is different for both of the dates. | |
1435 | + // Collect them distinctly so we can jam them together later. | |
1436 | + for (middleI=leftI; middleI<=rightI; middleI++) { | |
1437 | + middleStr1 += formatDateWithChunk(date1, chunks[middleI]); | |
1438 | + middleStr2 += formatDateWithChunk(date2, chunks[middleI]); | |
1439 | + } | |
1440 | + | |
1441 | + if (middleStr1 || middleStr2) { | |
1442 | + if (isRTL) { | |
1443 | + middleStr = middleStr2 + separator + middleStr1; | |
1444 | + } | |
1445 | + else { | |
1446 | + middleStr = middleStr1 + separator + middleStr2; | |
1447 | + } | |
1448 | + } | |
1449 | + | |
1450 | + return leftStr + middleStr + rightStr; | |
1451 | +} | |
1452 | + | |
1453 | + | |
1454 | +var similarUnitMap = { | |
1455 | + Y: 'year', | |
1456 | + M: 'month', | |
1457 | + D: 'day', // day of month | |
1458 | + d: 'day', // day of week | |
1459 | + // prevents a separator between anything time-related... | |
1460 | + A: 'second', // AM/PM | |
1461 | + a: 'second', // am/pm | |
1462 | + T: 'second', // A/P | |
1463 | + t: 'second', // a/p | |
1464 | + H: 'second', // hour (24) | |
1465 | + h: 'second', // hour (12) | |
1466 | + m: 'second', // minute | |
1467 | + s: 'second' // second | |
1468 | +}; | |
1469 | +// TODO: week maybe? | |
1470 | + | |
1471 | + | |
1472 | +// Given a formatting chunk, and given that both dates are similar in the regard the | |
1473 | +// formatting chunk is concerned, format date1 against `chunk`. Otherwise, return `false`. | |
1474 | +function formatSimilarChunk(date1, date2, chunk) { | |
1475 | + var token; | |
1476 | + var unit; | |
1477 | + | |
1478 | + if (typeof chunk === 'string') { // a literal string | |
1479 | + return chunk; | |
1480 | + } | |
1481 | + else if ((token = chunk.token)) { | |
1482 | + unit = similarUnitMap[token.charAt(0)]; | |
1483 | + // are the dates the same for this unit of measurement? | |
1484 | + if (unit && date1.isSame(date2, unit)) { | |
1485 | + return oldMomentFormat(date1, token); // would be the same if we used `date2` | |
1486 | + // BTW, don't support custom tokens | |
1487 | + } | |
1488 | + } | |
1489 | + | |
1490 | + return false; // the chunk is NOT the same for the two dates | |
1491 | + // BTW, don't support splitting on non-zero areas | |
1492 | +} | |
1493 | + | |
1494 | + | |
1495 | +// Chunking Utils | |
1496 | +// ------------------------------------------------------------------------------------------------- | |
1497 | + | |
1498 | + | |
1499 | +var formatStringChunkCache = {}; | |
1500 | + | |
1501 | + | |
1502 | +function getFormatStringChunks(formatStr) { | |
1503 | + if (formatStr in formatStringChunkCache) { | |
1504 | + return formatStringChunkCache[formatStr]; | |
1505 | + } | |
1506 | + return (formatStringChunkCache[formatStr] = chunkFormatString(formatStr)); | |
1507 | +} | |
1508 | + | |
1509 | + | |
1510 | +// Break the formatting string into an array of chunks | |
1511 | +function chunkFormatString(formatStr) { | |
1512 | + var chunks = []; | |
1513 | + var chunker = /\[([^\]]*)\]|\(([^\)]*)\)|(LTS|LT|(\w)\4*o?)|([^\w\[\(]+)/g; // TODO: more descrimination | |
1514 | + var match; | |
1515 | + | |
1516 | + while ((match = chunker.exec(formatStr))) { | |
1517 | + if (match[1]) { // a literal string inside [ ... ] | |
1518 | + chunks.push(match[1]); | |
1519 | + } | |
1520 | + else if (match[2]) { // non-zero formatting inside ( ... ) | |
1521 | + chunks.push({ maybe: chunkFormatString(match[2]) }); | |
1522 | + } | |
1523 | + else if (match[3]) { // a formatting token | |
1524 | + chunks.push({ token: match[3] }); | |
1525 | + } | |
1526 | + else if (match[5]) { // an unenclosed literal string | |
1527 | + chunks.push(match[5]); | |
1528 | + } | |
1529 | + } | |
1530 | + | |
1531 | + return chunks; | |
1532 | +} | |
1533 | + | |
1534 | +;; | |
1535 | + | |
1536 | +fc.Class = Class; // export | |
1537 | + | |
1538 | +// class that all other classes will inherit from | |
1539 | +function Class() { } | |
1540 | + | |
1541 | +// called upon a class to create a subclass | |
1542 | +Class.extend = function(members) { | |
1543 | + var superClass = this; | |
1544 | + var subClass; | |
1545 | + | |
1546 | + members = members || {}; | |
1547 | + | |
1548 | + // ensure a constructor for the subclass, forwarding all arguments to the super-constructor if it doesn't exist | |
1549 | + if (hasOwnProp(members, 'constructor')) { | |
1550 | + subClass = members.constructor; | |
1551 | + } | |
1552 | + if (typeof subClass !== 'function') { | |
1553 | + subClass = members.constructor = function() { | |
1554 | + superClass.apply(this, arguments); | |
1555 | + }; | |
1556 | + } | |
1557 | + | |
1558 | + // build the base prototype for the subclass, which is an new object chained to the superclass's prototype | |
1559 | + subClass.prototype = createObject(superClass.prototype); | |
1560 | + | |
1561 | + // copy each member variable/method onto the the subclass's prototype | |
1562 | + copyOwnProps(members, subClass.prototype); | |
1563 | + copyNativeMethods(members, subClass.prototype); // hack for IE8 | |
1564 | + | |
1565 | + // copy over all class variables/methods to the subclass, such as `extend` and `mixin` | |
1566 | + copyOwnProps(superClass, subClass); | |
1567 | + | |
1568 | + return subClass; | |
1569 | +}; | |
1570 | + | |
1571 | +// adds new member variables/methods to the class's prototype. | |
1572 | +// can be called with another class, or a plain object hash containing new members. | |
1573 | +Class.mixin = function(members) { | |
1574 | + copyOwnProps(members.prototype || members, this.prototype); | |
1575 | +}; | |
1576 | +;; | |
1577 | + | |
1578 | +/* A rectangular panel that is absolutely positioned over other content | |
1579 | +------------------------------------------------------------------------------------------------------------------------ | |
1580 | +Options: | |
1581 | + - className (string) | |
1582 | + - content (HTML string or jQuery element set) | |
1583 | + - parentEl | |
1584 | + - top | |
1585 | + - left | |
1586 | + - right (the x coord of where the right edge should be. not a "CSS" right) | |
1587 | + - autoHide (boolean) | |
1588 | + - show (callback) | |
1589 | + - hide (callback) | |
1590 | +*/ | |
1591 | + | |
1592 | +var Popover = Class.extend({ | |
1593 | + | |
1594 | + isHidden: true, | |
1595 | + options: null, | |
1596 | + el: null, // the container element for the popover. generated by this object | |
1597 | + documentMousedownProxy: null, // document mousedown handler bound to `this` | |
1598 | + margin: 10, // the space required between the popover and the edges of the scroll container | |
1599 | + | |
1600 | + | |
1601 | + constructor: function(options) { | |
1602 | + this.options = options || {}; | |
1603 | + }, | |
1604 | + | |
1605 | + | |
1606 | + // Shows the popover on the specified position. Renders it if not already | |
1607 | + show: function() { | |
1608 | + if (this.isHidden) { | |
1609 | + if (!this.el) { | |
1610 | + this.render(); | |
1611 | + } | |
1612 | + this.el.show(); | |
1613 | + this.position(); | |
1614 | + this.isHidden = false; | |
1615 | + this.trigger('show'); | |
1616 | + } | |
1617 | + }, | |
1618 | + | |
1619 | + | |
1620 | + // Hides the popover, through CSS, but does not remove it from the DOM | |
1621 | + hide: function() { | |
1622 | + if (!this.isHidden) { | |
1623 | + this.el.hide(); | |
1624 | + this.isHidden = true; | |
1625 | + this.trigger('hide'); | |
1626 | + } | |
1627 | + }, | |
1628 | + | |
1629 | + | |
1630 | + // Creates `this.el` and renders content inside of it | |
1631 | + render: function() { | |
1632 | + var _this = this; | |
1633 | + var options = this.options; | |
1634 | + | |
1635 | + this.el = $('<div class="fc-popover"/>') | |
1636 | + .addClass(options.className || '') | |
1637 | + .css({ | |
1638 | + // position initially to the top left to avoid creating scrollbars | |
1639 | + top: 0, | |
1640 | + left: 0 | |
1641 | + }) | |
1642 | + .append(options.content) | |
1643 | + .appendTo(options.parentEl); | |
1644 | + | |
1645 | + // when a click happens on anything inside with a 'fc-close' className, hide the popover | |
1646 | + this.el.on('click', '.fc-close', function() { | |
1647 | + _this.hide(); | |
1648 | + }); | |
1649 | + | |
1650 | + if (options.autoHide) { | |
1651 | + $(document).on('mousedown', this.documentMousedownProxy = proxy(this, 'documentMousedown')); | |
1652 | + } | |
1653 | + }, | |
1654 | + | |
1655 | + | |
1656 | + // Triggered when the user clicks *anywhere* in the document, for the autoHide feature | |
1657 | + documentMousedown: function(ev) { | |
1658 | + // only hide the popover if the click happened outside the popover | |
1659 | + if (this.el && !$(ev.target).closest(this.el).length) { | |
1660 | + this.hide(); | |
1661 | + } | |
1662 | + }, | |
1663 | + | |
1664 | + | |
1665 | + // Hides and unregisters any handlers | |
1666 | + destroy: function() { | |
1667 | + this.hide(); | |
1668 | + | |
1669 | + if (this.el) { | |
1670 | + this.el.remove(); | |
1671 | + this.el = null; | |
1672 | + } | |
1673 | + | |
1674 | + $(document).off('mousedown', this.documentMousedownProxy); | |
1675 | + }, | |
1676 | + | |
1677 | + | |
1678 | + // Positions the popover optimally, using the top/left/right options | |
1679 | + position: function() { | |
1680 | + var options = this.options; | |
1681 | + var origin = this.el.offsetParent().offset(); | |
1682 | + var width = this.el.outerWidth(); | |
1683 | + var height = this.el.outerHeight(); | |
1684 | + var windowEl = $(window); | |
1685 | + var viewportEl = getScrollParent(this.el); | |
1686 | + var viewportTop; | |
1687 | + var viewportLeft; | |
1688 | + var viewportOffset; | |
1689 | + var top; // the "position" (not "offset") values for the popover | |
1690 | + var left; // | |
1691 | + | |
1692 | + // compute top and left | |
1693 | + top = options.top || 0; | |
1694 | + if (options.left !== undefined) { | |
1695 | + left = options.left; | |
1696 | + } | |
1697 | + else if (options.right !== undefined) { | |
1698 | + left = options.right - width; // derive the left value from the right value | |
1699 | + } | |
1700 | + else { | |
1701 | + left = 0; | |
1702 | + } | |
1703 | + | |
1704 | + if (viewportEl.is(window) || viewportEl.is(document)) { // normalize getScrollParent's result | |
1705 | + viewportEl = windowEl; | |
1706 | + viewportTop = 0; // the window is always at the top left | |
1707 | + viewportLeft = 0; // (and .offset() won't work if called here) | |
1708 | + } | |
1709 | + else { | |
1710 | + viewportOffset = viewportEl.offset(); | |
1711 | + viewportTop = viewportOffset.top; | |
1712 | + viewportLeft = viewportOffset.left; | |
1713 | + } | |
1714 | + | |
1715 | + // if the window is scrolled, it causes the visible area to be further down | |
1716 | + viewportTop += windowEl.scrollTop(); | |
1717 | + viewportLeft += windowEl.scrollLeft(); | |
1718 | + | |
1719 | + // constrain to the view port. if constrained by two edges, give precedence to top/left | |
1720 | + if (options.viewportConstrain !== false) { | |
1721 | + top = Math.min(top, viewportTop + viewportEl.outerHeight() - height - this.margin); | |
1722 | + top = Math.max(top, viewportTop + this.margin); | |
1723 | + left = Math.min(left, viewportLeft + viewportEl.outerWidth() - width - this.margin); | |
1724 | + left = Math.max(left, viewportLeft + this.margin); | |
1725 | + } | |
1726 | + | |
1727 | + this.el.css({ | |
1728 | + top: top - origin.top, | |
1729 | + left: left - origin.left | |
1730 | + }); | |
1731 | + }, | |
1732 | + | |
1733 | + | |
1734 | + // Triggers a callback. Calls a function in the option hash of the same name. | |
1735 | + // Arguments beyond the first `name` are forwarded on. | |
1736 | + // TODO: better code reuse for this. Repeat code | |
1737 | + trigger: function(name) { | |
1738 | + if (this.options[name]) { | |
1739 | + this.options[name].apply(this, Array.prototype.slice.call(arguments, 1)); | |
1740 | + } | |
1741 | + } | |
1742 | + | |
1743 | +}); | |
1744 | + | |
1745 | +;; | |
1746 | + | |
1747 | +/* A "coordinate map" converts pixel coordinates into an associated cell, which has an associated date | |
1748 | +------------------------------------------------------------------------------------------------------------------------ | |
1749 | +Common interface: | |
1750 | + | |
1751 | + CoordMap.prototype = { | |
1752 | + build: function() {}, | |
1753 | + getCell: function(x, y) {} | |
1754 | + }; | |
1755 | + | |
1756 | +*/ | |
1757 | + | |
1758 | +/* Coordinate map for a grid component | |
1759 | +----------------------------------------------------------------------------------------------------------------------*/ | |
1760 | + | |
1761 | +var GridCoordMap = Class.extend({ | |
1762 | + | |
1763 | + grid: null, // reference to the Grid | |
1764 | + rowCoords: null, // array of {top,bottom} objects | |
1765 | + colCoords: null, // array of {left,right} objects | |
1766 | + | |
1767 | + containerEl: null, // container element that all coordinates are constrained to. optionally assigned | |
1768 | + bounds: null, | |
1769 | + | |
1770 | + | |
1771 | + constructor: function(grid) { | |
1772 | + this.grid = grid; | |
1773 | + }, | |
1774 | + | |
1775 | + | |
1776 | + // Queries the grid for the coordinates of all the cells | |
1777 | + build: function() { | |
1778 | + this.rowCoords = this.grid.computeRowCoords(); | |
1779 | + this.colCoords = this.grid.computeColCoords(); | |
1780 | + this.computeBounds(); | |
1781 | + }, | |
1782 | + | |
1783 | + | |
1784 | + // Clears the coordinates data to free up memory | |
1785 | + clear: function() { | |
1786 | + this.rowCoords = null; | |
1787 | + this.colCoords = null; | |
1788 | + }, | |
1789 | + | |
1790 | + | |
1791 | + // Given a coordinate of the document, gets the associated cell. If no cell is underneath, returns null | |
1792 | + getCell: function(x, y) { | |
1793 | + var rowCoords = this.rowCoords; | |
1794 | + var rowCnt = rowCoords.length; | |
1795 | + var colCoords = this.colCoords; | |
1796 | + var colCnt = colCoords.length; | |
1797 | + var hitRow = null; | |
1798 | + var hitCol = null; | |
1799 | + var i, coords; | |
1800 | + var cell; | |
1801 | + | |
1802 | + if (this.inBounds(x, y)) { | |
1803 | + | |
1804 | + for (i = 0; i < rowCnt; i++) { | |
1805 | + coords = rowCoords[i]; | |
1806 | + if (y >= coords.top && y < coords.bottom) { | |
1807 | + hitRow = i; | |
1808 | + break; | |
1809 | + } | |
1810 | + } | |
1811 | + | |
1812 | + for (i = 0; i < colCnt; i++) { | |
1813 | + coords = colCoords[i]; | |
1814 | + if (x >= coords.left && x < coords.right) { | |
1815 | + hitCol = i; | |
1816 | + break; | |
1817 | + } | |
1818 | + } | |
1819 | + | |
1820 | + if (hitRow !== null && hitCol !== null) { | |
1821 | + | |
1822 | + cell = this.grid.getCell(hitRow, hitCol); // expected to return a fresh object we can modify | |
1823 | + cell.grid = this.grid; // for CellDragListener's isCellsEqual. dragging between grids | |
1824 | + | |
1825 | + // make the coordinates available on the cell object | |
1826 | + $.extend(cell, rowCoords[hitRow], colCoords[hitCol]); | |
1827 | + | |
1828 | + return cell; | |
1829 | + } | |
1830 | + } | |
1831 | + | |
1832 | + return null; | |
1833 | + }, | |
1834 | + | |
1835 | + | |
1836 | + // If there is a containerEl, compute the bounds into min/max values | |
1837 | + computeBounds: function() { | |
1838 | + this.bounds = this.containerEl ? | |
1839 | + getClientRect(this.containerEl) : // area within scrollbars | |
1840 | + null; | |
1841 | + }, | |
1842 | + | |
1843 | + | |
1844 | + // Determines if the given coordinates are in bounds. If no `containerEl`, always true | |
1845 | + inBounds: function(x, y) { | |
1846 | + var bounds = this.bounds; | |
1847 | + | |
1848 | + if (bounds) { | |
1849 | + return x >= bounds.left && x < bounds.right && y >= bounds.top && y < bounds.bottom; | |
1850 | + } | |
1851 | + | |
1852 | + return true; | |
1853 | + } | |
1854 | + | |
1855 | +}); | |
1856 | + | |
1857 | + | |
1858 | +/* Coordinate map that is a combination of multiple other coordinate maps | |
1859 | +----------------------------------------------------------------------------------------------------------------------*/ | |
1860 | + | |
1861 | +var ComboCoordMap = Class.extend({ | |
1862 | + | |
1863 | + coordMaps: null, // an array of CoordMaps | |
1864 | + | |
1865 | + | |
1866 | + constructor: function(coordMaps) { | |
1867 | + this.coordMaps = coordMaps; | |
1868 | + }, | |
1869 | + | |
1870 | + | |
1871 | + // Builds all coordMaps | |
1872 | + build: function() { | |
1873 | + var coordMaps = this.coordMaps; | |
1874 | + var i; | |
1875 | + | |
1876 | + for (i = 0; i < coordMaps.length; i++) { | |
1877 | + coordMaps[i].build(); | |
1878 | + } | |
1879 | + }, | |
1880 | + | |
1881 | + | |
1882 | + // Queries all coordMaps for the cell underneath the given coordinates, returning the first result | |
1883 | + getCell: function(x, y) { | |
1884 | + var coordMaps = this.coordMaps; | |
1885 | + var cell = null; | |
1886 | + var i; | |
1887 | + | |
1888 | + for (i = 0; i < coordMaps.length && !cell; i++) { | |
1889 | + cell = coordMaps[i].getCell(x, y); | |
1890 | + } | |
1891 | + | |
1892 | + return cell; | |
1893 | + }, | |
1894 | + | |
1895 | + | |
1896 | + // Clears all coordMaps | |
1897 | + clear: function() { | |
1898 | + var coordMaps = this.coordMaps; | |
1899 | + var i; | |
1900 | + | |
1901 | + for (i = 0; i < coordMaps.length; i++) { | |
1902 | + coordMaps[i].clear(); | |
1903 | + } | |
1904 | + } | |
1905 | + | |
1906 | +}); | |
1907 | + | |
1908 | +;; | |
1909 | + | |
1910 | +/* Tracks a drag's mouse movement, firing various handlers | |
1911 | +----------------------------------------------------------------------------------------------------------------------*/ | |
1912 | + | |
1913 | +var DragListener = fc.DragListener = Class.extend({ | |
1914 | + | |
1915 | + options: null, | |
1916 | + | |
1917 | + isListening: false, | |
1918 | + isDragging: false, | |
1919 | + | |
1920 | + // coordinates of the initial mousedown | |
1921 | + originX: null, | |
1922 | + originY: null, | |
1923 | + | |
1924 | + // handler attached to the document, bound to the DragListener's `this` | |
1925 | + mousemoveProxy: null, | |
1926 | + mouseupProxy: null, | |
1927 | + | |
1928 | + // for IE8 bug-fighting behavior, for now | |
1929 | + subjectEl: null, // the element being draged. optional | |
1930 | + subjectHref: null, | |
1931 | + | |
1932 | + scrollEl: null, | |
1933 | + scrollBounds: null, // { top, bottom, left, right } | |
1934 | + scrollTopVel: null, // pixels per second | |
1935 | + scrollLeftVel: null, // pixels per second | |
1936 | + scrollIntervalId: null, // ID of setTimeout for scrolling animation loop | |
1937 | + scrollHandlerProxy: null, // this-scoped function for handling when scrollEl is scrolled | |
1938 | + | |
1939 | + scrollSensitivity: 30, // pixels from edge for scrolling to start | |
1940 | + scrollSpeed: 200, // pixels per second, at maximum speed | |
1941 | + scrollIntervalMs: 50, // millisecond wait between scroll increment | |
1942 | + | |
1943 | + | |
1944 | + constructor: function(options) { | |
1945 | + options = options || {}; | |
1946 | + this.options = options; | |
1947 | + this.subjectEl = options.subjectEl; | |
1948 | + }, | |
1949 | + | |
1950 | + | |
1951 | + // Call this when the user does a mousedown. Will probably lead to startListening | |
1952 | + mousedown: function(ev) { | |
1953 | + if (isPrimaryMouseButton(ev)) { | |
1954 | + | |
1955 | + ev.preventDefault(); // prevents native selection in most browsers | |
1956 | + | |
1957 | + this.startListening(ev); | |
1958 | + | |
1959 | + // start the drag immediately if there is no minimum distance for a drag start | |
1960 | + if (!this.options.distance) { | |
1961 | + this.startDrag(ev); | |
1962 | + } | |
1963 | + } | |
1964 | + }, | |
1965 | + | |
1966 | + | |
1967 | + // Call this to start tracking mouse movements | |
1968 | + startListening: function(ev) { | |
1969 | + var scrollParent; | |
1970 | + | |
1971 | + if (!this.isListening) { | |
1972 | + | |
1973 | + // grab scroll container and attach handler | |
1974 | + if (ev && this.options.scroll) { | |
1975 | + scrollParent = getScrollParent($(ev.target)); | |
1976 | + if (!scrollParent.is(window) && !scrollParent.is(document)) { | |
1977 | + this.scrollEl = scrollParent; | |
1978 | + | |
1979 | + // scope to `this`, and use `debounce` to make sure rapid calls don't happen | |
1980 | + this.scrollHandlerProxy = debounce(proxy(this, 'scrollHandler'), 100); | |
1981 | + this.scrollEl.on('scroll', this.scrollHandlerProxy); | |
1982 | + } | |
1983 | + } | |
1984 | + | |
1985 | + $(document) | |
1986 | + .on('mousemove', this.mousemoveProxy = proxy(this, 'mousemove')) | |
1987 | + .on('mouseup', this.mouseupProxy = proxy(this, 'mouseup')) | |
1988 | + .on('selectstart', this.preventDefault); // prevents native selection in IE<=8 | |
1989 | + | |
1990 | + if (ev) { | |
1991 | + this.originX = ev.pageX; | |
1992 | + this.originY = ev.pageY; | |
1993 | + } | |
1994 | + else { | |
1995 | + // if no starting information was given, origin will be the topleft corner of the screen. | |
1996 | + // if so, dx/dy in the future will be the absolute coordinates. | |
1997 | + this.originX = 0; | |
1998 | + this.originY = 0; | |
1999 | + } | |
2000 | + | |
2001 | + this.isListening = true; | |
2002 | + this.listenStart(ev); | |
2003 | + } | |
2004 | + }, | |
2005 | + | |
2006 | + | |
2007 | + // Called when drag listening has started (but a real drag has not necessarily began) | |
2008 | + listenStart: function(ev) { | |
2009 | + this.trigger('listenStart', ev); | |
2010 | + }, | |
2011 | + | |
2012 | + | |
2013 | + // Called when the user moves the mouse | |
2014 | + mousemove: function(ev) { | |
2015 | + var dx = ev.pageX - this.originX; | |
2016 | + var dy = ev.pageY - this.originY; | |
2017 | + var minDistance; | |
2018 | + var distanceSq; // current distance from the origin, squared | |
2019 | + | |
2020 | + if (!this.isDragging) { // if not already dragging... | |
2021 | + // then start the drag if the minimum distance criteria is met | |
2022 | + minDistance = this.options.distance || 1; | |
2023 | + distanceSq = dx * dx + dy * dy; | |
2024 | + if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem | |
2025 | + this.startDrag(ev); | |
2026 | + } | |
2027 | + } | |
2028 | + | |
2029 | + if (this.isDragging) { | |
2030 | + this.drag(dx, dy, ev); // report a drag, even if this mousemove initiated the drag | |
2031 | + } | |
2032 | + }, | |
2033 | + | |
2034 | + | |
2035 | + // Call this to initiate a legitimate drag. | |
2036 | + // This function is called internally from this class, but can also be called explicitly from outside | |
2037 | + startDrag: function(ev) { | |
2038 | + | |
2039 | + if (!this.isListening) { // startDrag must have manually initiated | |
2040 | + this.startListening(); | |
2041 | + } | |
2042 | + | |
2043 | + if (!this.isDragging) { | |
2044 | + this.isDragging = true; | |
2045 | + this.dragStart(ev); | |
2046 | + } | |
2047 | + }, | |
2048 | + | |
2049 | + | |
2050 | + // Called when the actual drag has started (went beyond minDistance) | |
2051 | + dragStart: function(ev) { | |
2052 | + var subjectEl = this.subjectEl; | |
2053 | + | |
2054 | + this.trigger('dragStart', ev); | |
2055 | + | |
2056 | + // remove a mousedown'd <a>'s href so it is not visited (IE8 bug) | |
2057 | + if ((this.subjectHref = subjectEl ? subjectEl.attr('href') : null)) { | |
2058 | + subjectEl.removeAttr('href'); | |
2059 | + } | |
2060 | + }, | |
2061 | + | |
2062 | + | |
2063 | + // Called while the mouse is being moved and when we know a legitimate drag is taking place | |
2064 | + drag: function(dx, dy, ev) { | |
2065 | + this.trigger('drag', dx, dy, ev); | |
2066 | + this.updateScroll(ev); // will possibly cause scrolling | |
2067 | + }, | |
2068 | + | |
2069 | + | |
2070 | + // Called when the user does a mouseup | |
2071 | + mouseup: function(ev) { | |
2072 | + this.stopListening(ev); | |
2073 | + }, | |
2074 | + | |
2075 | + | |
2076 | + // Called when the drag is over. Will not cause listening to stop however. | |
2077 | + // A concluding 'cellOut' event will NOT be triggered. | |
2078 | + stopDrag: function(ev) { | |
2079 | + if (this.isDragging) { | |
2080 | + this.stopScrolling(); | |
2081 | + this.dragStop(ev); | |
2082 | + this.isDragging = false; | |
2083 | + } | |
2084 | + }, | |
2085 | + | |
2086 | + | |
2087 | + // Called when dragging has been stopped | |
2088 | + dragStop: function(ev) { | |
2089 | + var _this = this; | |
2090 | + | |
2091 | + this.trigger('dragStop', ev); | |
2092 | + | |
2093 | + // restore a mousedown'd <a>'s href (for IE8 bug) | |
2094 | + setTimeout(function() { // must be outside of the click's execution | |
2095 | + if (_this.subjectHref) { | |
2096 | + _this.subjectEl.attr('href', _this.subjectHref); | |
2097 | + } | |
2098 | + }, 0); | |
2099 | + }, | |
2100 | + | |
2101 | + | |
2102 | + // Call this to stop listening to the user's mouse events | |
2103 | + stopListening: function(ev) { | |
2104 | + this.stopDrag(ev); // if there's a current drag, kill it | |
2105 | + | |
2106 | + if (this.isListening) { | |
2107 | + | |
2108 | + // remove the scroll handler if there is a scrollEl | |
2109 | + if (this.scrollEl) { | |
2110 | + this.scrollEl.off('scroll', this.scrollHandlerProxy); | |
2111 | + this.scrollHandlerProxy = null; | |
2112 | + } | |
2113 | + | |
2114 | + $(document) | |
2115 | + .off('mousemove', this.mousemoveProxy) | |
2116 | + .off('mouseup', this.mouseupProxy) | |
2117 | + .off('selectstart', this.preventDefault); | |
2118 | + | |
2119 | + this.mousemoveProxy = null; | |
2120 | + this.mouseupProxy = null; | |
2121 | + | |
2122 | + this.isListening = false; | |
2123 | + this.listenStop(ev); | |
2124 | + } | |
2125 | + }, | |
2126 | + | |
2127 | + | |
2128 | + // Called when drag listening has stopped | |
2129 | + listenStop: function(ev) { | |
2130 | + this.trigger('listenStop', ev); | |
2131 | + }, | |
2132 | + | |
2133 | + | |
2134 | + // Triggers a callback. Calls a function in the option hash of the same name. | |
2135 | + // Arguments beyond the first `name` are forwarded on. | |
2136 | + trigger: function(name) { | |
2137 | + if (this.options[name]) { | |
2138 | + this.options[name].apply(this, Array.prototype.slice.call(arguments, 1)); | |
2139 | + } | |
2140 | + }, | |
2141 | + | |
2142 | + | |
2143 | + // Stops a given mouse event from doing it's native browser action. In our case, text selection. | |
2144 | + preventDefault: function(ev) { | |
2145 | + ev.preventDefault(); | |
2146 | + }, | |
2147 | + | |
2148 | + | |
2149 | + /* Scrolling | |
2150 | + ------------------------------------------------------------------------------------------------------------------*/ | |
2151 | + | |
2152 | + | |
2153 | + // Computes and stores the bounding rectangle of scrollEl | |
2154 | + computeScrollBounds: function() { | |
2155 | + var el = this.scrollEl; | |
2156 | + | |
2157 | + this.scrollBounds = el ? getOuterRect(el) : null; | |
2158 | + // TODO: use getClientRect in future. but prevents auto scrolling when on top of scrollbars | |
2159 | + }, | |
2160 | + | |
2161 | + | |
2162 | + // Called when the dragging is in progress and scrolling should be updated | |
2163 | + updateScroll: function(ev) { | |
2164 | + var sensitivity = this.scrollSensitivity; | |
2165 | + var bounds = this.scrollBounds; | |
2166 | + var topCloseness, bottomCloseness; | |
2167 | + var leftCloseness, rightCloseness; | |
2168 | + var topVel = 0; | |
2169 | + var leftVel = 0; | |
2170 | + | |
2171 | + if (bounds) { // only scroll if scrollEl exists | |
2172 | + | |
2173 | + // compute closeness to edges. valid range is from 0.0 - 1.0 | |
2174 | + topCloseness = (sensitivity - (ev.pageY - bounds.top)) / sensitivity; | |
2175 | + bottomCloseness = (sensitivity - (bounds.bottom - ev.pageY)) / sensitivity; | |
2176 | + leftCloseness = (sensitivity - (ev.pageX - bounds.left)) / sensitivity; | |
2177 | + rightCloseness = (sensitivity - (bounds.right - ev.pageX)) / sensitivity; | |
2178 | + | |
2179 | + // translate vertical closeness into velocity. | |
2180 | + // mouse must be completely in bounds for velocity to happen. | |
2181 | + if (topCloseness >= 0 && topCloseness <= 1) { | |
2182 | + topVel = topCloseness * this.scrollSpeed * -1; // negative. for scrolling up | |
2183 | + } | |
2184 | + else if (bottomCloseness >= 0 && bottomCloseness <= 1) { | |
2185 | + topVel = bottomCloseness * this.scrollSpeed; | |
2186 | + } | |
2187 | + | |
2188 | + // translate horizontal closeness into velocity | |
2189 | + if (leftCloseness >= 0 && leftCloseness <= 1) { | |
2190 | + leftVel = leftCloseness * this.scrollSpeed * -1; // negative. for scrolling left | |
2191 | + } | |
2192 | + else if (rightCloseness >= 0 && rightCloseness <= 1) { | |
2193 | + leftVel = rightCloseness * this.scrollSpeed; | |
2194 | + } | |
2195 | + } | |
2196 | + | |
2197 | + this.setScrollVel(topVel, leftVel); | |
2198 | + }, | |
2199 | + | |
2200 | + | |
2201 | + // Sets the speed-of-scrolling for the scrollEl | |
2202 | + setScrollVel: function(topVel, leftVel) { | |
2203 | + | |
2204 | + this.scrollTopVel = topVel; | |
2205 | + this.scrollLeftVel = leftVel; | |
2206 | + | |
2207 | + this.constrainScrollVel(); // massages into realistic values | |
2208 | + | |
2209 | + // if there is non-zero velocity, and an animation loop hasn't already started, then START | |
2210 | + if ((this.scrollTopVel || this.scrollLeftVel) && !this.scrollIntervalId) { | |
2211 | + this.scrollIntervalId = setInterval( | |
2212 | + proxy(this, 'scrollIntervalFunc'), // scope to `this` | |
2213 | + this.scrollIntervalMs | |
2214 | + ); | |
2215 | + } | |
2216 | + }, | |
2217 | + | |
2218 | + | |
2219 | + // Forces scrollTopVel and scrollLeftVel to be zero if scrolling has already gone all the way | |
2220 | + constrainScrollVel: function() { | |
2221 | + var el = this.scrollEl; | |
2222 | + | |
2223 | + if (this.scrollTopVel < 0) { // scrolling up? | |
2224 | + if (el.scrollTop() <= 0) { // already scrolled all the way up? | |
2225 | + this.scrollTopVel = 0; | |
2226 | + } | |
2227 | + } | |
2228 | + else if (this.scrollTopVel > 0) { // scrolling down? | |
2229 | + if (el.scrollTop() + el[0].clientHeight >= el[0].scrollHeight) { // already scrolled all the way down? | |
2230 | + this.scrollTopVel = 0; | |
2231 | + } | |
2232 | + } | |
2233 | + | |
2234 | + if (this.scrollLeftVel < 0) { // scrolling left? | |
2235 | + if (el.scrollLeft() <= 0) { // already scrolled all the left? | |
2236 | + this.scrollLeftVel = 0; | |
2237 | + } | |
2238 | + } | |
2239 | + else if (this.scrollLeftVel > 0) { // scrolling right? | |
2240 | + if (el.scrollLeft() + el[0].clientWidth >= el[0].scrollWidth) { // already scrolled all the way right? | |
2241 | + this.scrollLeftVel = 0; | |
2242 | + } | |
2243 | + } | |
2244 | + }, | |
2245 | + | |
2246 | + | |
2247 | + // This function gets called during every iteration of the scrolling animation loop | |
2248 | + scrollIntervalFunc: function() { | |
2249 | + var el = this.scrollEl; | |
2250 | + var frac = this.scrollIntervalMs / 1000; // considering animation frequency, what the vel should be mult'd by | |
2251 | + | |
2252 | + // change the value of scrollEl's scroll | |
2253 | + if (this.scrollTopVel) { | |
2254 | + el.scrollTop(el.scrollTop() + this.scrollTopVel * frac); | |
2255 | + } | |
2256 | + if (this.scrollLeftVel) { | |
2257 | + el.scrollLeft(el.scrollLeft() + this.scrollLeftVel * frac); | |
2258 | + } | |
2259 | + | |
2260 | + this.constrainScrollVel(); // since the scroll values changed, recompute the velocities | |
2261 | + | |
2262 | + // if scrolled all the way, which causes the vels to be zero, stop the animation loop | |
2263 | + if (!this.scrollTopVel && !this.scrollLeftVel) { | |
2264 | + this.stopScrolling(); | |
2265 | + } | |
2266 | + }, | |
2267 | + | |
2268 | + | |
2269 | + // Kills any existing scrolling animation loop | |
2270 | + stopScrolling: function() { | |
2271 | + if (this.scrollIntervalId) { | |
2272 | + clearInterval(this.scrollIntervalId); | |
2273 | + this.scrollIntervalId = null; | |
2274 | + | |
2275 | + // when all done with scrolling, recompute positions since they probably changed | |
2276 | + this.scrollStop(); | |
2277 | + } | |
2278 | + }, | |
2279 | + | |
2280 | + | |
2281 | + // Get called when the scrollEl is scrolled (NOTE: this is delayed via debounce) | |
2282 | + scrollHandler: function() { | |
2283 | + // recompute all coordinates, but *only* if this is *not* part of our scrolling animation | |
2284 | + if (!this.scrollIntervalId) { | |
2285 | + this.scrollStop(); | |
2286 | + } | |
2287 | + }, | |
2288 | + | |
2289 | + | |
2290 | + // Called when scrolling has stopped, whether through auto scroll, or the user scrolling | |
2291 | + scrollStop: function() { | |
2292 | + } | |
2293 | + | |
2294 | +}); | |
2295 | + | |
2296 | +;; | |
2297 | + | |
2298 | +/* Tracks mouse movements over a CoordMap and raises events about which cell the mouse is over. | |
2299 | +------------------------------------------------------------------------------------------------------------------------ | |
2300 | +options: | |
2301 | +- subjectEl | |
2302 | +- subjectCenter | |
2303 | +*/ | |
2304 | + | |
2305 | +var CellDragListener = DragListener.extend({ | |
2306 | + | |
2307 | + coordMap: null, // converts coordinates to date cells | |
2308 | + origCell: null, // the cell the mouse was over when listening started | |
2309 | + cell: null, // the cell the mouse is over | |
2310 | + coordAdjust: null, // delta that will be added to the mouse coordinates when computing collisions | |
2311 | + | |
2312 | + | |
2313 | + constructor: function(coordMap, options) { | |
2314 | + DragListener.prototype.constructor.call(this, options); // call the super-constructor | |
2315 | + | |
2316 | + this.coordMap = coordMap; | |
2317 | + }, | |
2318 | + | |
2319 | + | |
2320 | + // Called when drag listening starts (but a real drag has not necessarily began). | |
2321 | + // ev might be undefined if dragging was started manually. | |
2322 | + listenStart: function(ev) { | |
2323 | + var subjectEl = this.subjectEl; | |
2324 | + var subjectRect; | |
2325 | + var origPoint; | |
2326 | + var point; | |
2327 | + | |
2328 | + DragListener.prototype.listenStart.apply(this, arguments); // call the super-method | |
2329 | + | |
2330 | + this.computeCoords(); | |
2331 | + | |
2332 | + if (ev) { | |
2333 | + origPoint = { left: ev.pageX, top: ev.pageY }; | |
2334 | + point = origPoint; | |
2335 | + | |
2336 | + // constrain the point to bounds of the element being dragged | |
2337 | + if (subjectEl) { | |
2338 | + subjectRect = getOuterRect(subjectEl); // used for centering as well | |
2339 | + point = constrainPoint(point, subjectRect); | |
2340 | + } | |
2341 | + | |
2342 | + this.origCell = this.getCell(point.left, point.top); | |
2343 | + | |
2344 | + // treat the center of the subject as the collision point? | |
2345 | + if (subjectEl && this.options.subjectCenter) { | |
2346 | + | |
2347 | + // only consider the area the subject overlaps the cell. best for large subjects | |
2348 | + if (this.origCell) { | |
2349 | + subjectRect = intersectRects(this.origCell, subjectRect) || | |
2350 | + subjectRect; // in case there is no intersection | |
2351 | + } | |
2352 | + | |
2353 | + point = getRectCenter(subjectRect); | |
2354 | + } | |
2355 | + | |
2356 | + this.coordAdjust = diffPoints(point, origPoint); // point - origPoint | |
2357 | + } | |
2358 | + else { | |
2359 | + this.origCell = null; | |
2360 | + this.coordAdjust = null; | |
2361 | + } | |
2362 | + }, | |
2363 | + | |
2364 | + | |
2365 | + // Recomputes the drag-critical positions of elements | |
2366 | + computeCoords: function() { | |
2367 | + this.coordMap.build(); | |
2368 | + this.computeScrollBounds(); | |
2369 | + }, | |
2370 | + | |
2371 | + | |
2372 | + // Called when the actual drag has started | |
2373 | + dragStart: function(ev) { | |
2374 | + var cell; | |
2375 | + | |
2376 | + DragListener.prototype.dragStart.apply(this, arguments); // call the super-method | |
2377 | + | |
2378 | + cell = this.getCell(ev.pageX, ev.pageY); // might be different from this.origCell if the min-distance is large | |
2379 | + | |
2380 | + // report the initial cell the mouse is over | |
2381 | + // especially important if no min-distance and drag starts immediately | |
2382 | + if (cell) { | |
2383 | + this.cellOver(cell); | |
2384 | + } | |
2385 | + }, | |
2386 | + | |
2387 | + | |
2388 | + // Called when the drag moves | |
2389 | + drag: function(dx, dy, ev) { | |
2390 | + var cell; | |
2391 | + | |
2392 | + DragListener.prototype.drag.apply(this, arguments); // call the super-method | |
2393 | + | |
2394 | + cell = this.getCell(ev.pageX, ev.pageY); | |
2395 | + | |
2396 | + if (!isCellsEqual(cell, this.cell)) { // a different cell than before? | |
2397 | + if (this.cell) { | |
2398 | + this.cellOut(); | |
2399 | + } | |
2400 | + if (cell) { | |
2401 | + this.cellOver(cell); | |
2402 | + } | |
2403 | + } | |
2404 | + }, | |
2405 | + | |
2406 | + | |
2407 | + // Called when dragging has been stopped | |
2408 | + dragStop: function() { | |
2409 | + this.cellDone(); | |
2410 | + DragListener.prototype.dragStop.apply(this, arguments); // call the super-method | |
2411 | + }, | |
2412 | + | |
2413 | + | |
2414 | + // Called when a the mouse has just moved over a new cell | |
2415 | + cellOver: function(cell) { | |
2416 | + this.cell = cell; | |
2417 | + this.trigger('cellOver', cell, isCellsEqual(cell, this.origCell), this.origCell); | |
2418 | + }, | |
2419 | + | |
2420 | + | |
2421 | + // Called when the mouse has just moved out of a cell | |
2422 | + cellOut: function() { | |
2423 | + if (this.cell) { | |
2424 | + this.trigger('cellOut', this.cell); | |
2425 | + this.cellDone(); | |
2426 | + this.cell = null; | |
2427 | + } | |
2428 | + }, | |
2429 | + | |
2430 | + | |
2431 | + // Called after a cellOut. Also called before a dragStop | |
2432 | + cellDone: function() { | |
2433 | + if (this.cell) { | |
2434 | + this.trigger('cellDone', this.cell); | |
2435 | + } | |
2436 | + }, | |
2437 | + | |
2438 | + | |
2439 | + // Called when drag listening has stopped | |
2440 | + listenStop: function() { | |
2441 | + DragListener.prototype.listenStop.apply(this, arguments); // call the super-method | |
2442 | + | |
2443 | + this.origCell = this.cell = null; | |
2444 | + this.coordMap.clear(); | |
2445 | + }, | |
2446 | + | |
2447 | + | |
2448 | + // Called when scrolling has stopped, whether through auto scroll, or the user scrolling | |
2449 | + scrollStop: function() { | |
2450 | + DragListener.prototype.scrollStop.apply(this, arguments); // call the super-method | |
2451 | + | |
2452 | + this.computeCoords(); // cells' absolute positions will be in new places. recompute | |
2453 | + }, | |
2454 | + | |
2455 | + | |
2456 | + // Gets the cell underneath the coordinates for the given mouse event | |
2457 | + getCell: function(left, top) { | |
2458 | + | |
2459 | + if (this.coordAdjust) { | |
2460 | + left += this.coordAdjust.left; | |
2461 | + top += this.coordAdjust.top; | |
2462 | + } | |
2463 | + | |
2464 | + return this.coordMap.getCell(left, top); | |
2465 | + } | |
2466 | + | |
2467 | +}); | |
2468 | + | |
2469 | + | |
2470 | +// Returns `true` if the cells are identically equal. `false` otherwise. | |
2471 | +// They must have the same row, col, and be from the same grid. | |
2472 | +// Two null values will be considered equal, as two "out of the grid" states are the same. | |
2473 | +function isCellsEqual(cell1, cell2) { | |
2474 | + | |
2475 | + if (!cell1 && !cell2) { | |
2476 | + return true; | |
2477 | + } | |
2478 | + | |
2479 | + if (cell1 && cell2) { | |
2480 | + return cell1.grid === cell2.grid && | |
2481 | + cell1.row === cell2.row && | |
2482 | + cell1.col === cell2.col; | |
2483 | + } | |
2484 | + | |
2485 | + return false; | |
2486 | +} | |
2487 | + | |
2488 | +;; | |
2489 | + | |
2490 | +/* Creates a clone of an element and lets it track the mouse as it moves | |
2491 | +----------------------------------------------------------------------------------------------------------------------*/ | |
2492 | + | |
2493 | +var MouseFollower = Class.extend({ | |
2494 | + | |
2495 | + options: null, | |
2496 | + | |
2497 | + sourceEl: null, // the element that will be cloned and made to look like it is dragging | |
2498 | + el: null, // the clone of `sourceEl` that will track the mouse | |
2499 | + parentEl: null, // the element that `el` (the clone) will be attached to | |
2500 | + | |
2501 | + // the initial position of el, relative to the offset parent. made to match the initial offset of sourceEl | |
2502 | + top0: null, | |
2503 | + left0: null, | |
2504 | + | |
2505 | + // the initial position of the mouse | |
2506 | + mouseY0: null, | |
2507 | + mouseX0: null, | |
2508 | + | |
2509 | + // the number of pixels the mouse has moved from its initial position | |
2510 | + topDelta: null, | |
2511 | + leftDelta: null, | |
2512 | + | |
2513 | + mousemoveProxy: null, // document mousemove handler, bound to the MouseFollower's `this` | |
2514 | + | |
2515 | + isFollowing: false, | |
2516 | + isHidden: false, | |
2517 | + isAnimating: false, // doing the revert animation? | |
2518 | + | |
2519 | + constructor: function(sourceEl, options) { | |
2520 | + this.options = options = options || {}; | |
2521 | + this.sourceEl = sourceEl; | |
2522 | + this.parentEl = options.parentEl ? $(options.parentEl) : sourceEl.parent(); // default to sourceEl's parent | |
2523 | + }, | |
2524 | + | |
2525 | + | |
2526 | + // Causes the element to start following the mouse | |
2527 | + start: function(ev) { | |
2528 | + if (!this.isFollowing) { | |
2529 | + this.isFollowing = true; | |
2530 | + | |
2531 | + this.mouseY0 = ev.pageY; | |
2532 | + this.mouseX0 = ev.pageX; | |
2533 | + this.topDelta = 0; | |
2534 | + this.leftDelta = 0; | |
2535 | + | |
2536 | + if (!this.isHidden) { | |
2537 | + this.updatePosition(); | |
2538 | + } | |
2539 | + | |
2540 | + $(document).on('mousemove', this.mousemoveProxy = proxy(this, 'mousemove')); | |
2541 | + } | |
2542 | + }, | |
2543 | + | |
2544 | + | |
2545 | + // Causes the element to stop following the mouse. If shouldRevert is true, will animate back to original position. | |
2546 | + // `callback` gets invoked when the animation is complete. If no animation, it is invoked immediately. | |
2547 | + stop: function(shouldRevert, callback) { | |
2548 | + var _this = this; | |
2549 | + var revertDuration = this.options.revertDuration; | |
2550 | + | |
2551 | + function complete() { | |
2552 | + this.isAnimating = false; | |
2553 | + _this.destroyEl(); | |
2554 | + | |
2555 | + this.top0 = this.left0 = null; // reset state for future updatePosition calls | |
2556 | + | |
2557 | + if (callback) { | |
2558 | + callback(); | |
2559 | + } | |
2560 | + } | |
2561 | + | |
2562 | + if (this.isFollowing && !this.isAnimating) { // disallow more than one stop animation at a time | |
2563 | + this.isFollowing = false; | |
2564 | + | |
2565 | + $(document).off('mousemove', this.mousemoveProxy); | |
2566 | + | |
2567 | + if (shouldRevert && revertDuration && !this.isHidden) { // do a revert animation? | |
2568 | + this.isAnimating = true; | |
2569 | + this.el.animate({ | |
2570 | + top: this.top0, | |
2571 | + left: this.left0 | |
2572 | + }, { | |
2573 | + duration: revertDuration, | |
2574 | + complete: complete | |
2575 | + }); | |
2576 | + } | |
2577 | + else { | |
2578 | + complete(); | |
2579 | + } | |
2580 | + } | |
2581 | + }, | |
2582 | + | |
2583 | + | |
2584 | + // Gets the tracking element. Create it if necessary | |
2585 | + getEl: function() { | |
2586 | + var el = this.el; | |
2587 | + | |
2588 | + if (!el) { | |
2589 | + this.sourceEl.width(); // hack to force IE8 to compute correct bounding box | |
2590 | + el = this.el = this.sourceEl.clone() | |
2591 | + .css({ | |
2592 | + position: 'absolute', | |
2593 | + visibility: '', // in case original element was hidden (commonly through hideEvents()) | |
2594 | + display: this.isHidden ? 'none' : '', // for when initially hidden | |
2595 | + margin: 0, | |
2596 | + right: 'auto', // erase and set width instead | |
2597 | + bottom: 'auto', // erase and set height instead | |
2598 | + width: this.sourceEl.width(), // explicit height in case there was a 'right' value | |
2599 | + height: this.sourceEl.height(), // explicit width in case there was a 'bottom' value | |
2600 | + opacity: this.options.opacity || '', | |
2601 | + zIndex: this.options.zIndex | |
2602 | + }) | |
2603 | + .appendTo(this.parentEl); | |
2604 | + } | |
2605 | + | |
2606 | + return el; | |
2607 | + }, | |
2608 | + | |
2609 | + | |
2610 | + // Removes the tracking element if it has already been created | |
2611 | + destroyEl: function() { | |
2612 | + if (this.el) { | |
2613 | + this.el.remove(); | |
2614 | + this.el = null; | |
2615 | + } | |
2616 | + }, | |
2617 | + | |
2618 | + | |
2619 | + // Update the CSS position of the tracking element | |
2620 | + updatePosition: function() { | |
2621 | + var sourceOffset; | |
2622 | + var origin; | |
2623 | + | |
2624 | + this.getEl(); // ensure this.el | |
2625 | + | |
2626 | + // make sure origin info was computed | |
2627 | + if (this.top0 === null) { | |
2628 | + this.sourceEl.width(); // hack to force IE8 to compute correct bounding box | |
2629 | + sourceOffset = this.sourceEl.offset(); | |
2630 | + origin = this.el.offsetParent().offset(); | |
2631 | + this.top0 = sourceOffset.top - origin.top; | |
2632 | + this.left0 = sourceOffset.left - origin.left; | |
2633 | + } | |
2634 | + | |
2635 | + this.el.css({ | |
2636 | + top: this.top0 + this.topDelta, | |
2637 | + left: this.left0 + this.leftDelta | |
2638 | + }); | |
2639 | + }, | |
2640 | + | |
2641 | + | |
2642 | + // Gets called when the user moves the mouse | |
2643 | + mousemove: function(ev) { | |
2644 | + this.topDelta = ev.pageY - this.mouseY0; | |
2645 | + this.leftDelta = ev.pageX - this.mouseX0; | |
2646 | + | |
2647 | + if (!this.isHidden) { | |
2648 | + this.updatePosition(); | |
2649 | + } | |
2650 | + }, | |
2651 | + | |
2652 | + | |
2653 | + // Temporarily makes the tracking element invisible. Can be called before following starts | |
2654 | + hide: function() { | |
2655 | + if (!this.isHidden) { | |
2656 | + this.isHidden = true; | |
2657 | + if (this.el) { | |
2658 | + this.el.hide(); | |
2659 | + } | |
2660 | + } | |
2661 | + }, | |
2662 | + | |
2663 | + | |
2664 | + // Show the tracking element after it has been temporarily hidden | |
2665 | + show: function() { | |
2666 | + if (this.isHidden) { | |
2667 | + this.isHidden = false; | |
2668 | + this.updatePosition(); | |
2669 | + this.getEl().show(); | |
2670 | + } | |
2671 | + } | |
2672 | + | |
2673 | +}); | |
2674 | + | |
2675 | +;; | |
2676 | + | |
2677 | +/* A utility class for rendering <tr> rows. | |
2678 | +----------------------------------------------------------------------------------------------------------------------*/ | |
2679 | +// It leverages methods of the subclass and the View to determine custom rendering behavior for each row "type" | |
2680 | +// (such as highlight rows, day rows, helper rows, etc). | |
2681 | + | |
2682 | +var RowRenderer = Class.extend({ | |
2683 | + | |
2684 | + view: null, // a View object | |
2685 | + isRTL: null, // shortcut to the view's isRTL option | |
2686 | + cellHtml: '<td/>', // plain default HTML used for a cell when no other is available | |
2687 | + | |
2688 | + | |
2689 | + constructor: function(view) { | |
2690 | + this.view = view; | |
2691 | + this.isRTL = view.opt('isRTL'); | |
2692 | + }, | |
2693 | + | |
2694 | + | |
2695 | + // Renders the HTML for a row, leveraging custom cell-HTML-renderers based on the `rowType`. | |
2696 | + // Also applies the "intro" and "outro" cells, which are specified by the subclass and views. | |
2697 | + // `row` is an optional row number. | |
2698 | + rowHtml: function(rowType, row) { | |
2699 | + var renderCell = this.getHtmlRenderer('cell', rowType); | |
2700 | + var rowCellHtml = ''; | |
2701 | + var col; | |
2702 | + var cell; | |
2703 | + | |
2704 | + row = row || 0; | |
2705 | + | |
2706 | + for (col = 0; col < this.colCnt; col++) { | |
2707 | + cell = this.getCell(row, col); | |
2708 | + rowCellHtml += renderCell(cell); | |
2709 | + } | |
2710 | + | |
2711 | + rowCellHtml = this.bookendCells(rowCellHtml, rowType, row); // apply intro and outro | |
2712 | + | |
2713 | + return '<tr>' + rowCellHtml + '</tr>'; | |
2714 | + }, | |
2715 | + | |
2716 | + | |
2717 | + // Applies the "intro" and "outro" HTML to the given cells. | |
2718 | + // Intro means the leftmost cell when the calendar is LTR and the rightmost cell when RTL. Vice-versa for outro. | |
2719 | + // `cells` can be an HTML string of <td>'s or a jQuery <tr> element | |
2720 | + // `row` is an optional row number. | |
2721 | + bookendCells: function(cells, rowType, row) { | |
2722 | + var intro = this.getHtmlRenderer('intro', rowType)(row || 0); | |
2723 | + var outro = this.getHtmlRenderer('outro', rowType)(row || 0); | |
2724 | + var prependHtml = this.isRTL ? outro : intro; | |
2725 | + var appendHtml = this.isRTL ? intro : outro; | |
2726 | + | |
2727 | + if (typeof cells === 'string') { | |
2728 | + return prependHtml + cells + appendHtml; | |
2729 | + } | |
2730 | + else { // a jQuery <tr> element | |
2731 | + return cells.prepend(prependHtml).append(appendHtml); | |
2732 | + } | |
2733 | + }, | |
2734 | + | |
2735 | + | |
2736 | + // Returns an HTML-rendering function given a specific `rendererName` (like cell, intro, or outro) and a specific | |
2737 | + // `rowType` (like day, eventSkeleton, helperSkeleton), which is optional. | |
2738 | + // If a renderer for the specific rowType doesn't exist, it will fall back to a generic renderer. | |
2739 | + // We will query the View object first for any custom rendering functions, then the methods of the subclass. | |
2740 | + getHtmlRenderer: function(rendererName, rowType) { | |
2741 | + var view = this.view; | |
2742 | + var generalName; // like "cellHtml" | |
2743 | + var specificName; // like "dayCellHtml". based on rowType | |
2744 | + var provider; // either the View or the RowRenderer subclass, whichever provided the method | |
2745 | + var renderer; | |
2746 | + | |
2747 | + generalName = rendererName + 'Html'; | |
2748 | + if (rowType) { | |
2749 | + specificName = rowType + capitaliseFirstLetter(rendererName) + 'Html'; | |
2750 | + } | |
2751 | + | |
2752 | + if (specificName && (renderer = view[specificName])) { | |
2753 | + provider = view; | |
2754 | + } | |
2755 | + else if (specificName && (renderer = this[specificName])) { | |
2756 | + provider = this; | |
2757 | + } | |
2758 | + else if ((renderer = view[generalName])) { | |
2759 | + provider = view; | |
2760 | + } | |
2761 | + else if ((renderer = this[generalName])) { | |
2762 | + provider = this; | |
2763 | + } | |
2764 | + | |
2765 | + if (typeof renderer === 'function') { | |
2766 | + return function() { | |
2767 | + return renderer.apply(provider, arguments) || ''; // use correct `this` and always return a string | |
2768 | + }; | |
2769 | + } | |
2770 | + | |
2771 | + // the rendered can be a plain string as well. if not specified, always an empty string. | |
2772 | + return function() { | |
2773 | + return renderer || ''; | |
2774 | + }; | |
2775 | + } | |
2776 | + | |
2777 | +}); | |
2778 | + | |
2779 | +;; | |
2780 | + | |
2781 | +/* An abstract class comprised of a "grid" of cells that each represent a specific datetime | |
2782 | +----------------------------------------------------------------------------------------------------------------------*/ | |
2783 | + | |
2784 | +var Grid = fc.Grid = RowRenderer.extend({ | |
2785 | + | |
2786 | + start: null, // the date of the first cell | |
2787 | + end: null, // the date after the last cell | |
2788 | + | |
2789 | + rowCnt: 0, // number of rows | |
2790 | + colCnt: 0, // number of cols | |
2791 | + rowData: null, // array of objects, holding misc data for each row | |
2792 | + colData: null, // array of objects, holding misc data for each column | |
2793 | + | |
2794 | + el: null, // the containing element | |
2795 | + coordMap: null, // a GridCoordMap that converts pixel values to datetimes | |
2796 | + elsByFill: null, // a hash of jQuery element sets used for rendering each fill. Keyed by fill name. | |
2797 | + | |
2798 | + externalDragStartProxy: null, // binds the Grid's scope to externalDragStart (in DayGrid.events) | |
2799 | + | |
2800 | + // derived from options | |
2801 | + colHeadFormat: null, // TODO: move to another class. not applicable to all Grids | |
2802 | + eventTimeFormat: null, | |
2803 | + displayEventTime: null, | |
2804 | + displayEventEnd: null, | |
2805 | + | |
2806 | + // if all cells are the same length of time, the duration they all share. optional. | |
2807 | + // when defined, allows the computeCellRange shortcut, as well as improved resizing behavior. | |
2808 | + cellDuration: null, | |
2809 | + | |
2810 | + // if defined, holds the unit identified (ex: "year" or "month") that determines the level of granularity | |
2811 | + // of the date cells. if not defined, assumes to be day and time granularity. | |
2812 | + largeUnit: null, | |
2813 | + | |
2814 | + | |
2815 | + constructor: function() { | |
2816 | + RowRenderer.apply(this, arguments); // call the super-constructor | |
2817 | + | |
2818 | + this.coordMap = new GridCoordMap(this); | |
2819 | + this.elsByFill = {}; | |
2820 | + this.externalDragStartProxy = proxy(this, 'externalDragStart'); | |
2821 | + }, | |
2822 | + | |
2823 | + | |
2824 | + /* Options | |
2825 | + ------------------------------------------------------------------------------------------------------------------*/ | |
2826 | + | |
2827 | + | |
2828 | + // Generates the format string used for the text in column headers, if not explicitly defined by 'columnFormat' | |
2829 | + // TODO: move to another class. not applicable to all Grids | |
2830 | + computeColHeadFormat: function() { | |
2831 | + // subclasses must implement if they want to use headHtml() | |
2832 | + }, | |
2833 | + | |
2834 | + | |
2835 | + // Generates the format string used for event time text, if not explicitly defined by 'timeFormat' | |
2836 | + computeEventTimeFormat: function() { | |
2837 | + return this.view.opt('smallTimeFormat'); | |
2838 | + }, | |
2839 | + | |
2840 | + | |
2841 | + // Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventTime'. | |
2842 | + // Only applies to non-all-day events. | |
2843 | + computeDisplayEventTime: function() { | |
2844 | + return true; | |
2845 | + }, | |
2846 | + | |
2847 | + | |
2848 | + // Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventEnd' | |
2849 | + computeDisplayEventEnd: function() { | |
2850 | + return true; | |
2851 | + }, | |
2852 | + | |
2853 | + | |
2854 | + /* Dates | |
2855 | + ------------------------------------------------------------------------------------------------------------------*/ | |
2856 | + | |
2857 | + | |
2858 | + // Tells the grid about what period of time to display. Grid will subsequently compute dates for cell system. | |
2859 | + setRange: function(range) { | |
2860 | + var view = this.view; | |
2861 | + var displayEventTime; | |
2862 | + var displayEventEnd; | |
2863 | + | |
2864 | + this.start = range.start.clone(); | |
2865 | + this.end = range.end.clone(); | |
2866 | + | |
2867 | + this.rowData = []; | |
2868 | + this.colData = []; | |
2869 | + this.updateCells(); | |
2870 | + | |
2871 | + // Populate option-derived settings. Look for override first, then compute if necessary. | |
2872 | + this.colHeadFormat = view.opt('columnFormat') || this.computeColHeadFormat(); | |
2873 | + | |
2874 | + this.eventTimeFormat = | |
2875 | + view.opt('eventTimeFormat') || | |
2876 | + view.opt('timeFormat') || // deprecated | |
2877 | + this.computeEventTimeFormat(); | |
2878 | + | |
2879 | + displayEventTime = view.opt('displayEventTime'); | |
2880 | + if (displayEventTime == null) { | |
2881 | + displayEventTime = this.computeDisplayEventTime(); // might be based off of range | |
2882 | + } | |
2883 | + | |
2884 | + displayEventEnd = view.opt('displayEventEnd'); | |
2885 | + if (displayEventEnd == null) { | |
2886 | + displayEventEnd = this.computeDisplayEventEnd(); // might be based off of range | |
2887 | + } | |
2888 | + | |
2889 | + this.displayEventTime = displayEventTime; | |
2890 | + this.displayEventEnd = displayEventEnd; | |
2891 | + }, | |
2892 | + | |
2893 | + | |
2894 | + // Responsible for setting rowCnt/colCnt and any other row/col data | |
2895 | + updateCells: function() { | |
2896 | + // subclasses must implement | |
2897 | + }, | |
2898 | + | |
2899 | + | |
2900 | + // Converts a range with an inclusive `start` and an exclusive `end` into an array of segment objects | |
2901 | + rangeToSegs: function(range) { | |
2902 | + // subclasses must implement | |
2903 | + }, | |
2904 | + | |
2905 | + | |
2906 | + // Diffs the two dates, returning a duration, based on granularity of the grid | |
2907 | + diffDates: function(a, b) { | |
2908 | + if (this.largeUnit) { | |
2909 | + return diffByUnit(a, b, this.largeUnit); | |
2910 | + } | |
2911 | + else { | |
2912 | + return diffDayTime(a, b); | |
2913 | + } | |
2914 | + }, | |
2915 | + | |
2916 | + | |
2917 | + /* Cells | |
2918 | + ------------------------------------------------------------------------------------------------------------------*/ | |
2919 | + // NOTE: columns are ordered left-to-right | |
2920 | + | |
2921 | + | |
2922 | + // Gets an object containing row/col number, misc data, and range information about the cell. | |
2923 | + // Accepts row/col values, an object with row/col properties, or a single-number offset from the first cell. | |
2924 | + getCell: function(row, col) { | |
2925 | + var cell; | |
2926 | + | |
2927 | + if (col == null) { | |
2928 | + if (typeof row === 'number') { // a single-number offset | |
2929 | + col = row % this.colCnt; | |
2930 | + row = Math.floor(row / this.colCnt); | |
2931 | + } | |
2932 | + else { // an object with row/col properties | |
2933 | + col = row.col; | |
2934 | + row = row.row; | |
2935 | + } | |
2936 | + } | |
2937 | + | |
2938 | + cell = { row: row, col: col }; | |
2939 | + | |
2940 | + $.extend(cell, this.getRowData(row), this.getColData(col)); | |
2941 | + $.extend(cell, this.computeCellRange(cell)); | |
2942 | + | |
2943 | + return cell; | |
2944 | + }, | |
2945 | + | |
2946 | + | |
2947 | + // Given a cell object with index and misc data, generates a range object | |
2948 | + // If the grid is leveraging cellDuration, this doesn't need to be defined. Only computeCellDate does. | |
2949 | + // If being overridden, should return a range with reference-free date copies. | |
2950 | + computeCellRange: function(cell) { | |
2951 | + var date = this.computeCellDate(cell); | |
2952 | + | |
2953 | + return { | |
2954 | + start: date, | |
2955 | + end: date.clone().add(this.cellDuration) | |
2956 | + }; | |
2957 | + }, | |
2958 | + | |
2959 | + | |
2960 | + // Given a cell, returns its start date. Should return a reference-free date copy. | |
2961 | + computeCellDate: function(cell) { | |
2962 | + // subclasses can implement | |
2963 | + }, | |
2964 | + | |
2965 | + | |
2966 | + // Retrieves misc data about the given row | |
2967 | + getRowData: function(row) { | |
2968 | + return this.rowData[row] || {}; | |
2969 | + }, | |
2970 | + | |
2971 | + | |
2972 | + // Retrieves misc data baout the given column | |
2973 | + getColData: function(col) { | |
2974 | + return this.colData[col] || {}; | |
2975 | + }, | |
2976 | + | |
2977 | + | |
2978 | + // Retrieves the element representing the given row | |
2979 | + getRowEl: function(row) { | |
2980 | + // subclasses should implement if leveraging the default getCellDayEl() or computeRowCoords() | |
2981 | + }, | |
2982 | + | |
2983 | + | |
2984 | + // Retrieves the element representing the given column | |
2985 | + getColEl: function(col) { | |
2986 | + // subclasses should implement if leveraging the default getCellDayEl() or computeColCoords() | |
2987 | + }, | |
2988 | + | |
2989 | + | |
2990 | + // Given a cell object, returns the element that represents the cell's whole-day | |
2991 | + getCellDayEl: function(cell) { | |
2992 | + return this.getColEl(cell.col) || this.getRowEl(cell.row); | |
2993 | + }, | |
2994 | + | |
2995 | + | |
2996 | + /* Cell Coordinates | |
2997 | + ------------------------------------------------------------------------------------------------------------------*/ | |
2998 | + | |
2999 | + | |
3000 | + // Computes the top/bottom coordinates of all rows. | |
3001 | + // By default, queries the dimensions of the element provided by getRowEl(). | |
3002 | + computeRowCoords: function() { | |
3003 | + var items = []; | |
3004 | + var i, el; | |
3005 | + var top; | |
3006 | + | |
3007 | + for (i = 0; i < this.rowCnt; i++) { | |
3008 | + el = this.getRowEl(i); | |
3009 | + top = el.offset().top; | |
3010 | + items.push({ | |
3011 | + top: top, | |
3012 | + bottom: top + el.outerHeight() | |
3013 | + }); | |
3014 | + } | |
3015 | + | |
3016 | + return items; | |
3017 | + }, | |
3018 | + | |
3019 | + | |
3020 | + // Computes the left/right coordinates of all rows. | |
3021 | + // By default, queries the dimensions of the element provided by getColEl(). Columns can be LTR or RTL. | |
3022 | + computeColCoords: function() { | |
3023 | + var items = []; | |
3024 | + var i, el; | |
3025 | + var left; | |
3026 | + | |
3027 | + for (i = 0; i < this.colCnt; i++) { | |
3028 | + el = this.getColEl(i); | |
3029 | + left = el.offset().left; | |
3030 | + items.push({ | |
3031 | + left: left, | |
3032 | + right: left + el.outerWidth() | |
3033 | + }); | |
3034 | + } | |
3035 | + | |
3036 | + return items; | |
3037 | + }, | |
3038 | + | |
3039 | + | |
3040 | + /* Rendering | |
3041 | + ------------------------------------------------------------------------------------------------------------------*/ | |
3042 | + | |
3043 | + | |
3044 | + // Sets the container element that the grid should render inside of. | |
3045 | + // Does other DOM-related initializations. | |
3046 | + setElement: function(el) { | |
3047 | + var _this = this; | |
3048 | + | |
3049 | + this.el = el; | |
3050 | + | |
3051 | + // attach a handler to the grid's root element. | |
3052 | + // jQuery will take care of unregistering them when removeElement gets called. | |
3053 | + el.on('mousedown', function(ev) { | |
3054 | + if ( | |
3055 | + !$(ev.target).is('.fc-event-container *, .fc-more') && // not an an event element, or "more.." link | |
3056 | + !$(ev.target).closest('.fc-popover').length // not on a popover (like the "more.." events one) | |
3057 | + ) { | |
3058 | + _this.dayMousedown(ev); | |
3059 | + } | |
3060 | + }); | |
3061 | + | |
3062 | + // attach event-element-related handlers. in Grid.events | |
3063 | + // same garbage collection note as above. | |
3064 | + this.bindSegHandlers(); | |
3065 | + | |
3066 | + this.bindGlobalHandlers(); | |
3067 | + }, | |
3068 | + | |
3069 | + | |
3070 | + // Removes the grid's container element from the DOM. Undoes any other DOM-related attachments. | |
3071 | + // DOES NOT remove any content before hand (doens't clear events or call destroyDates), unlike View | |
3072 | + removeElement: function() { | |
3073 | + this.unbindGlobalHandlers(); | |
3074 | + | |
3075 | + this.el.remove(); | |
3076 | + | |
3077 | + // NOTE: we don't null-out this.el for the same reasons we don't do it within View::removeElement | |
3078 | + }, | |
3079 | + | |
3080 | + | |
3081 | + // Renders the basic structure of grid view before any content is rendered | |
3082 | + renderSkeleton: function() { | |
3083 | + // subclasses should implement | |
3084 | + }, | |
3085 | + | |
3086 | + | |
3087 | + // Renders the grid's date-related content (like cells that represent days/times). | |
3088 | + // Assumes setRange has already been called and the skeleton has already been rendered. | |
3089 | + renderDates: function() { | |
3090 | + // subclasses should implement | |
3091 | + }, | |
3092 | + | |
3093 | + | |
3094 | + // Unrenders the grid's date-related content | |
3095 | + destroyDates: function() { | |
3096 | + // subclasses should implement | |
3097 | + }, | |
3098 | + | |
3099 | + | |
3100 | + /* Handlers | |
3101 | + ------------------------------------------------------------------------------------------------------------------*/ | |
3102 | + | |
3103 | + | |
3104 | + // Binds DOM handlers to elements that reside outside the grid, such as the document | |
3105 | + bindGlobalHandlers: function() { | |
3106 | + $(document).on('dragstart sortstart', this.externalDragStartProxy); // jqui | |
3107 | + }, | |
3108 | + | |
3109 | + | |
3110 | + // Unbinds DOM handlers from elements that reside outside the grid | |
3111 | + unbindGlobalHandlers: function() { | |
3112 | + $(document).off('dragstart sortstart', this.externalDragStartProxy); // jqui | |
3113 | + }, | |
3114 | + | |
3115 | + | |
3116 | + // Process a mousedown on an element that represents a day. For day clicking and selecting. | |
3117 | + dayMousedown: function(ev) { | |
3118 | + var _this = this; | |
3119 | + var view = this.view; | |
3120 | + var isSelectable = view.opt('selectable'); | |
3121 | + var dayClickCell; // null if invalid dayClick | |
3122 | + var selectionRange; // null if invalid selection | |
3123 | + | |
3124 | + // this listener tracks a mousedown on a day element, and a subsequent drag. | |
3125 | + // if the drag ends on the same day, it is a 'dayClick'. | |
3126 | + // if 'selectable' is enabled, this listener also detects selections. | |
3127 | + var dragListener = new CellDragListener(this.coordMap, { | |
3128 | + //distance: 5, // needs more work if we want dayClick to fire correctly | |
3129 | + scroll: view.opt('dragScroll'), | |
3130 | + dragStart: function() { | |
3131 | + view.unselect(); // since we could be rendering a new selection, we want to clear any old one | |
3132 | + }, | |
3133 | + cellOver: function(cell, isOrig, origCell) { | |
3134 | + if (origCell) { // click needs to have started on a cell | |
3135 | + dayClickCell = isOrig ? cell : null; // single-cell selection is a day click | |
3136 | + if (isSelectable) { | |
3137 | + selectionRange = _this.computeSelection(origCell, cell); | |
3138 | + if (selectionRange) { | |
3139 | + _this.renderSelection(selectionRange); | |
3140 | + } | |
3141 | + else { | |
3142 | + disableCursor(); | |
3143 | + } | |
3144 | + } | |
3145 | + } | |
3146 | + }, | |
3147 | + cellOut: function(cell) { | |
3148 | + dayClickCell = null; | |
3149 | + selectionRange = null; | |
3150 | + _this.destroySelection(); | |
3151 | + enableCursor(); | |
3152 | + }, | |
3153 | + listenStop: function(ev) { | |
3154 | + if (dayClickCell) { | |
3155 | + view.trigger('dayClick', _this.getCellDayEl(dayClickCell), dayClickCell.start, ev); | |
3156 | + } | |
3157 | + if (selectionRange) { | |
3158 | + // the selection will already have been rendered. just report it | |
3159 | + view.reportSelection(selectionRange, ev); | |
3160 | + } | |
3161 | + enableCursor(); | |
3162 | + } | |
3163 | + }); | |
3164 | + | |
3165 | + dragListener.mousedown(ev); // start listening, which will eventually initiate a dragStart | |
3166 | + }, | |
3167 | + | |
3168 | + | |
3169 | + /* Event Helper | |
3170 | + ------------------------------------------------------------------------------------------------------------------*/ | |
3171 | + // TODO: should probably move this to Grid.events, like we did event dragging / resizing | |
3172 | + | |
3173 | + | |
3174 | + // Renders a mock event over the given range | |
3175 | + renderRangeHelper: function(range, sourceSeg) { | |
3176 | + var fakeEvent = this.fabricateHelperEvent(range, sourceSeg); | |
3177 | + | |
3178 | + this.renderHelper(fakeEvent, sourceSeg); // do the actual rendering | |
3179 | + }, | |
3180 | + | |
3181 | + | |
3182 | + // Builds a fake event given a date range it should cover, and a segment is should be inspired from. | |
3183 | + // The range's end can be null, in which case the mock event that is rendered will have a null end time. | |
3184 | + // `sourceSeg` is the internal segment object involved in the drag. If null, something external is dragging. | |
3185 | + fabricateHelperEvent: function(range, sourceSeg) { | |
3186 | + var fakeEvent = sourceSeg ? createObject(sourceSeg.event) : {}; // mask the original event object if possible | |
3187 | + | |
3188 | + fakeEvent.start = range.start.clone(); | |
3189 | + fakeEvent.end = range.end ? range.end.clone() : null; | |
3190 | + fakeEvent.allDay = null; // force it to be freshly computed by normalizeEventRange | |
3191 | + this.view.calendar.normalizeEventRange(fakeEvent); | |
3192 | + | |
3193 | + // this extra className will be useful for differentiating real events from mock events in CSS | |
3194 | + fakeEvent.className = (fakeEvent.className || []).concat('fc-helper'); | |
3195 | + | |
3196 | + // if something external is being dragged in, don't render a resizer | |
3197 | + if (!sourceSeg) { | |
3198 | + fakeEvent.editable = false; | |
3199 | + } | |
3200 | + | |
3201 | + return fakeEvent; | |
3202 | + }, | |
3203 | + | |
3204 | + | |
3205 | + // Renders a mock event | |
3206 | + renderHelper: function(event, sourceSeg) { | |
3207 | + // subclasses must implement | |
3208 | + }, | |
3209 | + | |
3210 | + | |
3211 | + // Unrenders a mock event | |
3212 | + destroyHelper: function() { | |
3213 | + // subclasses must implement | |
3214 | + }, | |
3215 | + | |
3216 | + | |
3217 | + /* Selection | |
3218 | + ------------------------------------------------------------------------------------------------------------------*/ | |
3219 | + | |
3220 | + | |
3221 | + // Renders a visual indication of a selection. Will highlight by default but can be overridden by subclasses. | |
3222 | + renderSelection: function(range) { | |
3223 | + this.renderHighlight(range); | |
3224 | + }, | |
3225 | + | |
3226 | + | |
3227 | + // Unrenders any visual indications of a selection. Will unrender a highlight by default. | |
3228 | + destroySelection: function() { | |
3229 | + this.destroyHighlight(); | |
3230 | + }, | |
3231 | + | |
3232 | + | |
3233 | + // Given the first and last cells of a selection, returns a range object. | |
3234 | + // Will return something falsy if the selection is invalid (when outside of selectionConstraint for example). | |
3235 | + // Subclasses can override and provide additional data in the range object. Will be passed to renderSelection(). | |
3236 | + computeSelection: function(firstCell, lastCell) { | |
3237 | + var dates = [ | |
3238 | + firstCell.start, | |
3239 | + firstCell.end, | |
3240 | + lastCell.start, | |
3241 | + lastCell.end | |
3242 | + ]; | |
3243 | + var range; | |
3244 | + | |
3245 | + dates.sort(compareNumbers); // sorts chronologically. works with Moments | |
3246 | + | |
3247 | + range = { | |
3248 | + start: dates[0].clone(), | |
3249 | + end: dates[3].clone() | |
3250 | + }; | |
3251 | + | |
3252 | + if (!this.view.calendar.isSelectionRangeAllowed(range)) { | |
3253 | + return null; | |
3254 | + } | |
3255 | + | |
3256 | + return range; | |
3257 | + }, | |
3258 | + | |
3259 | + | |
3260 | + /* Highlight | |
3261 | + ------------------------------------------------------------------------------------------------------------------*/ | |
3262 | + | |
3263 | + | |
3264 | + // Renders an emphasis on the given date range. `start` is inclusive. `end` is exclusive. | |
3265 | + renderHighlight: function(range) { | |
3266 | + this.renderFill('highlight', this.rangeToSegs(range)); | |
3267 | + }, | |
3268 | + | |
3269 | + | |
3270 | + // Unrenders the emphasis on a date range | |
3271 | + destroyHighlight: function() { | |
3272 | + this.destroyFill('highlight'); | |
3273 | + }, | |
3274 | + | |
3275 | + | |
3276 | + // Generates an array of classNames for rendering the highlight. Used by the fill system. | |
3277 | + highlightSegClasses: function() { | |
3278 | + return [ 'fc-highlight' ]; | |
3279 | + }, | |
3280 | + | |
3281 | + | |
3282 | + /* Fill System (highlight, background events, business hours) | |
3283 | + ------------------------------------------------------------------------------------------------------------------*/ | |
3284 | + | |
3285 | + | |
3286 | + // Renders a set of rectangles over the given segments of time. | |
3287 | + // Returns a subset of segs, the segs that were actually rendered. | |
3288 | + // Responsible for populating this.elsByFill. TODO: better API for expressing this requirement | |
3289 | + renderFill: function(type, segs) { | |
3290 | + // subclasses must implement | |
3291 | + }, | |
3292 | + | |
3293 | + | |
3294 | + // Unrenders a specific type of fill that is currently rendered on the grid | |
3295 | + destroyFill: function(type) { | |
3296 | + var el = this.elsByFill[type]; | |
3297 | + | |
3298 | + if (el) { | |
3299 | + el.remove(); | |
3300 | + delete this.elsByFill[type]; | |
3301 | + } | |
3302 | + }, | |
3303 | + | |
3304 | + | |
3305 | + // Renders and assigns an `el` property for each fill segment. Generic enough to work with different types. | |
3306 | + // Only returns segments that successfully rendered. | |
3307 | + // To be harnessed by renderFill (implemented by subclasses). | |
3308 | + // Analagous to renderFgSegEls. | |
3309 | + renderFillSegEls: function(type, segs) { | |
3310 | + var _this = this; | |
3311 | + var segElMethod = this[type + 'SegEl']; | |
3312 | + var html = ''; | |
3313 | + var renderedSegs = []; | |
3314 | + var i; | |
3315 | + | |
3316 | + if (segs.length) { | |
3317 | + | |
3318 | + // build a large concatenation of segment HTML | |
3319 | + for (i = 0; i < segs.length; i++) { | |
3320 | + html += this.fillSegHtml(type, segs[i]); | |
3321 | + } | |
3322 | + | |
3323 | + // Grab individual elements from the combined HTML string. Use each as the default rendering. | |
3324 | + // Then, compute the 'el' for each segment. | |
3325 | + $(html).each(function(i, node) { | |
3326 | + var seg = segs[i]; | |
3327 | + var el = $(node); | |
3328 | + | |
3329 | + // allow custom filter methods per-type | |
3330 | + if (segElMethod) { | |
3331 | + el = segElMethod.call(_this, seg, el); | |
3332 | + } | |
3333 | + | |
3334 | + if (el) { // custom filters did not cancel the render | |
3335 | + el = $(el); // allow custom filter to return raw DOM node | |
3336 | + | |
3337 | + // correct element type? (would be bad if a non-TD were inserted into a table for example) | |
3338 | + if (el.is(_this.fillSegTag)) { | |
3339 | + seg.el = el; | |
3340 | + renderedSegs.push(seg); | |
3341 | + } | |
3342 | + } | |
3343 | + }); | |
3344 | + } | |
3345 | + | |
3346 | + return renderedSegs; | |
3347 | + }, | |
3348 | + | |
3349 | + | |
3350 | + fillSegTag: 'div', // subclasses can override | |
3351 | + | |
3352 | + | |
3353 | + // Builds the HTML needed for one fill segment. Generic enought o work with different types. | |
3354 | + fillSegHtml: function(type, seg) { | |
3355 | + | |
3356 | + // custom hooks per-type | |
3357 | + var classesMethod = this[type + 'SegClasses']; | |
3358 | + var cssMethod = this[type + 'SegCss']; | |
3359 | + | |
3360 | + var classes = classesMethod ? classesMethod.call(this, seg) : []; | |
3361 | + var css = cssToStr(cssMethod ? cssMethod.call(this, seg) : {}); | |
3362 | + | |
3363 | + return '<' + this.fillSegTag + | |
3364 | + (classes.length ? ' class="' + classes.join(' ') + '"' : '') + | |
3365 | + (css ? ' style="' + css + '"' : '') + | |
3366 | + ' />'; | |
3367 | + }, | |
3368 | + | |
3369 | + | |
3370 | + /* Generic rendering utilities for subclasses | |
3371 | + ------------------------------------------------------------------------------------------------------------------*/ | |
3372 | + | |
3373 | + | |
3374 | + // Renders a day-of-week header row. | |
3375 | + // TODO: move to another class. not applicable to all Grids | |
3376 | + headHtml: function() { | |
3377 | + return '' + | |
3378 | + '<div class="fc-row ' + this.view.widgetHeaderClass + '">' + | |
3379 | + '<table>' + | |
3380 | + '<thead>' + | |
3381 | + this.rowHtml('head') + // leverages RowRenderer | |
3382 | + '</thead>' + | |
3383 | + '</table>' + | |
3384 | + '</div>'; | |
3385 | + }, | |
3386 | + | |
3387 | + | |
3388 | + // Used by the `headHtml` method, via RowRenderer, for rendering the HTML of a day-of-week header cell | |
3389 | + // TODO: move to another class. not applicable to all Grids | |
3390 | + headCellHtml: function(cell) { | |
3391 | + var view = this.view; | |
3392 | + var date = cell.start; | |
3393 | + | |
3394 | + return '' + | |
3395 | + '<th class="fc-day-header ' + view.widgetHeaderClass + ' fc-' + dayIDs[date.day()] + '">' + | |
3396 | + htmlEscape(date.format(this.colHeadFormat)) + | |
3397 | + '</th>'; | |
3398 | + }, | |
3399 | + | |
3400 | + | |
3401 | + // Renders the HTML for a single-day background cell | |
3402 | + bgCellHtml: function(cell) { | |
3403 | + var view = this.view; | |
3404 | + var date = cell.start; | |
3405 | + var classes = this.getDayClasses(date); | |
3406 | + | |
3407 | + classes.unshift('fc-day', view.widgetContentClass); | |
3408 | + | |
3409 | + return '<td class="' + classes.join(' ') + '"' + | |
3410 | + ' data-date="' + date.format('YYYY-MM-DD') + '"' + // if date has a time, won't format it | |
3411 | + '></td>'; | |
3412 | + }, | |
3413 | + | |
3414 | + | |
3415 | + // Computes HTML classNames for a single-day cell | |
3416 | + getDayClasses: function(date) { | |
3417 | + var view = this.view; | |
3418 | + var today = view.calendar.getNow().stripTime(); | |
3419 | + var classes = [ 'fc-' + dayIDs[date.day()] ]; | |
3420 | + | |
3421 | + if ( | |
3422 | + view.intervalDuration.as('months') == 1 && | |
3423 | + date.month() != view.intervalStart.month() | |
3424 | + ) { | |
3425 | + classes.push('fc-other-month'); | |
3426 | + } | |
3427 | + | |
3428 | + if (date.isSame(today, 'day')) { | |
3429 | + classes.push( | |
3430 | + 'fc-today', | |
3431 | + view.highlightStateClass | |
3432 | + ); | |
3433 | + } | |
3434 | + else if (date < today) { | |
3435 | + classes.push('fc-past'); | |
3436 | + } | |
3437 | + else { | |
3438 | + classes.push('fc-future'); | |
3439 | + } | |
3440 | + | |
3441 | + return classes; | |
3442 | + } | |
3443 | + | |
3444 | +}); | |
3445 | + | |
3446 | +;; | |
3447 | + | |
3448 | +/* Event-rendering and event-interaction methods for the abstract Grid class | |
3449 | +----------------------------------------------------------------------------------------------------------------------*/ | |
3450 | + | |
3451 | +Grid.mixin({ | |
3452 | + | |
3453 | + mousedOverSeg: null, // the segment object the user's mouse is over. null if over nothing | |
3454 | + isDraggingSeg: false, // is a segment being dragged? boolean | |
3455 | + isResizingSeg: false, // is a segment being resized? boolean | |
3456 | + isDraggingExternal: false, // jqui-dragging an external element? boolean | |
3457 | + segs: null, // the event segments currently rendered in the grid | |
3458 | + | |
3459 | + | |
3460 | + // Renders the given events onto the grid | |
3461 | + renderEvents: function(events) { | |
3462 | + var segs = this.eventsToSegs(events); | |
3463 | + var bgSegs = []; | |
3464 | + var fgSegs = []; | |
3465 | + var i, seg; | |
3466 | + | |
3467 | + for (i = 0; i < segs.length; i++) { | |
3468 | + seg = segs[i]; | |
3469 | + | |
3470 | + if (isBgEvent(seg.event)) { | |
3471 | + bgSegs.push(seg); | |
3472 | + } | |
3473 | + else { | |
3474 | + fgSegs.push(seg); | |
3475 | + } | |
3476 | + } | |
3477 | + | |
3478 | + // Render each different type of segment. | |
3479 | + // Each function may return a subset of the segs, segs that were actually rendered. | |
3480 | + bgSegs = this.renderBgSegs(bgSegs) || bgSegs; | |
3481 | + fgSegs = this.renderFgSegs(fgSegs) || fgSegs; | |
3482 | + | |
3483 | + this.segs = bgSegs.concat(fgSegs); | |
3484 | + }, | |
3485 | + | |
3486 | + | |
3487 | + // Unrenders all events currently rendered on the grid | |
3488 | + destroyEvents: function() { | |
3489 | + this.triggerSegMouseout(); // trigger an eventMouseout if user's mouse is over an event | |
3490 | + | |
3491 | + this.destroyFgSegs(); | |
3492 | + this.destroyBgSegs(); | |
3493 | + | |
3494 | + this.segs = null; | |
3495 | + }, | |
3496 | + | |
3497 | + | |
3498 | + // Retrieves all rendered segment objects currently rendered on the grid | |
3499 | + getEventSegs: function() { | |
3500 | + return this.segs || []; | |
3501 | + }, | |
3502 | + | |
3503 | + | |
3504 | + /* Foreground Segment Rendering | |
3505 | + ------------------------------------------------------------------------------------------------------------------*/ | |
3506 | + | |
3507 | + | |
3508 | + // Renders foreground event segments onto the grid. May return a subset of segs that were rendered. | |
3509 | + renderFgSegs: function(segs) { | |
3510 | + // subclasses must implement | |
3511 | + }, | |
3512 | + | |
3513 | + | |
3514 | + // Unrenders all currently rendered foreground segments | |
3515 | + destroyFgSegs: function() { | |
3516 | + // subclasses must implement | |
3517 | + }, | |
3518 | + | |
3519 | + | |
3520 | + // Renders and assigns an `el` property for each foreground event segment. | |
3521 | + // Only returns segments that successfully rendered. | |
3522 | + // A utility that subclasses may use. | |
3523 | + renderFgSegEls: function(segs, disableResizing) { | |
3524 | + var view = this.view; | |
3525 | + var html = ''; | |
3526 | + var renderedSegs = []; | |
3527 | + var i; | |
3528 | + | |
3529 | + if (segs.length) { // don't build an empty html string | |
3530 | + | |
3531 | + // build a large concatenation of event segment HTML | |
3532 | + for (i = 0; i < segs.length; i++) { | |
3533 | + html += this.fgSegHtml(segs[i], disableResizing); | |
3534 | + } | |
3535 | + | |
3536 | + // Grab individual elements from the combined HTML string. Use each as the default rendering. | |
3537 | + // Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false. | |
3538 | + $(html).each(function(i, node) { | |
3539 | + var seg = segs[i]; | |
3540 | + var el = view.resolveEventEl(seg.event, $(node)); | |
3541 | + | |
3542 | + if (el) { | |
3543 | + el.data('fc-seg', seg); // used by handlers | |
3544 | + seg.el = el; | |
3545 | + renderedSegs.push(seg); | |
3546 | + } | |
3547 | + }); | |
3548 | + } | |
3549 | + | |
3550 | + return renderedSegs; | |
3551 | + }, | |
3552 | + | |
3553 | + | |
3554 | + // Generates the HTML for the default rendering of a foreground event segment. Used by renderFgSegEls() | |
3555 | + fgSegHtml: function(seg, disableResizing) { | |
3556 | + // subclasses should implement | |
3557 | + }, | |
3558 | + | |
3559 | + | |
3560 | + /* Background Segment Rendering | |
3561 | + ------------------------------------------------------------------------------------------------------------------*/ | |
3562 | + | |
3563 | + | |
3564 | + // Renders the given background event segments onto the grid. | |
3565 | + // Returns a subset of the segs that were actually rendered. | |
3566 | + renderBgSegs: function(segs) { | |
3567 | + return this.renderFill('bgEvent', segs); | |
3568 | + }, | |
3569 | + | |
3570 | + | |
3571 | + // Unrenders all the currently rendered background event segments | |
3572 | + destroyBgSegs: function() { | |
3573 | + this.destroyFill('bgEvent'); | |
3574 | + }, | |
3575 | + | |
3576 | + | |
3577 | + // Renders a background event element, given the default rendering. Called by the fill system. | |
3578 | + bgEventSegEl: function(seg, el) { | |
3579 | + return this.view.resolveEventEl(seg.event, el); // will filter through eventRender | |
3580 | + }, | |
3581 | + | |
3582 | + | |
3583 | + // Generates an array of classNames to be used for the default rendering of a background event. | |
3584 | + // Called by the fill system. | |
3585 | + bgEventSegClasses: function(seg) { | |
3586 | + var event = seg.event; | |
3587 | + var source = event.source || {}; | |
3588 | + | |
3589 | + return [ 'fc-bgevent' ].concat( | |
3590 | + event.className, | |
3591 | + source.className || [] | |
3592 | + ); | |
3593 | + }, | |
3594 | + | |
3595 | + | |
3596 | + // Generates a semicolon-separated CSS string to be used for the default rendering of a background event. | |
3597 | + // Called by the fill system. | |
3598 | + // TODO: consolidate with getEventSkinCss? | |
3599 | + bgEventSegCss: function(seg) { | |
3600 | + var view = this.view; | |
3601 | + var event = seg.event; | |
3602 | + var source = event.source || {}; | |
3603 | + | |
3604 | + return { | |
3605 | + 'background-color': | |
3606 | + event.backgroundColor || | |
3607 | + event.color || | |
3608 | + source.backgroundColor || | |
3609 | + source.color || | |
3610 | + view.opt('eventBackgroundColor') || | |
3611 | + view.opt('eventColor') | |
3612 | + }; | |
3613 | + }, | |
3614 | + | |
3615 | + | |
3616 | + // Generates an array of classNames to be used for the rendering business hours overlay. Called by the fill system. | |
3617 | + businessHoursSegClasses: function(seg) { | |
3618 | + return [ 'fc-nonbusiness', 'fc-bgevent' ]; | |
3619 | + }, | |
3620 | + | |
3621 | + | |
3622 | + /* Handlers | |
3623 | + ------------------------------------------------------------------------------------------------------------------*/ | |
3624 | + | |
3625 | + | |
3626 | + // Attaches event-element-related handlers to the container element and leverage bubbling | |
3627 | + bindSegHandlers: function() { | |
3628 | + var _this = this; | |
3629 | + var view = this.view; | |
3630 | + | |
3631 | + $.each( | |
3632 | + { | |
3633 | + mouseenter: function(seg, ev) { | |
3634 | + _this.triggerSegMouseover(seg, ev); | |
3635 | + }, | |
3636 | + mouseleave: function(seg, ev) { | |
3637 | + _this.triggerSegMouseout(seg, ev); | |
3638 | + }, | |
3639 | + click: function(seg, ev) { | |
3640 | + return view.trigger('eventClick', this, seg.event, ev); // can return `false` to cancel | |
3641 | + }, | |
3642 | + mousedown: function(seg, ev) { | |
3643 | + if ($(ev.target).is('.fc-resizer') && view.isEventResizable(seg.event)) { | |
3644 | + _this.segResizeMousedown(seg, ev, $(ev.target).is('.fc-start-resizer')); | |
3645 | + } | |
3646 | + else if (view.isEventDraggable(seg.event)) { | |
3647 | + _this.segDragMousedown(seg, ev); | |
3648 | + } | |
3649 | + } | |
3650 | + }, | |
3651 | + function(name, func) { | |
3652 | + // attach the handler to the container element and only listen for real event elements via bubbling | |
3653 | + _this.el.on(name, '.fc-event-container > *', function(ev) { | |
3654 | + var seg = $(this).data('fc-seg'); // grab segment data. put there by View::renderEvents | |
3655 | + | |
3656 | + // only call the handlers if there is not a drag/resize in progress | |
3657 | + if (seg && !_this.isDraggingSeg && !_this.isResizingSeg) { | |
3658 | + return func.call(this, seg, ev); // `this` will be the event element | |
3659 | + } | |
3660 | + }); | |
3661 | + } | |
3662 | + ); | |
3663 | + }, | |
3664 | + | |
3665 | + | |
3666 | + // Updates internal state and triggers handlers for when an event element is moused over | |
3667 | + triggerSegMouseover: function(seg, ev) { | |
3668 | + if (!this.mousedOverSeg) { | |
3669 | + this.mousedOverSeg = seg; | |
3670 | + this.view.trigger('eventMouseover', seg.el[0], seg.event, ev); | |
3671 | + } | |
3672 | + }, | |
3673 | + | |
3674 | + | |
3675 | + // Updates internal state and triggers handlers for when an event element is moused out. | |
3676 | + // Can be given no arguments, in which case it will mouseout the segment that was previously moused over. | |
3677 | + triggerSegMouseout: function(seg, ev) { | |
3678 | + ev = ev || {}; // if given no args, make a mock mouse event | |
3679 | + | |
3680 | + if (this.mousedOverSeg) { | |
3681 | + seg = seg || this.mousedOverSeg; // if given no args, use the currently moused-over segment | |
3682 | + this.mousedOverSeg = null; | |
3683 | + this.view.trigger('eventMouseout', seg.el[0], seg.event, ev); | |
3684 | + } | |
3685 | + }, | |
3686 | + | |
3687 | + | |
3688 | + /* Event Dragging | |
3689 | + ------------------------------------------------------------------------------------------------------------------*/ | |
3690 | + | |
3691 | + | |
3692 | + // Called when the user does a mousedown on an event, which might lead to dragging. | |
3693 | + // Generic enough to work with any type of Grid. | |
3694 | + segDragMousedown: function(seg, ev) { | |
3695 | + var _this = this; | |
3696 | + var view = this.view; | |
3697 | + var calendar = view.calendar; | |
3698 | + var el = seg.el; | |
3699 | + var event = seg.event; | |
3700 | + var dropLocation; | |
3701 | + | |
3702 | + // A clone of the original element that will move with the mouse | |
3703 | + var mouseFollower = new MouseFollower(seg.el, { | |
3704 | + parentEl: view.el, | |
3705 | + opacity: view.opt('dragOpacity'), | |
3706 | + revertDuration: view.opt('dragRevertDuration'), | |
3707 | + zIndex: 2 // one above the .fc-view | |
3708 | + }); | |
3709 | + | |
3710 | + // Tracks mouse movement over the *view's* coordinate map. Allows dragging and dropping between subcomponents | |
3711 | + // of the view. | |
3712 | + var dragListener = new CellDragListener(view.coordMap, { | |
3713 | + distance: 5, | |
3714 | + scroll: view.opt('dragScroll'), | |
3715 | + subjectEl: el, | |
3716 | + subjectCenter: true, | |
3717 | + listenStart: function(ev) { | |
3718 | + mouseFollower.hide(); // don't show until we know this is a real drag | |
3719 | + mouseFollower.start(ev); | |
3720 | + }, | |
3721 | + dragStart: function(ev) { | |
3722 | + _this.triggerSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported | |
3723 | + _this.segDragStart(seg, ev); | |
3724 | + view.hideEvent(event); // hide all event segments. our mouseFollower will take over | |
3725 | + }, | |
3726 | + cellOver: function(cell, isOrig, origCell) { | |
3727 | + | |
3728 | + // starting cell could be forced (DayGrid.limit) | |
3729 | + if (seg.cell) { | |
3730 | + origCell = seg.cell; | |
3731 | + } | |
3732 | + | |
3733 | + dropLocation = _this.computeEventDrop(origCell, cell, event); | |
3734 | + | |
3735 | + if (dropLocation && !calendar.isEventRangeAllowed(dropLocation, event)) { | |
3736 | + disableCursor(); | |
3737 | + dropLocation = null; | |
3738 | + } | |
3739 | + | |
3740 | + // if a valid drop location, have the subclass render a visual indication | |
3741 | + if (dropLocation && view.renderDrag(dropLocation, seg)) { | |
3742 | + mouseFollower.hide(); // if the subclass is already using a mock event "helper", hide our own | |
3743 | + } | |
3744 | + else { | |
3745 | + mouseFollower.show(); // otherwise, have the helper follow the mouse (no snapping) | |
3746 | + } | |
3747 | + | |
3748 | + if (isOrig) { | |
3749 | + dropLocation = null; // needs to have moved cells to be a valid drop | |
3750 | + } | |
3751 | + }, | |
3752 | + cellOut: function() { // called before mouse moves to a different cell OR moved out of all cells | |
3753 | + view.destroyDrag(); // unrender whatever was done in renderDrag | |
3754 | + mouseFollower.show(); // show in case we are moving out of all cells | |
3755 | + dropLocation = null; | |
3756 | + }, | |
3757 | + cellDone: function() { // Called after a cellOut OR before a dragStop | |
3758 | + enableCursor(); | |
3759 | + }, | |
3760 | + dragStop: function(ev) { | |
3761 | + // do revert animation if hasn't changed. calls a callback when finished (whether animation or not) | |
3762 | + mouseFollower.stop(!dropLocation, function() { | |
3763 | + view.destroyDrag(); | |
3764 | + view.showEvent(event); | |
3765 | + _this.segDragStop(seg, ev); | |
3766 | + | |
3767 | + if (dropLocation) { | |
3768 | + view.reportEventDrop(event, dropLocation, this.largeUnit, el, ev); | |
3769 | + } | |
3770 | + }); | |
3771 | + }, | |
3772 | + listenStop: function() { | |
3773 | + mouseFollower.stop(); // put in listenStop in case there was a mousedown but the drag never started | |
3774 | + } | |
3775 | + }); | |
3776 | + | |
3777 | + dragListener.mousedown(ev); // start listening, which will eventually lead to a dragStart | |
3778 | + }, | |
3779 | + | |
3780 | + | |
3781 | + // Called before event segment dragging starts | |
3782 | + segDragStart: function(seg, ev) { | |
3783 | + this.isDraggingSeg = true; | |
3784 | + this.view.trigger('eventDragStart', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy | |
3785 | + }, | |
3786 | + | |
3787 | + | |
3788 | + // Called after event segment dragging stops | |
3789 | + segDragStop: function(seg, ev) { | |
3790 | + this.isDraggingSeg = false; | |
3791 | + this.view.trigger('eventDragStop', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy | |
3792 | + }, | |
3793 | + | |
3794 | + | |
3795 | + // Given the cell an event drag began, and the cell event was dropped, calculates the new start/end/allDay | |
3796 | + // values for the event. Subclasses may override and set additional properties to be used by renderDrag. | |
3797 | + // A falsy returned value indicates an invalid drop. | |
3798 | + computeEventDrop: function(startCell, endCell, event) { | |
3799 | + var calendar = this.view.calendar; | |
3800 | + var dragStart = startCell.start; | |
3801 | + var dragEnd = endCell.start; | |
3802 | + var delta; | |
3803 | + var dropLocation; | |
3804 | + | |
3805 | + if (dragStart.hasTime() === dragEnd.hasTime()) { | |
3806 | + delta = this.diffDates(dragEnd, dragStart); | |
3807 | + | |
3808 | + // if an all-day event was in a timed area and it was dragged to a different time, | |
3809 | + // guarantee an end and adjust start/end to have times | |
3810 | + if (event.allDay && durationHasTime(delta)) { | |
3811 | + dropLocation = { | |
3812 | + start: event.start.clone(), | |
3813 | + end: calendar.getEventEnd(event), // will be an ambig day | |
3814 | + allDay: false // for normalizeEventRangeTimes | |
3815 | + }; | |
3816 | + calendar.normalizeEventRangeTimes(dropLocation); | |
3817 | + } | |
3818 | + // othewise, work off existing values | |
3819 | + else { | |
3820 | + dropLocation = { | |
3821 | + start: event.start.clone(), | |
3822 | + end: event.end ? event.end.clone() : null, | |
3823 | + allDay: event.allDay // keep it the same | |
3824 | + }; | |
3825 | + } | |
3826 | + | |
3827 | + dropLocation.start.add(delta); | |
3828 | + if (dropLocation.end) { | |
3829 | + dropLocation.end.add(delta); | |
3830 | + } | |
3831 | + } | |
3832 | + else { | |
3833 | + // if switching from day <-> timed, start should be reset to the dropped date, and the end cleared | |
3834 | + dropLocation = { | |
3835 | + start: dragEnd.clone(), | |
3836 | + end: null, // end should be cleared | |
3837 | + allDay: !dragEnd.hasTime() | |
3838 | + }; | |
3839 | + } | |
3840 | + | |
3841 | + return dropLocation; | |
3842 | + }, | |
3843 | + | |
3844 | + | |
3845 | + // Utility for apply dragOpacity to a jQuery set | |
3846 | + applyDragOpacity: function(els) { | |
3847 | + var opacity = this.view.opt('dragOpacity'); | |
3848 | + | |
3849 | + if (opacity != null) { | |
3850 | + els.each(function(i, node) { | |
3851 | + // Don't use jQuery (will set an IE filter), do it the old fashioned way. | |
3852 | + // In IE8, a helper element will disappears if there's a filter. | |
3853 | + node.style.opacity = opacity; | |
3854 | + }); | |
3855 | + } | |
3856 | + }, | |
3857 | + | |
3858 | + | |
3859 | + /* External Element Dragging | |
3860 | + ------------------------------------------------------------------------------------------------------------------*/ | |
3861 | + | |
3862 | + | |
3863 | + // Called when a jQuery UI drag is initiated anywhere in the DOM | |
3864 | + externalDragStart: function(ev, ui) { | |
3865 | + var view = this.view; | |
3866 | + var el; | |
3867 | + var accept; | |
3868 | + | |
3869 | + if (view.opt('droppable')) { // only listen if this setting is on | |
3870 | + el = $((ui ? ui.item : null) || ev.target); | |
3871 | + | |
3872 | + // Test that the dragged element passes the dropAccept selector or filter function. | |
3873 | + // FYI, the default is "*" (matches all) | |
3874 | + accept = view.opt('dropAccept'); | |
3875 | + if ($.isFunction(accept) ? accept.call(el[0], el) : el.is(accept)) { | |
3876 | + if (!this.isDraggingExternal) { // prevent double-listening if fired twice | |
3877 | + this.listenToExternalDrag(el, ev, ui); | |
3878 | + } | |
3879 | + } | |
3880 | + } | |
3881 | + }, | |
3882 | + | |
3883 | + | |
3884 | + // Called when a jQuery UI drag starts and it needs to be monitored for cell dropping | |
3885 | + listenToExternalDrag: function(el, ev, ui) { | |
3886 | + var _this = this; | |
3887 | + var meta = getDraggedElMeta(el); // extra data about event drop, including possible event to create | |
3888 | + var dragListener; | |
3889 | + var dropLocation; // a null value signals an unsuccessful drag | |
3890 | + | |
3891 | + // listener that tracks mouse movement over date-associated pixel regions | |
3892 | + dragListener = new CellDragListener(this.coordMap, { | |
3893 | + listenStart: function() { | |
3894 | + _this.isDraggingExternal = true; | |
3895 | + }, | |
3896 | + cellOver: function(cell) { | |
3897 | + dropLocation = _this.computeExternalDrop(cell, meta); | |
3898 | + if (dropLocation) { | |
3899 | + _this.renderDrag(dropLocation); // called without a seg parameter | |
3900 | + } | |
3901 | + else { // invalid drop cell | |
3902 | + disableCursor(); | |
3903 | + } | |
3904 | + }, | |
3905 | + cellOut: function() { | |
3906 | + dropLocation = null; // signal unsuccessful | |
3907 | + _this.destroyDrag(); | |
3908 | + enableCursor(); | |
3909 | + }, | |
3910 | + dragStop: function() { | |
3911 | + _this.destroyDrag(); | |
3912 | + enableCursor(); | |
3913 | + | |
3914 | + if (dropLocation) { // element was dropped on a valid date/time cell | |
3915 | + _this.view.reportExternalDrop(meta, dropLocation, el, ev, ui); | |
3916 | + } | |
3917 | + }, | |
3918 | + listenStop: function() { | |
3919 | + _this.isDraggingExternal = false; | |
3920 | + } | |
3921 | + }); | |
3922 | + | |
3923 | + dragListener.startDrag(ev); // start listening immediately | |
3924 | + }, | |
3925 | + | |
3926 | + | |
3927 | + // Given a cell to be dropped upon, and misc data associated with the jqui drag (guaranteed to be a plain object), | |
3928 | + // returns start/end dates for the event that would result from the hypothetical drop. end might be null. | |
3929 | + // Returning a null value signals an invalid drop cell. | |
3930 | + computeExternalDrop: function(cell, meta) { | |
3931 | + var dropLocation = { | |
3932 | + start: cell.start.clone(), | |
3933 | + end: null | |
3934 | + }; | |
3935 | + | |
3936 | + // if dropped on an all-day cell, and element's metadata specified a time, set it | |
3937 | + if (meta.startTime && !dropLocation.start.hasTime()) { | |
3938 | + dropLocation.start.time(meta.startTime); | |
3939 | + } | |
3940 | + | |
3941 | + if (meta.duration) { | |
3942 | + dropLocation.end = dropLocation.start.clone().add(meta.duration); | |
3943 | + } | |
3944 | + | |
3945 | + if (!this.view.calendar.isExternalDropRangeAllowed(dropLocation, meta.eventProps)) { | |
3946 | + return null; | |
3947 | + } | |
3948 | + | |
3949 | + return dropLocation; | |
3950 | + }, | |
3951 | + | |
3952 | + | |
3953 | + | |
3954 | + /* Drag Rendering (for both events and an external elements) | |
3955 | + ------------------------------------------------------------------------------------------------------------------*/ | |
3956 | + | |
3957 | + | |
3958 | + // Renders a visual indication of an event or external element being dragged. | |
3959 | + // `dropLocation` contains hypothetical start/end/allDay values the event would have if dropped. end can be null. | |
3960 | + // `seg` is the internal segment object that is being dragged. If dragging an external element, `seg` is null. | |
3961 | + // A truthy returned value indicates this method has rendered a helper element. | |
3962 | + renderDrag: function(dropLocation, seg) { | |
3963 | + // subclasses must implement | |
3964 | + }, | |
3965 | + | |
3966 | + | |
3967 | + // Unrenders a visual indication of an event or external element being dragged | |
3968 | + destroyDrag: function() { | |
3969 | + // subclasses must implement | |
3970 | + }, | |
3971 | + | |
3972 | + | |
3973 | + /* Resizing | |
3974 | + ------------------------------------------------------------------------------------------------------------------*/ | |
3975 | + | |
3976 | + | |
3977 | + // Called when the user does a mousedown on an event's resizer, which might lead to resizing. | |
3978 | + // Generic enough to work with any type of Grid. | |
3979 | + segResizeMousedown: function(seg, ev, isStart) { | |
3980 | + var _this = this; | |
3981 | + var view = this.view; | |
3982 | + var calendar = view.calendar; | |
3983 | + var el = seg.el; | |
3984 | + var event = seg.event; | |
3985 | + var eventEnd = calendar.getEventEnd(event); | |
3986 | + var dragListener; | |
3987 | + var resizeLocation; // falsy if invalid resize | |
3988 | + | |
3989 | + // Tracks mouse movement over the *grid's* coordinate map | |
3990 | + dragListener = new CellDragListener(this.coordMap, { | |
3991 | + distance: 5, | |
3992 | + scroll: view.opt('dragScroll'), | |
3993 | + subjectEl: el, | |
3994 | + dragStart: function(ev) { | |
3995 | + _this.triggerSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported | |
3996 | + _this.segResizeStart(seg, ev); | |
3997 | + }, | |
3998 | + cellOver: function(cell, isOrig, origCell) { | |
3999 | + resizeLocation = isStart ? | |
4000 | + _this.computeEventStartResize(origCell, cell, event) : | |
4001 | + _this.computeEventEndResize(origCell, cell, event); | |
4002 | + | |
4003 | + if (resizeLocation) { | |
4004 | + if (!calendar.isEventRangeAllowed(resizeLocation, event)) { | |
4005 | + disableCursor(); | |
4006 | + resizeLocation = null; | |
4007 | + } | |
4008 | + // no change? (TODO: how does this work with timezones?) | |
4009 | + else if (resizeLocation.start.isSame(event.start) && resizeLocation.end.isSame(eventEnd)) { | |
4010 | + resizeLocation = null; | |
4011 | + } | |
4012 | + } | |
4013 | + | |
4014 | + if (resizeLocation) { | |
4015 | + view.hideEvent(event); | |
4016 | + _this.renderEventResize(resizeLocation, seg); | |
4017 | + } | |
4018 | + }, | |
4019 | + cellOut: function() { // called before mouse moves to a different cell OR moved out of all cells | |
4020 | + resizeLocation = null; | |
4021 | + }, | |
4022 | + cellDone: function() { // resets the rendering to show the original event | |
4023 | + _this.destroyEventResize(); | |
4024 | + view.showEvent(event); | |
4025 | + enableCursor(); | |
4026 | + }, | |
4027 | + dragStop: function(ev) { | |
4028 | + _this.segResizeStop(seg, ev); | |
4029 | + | |
4030 | + if (resizeLocation) { // valid date to resize to? | |
4031 | + view.reportEventResize(event, resizeLocation, this.largeUnit, el, ev); | |
4032 | + } | |
4033 | + } | |
4034 | + }); | |
4035 | + | |
4036 | + dragListener.mousedown(ev); // start listening, which will eventually lead to a dragStart | |
4037 | + }, | |
4038 | + | |
4039 | + | |
4040 | + // Called before event segment resizing starts | |
4041 | + segResizeStart: function(seg, ev) { | |
4042 | + this.isResizingSeg = true; | |
4043 | + this.view.trigger('eventResizeStart', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy | |
4044 | + }, | |
4045 | + | |
4046 | + | |
4047 | + // Called after event segment resizing stops | |
4048 | + segResizeStop: function(seg, ev) { | |
4049 | + this.isResizingSeg = false; | |
4050 | + this.view.trigger('eventResizeStop', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy | |
4051 | + }, | |
4052 | + | |
4053 | + | |
4054 | + // Returns new date-information for an event segment being resized from its start | |
4055 | + computeEventStartResize: function(startCell, endCell, event) { | |
4056 | + return this.computeEventResize('start', startCell, endCell, event); | |
4057 | + }, | |
4058 | + | |
4059 | + | |
4060 | + // Returns new date-information for an event segment being resized from its end | |
4061 | + computeEventEndResize: function(startCell, endCell, event) { | |
4062 | + return this.computeEventResize('end', startCell, endCell, event); | |
4063 | + }, | |
4064 | + | |
4065 | + | |
4066 | + // Returns new date-information for an event segment being resized from its start OR end | |
4067 | + // `type` is either 'start' or 'end' | |
4068 | + computeEventResize: function(type, startCell, endCell, event) { | |
4069 | + var calendar = this.view.calendar; | |
4070 | + var delta = this.diffDates(endCell[type], startCell[type]); | |
4071 | + var range; | |
4072 | + var defaultDuration; | |
4073 | + | |
4074 | + // build original values to work from, guaranteeing a start and end | |
4075 | + range = { | |
4076 | + start: event.start.clone(), | |
4077 | + end: calendar.getEventEnd(event), | |
4078 | + allDay: event.allDay | |
4079 | + }; | |
4080 | + | |
4081 | + // if an all-day event was in a timed area and was resized to a time, adjust start/end to have times | |
4082 | + if (range.allDay && durationHasTime(delta)) { | |
4083 | + range.allDay = false; | |
4084 | + calendar.normalizeEventRangeTimes(range); | |
4085 | + } | |
4086 | + | |
4087 | + range[type].add(delta); // apply delta to start or end | |
4088 | + | |
4089 | + // if the event was compressed too small, find a new reasonable duration for it | |
4090 | + if (!range.start.isBefore(range.end)) { | |
4091 | + | |
4092 | + defaultDuration = event.allDay ? | |
4093 | + calendar.defaultAllDayEventDuration : | |
4094 | + calendar.defaultTimedEventDuration; | |
4095 | + | |
4096 | + // between the cell's duration and the event's default duration, use the smaller of the two. | |
4097 | + // example: if year-length slots, and compressed to one slot, we don't want the event to be a year long | |
4098 | + if (this.cellDuration && this.cellDuration < defaultDuration) { | |
4099 | + defaultDuration = this.cellDuration; | |
4100 | + } | |
4101 | + | |
4102 | + if (type == 'start') { // resizing the start? | |
4103 | + range.start = range.end.clone().subtract(defaultDuration); | |
4104 | + } | |
4105 | + else { // resizing the end? | |
4106 | + range.end = range.start.clone().add(defaultDuration); | |
4107 | + } | |
4108 | + } | |
4109 | + | |
4110 | + return range; | |
4111 | + }, | |
4112 | + | |
4113 | + | |
4114 | + // Renders a visual indication of an event being resized. | |
4115 | + // `range` has the updated dates of the event. `seg` is the original segment object involved in the drag. | |
4116 | + renderEventResize: function(range, seg) { | |
4117 | + // subclasses must implement | |
4118 | + }, | |
4119 | + | |
4120 | + | |
4121 | + // Unrenders a visual indication of an event being resized. | |
4122 | + destroyEventResize: function() { | |
4123 | + // subclasses must implement | |
4124 | + }, | |
4125 | + | |
4126 | + | |
4127 | + /* Rendering Utils | |
4128 | + ------------------------------------------------------------------------------------------------------------------*/ | |
4129 | + | |
4130 | + | |
4131 | + // Compute the text that should be displayed on an event's element. | |
4132 | + // `range` can be the Event object itself, or something range-like, with at least a `start`. | |
4133 | + // If event times are disabled, or the event has no time, will return a blank string. | |
4134 | + // If not specified, formatStr will default to the eventTimeFormat setting, | |
4135 | + // and displayEnd will default to the displayEventEnd setting. | |
4136 | + getEventTimeText: function(range, formatStr, displayEnd) { | |
4137 | + | |
4138 | + if (formatStr == null) { | |
4139 | + formatStr = this.eventTimeFormat; | |
4140 | + } | |
4141 | + | |
4142 | + if (displayEnd == null) { | |
4143 | + displayEnd = this.displayEventEnd; | |
4144 | + } | |
4145 | + | |
4146 | + if (this.displayEventTime && range.start.hasTime()) { | |
4147 | + if (displayEnd && range.end) { | |
4148 | + return this.view.formatRange(range, formatStr); | |
4149 | + } | |
4150 | + else { | |
4151 | + return range.start.format(formatStr); | |
4152 | + } | |
4153 | + } | |
4154 | + | |
4155 | + return ''; | |
4156 | + }, | |
4157 | + | |
4158 | + | |
4159 | + // Generic utility for generating the HTML classNames for an event segment's element | |
4160 | + getSegClasses: function(seg, isDraggable, isResizable) { | |
4161 | + var event = seg.event; | |
4162 | + var classes = [ | |
4163 | + 'fc-event', | |
4164 | + seg.isStart ? 'fc-start' : 'fc-not-start', | |
4165 | + seg.isEnd ? 'fc-end' : 'fc-not-end' | |
4166 | + ].concat( | |
4167 | + event.className, | |
4168 | + event.source ? event.source.className : [] | |
4169 | + ); | |
4170 | + | |
4171 | + if (isDraggable) { | |
4172 | + classes.push('fc-draggable'); | |
4173 | + } | |
4174 | + if (isResizable) { | |
4175 | + classes.push('fc-resizable'); | |
4176 | + } | |
4177 | + | |
4178 | + return classes; | |
4179 | + }, | |
4180 | + | |
4181 | + | |
4182 | + // Utility for generating event skin-related CSS properties | |
4183 | + getEventSkinCss: function(event) { | |
4184 | + var view = this.view; | |
4185 | + var source = event.source || {}; | |
4186 | + var eventColor = event.color; | |
4187 | + var sourceColor = source.color; | |
4188 | + var optionColor = view.opt('eventColor'); | |
4189 | + | |
4190 | + return { | |
4191 | + 'background-color': | |
4192 | + event.backgroundColor || | |
4193 | + eventColor || | |
4194 | + source.backgroundColor || | |
4195 | + sourceColor || | |
4196 | + view.opt('eventBackgroundColor') || | |
4197 | + optionColor, | |
4198 | + 'border-color': | |
4199 | + event.borderColor || | |
4200 | + eventColor || | |
4201 | + source.borderColor || | |
4202 | + sourceColor || | |
4203 | + view.opt('eventBorderColor') || | |
4204 | + optionColor, | |
4205 | + color: | |
4206 | + event.textColor || | |
4207 | + source.textColor || | |
4208 | + view.opt('eventTextColor') | |
4209 | + }; | |
4210 | + }, | |
4211 | + | |
4212 | + | |
4213 | + /* Converting events -> ranges -> segs | |
4214 | + ------------------------------------------------------------------------------------------------------------------*/ | |
4215 | + | |
4216 | + | |
4217 | + // Converts an array of event objects into an array of event segment objects. | |
4218 | + // A custom `rangeToSegsFunc` may be given for arbitrarily slicing up events. | |
4219 | + // Doesn't guarantee an order for the resulting array. | |
4220 | + eventsToSegs: function(events, rangeToSegsFunc) { | |
4221 | + var eventRanges = this.eventsToRanges(events); | |
4222 | + var segs = []; | |
4223 | + var i; | |
4224 | + | |
4225 | + for (i = 0; i < eventRanges.length; i++) { | |
4226 | + segs.push.apply( | |
4227 | + segs, | |
4228 | + this.eventRangeToSegs(eventRanges[i], rangeToSegsFunc) | |
4229 | + ); | |
4230 | + } | |
4231 | + | |
4232 | + return segs; | |
4233 | + }, | |
4234 | + | |
4235 | + | |
4236 | + // Converts an array of events into an array of "range" objects. | |
4237 | + // A "range" object is a plain object with start/end properties denoting the time it covers. Also an event property. | |
4238 | + // For "normal" events, this will be identical to the event's start/end, but for "inverse-background" events, | |
4239 | + // will create an array of ranges that span the time *not* covered by the given event. | |
4240 | + // Doesn't guarantee an order for the resulting array. | |
4241 | + eventsToRanges: function(events) { | |
4242 | + var _this = this; | |
4243 | + var eventsById = groupEventsById(events); | |
4244 | + var ranges = []; | |
4245 | + | |
4246 | + // group by ID so that related inverse-background events can be rendered together | |
4247 | + $.each(eventsById, function(id, eventGroup) { | |
4248 | + if (eventGroup.length) { | |
4249 | + ranges.push.apply( | |
4250 | + ranges, | |
4251 | + isInverseBgEvent(eventGroup[0]) ? | |
4252 | + _this.eventsToInverseRanges(eventGroup) : | |
4253 | + _this.eventsToNormalRanges(eventGroup) | |
4254 | + ); | |
4255 | + } | |
4256 | + }); | |
4257 | + | |
4258 | + return ranges; | |
4259 | + }, | |
4260 | + | |
4261 | + | |
4262 | + // Converts an array of "normal" events (not inverted rendering) into a parallel array of ranges | |
4263 | + eventsToNormalRanges: function(events) { | |
4264 | + var calendar = this.view.calendar; | |
4265 | + var ranges = []; | |
4266 | + var i, event; | |
4267 | + var eventStart, eventEnd; | |
4268 | + | |
4269 | + for (i = 0; i < events.length; i++) { | |
4270 | + event = events[i]; | |
4271 | + | |
4272 | + // make copies and normalize by stripping timezone | |
4273 | + eventStart = event.start.clone().stripZone(); | |
4274 | + eventEnd = calendar.getEventEnd(event).stripZone(); | |
4275 | + | |
4276 | + ranges.push({ | |
4277 | + event: event, | |
4278 | + start: eventStart, | |
4279 | + end: eventEnd, | |
4280 | + eventStartMS: +eventStart, | |
4281 | + eventDurationMS: eventEnd - eventStart | |
4282 | + }); | |
4283 | + } | |
4284 | + | |
4285 | + return ranges; | |
4286 | + }, | |
4287 | + | |
4288 | + | |
4289 | + // Converts an array of events, with inverse-background rendering, into an array of range objects. | |
4290 | + // The range objects will cover all the time NOT covered by the events. | |
4291 | + eventsToInverseRanges: function(events) { | |
4292 | + var view = this.view; | |
4293 | + var viewStart = view.start.clone().stripZone(); // normalize timezone | |
4294 | + var viewEnd = view.end.clone().stripZone(); // normalize timezone | |
4295 | + var normalRanges = this.eventsToNormalRanges(events); // will give us normalized dates we can use w/o copies | |
4296 | + var inverseRanges = []; | |
4297 | + var event0 = events[0]; // assign this to each range's `.event` | |
4298 | + var start = viewStart; // the end of the previous range. the start of the new range | |
4299 | + var i, normalRange; | |
4300 | + | |
4301 | + // ranges need to be in order. required for our date-walking algorithm | |
4302 | + normalRanges.sort(compareNormalRanges); | |
4303 | + | |
4304 | + for (i = 0; i < normalRanges.length; i++) { | |
4305 | + normalRange = normalRanges[i]; | |
4306 | + | |
4307 | + // add the span of time before the event (if there is any) | |
4308 | + if (normalRange.start > start) { // compare millisecond time (skip any ambig logic) | |
4309 | + inverseRanges.push({ | |
4310 | + event: event0, | |
4311 | + start: start, | |
4312 | + end: normalRange.start | |
4313 | + }); | |
4314 | + } | |
4315 | + | |
4316 | + start = normalRange.end; | |
4317 | + } | |
4318 | + | |
4319 | + // add the span of time after the last event (if there is any) | |
4320 | + if (start < viewEnd) { // compare millisecond time (skip any ambig logic) | |
4321 | + inverseRanges.push({ | |
4322 | + event: event0, | |
4323 | + start: start, | |
4324 | + end: viewEnd | |
4325 | + }); | |
4326 | + } | |
4327 | + | |
4328 | + return inverseRanges; | |
4329 | + }, | |
4330 | + | |
4331 | + | |
4332 | + // Slices the given event range into one or more segment objects. | |
4333 | + // A `rangeToSegsFunc` custom slicing function can be given. | |
4334 | + eventRangeToSegs: function(eventRange, rangeToSegsFunc) { | |
4335 | + var segs; | |
4336 | + var i, seg; | |
4337 | + | |
4338 | + if (rangeToSegsFunc) { | |
4339 | + segs = rangeToSegsFunc(eventRange); | |
4340 | + } | |
4341 | + else { | |
4342 | + segs = this.rangeToSegs(eventRange); // defined by the subclass | |
4343 | + } | |
4344 | + | |
4345 | + for (i = 0; i < segs.length; i++) { | |
4346 | + seg = segs[i]; | |
4347 | + seg.event = eventRange.event; | |
4348 | + seg.eventStartMS = eventRange.eventStartMS; | |
4349 | + seg.eventDurationMS = eventRange.eventDurationMS; | |
4350 | + } | |
4351 | + | |
4352 | + return segs; | |
4353 | + } | |
4354 | + | |
4355 | +}); | |
4356 | + | |
4357 | + | |
4358 | +/* Utilities | |
4359 | +----------------------------------------------------------------------------------------------------------------------*/ | |
4360 | + | |
4361 | + | |
4362 | +function isBgEvent(event) { // returns true if background OR inverse-background | |
4363 | + var rendering = getEventRendering(event); | |
4364 | + return rendering === 'background' || rendering === 'inverse-background'; | |
4365 | +} | |
4366 | + | |
4367 | + | |
4368 | +function isInverseBgEvent(event) { | |
4369 | + return getEventRendering(event) === 'inverse-background'; | |
4370 | +} | |
4371 | + | |
4372 | + | |
4373 | +function getEventRendering(event) { | |
4374 | + return firstDefined((event.source || {}).rendering, event.rendering); | |
4375 | +} | |
4376 | + | |
4377 | + | |
4378 | +function groupEventsById(events) { | |
4379 | + var eventsById = {}; | |
4380 | + var i, event; | |
4381 | + | |
4382 | + for (i = 0; i < events.length; i++) { | |
4383 | + event = events[i]; | |
4384 | + (eventsById[event._id] || (eventsById[event._id] = [])).push(event); | |
4385 | + } | |
4386 | + | |
4387 | + return eventsById; | |
4388 | +} | |
4389 | + | |
4390 | + | |
4391 | +// A cmp function for determining which non-inverted "ranges" (see above) happen earlier | |
4392 | +function compareNormalRanges(range1, range2) { | |
4393 | + return range1.eventStartMS - range2.eventStartMS; // earlier ranges go first | |
4394 | +} | |
4395 | + | |
4396 | + | |
4397 | +// A cmp function for determining which segments should take visual priority | |
4398 | +// DOES NOT WORK ON INVERTED BACKGROUND EVENTS because they have no eventStartMS/eventDurationMS | |
4399 | +function compareSegs(seg1, seg2) { | |
4400 | + return seg1.eventStartMS - seg2.eventStartMS || // earlier events go first | |
4401 | + seg2.eventDurationMS - seg1.eventDurationMS || // tie? longer events go first | |
4402 | + seg2.event.allDay - seg1.event.allDay || // tie? put all-day events first (booleans cast to 0/1) | |
4403 | + (seg1.event.title || '').localeCompare(seg2.event.title); // tie? alphabetically by title | |
4404 | +} | |
4405 | + | |
4406 | +fc.compareSegs = compareSegs; // export | |
4407 | + | |
4408 | + | |
4409 | +/* External-Dragging-Element Data | |
4410 | +----------------------------------------------------------------------------------------------------------------------*/ | |
4411 | + | |
4412 | +// Require all HTML5 data-* attributes used by FullCalendar to have this prefix. | |
4413 | +// A value of '' will query attributes like data-event. A value of 'fc' will query attributes like data-fc-event. | |
4414 | +fc.dataAttrPrefix = ''; | |
4415 | + | |
4416 | +// Given a jQuery element that might represent a dragged FullCalendar event, returns an intermediate data structure | |
4417 | +// to be used for Event Object creation. | |
4418 | +// A defined `.eventProps`, even when empty, indicates that an event should be created. | |
4419 | +function getDraggedElMeta(el) { | |
4420 | + var prefix = fc.dataAttrPrefix; | |
4421 | + var eventProps; // properties for creating the event, not related to date/time | |
4422 | + var startTime; // a Duration | |
4423 | + var duration; | |
4424 | + var stick; | |
4425 | + | |
4426 | + if (prefix) { prefix += '-'; } | |
4427 | + eventProps = el.data(prefix + 'event') || null; | |
4428 | + | |
4429 | + if (eventProps) { | |
4430 | + if (typeof eventProps === 'object') { | |
4431 | + eventProps = $.extend({}, eventProps); // make a copy | |
4432 | + } | |
4433 | + else { // something like 1 or true. still signal event creation | |
4434 | + eventProps = {}; | |
4435 | + } | |
4436 | + | |
4437 | + // pluck special-cased date/time properties | |
4438 | + startTime = eventProps.start; | |
4439 | + if (startTime == null) { startTime = eventProps.time; } // accept 'time' as well | |
4440 | + duration = eventProps.duration; | |
4441 | + stick = eventProps.stick; | |
4442 | + delete eventProps.start; | |
4443 | + delete eventProps.time; | |
4444 | + delete eventProps.duration; | |
4445 | + delete eventProps.stick; | |
4446 | + } | |
4447 | + | |
4448 | + // fallback to standalone attribute values for each of the date/time properties | |
4449 | + if (startTime == null) { startTime = el.data(prefix + 'start'); } | |
4450 | + if (startTime == null) { startTime = el.data(prefix + 'time'); } // accept 'time' as well | |
4451 | + if (duration == null) { duration = el.data(prefix + 'duration'); } | |
4452 | + if (stick == null) { stick = el.data(prefix + 'stick'); } | |
4453 | + | |
4454 | + // massage into correct data types | |
4455 | + startTime = startTime != null ? moment.duration(startTime) : null; | |
4456 | + duration = duration != null ? moment.duration(duration) : null; | |
4457 | + stick = Boolean(stick); | |
4458 | + | |
4459 | + return { eventProps: eventProps, startTime: startTime, duration: duration, stick: stick }; | |
4460 | +} | |
4461 | + | |
4462 | + | |
4463 | +;; | |
4464 | + | |
4465 | +/* A component that renders a grid of whole-days that runs horizontally. There can be multiple rows, one per week. | |
4466 | +----------------------------------------------------------------------------------------------------------------------*/ | |
4467 | + | |
4468 | +var DayGrid = Grid.extend({ | |
4469 | + | |
4470 | + numbersVisible: false, // should render a row for day/week numbers? set by outside view. TODO: make internal | |
4471 | + bottomCoordPadding: 0, // hack for extending the hit area for the last row of the coordinate grid | |
4472 | + breakOnWeeks: null, // should create a new row for each week? set by outside view | |
4473 | + | |
4474 | + cellDates: null, // flat chronological array of each cell's dates | |
4475 | + dayToCellOffsets: null, // maps days offsets from grid's start date, to cell offsets | |
4476 | + | |
4477 | + rowEls: null, // set of fake row elements | |
4478 | + dayEls: null, // set of whole-day elements comprising the row's background | |
4479 | + helperEls: null, // set of cell skeleton elements for rendering the mock event "helper" | |
4480 | + | |
4481 | + | |
4482 | + constructor: function() { | |
4483 | + Grid.apply(this, arguments); | |
4484 | + | |
4485 | + this.cellDuration = moment.duration(1, 'day'); // for Grid system | |
4486 | + }, | |
4487 | + | |
4488 | + | |
4489 | + // Renders the rows and columns into the component's `this.el`, which should already be assigned. | |
4490 | + // isRigid determins whether the individual rows should ignore the contents and be a constant height. | |
4491 | + // Relies on the view's colCnt and rowCnt. In the future, this component should probably be self-sufficient. | |
4492 | + renderDates: function(isRigid) { | |
4493 | + var view = this.view; | |
4494 | + var rowCnt = this.rowCnt; | |
4495 | + var colCnt = this.colCnt; | |
4496 | + var cellCnt = rowCnt * colCnt; | |
4497 | + var html = ''; | |
4498 | + var row; | |
4499 | + var i, cell; | |
4500 | + | |
4501 | + for (row = 0; row < rowCnt; row++) { | |
4502 | + html += this.dayRowHtml(row, isRigid); | |
4503 | + } | |
4504 | + this.el.html(html); | |
4505 | + | |
4506 | + this.rowEls = this.el.find('.fc-row'); | |
4507 | + this.dayEls = this.el.find('.fc-day'); | |
4508 | + | |
4509 | + // trigger dayRender with each cell's element | |
4510 | + for (i = 0; i < cellCnt; i++) { | |
4511 | + cell = this.getCell(i); | |
4512 | + view.trigger('dayRender', null, cell.start, this.dayEls.eq(i)); | |
4513 | + } | |
4514 | + }, | |
4515 | + | |
4516 | + | |
4517 | + destroyDates: function() { | |
4518 | + this.destroySegPopover(); | |
4519 | + }, | |
4520 | + | |
4521 | + | |
4522 | + renderBusinessHours: function() { | |
4523 | + var events = this.view.calendar.getBusinessHoursEvents(true); // wholeDay=true | |
4524 | + var segs = this.eventsToSegs(events); | |
4525 | + | |
4526 | + this.renderFill('businessHours', segs, 'bgevent'); | |
4527 | + }, | |
4528 | + | |
4529 | + | |
4530 | + // Generates the HTML for a single row. `row` is the row number. | |
4531 | + dayRowHtml: function(row, isRigid) { | |
4532 | + var view = this.view; | |
4533 | + var classes = [ 'fc-row', 'fc-week', view.widgetContentClass ]; | |
4534 | + | |
4535 | + if (isRigid) { | |
4536 | + classes.push('fc-rigid'); | |
4537 | + } | |
4538 | + | |
4539 | + return '' + | |
4540 | + '<div class="' + classes.join(' ') + '">' + | |
4541 | + '<div class="fc-bg">' + | |
4542 | + '<table>' + | |
4543 | + this.rowHtml('day', row) + // leverages RowRenderer. calls dayCellHtml() | |
4544 | + '</table>' + | |
4545 | + '</div>' + | |
4546 | + '<div class="fc-content-skeleton">' + | |
4547 | + '<table>' + | |
4548 | + (this.numbersVisible ? | |
4549 | + '<thead>' + | |
4550 | + this.rowHtml('number', row) + // leverages RowRenderer. View will define render method | |
4551 | + '</thead>' : | |
4552 | + '' | |
4553 | + ) + | |
4554 | + '</table>' + | |
4555 | + '</div>' + | |
4556 | + '</div>'; | |
4557 | + }, | |
4558 | + | |
4559 | + | |
4560 | + // Renders the HTML for a whole-day cell. Will eventually end up in the day-row's background. | |
4561 | + // We go through a 'day' row type instead of just doing a 'bg' row type so that the View can do custom rendering | |
4562 | + // specifically for whole-day rows, whereas a 'bg' might also be used for other purposes (TimeGrid bg for example). | |
4563 | + dayCellHtml: function(cell) { | |
4564 | + return this.bgCellHtml(cell); | |
4565 | + }, | |
4566 | + | |
4567 | + | |
4568 | + /* Options | |
4569 | + ------------------------------------------------------------------------------------------------------------------*/ | |
4570 | + | |
4571 | + | |
4572 | + // Computes a default column header formatting string if `colFormat` is not explicitly defined | |
4573 | + computeColHeadFormat: function() { | |
4574 | + if (this.rowCnt > 1) { // more than one week row. day numbers will be in each cell | |
4575 | + return 'ddd'; // "Sat" | |
4576 | + } | |
4577 | + else if (this.colCnt > 1) { // multiple days, so full single date string WON'T be in title text | |
4578 | + return this.view.opt('dayOfMonthFormat'); // "Sat 12/10" | |
4579 | + } | |
4580 | + else { // single day, so full single date string will probably be in title text | |
4581 | + return 'dddd'; // "Saturday" | |
4582 | + } | |
4583 | + }, | |
4584 | + | |
4585 | + | |
4586 | + // Computes a default event time formatting string if `timeFormat` is not explicitly defined | |
4587 | + computeEventTimeFormat: function() { | |
4588 | + return this.view.opt('extraSmallTimeFormat'); // like "6p" or "6:30p" | |
4589 | + }, | |
4590 | + | |
4591 | + | |
4592 | + // Computes a default `displayEventEnd` value if one is not expliclty defined | |
4593 | + computeDisplayEventEnd: function() { | |
4594 | + return this.colCnt == 1; // we'll likely have space if there's only one day | |
4595 | + }, | |
4596 | + | |
4597 | + | |
4598 | + /* Cell System | |
4599 | + ------------------------------------------------------------------------------------------------------------------*/ | |
4600 | + | |
4601 | + | |
4602 | + // Initializes row/col information | |
4603 | + updateCells: function() { | |
4604 | + var cellDates; | |
4605 | + var firstDay; | |
4606 | + var rowCnt; | |
4607 | + var colCnt; | |
4608 | + | |
4609 | + this.updateCellDates(); // populates cellDates and dayToCellOffsets | |
4610 | + cellDates = this.cellDates; | |
4611 | + | |
4612 | + if (this.breakOnWeeks) { | |
4613 | + // count columns until the day-of-week repeats | |
4614 | + firstDay = cellDates[0].day(); | |
4615 | + for (colCnt = 1; colCnt < cellDates.length; colCnt++) { | |
4616 | + if (cellDates[colCnt].day() == firstDay) { | |
4617 | + break; | |
4618 | + } | |
4619 | + } | |
4620 | + rowCnt = Math.ceil(cellDates.length / colCnt); | |
4621 | + } | |
4622 | + else { | |
4623 | + rowCnt = 1; | |
4624 | + colCnt = cellDates.length; | |
4625 | + } | |
4626 | + | |
4627 | + this.rowCnt = rowCnt; | |
4628 | + this.colCnt = colCnt; | |
4629 | + }, | |
4630 | + | |
4631 | + | |
4632 | + // Populates cellDates and dayToCellOffsets | |
4633 | + updateCellDates: function() { | |
4634 | + var view = this.view; | |
4635 | + var date = this.start.clone(); | |
4636 | + var dates = []; | |
4637 | + var offset = -1; | |
4638 | + var offsets = []; | |
4639 | + | |
4640 | + while (date.isBefore(this.end)) { // loop each day from start to end | |
4641 | + if (view.isHiddenDay(date)) { | |
4642 | + offsets.push(offset + 0.5); // mark that it's between offsets | |
4643 | + } | |
4644 | + else { | |
4645 | + offset++; | |
4646 | + offsets.push(offset); | |
4647 | + dates.push(date.clone()); | |
4648 | + } | |
4649 | + date.add(1, 'days'); | |
4650 | + } | |
4651 | + | |
4652 | + this.cellDates = dates; | |
4653 | + this.dayToCellOffsets = offsets; | |
4654 | + }, | |
4655 | + | |
4656 | + | |
4657 | + // Given a cell object, generates its start date. Returns a reference-free copy. | |
4658 | + computeCellDate: function(cell) { | |
4659 | + var colCnt = this.colCnt; | |
4660 | + var index = cell.row * colCnt + (this.isRTL ? colCnt - cell.col - 1 : cell.col); | |
4661 | + | |
4662 | + return this.cellDates[index].clone(); | |
4663 | + }, | |
4664 | + | |
4665 | + | |
4666 | + // Retrieves the element representing the given row | |
4667 | + getRowEl: function(row) { | |
4668 | + return this.rowEls.eq(row); | |
4669 | + }, | |
4670 | + | |
4671 | + | |
4672 | + // Retrieves the element representing the given column | |
4673 | + getColEl: function(col) { | |
4674 | + return this.dayEls.eq(col); | |
4675 | + }, | |
4676 | + | |
4677 | + | |
4678 | + // Gets the whole-day element associated with the cell | |
4679 | + getCellDayEl: function(cell) { | |
4680 | + return this.dayEls.eq(cell.row * this.colCnt + cell.col); | |
4681 | + }, | |
4682 | + | |
4683 | + | |
4684 | + // Overrides Grid's method for when row coordinates are computed | |
4685 | + computeRowCoords: function() { | |
4686 | + var rowCoords = Grid.prototype.computeRowCoords.call(this); // call the super-method | |
4687 | + | |
4688 | + // hack for extending last row (used by AgendaView) | |
4689 | + rowCoords[rowCoords.length - 1].bottom += this.bottomCoordPadding; | |
4690 | + | |
4691 | + return rowCoords; | |
4692 | + }, | |
4693 | + | |
4694 | + | |
4695 | + /* Dates | |
4696 | + ------------------------------------------------------------------------------------------------------------------*/ | |
4697 | + | |
4698 | + | |
4699 | + // Slices up a date range by row into an array of segments | |
4700 | + rangeToSegs: function(range) { | |
4701 | + var isRTL = this.isRTL; | |
4702 | + var rowCnt = this.rowCnt; | |
4703 | + var colCnt = this.colCnt; | |
4704 | + var segs = []; | |
4705 | + var first, last; // inclusive cell-offset range for given range | |
4706 | + var row; | |
4707 | + var rowFirst, rowLast; // inclusive cell-offset range for current row | |
4708 | + var isStart, isEnd; | |
4709 | + var segFirst, segLast; // inclusive cell-offset range for segment | |
4710 | + var seg; | |
4711 | + | |
4712 | + range = this.view.computeDayRange(range); // make whole-day range, considering nextDayThreshold | |
4713 | + first = this.dateToCellOffset(range.start); | |
4714 | + last = this.dateToCellOffset(range.end.subtract(1, 'days')); // offset of inclusive end date | |
4715 | + | |
4716 | + for (row = 0; row < rowCnt; row++) { | |
4717 | + rowFirst = row * colCnt; | |
4718 | + rowLast = rowFirst + colCnt - 1; | |
4719 | + | |
4720 | + // intersect segment's offset range with the row's | |
4721 | + segFirst = Math.max(rowFirst, first); | |
4722 | + segLast = Math.min(rowLast, last); | |
4723 | + | |
4724 | + // deal with in-between indices | |
4725 | + segFirst = Math.ceil(segFirst); // in-between starts round to next cell | |
4726 | + segLast = Math.floor(segLast); // in-between ends round to prev cell | |
4727 | + | |
4728 | + if (segFirst <= segLast) { // was there any intersection with the current row? | |
4729 | + | |
4730 | + // must be matching integers to be the segment's start/end | |
4731 | + isStart = segFirst === first; | |
4732 | + isEnd = segLast === last; | |
4733 | + | |
4734 | + // translate offsets to be relative to start-of-row | |
4735 | + segFirst -= rowFirst; | |
4736 | + segLast -= rowFirst; | |
4737 | + | |
4738 | + seg = { row: row, isStart: isStart, isEnd: isEnd }; | |
4739 | + if (isRTL) { | |
4740 | + seg.leftCol = colCnt - segLast - 1; | |
4741 | + seg.rightCol = colCnt - segFirst - 1; | |
4742 | + } | |
4743 | + else { | |
4744 | + seg.leftCol = segFirst; | |
4745 | + seg.rightCol = segLast; | |
4746 | + } | |
4747 | + segs.push(seg); | |
4748 | + } | |
4749 | + } | |
4750 | + | |
4751 | + return segs; | |
4752 | + }, | |
4753 | + | |
4754 | + | |
4755 | + // Given a date, returns its chronolocial cell-offset from the first cell of the grid. | |
4756 | + // If the date lies between cells (because of hiddenDays), returns a floating-point value between offsets. | |
4757 | + // If before the first offset, returns a negative number. | |
4758 | + // If after the last offset, returns an offset past the last cell offset. | |
4759 | + // Only works for *start* dates of cells. Will not work for exclusive end dates for cells. | |
4760 | + dateToCellOffset: function(date) { | |
4761 | + var offsets = this.dayToCellOffsets; | |
4762 | + var day = date.diff(this.start, 'days'); | |
4763 | + | |
4764 | + if (day < 0) { | |
4765 | + return offsets[0] - 1; | |
4766 | + } | |
4767 | + else if (day >= offsets.length) { | |
4768 | + return offsets[offsets.length - 1] + 1; | |
4769 | + } | |
4770 | + else { | |
4771 | + return offsets[day]; | |
4772 | + } | |
4773 | + }, | |
4774 | + | |
4775 | + | |
4776 | + /* Event Drag Visualization | |
4777 | + ------------------------------------------------------------------------------------------------------------------*/ | |
4778 | + // TODO: move to DayGrid.event, similar to what we did with Grid's drag methods | |
4779 | + | |
4780 | + | |
4781 | + // Renders a visual indication of an event or external element being dragged. | |
4782 | + // The dropLocation's end can be null. seg can be null. See Grid::renderDrag for more info. | |
4783 | + renderDrag: function(dropLocation, seg) { | |
4784 | + | |
4785 | + // always render a highlight underneath | |
4786 | + this.renderHighlight( | |
4787 | + this.view.calendar.ensureVisibleEventRange(dropLocation) // needs to be a proper range | |
4788 | + ); | |
4789 | + | |
4790 | + // if a segment from the same calendar but another component is being dragged, render a helper event | |
4791 | + if (seg && !seg.el.closest(this.el).length) { | |
4792 | + | |
4793 | + this.renderRangeHelper(dropLocation, seg); | |
4794 | + this.applyDragOpacity(this.helperEls); | |
4795 | + | |
4796 | + return true; // a helper has been rendered | |
4797 | + } | |
4798 | + }, | |
4799 | + | |
4800 | + | |
4801 | + // Unrenders any visual indication of a hovering event | |
4802 | + destroyDrag: function() { | |
4803 | + this.destroyHighlight(); | |
4804 | + this.destroyHelper(); | |
4805 | + }, | |
4806 | + | |
4807 | + | |
4808 | + /* Event Resize Visualization | |
4809 | + ------------------------------------------------------------------------------------------------------------------*/ | |
4810 | + | |
4811 | + | |
4812 | + // Renders a visual indication of an event being resized | |
4813 | + renderEventResize: function(range, seg) { | |
4814 | + this.renderHighlight(range); | |
4815 | + this.renderRangeHelper(range, seg); | |
4816 | + }, | |
4817 | + | |
4818 | + | |
4819 | + // Unrenders a visual indication of an event being resized | |
4820 | + destroyEventResize: function() { | |
4821 | + this.destroyHighlight(); | |
4822 | + this.destroyHelper(); | |
4823 | + }, | |
4824 | + | |
4825 | + | |
4826 | + /* Event Helper | |
4827 | + ------------------------------------------------------------------------------------------------------------------*/ | |
4828 | + | |
4829 | + | |
4830 | + // Renders a mock "helper" event. `sourceSeg` is the associated internal segment object. It can be null. | |
4831 | + renderHelper: function(event, sourceSeg) { | |
4832 | + var helperNodes = []; | |
4833 | + var segs = this.eventsToSegs([ event ]); | |
4834 | + var rowStructs; | |
4835 | + | |
4836 | + segs = this.renderFgSegEls(segs); // assigns each seg's el and returns a subset of segs that were rendered | |
4837 | + rowStructs = this.renderSegRows(segs); | |
4838 | + | |
4839 | + // inject each new event skeleton into each associated row | |
4840 | + this.rowEls.each(function(row, rowNode) { | |
4841 | + var rowEl = $(rowNode); // the .fc-row | |
4842 | + var skeletonEl = $('<div class="fc-helper-skeleton"><table/></div>'); // will be absolutely positioned | |
4843 | + var skeletonTop; | |
4844 | + | |
4845 | + // If there is an original segment, match the top position. Otherwise, put it at the row's top level | |
4846 | + if (sourceSeg && sourceSeg.row === row) { | |
4847 | + skeletonTop = sourceSeg.el.position().top; | |
4848 | + } | |
4849 | + else { | |
4850 | + skeletonTop = rowEl.find('.fc-content-skeleton tbody').position().top; | |
4851 | + } | |
4852 | + | |
4853 | + skeletonEl.css('top', skeletonTop) | |
4854 | + .find('table') | |
4855 | + .append(rowStructs[row].tbodyEl); | |
4856 | + | |
4857 | + rowEl.append(skeletonEl); | |
4858 | + helperNodes.push(skeletonEl[0]); | |
4859 | + }); | |
4860 | + | |
4861 | + this.helperEls = $(helperNodes); // array -> jQuery set | |
4862 | + }, | |
4863 | + | |
4864 | + | |
4865 | + // Unrenders any visual indication of a mock helper event | |
4866 | + destroyHelper: function() { | |
4867 | + if (this.helperEls) { | |
4868 | + this.helperEls.remove(); | |
4869 | + this.helperEls = null; | |
4870 | + } | |
4871 | + }, | |
4872 | + | |
4873 | + | |
4874 | + /* Fill System (highlight, background events, business hours) | |
4875 | + ------------------------------------------------------------------------------------------------------------------*/ | |
4876 | + | |
4877 | + | |
4878 | + fillSegTag: 'td', // override the default tag name | |
4879 | + | |
4880 | + | |
4881 | + // Renders a set of rectangles over the given segments of days. | |
4882 | + // Only returns segments that successfully rendered. | |
4883 | + renderFill: function(type, segs, className) { | |
4884 | + var nodes = []; | |
4885 | + var i, seg; | |
4886 | + var skeletonEl; | |
4887 | + | |
4888 | + segs = this.renderFillSegEls(type, segs); // assignes `.el` to each seg. returns successfully rendered segs | |
4889 | + | |
4890 | + for (i = 0; i < segs.length; i++) { | |
4891 | + seg = segs[i]; | |
4892 | + skeletonEl = this.renderFillRow(type, seg, className); | |
4893 | + this.rowEls.eq(seg.row).append(skeletonEl); | |
4894 | + nodes.push(skeletonEl[0]); | |
4895 | + } | |
4896 | + | |
4897 | + this.elsByFill[type] = $(nodes); | |
4898 | + | |
4899 | + return segs; | |
4900 | + }, | |
4901 | + | |
4902 | + | |
4903 | + // Generates the HTML needed for one row of a fill. Requires the seg's el to be rendered. | |
4904 | + renderFillRow: function(type, seg, className) { | |
4905 | + var colCnt = this.colCnt; | |
4906 | + var startCol = seg.leftCol; | |
4907 | + var endCol = seg.rightCol + 1; | |
4908 | + var skeletonEl; | |
4909 | + var trEl; | |
4910 | + | |
4911 | + className = className || type.toLowerCase(); | |
4912 | + | |
4913 | + skeletonEl = $( | |
4914 | + '<div class="fc-' + className + '-skeleton">' + | |
4915 | + '<table><tr/></table>' + | |
4916 | + '</div>' | |
4917 | + ); | |
4918 | + trEl = skeletonEl.find('tr'); | |
4919 | + | |
4920 | + if (startCol > 0) { | |
4921 | + trEl.append('<td colspan="' + startCol + '"/>'); | |
4922 | + } | |
4923 | + | |
4924 | + trEl.append( | |
4925 | + seg.el.attr('colspan', endCol - startCol) | |
4926 | + ); | |
4927 | + | |
4928 | + if (endCol < colCnt) { | |
4929 | + trEl.append('<td colspan="' + (colCnt - endCol) + '"/>'); | |
4930 | + } | |
4931 | + | |
4932 | + this.bookendCells(trEl, type); | |
4933 | + | |
4934 | + return skeletonEl; | |
4935 | + } | |
4936 | + | |
4937 | +}); | |
4938 | + | |
4939 | +;; | |
4940 | + | |
4941 | +/* Event-rendering methods for the DayGrid class | |
4942 | +----------------------------------------------------------------------------------------------------------------------*/ | |
4943 | + | |
4944 | +DayGrid.mixin({ | |
4945 | + | |
4946 | + rowStructs: null, // an array of objects, each holding information about a row's foreground event-rendering | |
4947 | + | |
4948 | + | |
4949 | + // Unrenders all events currently rendered on the grid | |
4950 | + destroyEvents: function() { | |
4951 | + this.destroySegPopover(); // removes the "more.." events popover | |
4952 | + Grid.prototype.destroyEvents.apply(this, arguments); // calls the super-method | |
4953 | + }, | |
4954 | + | |
4955 | + | |
4956 | + // Retrieves all rendered segment objects currently rendered on the grid | |
4957 | + getEventSegs: function() { | |
4958 | + return Grid.prototype.getEventSegs.call(this) // get the segments from the super-method | |
4959 | + .concat(this.popoverSegs || []); // append the segments from the "more..." popover | |
4960 | + }, | |
4961 | + | |
4962 | + | |
4963 | + // Renders the given background event segments onto the grid | |
4964 | + renderBgSegs: function(segs) { | |
4965 | + | |
4966 | + // don't render timed background events | |
4967 | + var allDaySegs = $.grep(segs, function(seg) { | |
4968 | + return seg.event.allDay; | |
4969 | + }); | |
4970 | + | |
4971 | + return Grid.prototype.renderBgSegs.call(this, allDaySegs); // call the super-method | |
4972 | + }, | |
4973 | + | |
4974 | + | |
4975 | + // Renders the given foreground event segments onto the grid | |
4976 | + renderFgSegs: function(segs) { | |
4977 | + var rowStructs; | |
4978 | + | |
4979 | + // render an `.el` on each seg | |
4980 | + // returns a subset of the segs. segs that were actually rendered | |
4981 | + segs = this.renderFgSegEls(segs); | |
4982 | + | |
4983 | + rowStructs = this.rowStructs = this.renderSegRows(segs); | |
4984 | + | |
4985 | + // append to each row's content skeleton | |
4986 | + this.rowEls.each(function(i, rowNode) { | |
4987 | + $(rowNode).find('.fc-content-skeleton > table').append( | |
4988 | + rowStructs[i].tbodyEl | |
4989 | + ); | |
4990 | + }); | |
4991 | + | |
4992 | + return segs; // return only the segs that were actually rendered | |
4993 | + }, | |
4994 | + | |
4995 | + | |
4996 | + // Unrenders all currently rendered foreground event segments | |
4997 | + destroyFgSegs: function() { | |
4998 | + var rowStructs = this.rowStructs || []; | |
4999 | + var rowStruct; | |
5000 | + | |
5001 | + while ((rowStruct = rowStructs.pop())) { | |
5002 | + rowStruct.tbodyEl.remove(); | |
5003 | + } | |
5004 | + | |
5005 | + this.rowStructs = null; | |
5006 | + }, | |
5007 | + | |
5008 | + | |
5009 | + // Uses the given events array to generate <tbody> elements that should be appended to each row's content skeleton. | |
5010 | + // Returns an array of rowStruct objects (see the bottom of `renderSegRow`). | |
5011 | + // PRECONDITION: each segment shoud already have a rendered and assigned `.el` | |
5012 | + renderSegRows: function(segs) { | |
5013 | + var rowStructs = []; | |
5014 | + var segRows; | |
5015 | + var row; | |
5016 | + | |
5017 | + segRows = this.groupSegRows(segs); // group into nested arrays | |
5018 | + | |
5019 | + // iterate each row of segment groupings | |
5020 | + for (row = 0; row < segRows.length; row++) { | |
5021 | + rowStructs.push( | |
5022 | + this.renderSegRow(row, segRows[row]) | |
5023 | + ); | |
5024 | + } | |
5025 | + | |
5026 | + return rowStructs; | |
5027 | + }, | |
5028 | + | |
5029 | + | |
5030 | + // Builds the HTML to be used for the default element for an individual segment | |
5031 | + fgSegHtml: function(seg, disableResizing) { | |
5032 | + var view = this.view; | |
5033 | + var event = seg.event; | |
5034 | + var isDraggable = view.isEventDraggable(event); | |
5035 | + var isResizableFromStart = !disableResizing && event.allDay && | |
5036 | + seg.isStart && view.isEventResizableFromStart(event); | |
5037 | + var isResizableFromEnd = !disableResizing && event.allDay && | |
5038 | + seg.isEnd && view.isEventResizableFromEnd(event); | |
5039 | + var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd); | |
5040 | + var skinCss = cssToStr(this.getEventSkinCss(event)); | |
5041 | + var timeHtml = ''; | |
5042 | + var timeText; | |
5043 | + var titleHtml; | |
5044 | + | |
5045 | + classes.unshift('fc-day-grid-event', 'fc-h-event'); | |
5046 | + | |
5047 | + // Only display a timed events time if it is the starting segment | |
5048 | + if (seg.isStart) { | |
5049 | + timeText = this.getEventTimeText(event); | |
5050 | + if (timeText) { | |
5051 | + timeHtml = '<span class="fc-time">' + htmlEscape(timeText) + '</span>'; | |
5052 | + } | |
5053 | + } | |
5054 | + | |
5055 | + titleHtml = | |
5056 | + '<span class="fc-title">' + | |
5057 | + (htmlEscape(event.title || '') || ' ') + // we always want one line of height | |
5058 | + '</span>'; | |
5059 | + | |
5060 | + return '<a class="' + classes.join(' ') + '"' + | |
5061 | + (event.url ? | |
5062 | + ' href="' + htmlEscape(event.url) + '"' : | |
5063 | + '' | |
5064 | + ) + | |
5065 | + (skinCss ? | |
5066 | + ' style="' + skinCss + '"' : | |
5067 | + '' | |
5068 | + ) + | |
5069 | + '>' + | |
5070 | + '<div class="fc-content">' + | |
5071 | + (this.isRTL ? | |
5072 | + titleHtml + ' ' + timeHtml : // put a natural space in between | |
5073 | + timeHtml + ' ' + titleHtml // | |
5074 | + ) + | |
5075 | + '</div>' + | |
5076 | + (isResizableFromStart ? | |
5077 | + '<div class="fc-resizer fc-start-resizer" />' : | |
5078 | + '' | |
5079 | + ) + | |
5080 | + (isResizableFromEnd ? | |
5081 | + '<div class="fc-resizer fc-end-resizer" />' : | |
5082 | + '' | |
5083 | + ) + | |
5084 | + '</a>'; | |
5085 | + }, | |
5086 | + | |
5087 | + | |
5088 | + // Given a row # and an array of segments all in the same row, render a <tbody> element, a skeleton that contains | |
5089 | + // the segments. Returns object with a bunch of internal data about how the render was calculated. | |
5090 | + // NOTE: modifies rowSegs | |
5091 | + renderSegRow: function(row, rowSegs) { | |
5092 | + var colCnt = this.colCnt; | |
5093 | + var segLevels = this.buildSegLevels(rowSegs); // group into sub-arrays of levels | |
5094 | + var levelCnt = Math.max(1, segLevels.length); // ensure at least one level | |
5095 | + var tbody = $('<tbody/>'); | |
5096 | + var segMatrix = []; // lookup for which segments are rendered into which level+col cells | |
5097 | + var cellMatrix = []; // lookup for all <td> elements of the level+col matrix | |
5098 | + var loneCellMatrix = []; // lookup for <td> elements that only take up a single column | |
5099 | + var i, levelSegs; | |
5100 | + var col; | |
5101 | + var tr; | |
5102 | + var j, seg; | |
5103 | + var td; | |
5104 | + | |
5105 | + // populates empty cells from the current column (`col`) to `endCol` | |
5106 | + function emptyCellsUntil(endCol) { | |
5107 | + while (col < endCol) { | |
5108 | + // try to grab a cell from the level above and extend its rowspan. otherwise, create a fresh cell | |
5109 | + td = (loneCellMatrix[i - 1] || [])[col]; | |
5110 | + if (td) { | |
5111 | + td.attr( | |
5112 | + 'rowspan', | |
5113 | + parseInt(td.attr('rowspan') || 1, 10) + 1 | |
5114 | + ); | |
5115 | + } | |
5116 | + else { | |
5117 | + td = $('<td/>'); | |
5118 | + tr.append(td); | |
5119 | + } | |
5120 | + cellMatrix[i][col] = td; | |
5121 | + loneCellMatrix[i][col] = td; | |
5122 | + col++; | |
5123 | + } | |
5124 | + } | |
5125 | + | |
5126 | + for (i = 0; i < levelCnt; i++) { // iterate through all levels | |
5127 | + levelSegs = segLevels[i]; | |
5128 | + col = 0; | |
5129 | + tr = $('<tr/>'); | |
5130 | + | |
5131 | + segMatrix.push([]); | |
5132 | + cellMatrix.push([]); | |
5133 | + loneCellMatrix.push([]); | |
5134 | + | |
5135 | + // levelCnt might be 1 even though there are no actual levels. protect against this. | |
5136 | + // this single empty row is useful for styling. | |
5137 | + if (levelSegs) { | |
5138 | + for (j = 0; j < levelSegs.length; j++) { // iterate through segments in level | |
5139 | + seg = levelSegs[j]; | |
5140 | + | |
5141 | + emptyCellsUntil(seg.leftCol); | |
5142 | + | |
5143 | + // create a container that occupies or more columns. append the event element. | |
5144 | + td = $('<td class="fc-event-container"/>').append(seg.el); | |
5145 | + if (seg.leftCol != seg.rightCol) { | |
5146 | + td.attr('colspan', seg.rightCol - seg.leftCol + 1); | |
5147 | + } | |
5148 | + else { // a single-column segment | |
5149 | + loneCellMatrix[i][col] = td; | |
5150 | + } | |
5151 | + | |
5152 | + while (col <= seg.rightCol) { | |
5153 | + cellMatrix[i][col] = td; | |
5154 | + segMatrix[i][col] = seg; | |
5155 | + col++; | |
5156 | + } | |
5157 | + | |
5158 | + tr.append(td); | |
5159 | + } | |
5160 | + } | |
5161 | + | |
5162 | + emptyCellsUntil(colCnt); // finish off the row | |
5163 | + this.bookendCells(tr, 'eventSkeleton'); | |
5164 | + tbody.append(tr); | |
5165 | + } | |
5166 | + | |
5167 | + return { // a "rowStruct" | |
5168 | + row: row, // the row number | |
5169 | + tbodyEl: tbody, | |
5170 | + cellMatrix: cellMatrix, | |
5171 | + segMatrix: segMatrix, | |
5172 | + segLevels: segLevels, | |
5173 | + segs: rowSegs | |
5174 | + }; | |
5175 | + }, | |
5176 | + | |
5177 | + | |
5178 | + // Stacks a flat array of segments, which are all assumed to be in the same row, into subarrays of vertical levels. | |
5179 | + // NOTE: modifies segs | |
5180 | + buildSegLevels: function(segs) { | |
5181 | + var levels = []; | |
5182 | + var i, seg; | |
5183 | + var j; | |
5184 | + | |
5185 | + // Give preference to elements with certain criteria, so they have | |
5186 | + // a chance to be closer to the top. | |
5187 | + segs.sort(compareSegs); | |
5188 | + | |
5189 | + for (i = 0; i < segs.length; i++) { | |
5190 | + seg = segs[i]; | |
5191 | + | |
5192 | + // loop through levels, starting with the topmost, until the segment doesn't collide with other segments | |
5193 | + for (j = 0; j < levels.length; j++) { | |
5194 | + if (!isDaySegCollision(seg, levels[j])) { | |
5195 | + break; | |
5196 | + } | |
5197 | + } | |
5198 | + // `j` now holds the desired subrow index | |
5199 | + seg.level = j; | |
5200 | + | |
5201 | + // create new level array if needed and append segment | |
5202 | + (levels[j] || (levels[j] = [])).push(seg); | |
5203 | + } | |
5204 | + | |
5205 | + // order segments left-to-right. very important if calendar is RTL | |
5206 | + for (j = 0; j < levels.length; j++) { | |
5207 | + levels[j].sort(compareDaySegCols); | |
5208 | + } | |
5209 | + | |
5210 | + return levels; | |
5211 | + }, | |
5212 | + | |
5213 | + | |
5214 | + // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's row | |
5215 | + groupSegRows: function(segs) { | |
5216 | + var segRows = []; | |
5217 | + var i; | |
5218 | + | |
5219 | + for (i = 0; i < this.rowCnt; i++) { | |
5220 | + segRows.push([]); | |
5221 | + } | |
5222 | + | |
5223 | + for (i = 0; i < segs.length; i++) { | |
5224 | + segRows[segs[i].row].push(segs[i]); | |
5225 | + } | |
5226 | + | |
5227 | + return segRows; | |
5228 | + } | |
5229 | + | |
5230 | +}); | |
5231 | + | |
5232 | + | |
5233 | +// Computes whether two segments' columns collide. They are assumed to be in the same row. | |
5234 | +function isDaySegCollision(seg, otherSegs) { | |
5235 | + var i, otherSeg; | |
5236 | + | |
5237 | + for (i = 0; i < otherSegs.length; i++) { | |
5238 | + otherSeg = otherSegs[i]; | |
5239 | + | |
5240 | + if ( | |
5241 | + otherSeg.leftCol <= seg.rightCol && | |
5242 | + otherSeg.rightCol >= seg.leftCol | |
5243 | + ) { | |
5244 | + return true; | |
5245 | + } | |
5246 | + } | |
5247 | + | |
5248 | + return false; | |
5249 | +} | |
5250 | + | |
5251 | + | |
5252 | +// A cmp function for determining the leftmost event | |
5253 | +function compareDaySegCols(a, b) { | |
5254 | + return a.leftCol - b.leftCol; | |
5255 | +} | |
5256 | + | |
5257 | +;; | |
5258 | + | |
5259 | +/* Methods relate to limiting the number events for a given day on a DayGrid | |
5260 | +----------------------------------------------------------------------------------------------------------------------*/ | |
5261 | +// NOTE: all the segs being passed around in here are foreground segs | |
5262 | + | |
5263 | +DayGrid.mixin({ | |
5264 | + | |
5265 | + segPopover: null, // the Popover that holds events that can't fit in a cell. null when not visible | |
5266 | + popoverSegs: null, // an array of segment objects that the segPopover holds. null when not visible | |
5267 | + | |
5268 | + | |
5269 | + destroySegPopover: function() { | |
5270 | + if (this.segPopover) { | |
5271 | + this.segPopover.hide(); // will trigger destruction of `segPopover` and `popoverSegs` | |
5272 | + } | |
5273 | + }, | |
5274 | + | |
5275 | + | |
5276 | + // Limits the number of "levels" (vertically stacking layers of events) for each row of the grid. | |
5277 | + // `levelLimit` can be false (don't limit), a number, or true (should be computed). | |
5278 | + limitRows: function(levelLimit) { | |
5279 | + var rowStructs = this.rowStructs || []; | |
5280 | + var row; // row # | |
5281 | + var rowLevelLimit; | |
5282 | + | |
5283 | + for (row = 0; row < rowStructs.length; row++) { | |
5284 | + this.unlimitRow(row); | |
5285 | + | |
5286 | + if (!levelLimit) { | |
5287 | + rowLevelLimit = false; | |
5288 | + } | |
5289 | + else if (typeof levelLimit === 'number') { | |
5290 | + rowLevelLimit = levelLimit; | |
5291 | + } | |
5292 | + else { | |
5293 | + rowLevelLimit = this.computeRowLevelLimit(row); | |
5294 | + } | |
5295 | + | |
5296 | + if (rowLevelLimit !== false) { | |
5297 | + this.limitRow(row, rowLevelLimit); | |
5298 | + } | |
5299 | + } | |
5300 | + }, | |
5301 | + | |
5302 | + | |
5303 | + // Computes the number of levels a row will accomodate without going outside its bounds. | |
5304 | + // Assumes the row is "rigid" (maintains a constant height regardless of what is inside). | |
5305 | + // `row` is the row number. | |
5306 | + computeRowLevelLimit: function(row) { | |
5307 | + var rowEl = this.rowEls.eq(row); // the containing "fake" row div | |
5308 | + var rowHeight = rowEl.height(); // TODO: cache somehow? | |
5309 | + var trEls = this.rowStructs[row].tbodyEl.children(); | |
5310 | + var i, trEl; | |
5311 | + var trHeight; | |
5312 | + | |
5313 | + function iterInnerHeights(i, childNode) { | |
5314 | + trHeight = Math.max(trHeight, $(childNode).outerHeight()); | |
5315 | + } | |
5316 | + | |
5317 | + // Reveal one level <tr> at a time and stop when we find one out of bounds | |
5318 | + for (i = 0; i < trEls.length; i++) { | |
5319 | + trEl = trEls.eq(i).removeClass('fc-limited'); // reset to original state (reveal) | |
5320 | + | |
5321 | + // with rowspans>1 and IE8, trEl.outerHeight() would return the height of the largest cell, | |
5322 | + // so instead, find the tallest inner content element. | |
5323 | + trHeight = 0; | |
5324 | + trEl.find('> td > :first-child').each(iterInnerHeights); | |
5325 | + | |
5326 | + if (trEl.position().top + trHeight > rowHeight) { | |
5327 | + return i; | |
5328 | + } | |
5329 | + } | |
5330 | + | |
5331 | + return false; // should not limit at all | |
5332 | + }, | |
5333 | + | |
5334 | + | |
5335 | + // Limits the given grid row to the maximum number of levels and injects "more" links if necessary. | |
5336 | + // `row` is the row number. | |
5337 | + // `levelLimit` is a number for the maximum (inclusive) number of levels allowed. | |
5338 | + limitRow: function(row, levelLimit) { | |
5339 | + var _this = this; | |
5340 | + var rowStruct = this.rowStructs[row]; | |
5341 | + var moreNodes = []; // array of "more" <a> links and <td> DOM nodes | |
5342 | + var col = 0; // col #, left-to-right (not chronologically) | |
5343 | + var cell; | |
5344 | + var levelSegs; // array of segment objects in the last allowable level, ordered left-to-right | |
5345 | + var cellMatrix; // a matrix (by level, then column) of all <td> jQuery elements in the row | |
5346 | + var limitedNodes; // array of temporarily hidden level <tr> and segment <td> DOM nodes | |
5347 | + var i, seg; | |
5348 | + var segsBelow; // array of segment objects below `seg` in the current `col` | |
5349 | + var totalSegsBelow; // total number of segments below `seg` in any of the columns `seg` occupies | |
5350 | + var colSegsBelow; // array of segment arrays, below seg, one for each column (offset from segs's first column) | |
5351 | + var td, rowspan; | |
5352 | + var segMoreNodes; // array of "more" <td> cells that will stand-in for the current seg's cell | |
5353 | + var j; | |
5354 | + var moreTd, moreWrap, moreLink; | |
5355 | + | |
5356 | + // Iterates through empty level cells and places "more" links inside if need be | |
5357 | + function emptyCellsUntil(endCol) { // goes from current `col` to `endCol` | |
5358 | + while (col < endCol) { | |
5359 | + cell = _this.getCell(row, col); | |
5360 | + segsBelow = _this.getCellSegs(cell, levelLimit); | |
5361 | + if (segsBelow.length) { | |
5362 | + td = cellMatrix[levelLimit - 1][col]; | |
5363 | + moreLink = _this.renderMoreLink(cell, segsBelow); | |
5364 | + moreWrap = $('<div/>').append(moreLink); | |
5365 | + td.append(moreWrap); | |
5366 | + moreNodes.push(moreWrap[0]); | |
5367 | + } | |
5368 | + col++; | |
5369 | + } | |
5370 | + } | |
5371 | + | |
5372 | + if (levelLimit && levelLimit < rowStruct.segLevels.length) { // is it actually over the limit? | |
5373 | + levelSegs = rowStruct.segLevels[levelLimit - 1]; | |
5374 | + cellMatrix = rowStruct.cellMatrix; | |
5375 | + | |
5376 | + limitedNodes = rowStruct.tbodyEl.children().slice(levelLimit) // get level <tr> elements past the limit | |
5377 | + .addClass('fc-limited').get(); // hide elements and get a simple DOM-nodes array | |
5378 | + | |
5379 | + // iterate though segments in the last allowable level | |
5380 | + for (i = 0; i < levelSegs.length; i++) { | |
5381 | + seg = levelSegs[i]; | |
5382 | + emptyCellsUntil(seg.leftCol); // process empty cells before the segment | |
5383 | + | |
5384 | + // determine *all* segments below `seg` that occupy the same columns | |
5385 | + colSegsBelow = []; | |
5386 | + totalSegsBelow = 0; | |
5387 | + while (col <= seg.rightCol) { | |
5388 | + cell = this.getCell(row, col); | |
5389 | + segsBelow = this.getCellSegs(cell, levelLimit); | |
5390 | + colSegsBelow.push(segsBelow); | |
5391 | + totalSegsBelow += segsBelow.length; | |
5392 | + col++; | |
5393 | + } | |
5394 | + | |
5395 | + if (totalSegsBelow) { // do we need to replace this segment with one or many "more" links? | |
5396 | + td = cellMatrix[levelLimit - 1][seg.leftCol]; // the segment's parent cell | |
5397 | + rowspan = td.attr('rowspan') || 1; | |
5398 | + segMoreNodes = []; | |
5399 | + | |
5400 | + // make a replacement <td> for each column the segment occupies. will be one for each colspan | |
5401 | + for (j = 0; j < colSegsBelow.length; j++) { | |
5402 | + moreTd = $('<td class="fc-more-cell"/>').attr('rowspan', rowspan); | |
5403 | + segsBelow = colSegsBelow[j]; | |
5404 | + cell = this.getCell(row, seg.leftCol + j); | |
5405 | + moreLink = this.renderMoreLink(cell, [ seg ].concat(segsBelow)); // count seg as hidden too | |
5406 | + moreWrap = $('<div/>').append(moreLink); | |
5407 | + moreTd.append(moreWrap); | |
5408 | + segMoreNodes.push(moreTd[0]); | |
5409 | + moreNodes.push(moreTd[0]); | |
5410 | + } | |
5411 | + | |
5412 | + td.addClass('fc-limited').after($(segMoreNodes)); // hide original <td> and inject replacements | |
5413 | + limitedNodes.push(td[0]); | |
5414 | + } | |
5415 | + } | |
5416 | + | |
5417 | + emptyCellsUntil(this.colCnt); // finish off the level | |
5418 | + rowStruct.moreEls = $(moreNodes); // for easy undoing later | |
5419 | + rowStruct.limitedEls = $(limitedNodes); // for easy undoing later | |
5420 | + } | |
5421 | + }, | |
5422 | + | |
5423 | + | |
5424 | + // Reveals all levels and removes all "more"-related elements for a grid's row. | |
5425 | + // `row` is a row number. | |
5426 | + unlimitRow: function(row) { | |
5427 | + var rowStruct = this.rowStructs[row]; | |
5428 | + | |
5429 | + if (rowStruct.moreEls) { | |
5430 | + rowStruct.moreEls.remove(); | |
5431 | + rowStruct.moreEls = null; | |
5432 | + } | |
5433 | + | |
5434 | + if (rowStruct.limitedEls) { | |
5435 | + rowStruct.limitedEls.removeClass('fc-limited'); | |
5436 | + rowStruct.limitedEls = null; | |
5437 | + } | |
5438 | + }, | |
5439 | + | |
5440 | + | |
5441 | + // Renders an <a> element that represents hidden event element for a cell. | |
5442 | + // Responsible for attaching click handler as well. | |
5443 | + renderMoreLink: function(cell, hiddenSegs) { | |
5444 | + var _this = this; | |
5445 | + var view = this.view; | |
5446 | + | |
5447 | + return $('<a class="fc-more"/>') | |
5448 | + .text( | |
5449 | + this.getMoreLinkText(hiddenSegs.length) | |
5450 | + ) | |
5451 | + .on('click', function(ev) { | |
5452 | + var clickOption = view.opt('eventLimitClick'); | |
5453 | + var date = cell.start; | |
5454 | + var moreEl = $(this); | |
5455 | + var dayEl = _this.getCellDayEl(cell); | |
5456 | + var allSegs = _this.getCellSegs(cell); | |
5457 | + | |
5458 | + // rescope the segments to be within the cell's date | |
5459 | + var reslicedAllSegs = _this.resliceDaySegs(allSegs, date); | |
5460 | + var reslicedHiddenSegs = _this.resliceDaySegs(hiddenSegs, date); | |
5461 | + | |
5462 | + if (typeof clickOption === 'function') { | |
5463 | + // the returned value can be an atomic option | |
5464 | + clickOption = view.trigger('eventLimitClick', null, { | |
5465 | + date: date, | |
5466 | + dayEl: dayEl, | |
5467 | + moreEl: moreEl, | |
5468 | + segs: reslicedAllSegs, | |
5469 | + hiddenSegs: reslicedHiddenSegs | |
5470 | + }, ev); | |
5471 | + } | |
5472 | + | |
5473 | + if (clickOption === 'popover') { | |
5474 | + _this.showSegPopover(cell, moreEl, reslicedAllSegs); | |
5475 | + } | |
5476 | + else if (typeof clickOption === 'string') { // a view name | |
5477 | + view.calendar.zoomTo(date, clickOption); | |
5478 | + } | |
5479 | + }); | |
5480 | + }, | |
5481 | + | |
5482 | + | |
5483 | + // Reveals the popover that displays all events within a cell | |
5484 | + showSegPopover: function(cell, moreLink, segs) { | |
5485 | + var _this = this; | |
5486 | + var view = this.view; | |
5487 | + var moreWrap = moreLink.parent(); // the <div> wrapper around the <a> | |
5488 | + var topEl; // the element we want to match the top coordinate of | |
5489 | + var options; | |
5490 | + | |
5491 | + if (this.rowCnt == 1) { | |
5492 | + topEl = view.el; // will cause the popover to cover any sort of header | |
5493 | + } | |
5494 | + else { | |
5495 | + topEl = this.rowEls.eq(cell.row); // will align with top of row | |
5496 | + } | |
5497 | + | |
5498 | + options = { | |
5499 | + className: 'fc-more-popover', | |
5500 | + content: this.renderSegPopoverContent(cell, segs), | |
5501 | + parentEl: this.el, | |
5502 | + top: topEl.offset().top, | |
5503 | + autoHide: true, // when the user clicks elsewhere, hide the popover | |
5504 | + viewportConstrain: view.opt('popoverViewportConstrain'), | |
5505 | + hide: function() { | |
5506 | + // destroy everything when the popover is hidden | |
5507 | + _this.segPopover.destroy(); | |
5508 | + _this.segPopover = null; | |
5509 | + _this.popoverSegs = null; | |
5510 | + } | |
5511 | + }; | |
5512 | + | |
5513 | + // Determine horizontal coordinate. | |
5514 | + // We use the moreWrap instead of the <td> to avoid border confusion. | |
5515 | + if (this.isRTL) { | |
5516 | + options.right = moreWrap.offset().left + moreWrap.outerWidth() + 1; // +1 to be over cell border | |
5517 | + } | |
5518 | + else { | |
5519 | + options.left = moreWrap.offset().left - 1; // -1 to be over cell border | |
5520 | + } | |
5521 | + | |
5522 | + this.segPopover = new Popover(options); | |
5523 | + this.segPopover.show(); | |
5524 | + }, | |
5525 | + | |
5526 | + | |
5527 | + // Builds the inner DOM contents of the segment popover | |
5528 | + renderSegPopoverContent: function(cell, segs) { | |
5529 | + var view = this.view; | |
5530 | + var isTheme = view.opt('theme'); | |
5531 | + var title = cell.start.format(view.opt('dayPopoverFormat')); | |
5532 | + var content = $( | |
5533 | + '<div class="fc-header ' + view.widgetHeaderClass + '">' + | |
5534 | + '<span class="fc-close ' + | |
5535 | + (isTheme ? 'ui-icon ui-icon-closethick' : 'fc-icon fc-icon-x') + | |
5536 | + '"></span>' + | |
5537 | + '<span class="fc-title">' + | |
5538 | + htmlEscape(title) + | |
5539 | + '</span>' + | |
5540 | + '<div class="fc-clear"/>' + | |
5541 | + '</div>' + | |
5542 | + '<div class="fc-body ' + view.widgetContentClass + '">' + | |
5543 | + '<div class="fc-event-container"></div>' + | |
5544 | + '</div>' | |
5545 | + ); | |
5546 | + var segContainer = content.find('.fc-event-container'); | |
5547 | + var i; | |
5548 | + | |
5549 | + // render each seg's `el` and only return the visible segs | |
5550 | + segs = this.renderFgSegEls(segs, true); // disableResizing=true | |
5551 | + this.popoverSegs = segs; | |
5552 | + | |
5553 | + for (i = 0; i < segs.length; i++) { | |
5554 | + | |
5555 | + // because segments in the popover are not part of a grid coordinate system, provide a hint to any | |
5556 | + // grids that want to do drag-n-drop about which cell it came from | |
5557 | + segs[i].cell = cell; | |
5558 | + | |
5559 | + segContainer.append(segs[i].el); | |
5560 | + } | |
5561 | + | |
5562 | + return content; | |
5563 | + }, | |
5564 | + | |
5565 | + | |
5566 | + // Given the events within an array of segment objects, reslice them to be in a single day | |
5567 | + resliceDaySegs: function(segs, dayDate) { | |
5568 | + | |
5569 | + // build an array of the original events | |
5570 | + var events = $.map(segs, function(seg) { | |
5571 | + return seg.event; | |
5572 | + }); | |
5573 | + | |
5574 | + var dayStart = dayDate.clone().stripTime(); | |
5575 | + var dayEnd = dayStart.clone().add(1, 'days'); | |
5576 | + var dayRange = { start: dayStart, end: dayEnd }; | |
5577 | + | |
5578 | + // slice the events with a custom slicing function | |
5579 | + segs = this.eventsToSegs( | |
5580 | + events, | |
5581 | + function(range) { | |
5582 | + var seg = intersectionToSeg(range, dayRange); // undefind if no intersection | |
5583 | + return seg ? [ seg ] : []; // must return an array of segments | |
5584 | + } | |
5585 | + ); | |
5586 | + | |
5587 | + // force an order because eventsToSegs doesn't guarantee one | |
5588 | + segs.sort(compareSegs); | |
5589 | + | |
5590 | + return segs; | |
5591 | + }, | |
5592 | + | |
5593 | + | |
5594 | + // Generates the text that should be inside a "more" link, given the number of events it represents | |
5595 | + getMoreLinkText: function(num) { | |
5596 | + var opt = this.view.opt('eventLimitText'); | |
5597 | + | |
5598 | + if (typeof opt === 'function') { | |
5599 | + return opt(num); | |
5600 | + } | |
5601 | + else { | |
5602 | + return '+' + num + ' ' + opt; | |
5603 | + } | |
5604 | + }, | |
5605 | + | |
5606 | + | |
5607 | + // Returns segments within a given cell. | |
5608 | + // If `startLevel` is specified, returns only events including and below that level. Otherwise returns all segs. | |
5609 | + getCellSegs: function(cell, startLevel) { | |
5610 | + var segMatrix = this.rowStructs[cell.row].segMatrix; | |
5611 | + var level = startLevel || 0; | |
5612 | + var segs = []; | |
5613 | + var seg; | |
5614 | + | |
5615 | + while (level < segMatrix.length) { | |
5616 | + seg = segMatrix[level][cell.col]; | |
5617 | + if (seg) { | |
5618 | + segs.push(seg); | |
5619 | + } | |
5620 | + level++; | |
5621 | + } | |
5622 | + | |
5623 | + return segs; | |
5624 | + } | |
5625 | + | |
5626 | +}); | |
5627 | + | |
5628 | +;; | |
5629 | + | |
5630 | +/* A component that renders one or more columns of vertical time slots | |
5631 | +----------------------------------------------------------------------------------------------------------------------*/ | |
5632 | + | |
5633 | +var TimeGrid = Grid.extend({ | |
5634 | + | |
5635 | + slotDuration: null, // duration of a "slot", a distinct time segment on given day, visualized by lines | |
5636 | + snapDuration: null, // granularity of time for dragging and selecting | |
5637 | + | |
5638 | + minTime: null, // Duration object that denotes the first visible time of any given day | |
5639 | + maxTime: null, // Duration object that denotes the exclusive visible end time of any given day | |
5640 | + | |
5641 | + axisFormat: null, // formatting string for times running along vertical axis | |
5642 | + | |
5643 | + dayEls: null, // cells elements in the day-row background | |
5644 | + slatEls: null, // elements running horizontally across all columns | |
5645 | + | |
5646 | + slatTops: null, // an array of top positions, relative to the container. last item holds bottom of last slot | |
5647 | + | |
5648 | + helperEl: null, // cell skeleton element for rendering the mock event "helper" | |
5649 | + | |
5650 | + businessHourSegs: null, | |
5651 | + | |
5652 | + | |
5653 | + constructor: function() { | |
5654 | + Grid.apply(this, arguments); // call the super-constructor | |
5655 | + this.processOptions(); | |
5656 | + }, | |
5657 | + | |
5658 | + | |
5659 | + // Renders the time grid into `this.el`, which should already be assigned. | |
5660 | + // Relies on the view's colCnt. In the future, this component should probably be self-sufficient. | |
5661 | + renderDates: function() { | |
5662 | + this.el.html(this.renderHtml()); | |
5663 | + this.dayEls = this.el.find('.fc-day'); | |
5664 | + this.slatEls = this.el.find('.fc-slats tr'); | |
5665 | + }, | |
5666 | + | |
5667 | + | |
5668 | + renderBusinessHours: function() { | |
5669 | + var events = this.view.calendar.getBusinessHoursEvents(); | |
5670 | + this.businessHourSegs = this.renderFill('businessHours', this.eventsToSegs(events), 'bgevent'); | |
5671 | + }, | |
5672 | + | |
5673 | + | |
5674 | + // Renders the basic HTML skeleton for the grid | |
5675 | + renderHtml: function() { | |
5676 | + return '' + | |
5677 | + '<div class="fc-bg">' + | |
5678 | + '<table>' + | |
5679 | + this.rowHtml('slotBg') + // leverages RowRenderer, which will call slotBgCellHtml | |
5680 | + '</table>' + | |
5681 | + '</div>' + | |
5682 | + '<div class="fc-slats">' + | |
5683 | + '<table>' + | |
5684 | + this.slatRowHtml() + | |
5685 | + '</table>' + | |
5686 | + '</div>'; | |
5687 | + }, | |
5688 | + | |
5689 | + | |
5690 | + // Renders the HTML for a vertical background cell behind the slots. | |
5691 | + // This method is distinct from 'bg' because we wanted a new `rowType` so the View could customize the rendering. | |
5692 | + slotBgCellHtml: function(cell) { | |
5693 | + return this.bgCellHtml(cell); | |
5694 | + }, | |
5695 | + | |
5696 | + | |
5697 | + // Generates the HTML for the horizontal "slats" that run width-wise. Has a time axis on a side. Depends on RTL. | |
5698 | + slatRowHtml: function() { | |
5699 | + var view = this.view; | |
5700 | + var isRTL = this.isRTL; | |
5701 | + var html = ''; | |
5702 | + var slotNormal = this.slotDuration.asMinutes() % 15 === 0; | |
5703 | + var slotTime = moment.duration(+this.minTime); // wish there was .clone() for durations | |
5704 | + var slotDate; // will be on the view's first day, but we only care about its time | |
5705 | + var minutes; | |
5706 | + var axisHtml; | |
5707 | + | |
5708 | + // Calculate the time for each slot | |
5709 | + while (slotTime < this.maxTime) { | |
5710 | + slotDate = this.start.clone().time(slotTime); // will be in UTC but that's good. to avoid DST issues | |
5711 | + minutes = slotDate.minutes(); | |
5712 | + | |
5713 | + axisHtml = | |
5714 | + '<td class="fc-axis fc-time ' + view.widgetContentClass + '" ' + view.axisStyleAttr() + '>' + | |
5715 | + ((!slotNormal || !minutes) ? // if irregular slot duration, or on the hour, then display the time | |
5716 | + '<span>' + // for matchCellWidths | |
5717 | + htmlEscape(slotDate.format(this.axisFormat)) + | |
5718 | + '</span>' : | |
5719 | + '' | |
5720 | + ) + | |
5721 | + '</td>'; | |
5722 | + | |
5723 | + html += | |
5724 | + '<tr ' + (!minutes ? '' : 'class="fc-minor"') + '>' + | |
5725 | + (!isRTL ? axisHtml : '') + | |
5726 | + '<td class="' + view.widgetContentClass + '"/>' + | |
5727 | + (isRTL ? axisHtml : '') + | |
5728 | + "</tr>"; | |
5729 | + | |
5730 | + slotTime.add(this.slotDuration); | |
5731 | + } | |
5732 | + | |
5733 | + return html; | |
5734 | + }, | |
5735 | + | |
5736 | + | |
5737 | + /* Options | |
5738 | + ------------------------------------------------------------------------------------------------------------------*/ | |
5739 | + | |
5740 | + | |
5741 | + // Parses various options into properties of this object | |
5742 | + processOptions: function() { | |
5743 | + var view = this.view; | |
5744 | + var slotDuration = view.opt('slotDuration'); | |
5745 | + var snapDuration = view.opt('snapDuration'); | |
5746 | + | |
5747 | + slotDuration = moment.duration(slotDuration); | |
5748 | + snapDuration = snapDuration ? moment.duration(snapDuration) : slotDuration; | |
5749 | + | |
5750 | + this.slotDuration = slotDuration; | |
5751 | + this.snapDuration = snapDuration; | |
5752 | + this.cellDuration = snapDuration; // for Grid system | |
5753 | + | |
5754 | + this.minTime = moment.duration(view.opt('minTime')); | |
5755 | + this.maxTime = moment.duration(view.opt('maxTime')); | |
5756 | + | |
5757 | + this.axisFormat = view.opt('axisFormat') || view.opt('smallTimeFormat'); | |
5758 | + }, | |
5759 | + | |
5760 | + | |
5761 | + // Computes a default column header formatting string if `colFormat` is not explicitly defined | |
5762 | + computeColHeadFormat: function() { | |
5763 | + if (this.colCnt > 1) { // multiple days, so full single date string WON'T be in title text | |
5764 | + return this.view.opt('dayOfMonthFormat'); // "Sat 12/10" | |
5765 | + } | |
5766 | + else { // single day, so full single date string will probably be in title text | |
5767 | + return 'dddd'; // "Saturday" | |
5768 | + } | |
5769 | + }, | |
5770 | + | |
5771 | + | |
5772 | + // Computes a default event time formatting string if `timeFormat` is not explicitly defined | |
5773 | + computeEventTimeFormat: function() { | |
5774 | + return this.view.opt('noMeridiemTimeFormat'); // like "6:30" (no AM/PM) | |
5775 | + }, | |
5776 | + | |
5777 | + | |
5778 | + // Computes a default `displayEventEnd` value if one is not expliclty defined | |
5779 | + computeDisplayEventEnd: function() { | |
5780 | + return true; | |
5781 | + }, | |
5782 | + | |
5783 | + | |
5784 | + /* Cell System | |
5785 | + ------------------------------------------------------------------------------------------------------------------*/ | |
5786 | + | |
5787 | + | |
5788 | + // Initializes row/col information | |
5789 | + updateCells: function() { | |
5790 | + var view = this.view; | |
5791 | + var colData = []; | |
5792 | + var date; | |
5793 | + | |
5794 | + date = this.start.clone(); | |
5795 | + while (date.isBefore(this.end)) { | |
5796 | + colData.push({ | |
5797 | + day: date.clone() | |
5798 | + }); | |
5799 | + date.add(1, 'day'); | |
5800 | + date = view.skipHiddenDays(date); | |
5801 | + } | |
5802 | + | |
5803 | + if (this.isRTL) { | |
5804 | + colData.reverse(); | |
5805 | + } | |
5806 | + | |
5807 | + this.colData = colData; | |
5808 | + this.colCnt = colData.length; | |
5809 | + this.rowCnt = Math.ceil((this.maxTime - this.minTime) / this.snapDuration); // # of vertical snaps | |
5810 | + }, | |
5811 | + | |
5812 | + | |
5813 | + // Given a cell object, generates its start date. Returns a reference-free copy. | |
5814 | + computeCellDate: function(cell) { | |
5815 | + var time = this.computeSnapTime(cell.row); | |
5816 | + | |
5817 | + return this.view.calendar.rezoneDate(cell.day).time(time); | |
5818 | + }, | |
5819 | + | |
5820 | + | |
5821 | + // Retrieves the element representing the given column | |
5822 | + getColEl: function(col) { | |
5823 | + return this.dayEls.eq(col); | |
5824 | + }, | |
5825 | + | |
5826 | + | |
5827 | + /* Dates | |
5828 | + ------------------------------------------------------------------------------------------------------------------*/ | |
5829 | + | |
5830 | + | |
5831 | + // Given a row number of the grid, representing a "snap", returns a time (Duration) from its start-of-day | |
5832 | + computeSnapTime: function(row) { | |
5833 | + return moment.duration(this.minTime + this.snapDuration * row); | |
5834 | + }, | |
5835 | + | |
5836 | + | |
5837 | + // Slices up a date range by column into an array of segments | |
5838 | + rangeToSegs: function(range) { | |
5839 | + var colCnt = this.colCnt; | |
5840 | + var segs = []; | |
5841 | + var seg; | |
5842 | + var col; | |
5843 | + var colDate; | |
5844 | + var colRange; | |
5845 | + | |
5846 | + // normalize :( | |
5847 | + range = { | |
5848 | + start: range.start.clone().stripZone(), | |
5849 | + end: range.end.clone().stripZone() | |
5850 | + }; | |
5851 | + | |
5852 | + for (col = 0; col < colCnt; col++) { | |
5853 | + colDate = this.colData[col].day; // will be ambig time/timezone | |
5854 | + colRange = { | |
5855 | + start: colDate.clone().time(this.minTime), | |
5856 | + end: colDate.clone().time(this.maxTime) | |
5857 | + }; | |
5858 | + seg = intersectionToSeg(range, colRange); // both will be ambig timezone | |
5859 | + if (seg) { | |
5860 | + seg.col = col; | |
5861 | + segs.push(seg); | |
5862 | + } | |
5863 | + } | |
5864 | + | |
5865 | + return segs; | |
5866 | + }, | |
5867 | + | |
5868 | + | |
5869 | + /* Coordinates | |
5870 | + ------------------------------------------------------------------------------------------------------------------*/ | |
5871 | + | |
5872 | + | |
5873 | + updateSize: function(isResize) { // NOT a standard Grid method | |
5874 | + this.computeSlatTops(); | |
5875 | + | |
5876 | + if (isResize) { | |
5877 | + this.updateSegVerticals(); | |
5878 | + } | |
5879 | + }, | |
5880 | + | |
5881 | + | |
5882 | + // Computes the top/bottom coordinates of each "snap" rows | |
5883 | + computeRowCoords: function() { | |
5884 | + var originTop = this.el.offset().top; | |
5885 | + var items = []; | |
5886 | + var i; | |
5887 | + var item; | |
5888 | + | |
5889 | + for (i = 0; i < this.rowCnt; i++) { | |
5890 | + item = { | |
5891 | + top: originTop + this.computeTimeTop(this.computeSnapTime(i)) | |
5892 | + }; | |
5893 | + if (i > 0) { | |
5894 | + items[i - 1].bottom = item.top; | |
5895 | + } | |
5896 | + items.push(item); | |
5897 | + } | |
5898 | + item.bottom = item.top + this.computeTimeTop(this.computeSnapTime(i)); | |
5899 | + | |
5900 | + return items; | |
5901 | + }, | |
5902 | + | |
5903 | + | |
5904 | + // Computes the top coordinate, relative to the bounds of the grid, of the given date. | |
5905 | + // A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight. | |
5906 | + computeDateTop: function(date, startOfDayDate) { | |
5907 | + return this.computeTimeTop( | |
5908 | + moment.duration( | |
5909 | + date.clone().stripZone() - startOfDayDate.clone().stripTime() | |
5910 | + ) | |
5911 | + ); | |
5912 | + }, | |
5913 | + | |
5914 | + | |
5915 | + // Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration). | |
5916 | + computeTimeTop: function(time) { | |
5917 | + var slatCoverage = (time - this.minTime) / this.slotDuration; // floating-point value of # of slots covered | |
5918 | + var slatIndex; | |
5919 | + var slatRemainder; | |
5920 | + var slatTop; | |
5921 | + var slatBottom; | |
5922 | + | |
5923 | + // constrain. because minTime/maxTime might be customized | |
5924 | + slatCoverage = Math.max(0, slatCoverage); | |
5925 | + slatCoverage = Math.min(this.slatEls.length, slatCoverage); | |
5926 | + | |
5927 | + slatIndex = Math.floor(slatCoverage); // an integer index of the furthest whole slot | |
5928 | + slatRemainder = slatCoverage - slatIndex; | |
5929 | + slatTop = this.slatTops[slatIndex]; // the top position of the furthest whole slot | |
5930 | + | |
5931 | + if (slatRemainder) { // time spans part-way into the slot | |
5932 | + slatBottom = this.slatTops[slatIndex + 1]; | |
5933 | + return slatTop + (slatBottom - slatTop) * slatRemainder; // part-way between slots | |
5934 | + } | |
5935 | + else { | |
5936 | + return slatTop; | |
5937 | + } | |
5938 | + }, | |
5939 | + | |
5940 | + | |
5941 | + // Queries each `slatEl` for its position relative to the grid's container and stores it in `slatTops`. | |
5942 | + // Includes the the bottom of the last slat as the last item in the array. | |
5943 | + computeSlatTops: function() { | |
5944 | + var tops = []; | |
5945 | + var top; | |
5946 | + | |
5947 | + this.slatEls.each(function(i, node) { | |
5948 | + top = $(node).position().top; | |
5949 | + tops.push(top); | |
5950 | + }); | |
5951 | + | |
5952 | + tops.push(top + this.slatEls.last().outerHeight()); // bottom of the last slat | |
5953 | + | |
5954 | + this.slatTops = tops; | |
5955 | + }, | |
5956 | + | |
5957 | + | |
5958 | + /* Event Drag Visualization | |
5959 | + ------------------------------------------------------------------------------------------------------------------*/ | |
5960 | + | |
5961 | + | |
5962 | + // Renders a visual indication of an event being dragged over the specified date(s). | |
5963 | + // dropLocation's end might be null, as well as `seg`. See Grid::renderDrag for more info. | |
5964 | + // A returned value of `true` signals that a mock "helper" event has been rendered. | |
5965 | + renderDrag: function(dropLocation, seg) { | |
5966 | + | |
5967 | + if (seg) { // if there is event information for this drag, render a helper event | |
5968 | + this.renderRangeHelper(dropLocation, seg); | |
5969 | + this.applyDragOpacity(this.helperEl); | |
5970 | + | |
5971 | + return true; // signal that a helper has been rendered | |
5972 | + } | |
5973 | + else { | |
5974 | + // otherwise, just render a highlight | |
5975 | + this.renderHighlight( | |
5976 | + this.view.calendar.ensureVisibleEventRange(dropLocation) // needs to be a proper range | |
5977 | + ); | |
5978 | + } | |
5979 | + }, | |
5980 | + | |
5981 | + | |
5982 | + // Unrenders any visual indication of an event being dragged | |
5983 | + destroyDrag: function() { | |
5984 | + this.destroyHelper(); | |
5985 | + this.destroyHighlight(); | |
5986 | + }, | |
5987 | + | |
5988 | + | |
5989 | + /* Event Resize Visualization | |
5990 | + ------------------------------------------------------------------------------------------------------------------*/ | |
5991 | + | |
5992 | + | |
5993 | + // Renders a visual indication of an event being resized | |
5994 | + renderEventResize: function(range, seg) { | |
5995 | + this.renderRangeHelper(range, seg); | |
5996 | + }, | |
5997 | + | |
5998 | + | |
5999 | + // Unrenders any visual indication of an event being resized | |
6000 | + destroyEventResize: function() { | |
6001 | + this.destroyHelper(); | |
6002 | + }, | |
6003 | + | |
6004 | + | |
6005 | + /* Event Helper | |
6006 | + ------------------------------------------------------------------------------------------------------------------*/ | |
6007 | + | |
6008 | + | |
6009 | + // Renders a mock "helper" event. `sourceSeg` is the original segment object and might be null (an external drag) | |
6010 | + renderHelper: function(event, sourceSeg) { | |
6011 | + var segs = this.eventsToSegs([ event ]); | |
6012 | + var tableEl; | |
6013 | + var i, seg; | |
6014 | + var sourceEl; | |
6015 | + | |
6016 | + segs = this.renderFgSegEls(segs); // assigns each seg's el and returns a subset of segs that were rendered | |
6017 | + tableEl = this.renderSegTable(segs); | |
6018 | + | |
6019 | + // Try to make the segment that is in the same row as sourceSeg look the same | |
6020 | + for (i = 0; i < segs.length; i++) { | |
6021 | + seg = segs[i]; | |
6022 | + if (sourceSeg && sourceSeg.col === seg.col) { | |
6023 | + sourceEl = sourceSeg.el; | |
6024 | + seg.el.css({ | |
6025 | + left: sourceEl.css('left'), | |
6026 | + right: sourceEl.css('right'), | |
6027 | + 'margin-left': sourceEl.css('margin-left'), | |
6028 | + 'margin-right': sourceEl.css('margin-right') | |
6029 | + }); | |
6030 | + } | |
6031 | + } | |
6032 | + | |
6033 | + this.helperEl = $('<div class="fc-helper-skeleton"/>') | |
6034 | + .append(tableEl) | |
6035 | + .appendTo(this.el); | |
6036 | + }, | |
6037 | + | |
6038 | + | |
6039 | + // Unrenders any mock helper event | |
6040 | + destroyHelper: function() { | |
6041 | + if (this.helperEl) { | |
6042 | + this.helperEl.remove(); | |
6043 | + this.helperEl = null; | |
6044 | + } | |
6045 | + }, | |
6046 | + | |
6047 | + | |
6048 | + /* Selection | |
6049 | + ------------------------------------------------------------------------------------------------------------------*/ | |
6050 | + | |
6051 | + | |
6052 | + // Renders a visual indication of a selection. Overrides the default, which was to simply render a highlight. | |
6053 | + renderSelection: function(range) { | |
6054 | + if (this.view.opt('selectHelper')) { // this setting signals that a mock helper event should be rendered | |
6055 | + this.renderRangeHelper(range); | |
6056 | + } | |
6057 | + else { | |
6058 | + this.renderHighlight(range); | |
6059 | + } | |
6060 | + }, | |
6061 | + | |
6062 | + | |
6063 | + // Unrenders any visual indication of a selection | |
6064 | + destroySelection: function() { | |
6065 | + this.destroyHelper(); | |
6066 | + this.destroyHighlight(); | |
6067 | + }, | |
6068 | + | |
6069 | + | |
6070 | + /* Fill System (highlight, background events, business hours) | |
6071 | + ------------------------------------------------------------------------------------------------------------------*/ | |
6072 | + | |
6073 | + | |
6074 | + // Renders a set of rectangles over the given time segments. | |
6075 | + // Only returns segments that successfully rendered. | |
6076 | + renderFill: function(type, segs, className) { | |
6077 | + var segCols; | |
6078 | + var skeletonEl; | |
6079 | + var trEl; | |
6080 | + var col, colSegs; | |
6081 | + var tdEl; | |
6082 | + var containerEl; | |
6083 | + var dayDate; | |
6084 | + var i, seg; | |
6085 | + | |
6086 | + if (segs.length) { | |
6087 | + | |
6088 | + segs = this.renderFillSegEls(type, segs); // assignes `.el` to each seg. returns successfully rendered segs | |
6089 | + segCols = this.groupSegCols(segs); // group into sub-arrays, and assigns 'col' to each seg | |
6090 | + | |
6091 | + className = className || type.toLowerCase(); | |
6092 | + skeletonEl = $( | |
6093 | + '<div class="fc-' + className + '-skeleton">' + | |
6094 | + '<table><tr/></table>' + | |
6095 | + '</div>' | |
6096 | + ); | |
6097 | + trEl = skeletonEl.find('tr'); | |
6098 | + | |
6099 | + for (col = 0; col < segCols.length; col++) { | |
6100 | + colSegs = segCols[col]; | |
6101 | + tdEl = $('<td/>').appendTo(trEl); | |
6102 | + | |
6103 | + if (colSegs.length) { | |
6104 | + containerEl = $('<div class="fc-' + className + '-container"/>').appendTo(tdEl); | |
6105 | + dayDate = this.colData[col].day; | |
6106 | + | |
6107 | + for (i = 0; i < colSegs.length; i++) { | |
6108 | + seg = colSegs[i]; | |
6109 | + containerEl.append( | |
6110 | + seg.el.css({ | |
6111 | + top: this.computeDateTop(seg.start, dayDate), | |
6112 | + bottom: -this.computeDateTop(seg.end, dayDate) // the y position of the bottom edge | |
6113 | + }) | |
6114 | + ); | |
6115 | + } | |
6116 | + } | |
6117 | + } | |
6118 | + | |
6119 | + this.bookendCells(trEl, type); | |
6120 | + | |
6121 | + this.el.append(skeletonEl); | |
6122 | + this.elsByFill[type] = skeletonEl; | |
6123 | + } | |
6124 | + | |
6125 | + return segs; | |
6126 | + } | |
6127 | + | |
6128 | +}); | |
6129 | + | |
6130 | +;; | |
6131 | + | |
6132 | +/* Event-rendering methods for the TimeGrid class | |
6133 | +----------------------------------------------------------------------------------------------------------------------*/ | |
6134 | + | |
6135 | +TimeGrid.mixin({ | |
6136 | + | |
6137 | + eventSkeletonEl: null, // has cells with event-containers, which contain absolutely positioned event elements | |
6138 | + | |
6139 | + | |
6140 | + // Renders the given foreground event segments onto the grid | |
6141 | + renderFgSegs: function(segs) { | |
6142 | + segs = this.renderFgSegEls(segs); // returns a subset of the segs. segs that were actually rendered | |
6143 | + | |
6144 | + this.el.append( | |
6145 | + this.eventSkeletonEl = $('<div class="fc-content-skeleton"/>') | |
6146 | + .append(this.renderSegTable(segs)) | |
6147 | + ); | |
6148 | + | |
6149 | + return segs; // return only the segs that were actually rendered | |
6150 | + }, | |
6151 | + | |
6152 | + | |
6153 | + // Unrenders all currently rendered foreground event segments | |
6154 | + destroyFgSegs: function(segs) { | |
6155 | + if (this.eventSkeletonEl) { | |
6156 | + this.eventSkeletonEl.remove(); | |
6157 | + this.eventSkeletonEl = null; | |
6158 | + } | |
6159 | + }, | |
6160 | + | |
6161 | + | |
6162 | + // Renders and returns the <table> portion of the event-skeleton. | |
6163 | + // Returns an object with properties 'tbodyEl' and 'segs'. | |
6164 | + renderSegTable: function(segs) { | |
6165 | + var tableEl = $('<table><tr/></table>'); | |
6166 | + var trEl = tableEl.find('tr'); | |
6167 | + var segCols; | |
6168 | + var i, seg; | |
6169 | + var col, colSegs; | |
6170 | + var containerEl; | |
6171 | + | |
6172 | + segCols = this.groupSegCols(segs); // group into sub-arrays, and assigns 'col' to each seg | |
6173 | + | |
6174 | + this.computeSegVerticals(segs); // compute and assign top/bottom | |
6175 | + | |
6176 | + for (col = 0; col < segCols.length; col++) { // iterate each column grouping | |
6177 | + colSegs = segCols[col]; | |
6178 | + placeSlotSegs(colSegs); // compute horizontal coordinates, z-index's, and reorder the array | |
6179 | + | |
6180 | + containerEl = $('<div class="fc-event-container"/>'); | |
6181 | + | |
6182 | + // assign positioning CSS and insert into container | |
6183 | + for (i = 0; i < colSegs.length; i++) { | |
6184 | + seg = colSegs[i]; | |
6185 | + seg.el.css(this.generateSegPositionCss(seg)); | |
6186 | + | |
6187 | + // if the height is short, add a className for alternate styling | |
6188 | + if (seg.bottom - seg.top < 30) { | |
6189 | + seg.el.addClass('fc-short'); | |
6190 | + } | |
6191 | + | |
6192 | + containerEl.append(seg.el); | |
6193 | + } | |
6194 | + | |
6195 | + trEl.append($('<td/>').append(containerEl)); | |
6196 | + } | |
6197 | + | |
6198 | + this.bookendCells(trEl, 'eventSkeleton'); | |
6199 | + | |
6200 | + return tableEl; | |
6201 | + }, | |
6202 | + | |
6203 | + | |
6204 | + // Refreshes the CSS top/bottom coordinates for each segment element. Probably after a window resize/zoom. | |
6205 | + // Repositions business hours segs too, so not just for events. Maybe shouldn't be here. | |
6206 | + updateSegVerticals: function() { | |
6207 | + var allSegs = (this.segs || []).concat(this.businessHourSegs || []); | |
6208 | + var i; | |
6209 | + | |
6210 | + this.computeSegVerticals(allSegs); | |
6211 | + | |
6212 | + for (i = 0; i < allSegs.length; i++) { | |
6213 | + allSegs[i].el.css( | |
6214 | + this.generateSegVerticalCss(allSegs[i]) | |
6215 | + ); | |
6216 | + } | |
6217 | + }, | |
6218 | + | |
6219 | + | |
6220 | + // For each segment in an array, computes and assigns its top and bottom properties | |
6221 | + computeSegVerticals: function(segs) { | |
6222 | + var i, seg; | |
6223 | + | |
6224 | + for (i = 0; i < segs.length; i++) { | |
6225 | + seg = segs[i]; | |
6226 | + seg.top = this.computeDateTop(seg.start, seg.start); | |
6227 | + seg.bottom = this.computeDateTop(seg.end, seg.start); | |
6228 | + } | |
6229 | + }, | |
6230 | + | |
6231 | + | |
6232 | + // Renders the HTML for a single event segment's default rendering | |
6233 | + fgSegHtml: function(seg, disableResizing) { | |
6234 | + var view = this.view; | |
6235 | + var event = seg.event; | |
6236 | + var isDraggable = view.isEventDraggable(event); | |
6237 | + var isResizableFromStart = !disableResizing && seg.isStart && view.isEventResizableFromStart(event); | |
6238 | + var isResizableFromEnd = !disableResizing && seg.isEnd && view.isEventResizableFromEnd(event); | |
6239 | + var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd); | |
6240 | + var skinCss = cssToStr(this.getEventSkinCss(event)); | |
6241 | + var timeText; | |
6242 | + var fullTimeText; // more verbose time text. for the print stylesheet | |
6243 | + var startTimeText; // just the start time text | |
6244 | + | |
6245 | + classes.unshift('fc-time-grid-event', 'fc-v-event'); | |
6246 | + | |
6247 | + if (view.isMultiDayEvent(event)) { // if the event appears to span more than one day... | |
6248 | + // Don't display time text on segments that run entirely through a day. | |
6249 | + // That would appear as midnight-midnight and would look dumb. | |
6250 | + // Otherwise, display the time text for the *segment's* times (like 6pm-midnight or midnight-10am) | |
6251 | + if (seg.isStart || seg.isEnd) { | |
6252 | + timeText = this.getEventTimeText(seg); | |
6253 | + fullTimeText = this.getEventTimeText(seg, 'LT'); | |
6254 | + startTimeText = this.getEventTimeText(seg, null, false); // displayEnd=false | |
6255 | + } | |
6256 | + } else { | |
6257 | + // Display the normal time text for the *event's* times | |
6258 | + timeText = this.getEventTimeText(event); | |
6259 | + fullTimeText = this.getEventTimeText(event, 'LT'); | |
6260 | + startTimeText = this.getEventTimeText(event, null, false); // displayEnd=false | |
6261 | + } | |
6262 | + | |
6263 | + return '<a class="' + classes.join(' ') + '"' + | |
6264 | + (event.url ? | |
6265 | + ' href="' + htmlEscape(event.url) + '"' : | |
6266 | + '' | |
6267 | + ) + | |
6268 | + (skinCss ? | |
6269 | + ' style="' + skinCss + '"' : | |
6270 | + '' | |
6271 | + ) + | |
6272 | + '>' + | |
6273 | + '<div class="fc-content">' + | |
6274 | + (timeText ? | |
6275 | + '<div class="fc-time"' + | |
6276 | + ' data-start="' + htmlEscape(startTimeText) + '"' + | |
6277 | + ' data-full="' + htmlEscape(fullTimeText) + '"' + | |
6278 | + '>' + | |
6279 | + '<span>' + htmlEscape(timeText) + '</span>' + | |
6280 | + '</div>' : | |
6281 | + '' | |
6282 | + ) + | |
6283 | + (event.title ? | |
6284 | + '<div class="fc-title">' + | |
6285 | + htmlEscape(event.title) + | |
6286 | + '</div>' : | |
6287 | + '' | |
6288 | + ) + | |
6289 | + '</div>' + | |
6290 | + '<div class="fc-bg"/>' + | |
6291 | + /* TODO: write CSS for this | |
6292 | + (isResizableFromStart ? | |
6293 | + '<div class="fc-resizer fc-start-resizer" />' : | |
6294 | + '' | |
6295 | + ) + | |
6296 | + */ | |
6297 | + (isResizableFromEnd ? | |
6298 | + '<div class="fc-resizer fc-end-resizer" />' : | |
6299 | + '' | |
6300 | + ) + | |
6301 | + '</a>'; | |
6302 | + }, | |
6303 | + | |
6304 | + | |
6305 | + // Generates an object with CSS properties/values that should be applied to an event segment element. | |
6306 | + // Contains important positioning-related properties that should be applied to any event element, customized or not. | |
6307 | + generateSegPositionCss: function(seg) { | |
6308 | + var shouldOverlap = this.view.opt('slotEventOverlap'); | |
6309 | + var backwardCoord = seg.backwardCoord; // the left side if LTR. the right side if RTL. floating-point | |
6310 | + var forwardCoord = seg.forwardCoord; // the right side if LTR. the left side if RTL. floating-point | |
6311 | + var props = this.generateSegVerticalCss(seg); // get top/bottom first | |
6312 | + var left; // amount of space from left edge, a fraction of the total width | |
6313 | + var right; // amount of space from right edge, a fraction of the total width | |
6314 | + | |
6315 | + if (shouldOverlap) { | |
6316 | + // double the width, but don't go beyond the maximum forward coordinate (1.0) | |
6317 | + forwardCoord = Math.min(1, backwardCoord + (forwardCoord - backwardCoord) * 2); | |
6318 | + } | |
6319 | + | |
6320 | + if (this.isRTL) { | |
6321 | + left = 1 - forwardCoord; | |
6322 | + right = backwardCoord; | |
6323 | + } | |
6324 | + else { | |
6325 | + left = backwardCoord; | |
6326 | + right = 1 - forwardCoord; | |
6327 | + } | |
6328 | + | |
6329 | + props.zIndex = seg.level + 1; // convert from 0-base to 1-based | |
6330 | + props.left = left * 100 + '%'; | |
6331 | + props.right = right * 100 + '%'; | |
6332 | + | |
6333 | + if (shouldOverlap && seg.forwardPressure) { | |
6334 | + // add padding to the edge so that forward stacked events don't cover the resizer's icon | |
6335 | + props[this.isRTL ? 'marginLeft' : 'marginRight'] = 10 * 2; // 10 is a guesstimate of the icon's width | |
6336 | + } | |
6337 | + | |
6338 | + return props; | |
6339 | + }, | |
6340 | + | |
6341 | + | |
6342 | + // Generates an object with CSS properties for the top/bottom coordinates of a segment element | |
6343 | + generateSegVerticalCss: function(seg) { | |
6344 | + return { | |
6345 | + top: seg.top, | |
6346 | + bottom: -seg.bottom // flipped because needs to be space beyond bottom edge of event container | |
6347 | + }; | |
6348 | + }, | |
6349 | + | |
6350 | + | |
6351 | + // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's col | |
6352 | + groupSegCols: function(segs) { | |
6353 | + var segCols = []; | |
6354 | + var i; | |
6355 | + | |
6356 | + for (i = 0; i < this.colCnt; i++) { | |
6357 | + segCols.push([]); | |
6358 | + } | |
6359 | + | |
6360 | + for (i = 0; i < segs.length; i++) { | |
6361 | + segCols[segs[i].col].push(segs[i]); | |
6362 | + } | |
6363 | + | |
6364 | + return segCols; | |
6365 | + } | |
6366 | + | |
6367 | +}); | |
6368 | + | |
6369 | + | |
6370 | +// Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each. | |
6371 | +// NOTE: Also reorders the given array by date! | |
6372 | +function placeSlotSegs(segs) { | |
6373 | + var levels; | |
6374 | + var level0; | |
6375 | + var i; | |
6376 | + | |
6377 | + segs.sort(compareSegs); // order by date | |
6378 | + levels = buildSlotSegLevels(segs); | |
6379 | + computeForwardSlotSegs(levels); | |
6380 | + | |
6381 | + if ((level0 = levels[0])) { | |
6382 | + | |
6383 | + for (i = 0; i < level0.length; i++) { | |
6384 | + computeSlotSegPressures(level0[i]); | |
6385 | + } | |
6386 | + | |
6387 | + for (i = 0; i < level0.length; i++) { | |
6388 | + computeSlotSegCoords(level0[i], 0, 0); | |
6389 | + } | |
6390 | + } | |
6391 | +} | |
6392 | + | |
6393 | + | |
6394 | +// Builds an array of segments "levels". The first level will be the leftmost tier of segments if the calendar is | |
6395 | +// left-to-right, or the rightmost if the calendar is right-to-left. Assumes the segments are already ordered by date. | |
6396 | +function buildSlotSegLevels(segs) { | |
6397 | + var levels = []; | |
6398 | + var i, seg; | |
6399 | + var j; | |
6400 | + | |
6401 | + for (i=0; i<segs.length; i++) { | |
6402 | + seg = segs[i]; | |
6403 | + | |
6404 | + // go through all the levels and stop on the first level where there are no collisions | |
6405 | + for (j=0; j<levels.length; j++) { | |
6406 | + if (!computeSlotSegCollisions(seg, levels[j]).length) { | |
6407 | + break; | |
6408 | + } | |
6409 | + } | |
6410 | + | |
6411 | + seg.level = j; | |
6412 | + | |
6413 | + (levels[j] || (levels[j] = [])).push(seg); | |
6414 | + } | |
6415 | + | |
6416 | + return levels; | |
6417 | +} | |
6418 | + | |
6419 | + | |
6420 | +// For every segment, figure out the other segments that are in subsequent | |
6421 | +// levels that also occupy the same vertical space. Accumulate in seg.forwardSegs | |
6422 | +function computeForwardSlotSegs(levels) { | |
6423 | + var i, level; | |
6424 | + var j, seg; | |
6425 | + var k; | |
6426 | + | |
6427 | + for (i=0; i<levels.length; i++) { | |
6428 | + level = levels[i]; | |
6429 | + | |
6430 | + for (j=0; j<level.length; j++) { | |
6431 | + seg = level[j]; | |
6432 | + | |
6433 | + seg.forwardSegs = []; | |
6434 | + for (k=i+1; k<levels.length; k++) { | |
6435 | + computeSlotSegCollisions(seg, levels[k], seg.forwardSegs); | |
6436 | + } | |
6437 | + } | |
6438 | + } | |
6439 | +} | |
6440 | + | |
6441 | + | |
6442 | +// Figure out which path forward (via seg.forwardSegs) results in the longest path until | |
6443 | +// the furthest edge is reached. The number of segments in this path will be seg.forwardPressure | |
6444 | +function computeSlotSegPressures(seg) { | |
6445 | + var forwardSegs = seg.forwardSegs; | |
6446 | + var forwardPressure = 0; | |
6447 | + var i, forwardSeg; | |
6448 | + | |
6449 | + if (seg.forwardPressure === undefined) { // not already computed | |
6450 | + | |
6451 | + for (i=0; i<forwardSegs.length; i++) { | |
6452 | + forwardSeg = forwardSegs[i]; | |
6453 | + | |
6454 | + // figure out the child's maximum forward path | |
6455 | + computeSlotSegPressures(forwardSeg); | |
6456 | + | |
6457 | + // either use the existing maximum, or use the child's forward pressure | |
6458 | + // plus one (for the forwardSeg itself) | |
6459 | + forwardPressure = Math.max( | |
6460 | + forwardPressure, | |
6461 | + 1 + forwardSeg.forwardPressure | |
6462 | + ); | |
6463 | + } | |
6464 | + | |
6465 | + seg.forwardPressure = forwardPressure; | |
6466 | + } | |
6467 | +} | |
6468 | + | |
6469 | + | |
6470 | +// Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range | |
6471 | +// from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and | |
6472 | +// seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left. | |
6473 | +// | |
6474 | +// The segment might be part of a "series", which means consecutive segments with the same pressure | |
6475 | +// who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of | |
6476 | +// segments behind this one in the current series, and `seriesBackwardCoord` is the starting | |
6477 | +// coordinate of the first segment in the series. | |
6478 | +function computeSlotSegCoords(seg, seriesBackwardPressure, seriesBackwardCoord) { | |
6479 | + var forwardSegs = seg.forwardSegs; | |
6480 | + var i; | |
6481 | + | |
6482 | + if (seg.forwardCoord === undefined) { // not already computed | |
6483 | + | |
6484 | + if (!forwardSegs.length) { | |
6485 | + | |
6486 | + // if there are no forward segments, this segment should butt up against the edge | |
6487 | + seg.forwardCoord = 1; | |
6488 | + } | |
6489 | + else { | |
6490 | + | |
6491 | + // sort highest pressure first | |
6492 | + forwardSegs.sort(compareForwardSlotSegs); | |
6493 | + | |
6494 | + // this segment's forwardCoord will be calculated from the backwardCoord of the | |
6495 | + // highest-pressure forward segment. | |
6496 | + computeSlotSegCoords(forwardSegs[0], seriesBackwardPressure + 1, seriesBackwardCoord); | |
6497 | + seg.forwardCoord = forwardSegs[0].backwardCoord; | |
6498 | + } | |
6499 | + | |
6500 | + // calculate the backwardCoord from the forwardCoord. consider the series | |
6501 | + seg.backwardCoord = seg.forwardCoord - | |
6502 | + (seg.forwardCoord - seriesBackwardCoord) / // available width for series | |
6503 | + (seriesBackwardPressure + 1); // # of segments in the series | |
6504 | + | |
6505 | + // use this segment's coordinates to computed the coordinates of the less-pressurized | |
6506 | + // forward segments | |
6507 | + for (i=0; i<forwardSegs.length; i++) { | |
6508 | + computeSlotSegCoords(forwardSegs[i], 0, seg.forwardCoord); | |
6509 | + } | |
6510 | + } | |
6511 | +} | |
6512 | + | |
6513 | + | |
6514 | +// Find all the segments in `otherSegs` that vertically collide with `seg`. | |
6515 | +// Append into an optionally-supplied `results` array and return. | |
6516 | +function computeSlotSegCollisions(seg, otherSegs, results) { | |
6517 | + results = results || []; | |
6518 | + | |
6519 | + for (var i=0; i<otherSegs.length; i++) { | |
6520 | + if (isSlotSegCollision(seg, otherSegs[i])) { | |
6521 | + results.push(otherSegs[i]); | |
6522 | + } | |
6523 | + } | |
6524 | + | |
6525 | + return results; | |
6526 | +} | |
6527 | + | |
6528 | + | |
6529 | +// Do these segments occupy the same vertical space? | |
6530 | +function isSlotSegCollision(seg1, seg2) { | |
6531 | + return seg1.bottom > seg2.top && seg1.top < seg2.bottom; | |
6532 | +} | |
6533 | + | |
6534 | + | |
6535 | +// A cmp function for determining which forward segment to rely on more when computing coordinates. | |
6536 | +function compareForwardSlotSegs(seg1, seg2) { | |
6537 | + // put higher-pressure first | |
6538 | + return seg2.forwardPressure - seg1.forwardPressure || | |
6539 | + // put segments that are closer to initial edge first (and favor ones with no coords yet) | |
6540 | + (seg1.backwardCoord || 0) - (seg2.backwardCoord || 0) || | |
6541 | + // do normal sorting... | |
6542 | + compareSegs(seg1, seg2); | |
6543 | +} | |
6544 | + | |
6545 | +;; | |
6546 | + | |
6547 | +/* An abstract class from which other views inherit from | |
6548 | +----------------------------------------------------------------------------------------------------------------------*/ | |
6549 | + | |
6550 | +var View = fc.View = Class.extend({ | |
6551 | + | |
6552 | + type: null, // subclass' view name (string) | |
6553 | + name: null, // deprecated. use `type` instead | |
6554 | + title: null, // the text that will be displayed in the header's title | |
6555 | + | |
6556 | + calendar: null, // owner Calendar object | |
6557 | + options: null, // hash containing all options. already merged with view-specific-options | |
6558 | + coordMap: null, // a CoordMap object for converting pixel regions to dates | |
6559 | + el: null, // the view's containing element. set by Calendar | |
6560 | + | |
6561 | + isDisplayed: false, | |
6562 | + isSkeletonRendered: false, | |
6563 | + isEventsRendered: false, | |
6564 | + | |
6565 | + // range the view is actually displaying (moments) | |
6566 | + start: null, | |
6567 | + end: null, // exclusive | |
6568 | + | |
6569 | + // range the view is formally responsible for (moments) | |
6570 | + // may be different from start/end. for example, a month view might have 1st-31st, excluding padded dates | |
6571 | + intervalStart: null, | |
6572 | + intervalEnd: null, // exclusive | |
6573 | + intervalDuration: null, | |
6574 | + intervalUnit: null, // name of largest unit being displayed, like "month" or "week" | |
6575 | + | |
6576 | + isSelected: false, // boolean whether a range of time is user-selected or not | |
6577 | + | |
6578 | + // subclasses can optionally use a scroll container | |
6579 | + scrollerEl: null, // the element that will most likely scroll when content is too tall | |
6580 | + scrollTop: null, // cached vertical scroll value | |
6581 | + | |
6582 | + // classNames styled by jqui themes | |
6583 | + widgetHeaderClass: null, | |
6584 | + widgetContentClass: null, | |
6585 | + highlightStateClass: null, | |
6586 | + | |
6587 | + // for date utils, computed from options | |
6588 | + nextDayThreshold: null, | |
6589 | + isHiddenDayHash: null, | |
6590 | + | |
6591 | + // document handlers, bound to `this` object | |
6592 | + documentMousedownProxy: null, // TODO: doesn't work with touch | |
6593 | + | |
6594 | + | |
6595 | + constructor: function(calendar, type, options, intervalDuration) { | |
6596 | + | |
6597 | + this.calendar = calendar; | |
6598 | + this.type = this.name = type; // .name is deprecated | |
6599 | + this.options = options; | |
6600 | + this.intervalDuration = intervalDuration || moment.duration(1, 'day'); | |
6601 | + | |
6602 | + this.nextDayThreshold = moment.duration(this.opt('nextDayThreshold')); | |
6603 | + this.initThemingProps(); | |
6604 | + this.initHiddenDays(); | |
6605 | + | |
6606 | + this.documentMousedownProxy = proxy(this, 'documentMousedown'); | |
6607 | + | |
6608 | + this.initialize(); | |
6609 | + }, | |
6610 | + | |
6611 | + | |
6612 | + // A good place for subclasses to initialize member variables | |
6613 | + initialize: function() { | |
6614 | + // subclasses can implement | |
6615 | + }, | |
6616 | + | |
6617 | + | |
6618 | + // Retrieves an option with the given name | |
6619 | + opt: function(name) { | |
6620 | + return this.options[name]; | |
6621 | + }, | |
6622 | + | |
6623 | + | |
6624 | + // Triggers handlers that are view-related. Modifies args before passing to calendar. | |
6625 | + trigger: function(name, thisObj) { // arguments beyond thisObj are passed along | |
6626 | + var calendar = this.calendar; | |
6627 | + | |
6628 | + return calendar.trigger.apply( | |
6629 | + calendar, | |
6630 | + [name, thisObj || this].concat( | |
6631 | + Array.prototype.slice.call(arguments, 2), // arguments beyond thisObj | |
6632 | + [ this ] // always make the last argument a reference to the view. TODO: deprecate | |
6633 | + ) | |
6634 | + ); | |
6635 | + }, | |
6636 | + | |
6637 | + | |
6638 | + /* Dates | |
6639 | + ------------------------------------------------------------------------------------------------------------------*/ | |
6640 | + | |
6641 | + | |
6642 | + // Updates all internal dates to center around the given current date | |
6643 | + setDate: function(date) { | |
6644 | + this.setRange(this.computeRange(date)); | |
6645 | + }, | |
6646 | + | |
6647 | + | |
6648 | + // Updates all internal dates for displaying the given range. | |
6649 | + // Expects all values to be normalized (like what computeRange does). | |
6650 | + setRange: function(range) { | |
6651 | + $.extend(this, range); | |
6652 | + this.updateTitle(); | |
6653 | + }, | |
6654 | + | |
6655 | + | |
6656 | + // Given a single current date, produce information about what range to display. | |
6657 | + // Subclasses can override. Must return all properties. | |
6658 | + computeRange: function(date) { | |
6659 | + var intervalUnit = computeIntervalUnit(this.intervalDuration); | |
6660 | + var intervalStart = date.clone().startOf(intervalUnit); | |
6661 | + var intervalEnd = intervalStart.clone().add(this.intervalDuration); | |
6662 | + var start, end; | |
6663 | + | |
6664 | + // normalize the range's time-ambiguity | |
6665 | + if (/year|month|week|day/.test(intervalUnit)) { // whole-days? | |
6666 | + intervalStart.stripTime(); | |
6667 | + intervalEnd.stripTime(); | |
6668 | + } | |
6669 | + else { // needs to have a time? | |
6670 | + if (!intervalStart.hasTime()) { | |
6671 | + intervalStart = this.calendar.rezoneDate(intervalStart); // convert to current timezone, with 00:00 | |
6672 | + } | |
6673 | + if (!intervalEnd.hasTime()) { | |
6674 | + intervalEnd = this.calendar.rezoneDate(intervalEnd); // convert to current timezone, with 00:00 | |
6675 | + } | |
6676 | + } | |
6677 | + | |
6678 | + start = intervalStart.clone(); | |
6679 | + start = this.skipHiddenDays(start); | |
6680 | + end = intervalEnd.clone(); | |
6681 | + end = this.skipHiddenDays(end, -1, true); // exclusively move backwards | |
6682 | + | |
6683 | + return { | |
6684 | + intervalUnit: intervalUnit, | |
6685 | + intervalStart: intervalStart, | |
6686 | + intervalEnd: intervalEnd, | |
6687 | + start: start, | |
6688 | + end: end | |
6689 | + }; | |
6690 | + }, | |
6691 | + | |
6692 | + | |
6693 | + // Computes the new date when the user hits the prev button, given the current date | |
6694 | + computePrevDate: function(date) { | |
6695 | + return this.massageCurrentDate( | |
6696 | + date.clone().startOf(this.intervalUnit).subtract(this.intervalDuration), -1 | |
6697 | + ); | |
6698 | + }, | |
6699 | + | |
6700 | + | |
6701 | + // Computes the new date when the user hits the next button, given the current date | |
6702 | + computeNextDate: function(date) { | |
6703 | + return this.massageCurrentDate( | |
6704 | + date.clone().startOf(this.intervalUnit).add(this.intervalDuration) | |
6705 | + ); | |
6706 | + }, | |
6707 | + | |
6708 | + | |
6709 | + // Given an arbitrarily calculated current date of the calendar, returns a date that is ensured to be completely | |
6710 | + // visible. `direction` is optional and indicates which direction the current date was being | |
6711 | + // incremented or decremented (1 or -1). | |
6712 | + massageCurrentDate: function(date, direction) { | |
6713 | + if (this.intervalDuration.as('days') <= 1) { // if the view displays a single day or smaller | |
6714 | + if (this.isHiddenDay(date)) { | |
6715 | + date = this.skipHiddenDays(date, direction); | |
6716 | + date.startOf('day'); | |
6717 | + } | |
6718 | + } | |
6719 | + | |
6720 | + return date; | |
6721 | + }, | |
6722 | + | |
6723 | + | |
6724 | + /* Title and Date Formatting | |
6725 | + ------------------------------------------------------------------------------------------------------------------*/ | |
6726 | + | |
6727 | + | |
6728 | + // Sets the view's title property to the most updated computed value | |
6729 | + updateTitle: function() { | |
6730 | + this.title = this.computeTitle(); | |
6731 | + }, | |
6732 | + | |
6733 | + | |
6734 | + // Computes what the title at the top of the calendar should be for this view | |
6735 | + computeTitle: function() { | |
6736 | + return this.formatRange( | |
6737 | + { start: this.intervalStart, end: this.intervalEnd }, | |
6738 | + this.opt('titleFormat') || this.computeTitleFormat(), | |
6739 | + this.opt('titleRangeSeparator') | |
6740 | + ); | |
6741 | + }, | |
6742 | + | |
6743 | + | |
6744 | + // Generates the format string that should be used to generate the title for the current date range. | |
6745 | + // Attempts to compute the most appropriate format if not explicitly specified with `titleFormat`. | |
6746 | + computeTitleFormat: function() { | |
6747 | + if (this.intervalUnit == 'year') { | |
6748 | + return 'YYYY'; | |
6749 | + } | |
6750 | + else if (this.intervalUnit == 'month') { | |
6751 | + return this.opt('monthYearFormat'); // like "September 2014" | |
6752 | + } | |
6753 | + else if (this.intervalDuration.as('days') > 1) { | |
6754 | + return 'll'; // multi-day range. shorter, like "Sep 9 - 10 2014" | |
6755 | + } | |
6756 | + else { | |
6757 | + return 'LL'; // one day. longer, like "September 9 2014" | |
6758 | + } | |
6759 | + }, | |
6760 | + | |
6761 | + | |
6762 | + // Utility for formatting a range. Accepts a range object, formatting string, and optional separator. | |
6763 | + // Displays all-day ranges naturally, with an inclusive end. Takes the current isRTL into account. | |
6764 | + formatRange: function(range, formatStr, separator) { | |
6765 | + var end = range.end; | |
6766 | + | |
6767 | + if (!end.hasTime()) { // all-day? | |
6768 | + end = end.clone().subtract(1); // convert to inclusive. last ms of previous day | |
6769 | + } | |
6770 | + | |
6771 | + return formatRange(range.start, end, formatStr, separator, this.opt('isRTL')); | |
6772 | + }, | |
6773 | + | |
6774 | + | |
6775 | + /* Rendering | |
6776 | + ------------------------------------------------------------------------------------------------------------------*/ | |
6777 | + | |
6778 | + | |
6779 | + // Sets the container element that the view should render inside of. | |
6780 | + // Does other DOM-related initializations. | |
6781 | + setElement: function(el) { | |
6782 | + this.el = el; | |
6783 | + this.bindGlobalHandlers(); | |
6784 | + }, | |
6785 | + | |
6786 | + | |
6787 | + // Removes the view's container element from the DOM, clearing any content beforehand. | |
6788 | + // Undoes any other DOM-related attachments. | |
6789 | + removeElement: function() { | |
6790 | + this.clear(); // clears all content | |
6791 | + | |
6792 | + // clean up the skeleton | |
6793 | + if (this.isSkeletonRendered) { | |
6794 | + this.destroySkeleton(); | |
6795 | + this.isSkeletonRendered = false; | |
6796 | + } | |
6797 | + | |
6798 | + this.unbindGlobalHandlers(); | |
6799 | + | |
6800 | + this.el.remove(); | |
6801 | + | |
6802 | + // NOTE: don't null-out this.el in case the View was destroyed within an API callback. | |
6803 | + // We don't null-out the View's other jQuery element references upon destroy, so why should we kill this.el? | |
6804 | + }, | |
6805 | + | |
6806 | + | |
6807 | + // Does everything necessary to display the view centered around the given date. | |
6808 | + // Does every type of rendering EXCEPT rendering events. | |
6809 | + display: function(date) { | |
6810 | + var scrollState = null; | |
6811 | + | |
6812 | + if (this.isDisplayed) { | |
6813 | + scrollState = this.queryScroll(); | |
6814 | + } | |
6815 | + | |
6816 | + this.clear(); // clear the old content | |
6817 | + this.setDate(date); | |
6818 | + this.render(); | |
6819 | + this.updateSize(); | |
6820 | + this.renderBusinessHours(); // might need coordinates, so should go after updateSize() | |
6821 | + this.isDisplayed = true; | |
6822 | + | |
6823 | + scrollState = this.computeInitialScroll(scrollState); | |
6824 | + this.forceScroll(scrollState); | |
6825 | + | |
6826 | + this.triggerRender(); | |
6827 | + }, | |
6828 | + | |
6829 | + | |
6830 | + // Does everything necessary to clear the content of the view. | |
6831 | + // Clears dates and events. Does not clear the skeleton. | |
6832 | + clear: function() { // clears the view of *content* but not the skeleton | |
6833 | + if (this.isDisplayed) { | |
6834 | + this.unselect(); | |
6835 | + this.clearEvents(); | |
6836 | + this.triggerDestroy(); | |
6837 | + this.destroyBusinessHours(); | |
6838 | + this.destroy(); | |
6839 | + this.isDisplayed = false; | |
6840 | + } | |
6841 | + }, | |
6842 | + | |
6843 | + | |
6844 | + // Renders the view's date-related content, rendering the view's non-content skeleton if necessary | |
6845 | + render: function() { | |
6846 | + if (!this.isSkeletonRendered) { | |
6847 | + this.renderSkeleton(); | |
6848 | + this.isSkeletonRendered = true; | |
6849 | + } | |
6850 | + this.renderDates(); | |
6851 | + }, | |
6852 | + | |
6853 | + | |
6854 | + // Unrenders the view's date-related content. | |
6855 | + // Call this instead of destroyDates directly in case the View subclass wants to use a render/destroy pattern | |
6856 | + // where both the skeleton and the content always get rendered/unrendered together. | |
6857 | + destroy: function() { | |
6858 | + this.destroyDates(); | |
6859 | + }, | |
6860 | + | |
6861 | + | |
6862 | + // Renders the basic structure of the view before any content is rendered | |
6863 | + renderSkeleton: function() { | |
6864 | + // subclasses should implement | |
6865 | + }, | |
6866 | + | |
6867 | + | |
6868 | + // Unrenders the basic structure of the view | |
6869 | + destroySkeleton: function() { | |
6870 | + // subclasses should implement | |
6871 | + }, | |
6872 | + | |
6873 | + | |
6874 | + // Renders the view's date-related content (like cells that represent days/times). | |
6875 | + // Assumes setRange has already been called and the skeleton has already been rendered. | |
6876 | + renderDates: function() { | |
6877 | + // subclasses should implement | |
6878 | + }, | |
6879 | + | |
6880 | + | |
6881 | + // Unrenders the view's date-related content | |
6882 | + destroyDates: function() { | |
6883 | + // subclasses should override | |
6884 | + }, | |
6885 | + | |
6886 | + | |
6887 | + // Renders business-hours onto the view. Assumes updateSize has already been called. | |
6888 | + renderBusinessHours: function() { | |
6889 | + // subclasses should implement | |
6890 | + }, | |
6891 | + | |
6892 | + | |
6893 | + // Unrenders previously-rendered business-hours | |
6894 | + destroyBusinessHours: function() { | |
6895 | + // subclasses should implement | |
6896 | + }, | |
6897 | + | |
6898 | + | |
6899 | + // Signals that the view's content has been rendered | |
6900 | + triggerRender: function() { | |
6901 | + this.trigger('viewRender', this, this, this.el); | |
6902 | + }, | |
6903 | + | |
6904 | + | |
6905 | + // Signals that the view's content is about to be unrendered | |
6906 | + triggerDestroy: function() { | |
6907 | + this.trigger('viewDestroy', this, this, this.el); | |
6908 | + }, | |
6909 | + | |
6910 | + | |
6911 | + // Binds DOM handlers to elements that reside outside the view container, such as the document | |
6912 | + bindGlobalHandlers: function() { | |
6913 | + $(document).on('mousedown', this.documentMousedownProxy); | |
6914 | + }, | |
6915 | + | |
6916 | + | |
6917 | + // Unbinds DOM handlers from elements that reside outside the view container | |
6918 | + unbindGlobalHandlers: function() { | |
6919 | + $(document).off('mousedown', this.documentMousedownProxy); | |
6920 | + }, | |
6921 | + | |
6922 | + | |
6923 | + // Initializes internal variables related to theming | |
6924 | + initThemingProps: function() { | |
6925 | + var tm = this.opt('theme') ? 'ui' : 'fc'; | |
6926 | + | |
6927 | + this.widgetHeaderClass = tm + '-widget-header'; | |
6928 | + this.widgetContentClass = tm + '-widget-content'; | |
6929 | + this.highlightStateClass = tm + '-state-highlight'; | |
6930 | + }, | |
6931 | + | |
6932 | + | |
6933 | + /* Dimensions | |
6934 | + ------------------------------------------------------------------------------------------------------------------*/ | |
6935 | + | |
6936 | + | |
6937 | + // Refreshes anything dependant upon sizing of the container element of the grid | |
6938 | + updateSize: function(isResize) { | |
6939 | + var scrollState; | |
6940 | + | |
6941 | + if (isResize) { | |
6942 | + scrollState = this.queryScroll(); | |
6943 | + } | |
6944 | + | |
6945 | + this.updateHeight(); | |
6946 | + this.updateWidth(); | |
6947 | + | |
6948 | + if (isResize) { | |
6949 | + this.setScroll(scrollState); | |
6950 | + } | |
6951 | + }, | |
6952 | + | |
6953 | + | |
6954 | + // Refreshes the horizontal dimensions of the calendar | |
6955 | + updateWidth: function() { | |
6956 | + // subclasses should implement | |
6957 | + }, | |
6958 | + | |
6959 | + | |
6960 | + // Refreshes the vertical dimensions of the calendar | |
6961 | + updateHeight: function() { | |
6962 | + var calendar = this.calendar; // we poll the calendar for height information | |
6963 | + | |
6964 | + this.setHeight( | |
6965 | + calendar.getSuggestedViewHeight(), | |
6966 | + calendar.isHeightAuto() | |
6967 | + ); | |
6968 | + }, | |
6969 | + | |
6970 | + | |
6971 | + // Updates the vertical dimensions of the calendar to the specified height. | |
6972 | + // if `isAuto` is set to true, height becomes merely a suggestion and the view should use its "natural" height. | |
6973 | + setHeight: function(height, isAuto) { | |
6974 | + // subclasses should implement | |
6975 | + }, | |
6976 | + | |
6977 | + | |
6978 | + /* Scroller | |
6979 | + ------------------------------------------------------------------------------------------------------------------*/ | |
6980 | + | |
6981 | + | |
6982 | + // Given the total height of the view, return the number of pixels that should be used for the scroller. | |
6983 | + // Utility for subclasses. | |
6984 | + computeScrollerHeight: function(totalHeight) { | |
6985 | + var scrollerEl = this.scrollerEl; | |
6986 | + var both; | |
6987 | + var otherHeight; // cumulative height of everything that is not the scrollerEl in the view (header+borders) | |
6988 | + | |
6989 | + both = this.el.add(scrollerEl); | |
6990 | + | |
6991 | + // fuckin IE8/9/10/11 sometimes returns 0 for dimensions. this weird hack was the only thing that worked | |
6992 | + both.css({ | |
6993 | + position: 'relative', // cause a reflow, which will force fresh dimension recalculation | |
6994 | + left: -1 // ensure reflow in case the el was already relative. negative is less likely to cause new scroll | |
6995 | + }); | |
6996 | + otherHeight = this.el.outerHeight() - scrollerEl.height(); // grab the dimensions | |
6997 | + both.css({ position: '', left: '' }); // undo hack | |
6998 | + | |
6999 | + return totalHeight - otherHeight; | |
7000 | + }, | |
7001 | + | |
7002 | + | |
7003 | + // Computes the initial pre-configured scroll state prior to allowing the user to change it. | |
7004 | + // Given the scroll state from the previous rendering. If first time rendering, given null. | |
7005 | + computeInitialScroll: function(previousScrollState) { | |
7006 | + return 0; | |
7007 | + }, | |
7008 | + | |
7009 | + | |
7010 | + // Retrieves the view's current natural scroll state. Can return an arbitrary format. | |
7011 | + queryScroll: function() { | |
7012 | + if (this.scrollerEl) { | |
7013 | + return this.scrollerEl.scrollTop(); // operates on scrollerEl by default | |
7014 | + } | |
7015 | + }, | |
7016 | + | |
7017 | + | |
7018 | + // Sets the view's scroll state. Will accept the same format computeInitialScroll and queryScroll produce. | |
7019 | + setScroll: function(scrollState) { | |
7020 | + if (this.scrollerEl) { | |
7021 | + return this.scrollerEl.scrollTop(scrollState); // operates on scrollerEl by default | |
7022 | + } | |
7023 | + }, | |
7024 | + | |
7025 | + | |
7026 | + // Sets the scroll state, making sure to overcome any predefined scroll value the browser has in mind | |
7027 | + forceScroll: function(scrollState) { | |
7028 | + var _this = this; | |
7029 | + | |
7030 | + this.setScroll(scrollState); | |
7031 | + setTimeout(function() { | |
7032 | + _this.setScroll(scrollState); | |
7033 | + }, 0); | |
7034 | + }, | |
7035 | + | |
7036 | + | |
7037 | + /* Event Elements / Segments | |
7038 | + ------------------------------------------------------------------------------------------------------------------*/ | |
7039 | + | |
7040 | + | |
7041 | + // Does everything necessary to display the given events onto the current view | |
7042 | + displayEvents: function(events) { | |
7043 | + var scrollState = this.queryScroll(); | |
7044 | + | |
7045 | + this.clearEvents(); | |
7046 | + this.renderEvents(events); | |
7047 | + this.isEventsRendered = true; | |
7048 | + this.setScroll(scrollState); | |
7049 | + this.triggerEventRender(); | |
7050 | + }, | |
7051 | + | |
7052 | + | |
7053 | + // Does everything necessary to clear the view's currently-rendered events | |
7054 | + clearEvents: function() { | |
7055 | + if (this.isEventsRendered) { | |
7056 | + this.triggerEventDestroy(); | |
7057 | + this.destroyEvents(); | |
7058 | + this.isEventsRendered = false; | |
7059 | + } | |
7060 | + }, | |
7061 | + | |
7062 | + | |
7063 | + // Renders the events onto the view. | |
7064 | + renderEvents: function(events) { | |
7065 | + // subclasses should implement | |
7066 | + }, | |
7067 | + | |
7068 | + | |
7069 | + // Removes event elements from the view. | |
7070 | + destroyEvents: function() { | |
7071 | + // subclasses should implement | |
7072 | + }, | |
7073 | + | |
7074 | + | |
7075 | + // Signals that all events have been rendered | |
7076 | + triggerEventRender: function() { | |
7077 | + this.renderedEventSegEach(function(seg) { | |
7078 | + this.trigger('eventAfterRender', seg.event, seg.event, seg.el); | |
7079 | + }); | |
7080 | + this.trigger('eventAfterAllRender'); | |
7081 | + }, | |
7082 | + | |
7083 | + | |
7084 | + // Signals that all event elements are about to be removed | |
7085 | + triggerEventDestroy: function() { | |
7086 | + this.renderedEventSegEach(function(seg) { | |
7087 | + this.trigger('eventDestroy', seg.event, seg.event, seg.el); | |
7088 | + }); | |
7089 | + }, | |
7090 | + | |
7091 | + | |
7092 | + // Given an event and the default element used for rendering, returns the element that should actually be used. | |
7093 | + // Basically runs events and elements through the eventRender hook. | |
7094 | + resolveEventEl: function(event, el) { | |
7095 | + var custom = this.trigger('eventRender', event, event, el); | |
7096 | + | |
7097 | + if (custom === false) { // means don't render at all | |
7098 | + el = null; | |
7099 | + } | |
7100 | + else if (custom && custom !== true) { | |
7101 | + el = $(custom); | |
7102 | + } | |
7103 | + | |
7104 | + return el; | |
7105 | + }, | |
7106 | + | |
7107 | + | |
7108 | + // Hides all rendered event segments linked to the given event | |
7109 | + showEvent: function(event) { | |
7110 | + this.renderedEventSegEach(function(seg) { | |
7111 | + seg.el.css('visibility', ''); | |
7112 | + }, event); | |
7113 | + }, | |
7114 | + | |
7115 | + | |
7116 | + // Shows all rendered event segments linked to the given event | |
7117 | + hideEvent: function(event) { | |
7118 | + this.renderedEventSegEach(function(seg) { | |
7119 | + seg.el.css('visibility', 'hidden'); | |
7120 | + }, event); | |
7121 | + }, | |
7122 | + | |
7123 | + | |
7124 | + // Iterates through event segments that have been rendered (have an el). Goes through all by default. | |
7125 | + // If the optional `event` argument is specified, only iterates through segments linked to that event. | |
7126 | + // The `this` value of the callback function will be the view. | |
7127 | + renderedEventSegEach: function(func, event) { | |
7128 | + var segs = this.getEventSegs(); | |
7129 | + var i; | |
7130 | + | |
7131 | + for (i = 0; i < segs.length; i++) { | |
7132 | + if (!event || segs[i].event._id === event._id) { | |
7133 | + if (segs[i].el) { | |
7134 | + func.call(this, segs[i]); | |
7135 | + } | |
7136 | + } | |
7137 | + } | |
7138 | + }, | |
7139 | + | |
7140 | + | |
7141 | + // Retrieves all the rendered segment objects for the view | |
7142 | + getEventSegs: function() { | |
7143 | + // subclasses must implement | |
7144 | + return []; | |
7145 | + }, | |
7146 | + | |
7147 | + | |
7148 | + /* Event Drag-n-Drop | |
7149 | + ------------------------------------------------------------------------------------------------------------------*/ | |
7150 | + | |
7151 | + | |
7152 | + // Computes if the given event is allowed to be dragged by the user | |
7153 | + isEventDraggable: function(event) { | |
7154 | + var source = event.source || {}; | |
7155 | + | |
7156 | + return firstDefined( | |
7157 | + event.startEditable, | |
7158 | + source.startEditable, | |
7159 | + this.opt('eventStartEditable'), | |
7160 | + event.editable, | |
7161 | + source.editable, | |
7162 | + this.opt('editable') | |
7163 | + ); | |
7164 | + }, | |
7165 | + | |
7166 | + | |
7167 | + // Must be called when an event in the view is dropped onto new location. | |
7168 | + // `dropLocation` is an object that contains the new start/end/allDay values for the event. | |
7169 | + reportEventDrop: function(event, dropLocation, largeUnit, el, ev) { | |
7170 | + var calendar = this.calendar; | |
7171 | + var mutateResult = calendar.mutateEvent(event, dropLocation, largeUnit); | |
7172 | + var undoFunc = function() { | |
7173 | + mutateResult.undo(); | |
7174 | + calendar.reportEventChange(); | |
7175 | + }; | |
7176 | + | |
7177 | + this.triggerEventDrop(event, mutateResult.dateDelta, undoFunc, el, ev); | |
7178 | + calendar.reportEventChange(); // will rerender events | |
7179 | + }, | |
7180 | + | |
7181 | + | |
7182 | + // Triggers event-drop handlers that have subscribed via the API | |
7183 | + triggerEventDrop: function(event, dateDelta, undoFunc, el, ev) { | |
7184 | + this.trigger('eventDrop', el[0], event, dateDelta, undoFunc, ev, {}); // {} = jqui dummy | |
7185 | + }, | |
7186 | + | |
7187 | + | |
7188 | + /* External Element Drag-n-Drop | |
7189 | + ------------------------------------------------------------------------------------------------------------------*/ | |
7190 | + | |
7191 | + | |
7192 | + // Must be called when an external element, via jQuery UI, has been dropped onto the calendar. | |
7193 | + // `meta` is the parsed data that has been embedded into the dragging event. | |
7194 | + // `dropLocation` is an object that contains the new start/end/allDay values for the event. | |
7195 | + reportExternalDrop: function(meta, dropLocation, el, ev, ui) { | |
7196 | + var eventProps = meta.eventProps; | |
7197 | + var eventInput; | |
7198 | + var event; | |
7199 | + | |
7200 | + // Try to build an event object and render it. TODO: decouple the two | |
7201 | + if (eventProps) { | |
7202 | + eventInput = $.extend({}, eventProps, dropLocation); | |
7203 | + event = this.calendar.renderEvent(eventInput, meta.stick)[0]; // renderEvent returns an array | |
7204 | + } | |
7205 | + | |
7206 | + this.triggerExternalDrop(event, dropLocation, el, ev, ui); | |
7207 | + }, | |
7208 | + | |
7209 | + | |
7210 | + // Triggers external-drop handlers that have subscribed via the API | |
7211 | + triggerExternalDrop: function(event, dropLocation, el, ev, ui) { | |
7212 | + | |
7213 | + // trigger 'drop' regardless of whether element represents an event | |
7214 | + this.trigger('drop', el[0], dropLocation.start, ev, ui); | |
7215 | + | |
7216 | + if (event) { | |
7217 | + this.trigger('eventReceive', null, event); // signal an external event landed | |
7218 | + } | |
7219 | + }, | |
7220 | + | |
7221 | + | |
7222 | + /* Drag-n-Drop Rendering (for both events and external elements) | |
7223 | + ------------------------------------------------------------------------------------------------------------------*/ | |
7224 | + | |
7225 | + | |
7226 | + // Renders a visual indication of a event or external-element drag over the given drop zone. | |
7227 | + // If an external-element, seg will be `null` | |
7228 | + renderDrag: function(dropLocation, seg) { | |
7229 | + // subclasses must implement | |
7230 | + }, | |
7231 | + | |
7232 | + | |
7233 | + // Unrenders a visual indication of an event or external-element being dragged. | |
7234 | + destroyDrag: function() { | |
7235 | + // subclasses must implement | |
7236 | + }, | |
7237 | + | |
7238 | + | |
7239 | + /* Event Resizing | |
7240 | + ------------------------------------------------------------------------------------------------------------------*/ | |
7241 | + | |
7242 | + | |
7243 | + // Computes if the given event is allowed to be resized from its starting edge | |
7244 | + isEventResizableFromStart: function(event) { | |
7245 | + return this.opt('eventResizableFromStart') && this.isEventResizable(event); | |
7246 | + }, | |
7247 | + | |
7248 | + | |
7249 | + // Computes if the given event is allowed to be resized from its ending edge | |
7250 | + isEventResizableFromEnd: function(event) { | |
7251 | + return this.isEventResizable(event); | |
7252 | + }, | |
7253 | + | |
7254 | + | |
7255 | + // Computes if the given event is allowed to be resized by the user at all | |
7256 | + isEventResizable: function(event) { | |
7257 | + var source = event.source || {}; | |
7258 | + | |
7259 | + return firstDefined( | |
7260 | + event.durationEditable, | |
7261 | + source.durationEditable, | |
7262 | + this.opt('eventDurationEditable'), | |
7263 | + event.editable, | |
7264 | + source.editable, | |
7265 | + this.opt('editable') | |
7266 | + ); | |
7267 | + }, | |
7268 | + | |
7269 | + | |
7270 | + // Must be called when an event in the view has been resized to a new length | |
7271 | + reportEventResize: function(event, resizeLocation, largeUnit, el, ev) { | |
7272 | + var calendar = this.calendar; | |
7273 | + var mutateResult = calendar.mutateEvent(event, resizeLocation, largeUnit); | |
7274 | + var undoFunc = function() { | |
7275 | + mutateResult.undo(); | |
7276 | + calendar.reportEventChange(); | |
7277 | + }; | |
7278 | + | |
7279 | + this.triggerEventResize(event, mutateResult.durationDelta, undoFunc, el, ev); | |
7280 | + calendar.reportEventChange(); // will rerender events | |
7281 | + }, | |
7282 | + | |
7283 | + | |
7284 | + // Triggers event-resize handlers that have subscribed via the API | |
7285 | + triggerEventResize: function(event, durationDelta, undoFunc, el, ev) { | |
7286 | + this.trigger('eventResize', el[0], event, durationDelta, undoFunc, ev, {}); // {} = jqui dummy | |
7287 | + }, | |
7288 | + | |
7289 | + | |
7290 | + /* Selection | |
7291 | + ------------------------------------------------------------------------------------------------------------------*/ | |
7292 | + | |
7293 | + | |
7294 | + // Selects a date range on the view. `start` and `end` are both Moments. | |
7295 | + // `ev` is the native mouse event that begin the interaction. | |
7296 | + select: function(range, ev) { | |
7297 | + this.unselect(ev); | |
7298 | + this.renderSelection(range); | |
7299 | + this.reportSelection(range, ev); | |
7300 | + }, | |
7301 | + | |
7302 | + | |
7303 | + // Renders a visual indication of the selection | |
7304 | + renderSelection: function(range) { | |
7305 | + // subclasses should implement | |
7306 | + }, | |
7307 | + | |
7308 | + | |
7309 | + // Called when a new selection is made. Updates internal state and triggers handlers. | |
7310 | + reportSelection: function(range, ev) { | |
7311 | + this.isSelected = true; | |
7312 | + this.trigger('select', null, range.start, range.end, ev); | |
7313 | + }, | |
7314 | + | |
7315 | + | |
7316 | + // Undoes a selection. updates in the internal state and triggers handlers. | |
7317 | + // `ev` is the native mouse event that began the interaction. | |
7318 | + unselect: function(ev) { | |
7319 | + if (this.isSelected) { | |
7320 | + this.isSelected = false; | |
7321 | + this.destroySelection(); | |
7322 | + this.trigger('unselect', null, ev); | |
7323 | + } | |
7324 | + }, | |
7325 | + | |
7326 | + | |
7327 | + // Unrenders a visual indication of selection | |
7328 | + destroySelection: function() { | |
7329 | + // subclasses should implement | |
7330 | + }, | |
7331 | + | |
7332 | + | |
7333 | + // Handler for unselecting when the user clicks something and the 'unselectAuto' setting is on | |
7334 | + documentMousedown: function(ev) { | |
7335 | + var ignore; | |
7336 | + | |
7337 | + // is there a selection, and has the user made a proper left click? | |
7338 | + if (this.isSelected && this.opt('unselectAuto') && isPrimaryMouseButton(ev)) { | |
7339 | + | |
7340 | + // only unselect if the clicked element is not identical to or inside of an 'unselectCancel' element | |
7341 | + ignore = this.opt('unselectCancel'); | |
7342 | + if (!ignore || !$(ev.target).closest(ignore).length) { | |
7343 | + this.unselect(ev); | |
7344 | + } | |
7345 | + } | |
7346 | + }, | |
7347 | + | |
7348 | + | |
7349 | + /* Date Utils | |
7350 | + ------------------------------------------------------------------------------------------------------------------*/ | |
7351 | + | |
7352 | + | |
7353 | + // Initializes internal variables related to calculating hidden days-of-week | |
7354 | + initHiddenDays: function() { | |
7355 | + var hiddenDays = this.opt('hiddenDays') || []; // array of day-of-week indices that are hidden | |
7356 | + var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool) | |
7357 | + var dayCnt = 0; | |
7358 | + var i; | |
7359 | + | |
7360 | + if (this.opt('weekends') === false) { | |
7361 | + hiddenDays.push(0, 6); // 0=sunday, 6=saturday | |
7362 | + } | |
7363 | + | |
7364 | + for (i = 0; i < 7; i++) { | |
7365 | + if ( | |
7366 | + !(isHiddenDayHash[i] = $.inArray(i, hiddenDays) !== -1) | |
7367 | + ) { | |
7368 | + dayCnt++; | |
7369 | + } | |
7370 | + } | |
7371 | + | |
7372 | + if (!dayCnt) { | |
7373 | + throw 'invalid hiddenDays'; // all days were hidden? bad. | |
7374 | + } | |
7375 | + | |
7376 | + this.isHiddenDayHash = isHiddenDayHash; | |
7377 | + }, | |
7378 | + | |
7379 | + | |
7380 | + // Is the current day hidden? | |
7381 | + // `day` is a day-of-week index (0-6), or a Moment | |
7382 | + isHiddenDay: function(day) { | |
7383 | + if (moment.isMoment(day)) { | |
7384 | + day = day.day(); | |
7385 | + } | |
7386 | + return this.isHiddenDayHash[day]; | |
7387 | + }, | |
7388 | + | |
7389 | + | |
7390 | + // Incrementing the current day until it is no longer a hidden day, returning a copy. | |
7391 | + // If the initial value of `date` is not a hidden day, don't do anything. | |
7392 | + // Pass `isExclusive` as `true` if you are dealing with an end date. | |
7393 | + // `inc` defaults to `1` (increment one day forward each time) | |
7394 | + skipHiddenDays: function(date, inc, isExclusive) { | |
7395 | + var out = date.clone(); | |
7396 | + inc = inc || 1; | |
7397 | + while ( | |
7398 | + this.isHiddenDayHash[(out.day() + (isExclusive ? inc : 0) + 7) % 7] | |
7399 | + ) { | |
7400 | + out.add(inc, 'days'); | |
7401 | + } | |
7402 | + return out; | |
7403 | + }, | |
7404 | + | |
7405 | + | |
7406 | + // Returns the date range of the full days the given range visually appears to occupy. | |
7407 | + // Returns a new range object. | |
7408 | + computeDayRange: function(range) { | |
7409 | + var startDay = range.start.clone().stripTime(); // the beginning of the day the range starts | |
7410 | + var end = range.end; | |
7411 | + var endDay = null; | |
7412 | + var endTimeMS; | |
7413 | + | |
7414 | + if (end) { | |
7415 | + endDay = end.clone().stripTime(); // the beginning of the day the range exclusively ends | |
7416 | + endTimeMS = +end.time(); // # of milliseconds into `endDay` | |
7417 | + | |
7418 | + // If the end time is actually inclusively part of the next day and is equal to or | |
7419 | + // beyond the next day threshold, adjust the end to be the exclusive end of `endDay`. | |
7420 | + // Otherwise, leaving it as inclusive will cause it to exclude `endDay`. | |
7421 | + if (endTimeMS && endTimeMS >= this.nextDayThreshold) { | |
7422 | + endDay.add(1, 'days'); | |
7423 | + } | |
7424 | + } | |
7425 | + | |
7426 | + // If no end was specified, or if it is within `startDay` but not past nextDayThreshold, | |
7427 | + // assign the default duration of one day. | |
7428 | + if (!end || endDay <= startDay) { | |
7429 | + endDay = startDay.clone().add(1, 'days'); | |
7430 | + } | |
7431 | + | |
7432 | + return { start: startDay, end: endDay }; | |
7433 | + }, | |
7434 | + | |
7435 | + | |
7436 | + // Does the given event visually appear to occupy more than one day? | |
7437 | + isMultiDayEvent: function(event) { | |
7438 | + var range = this.computeDayRange(event); // event is range-ish | |
7439 | + | |
7440 | + return range.end.diff(range.start, 'days') > 1; | |
7441 | + } | |
7442 | + | |
7443 | +}); | |
7444 | + | |
7445 | +;; | |
7446 | + | |
7447 | +var Calendar = fc.Calendar = fc.CalendarBase = Class.extend({ | |
7448 | + | |
7449 | + dirDefaults: null, // option defaults related to LTR or RTL | |
7450 | + langDefaults: null, // option defaults related to current locale | |
7451 | + overrides: null, // option overrides given to the fullCalendar constructor | |
7452 | + options: null, // all defaults combined with overrides | |
7453 | + viewSpecCache: null, // cache of view definitions | |
7454 | + view: null, // current View object | |
7455 | + header: null, | |
7456 | + | |
7457 | + | |
7458 | + // a lot of this class' OOP logic is scoped within this constructor function, | |
7459 | + // but in the future, write individual methods on the prototype. | |
7460 | + constructor: Calendar_constructor, | |
7461 | + | |
7462 | + | |
7463 | + // Initializes `this.options` and other important options-related objects | |
7464 | + initOptions: function(overrides) { | |
7465 | + var lang, langDefaults; | |
7466 | + var isRTL, dirDefaults; | |
7467 | + | |
7468 | + // converts legacy options into non-legacy ones. | |
7469 | + // in the future, when this is removed, don't use `overrides` reference. make a copy. | |
7470 | + overrides = massageOverrides(overrides); | |
7471 | + | |
7472 | + lang = overrides.lang; | |
7473 | + langDefaults = langOptionHash[lang]; | |
7474 | + if (!langDefaults) { | |
7475 | + lang = Calendar.defaults.lang; | |
7476 | + langDefaults = langOptionHash[lang] || {}; | |
7477 | + } | |
7478 | + | |
7479 | + isRTL = firstDefined( | |
7480 | + overrides.isRTL, | |
7481 | + langDefaults.isRTL, | |
7482 | + Calendar.defaults.isRTL | |
7483 | + ); | |
7484 | + dirDefaults = isRTL ? Calendar.rtlDefaults : {}; | |
7485 | + | |
7486 | + this.dirDefaults = dirDefaults; | |
7487 | + this.langDefaults = langDefaults; | |
7488 | + this.overrides = overrides; | |
7489 | + this.options = mergeOptions( // merge defaults and overrides. lowest to highest precedence | |
7490 | + Calendar.defaults, // global defaults | |
7491 | + dirDefaults, | |
7492 | + langDefaults, | |
7493 | + overrides | |
7494 | + ); | |
7495 | + populateInstanceComputableOptions(this.options); | |
7496 | + | |
7497 | + this.viewSpecCache = {}; // somewhat unrelated | |
7498 | + }, | |
7499 | + | |
7500 | + | |
7501 | + // Gets information about how to create a view. Will use a cache. | |
7502 | + getViewSpec: function(viewType) { | |
7503 | + var cache = this.viewSpecCache; | |
7504 | + | |
7505 | + return cache[viewType] || (cache[viewType] = this.buildViewSpec(viewType)); | |
7506 | + }, | |
7507 | + | |
7508 | + | |
7509 | + // Given a duration singular unit, like "week" or "day", finds a matching view spec. | |
7510 | + // Preference is given to views that have corresponding buttons. | |
7511 | + getUnitViewSpec: function(unit) { | |
7512 | + var viewTypes; | |
7513 | + var i; | |
7514 | + var spec; | |
7515 | + | |
7516 | + if ($.inArray(unit, intervalUnits) != -1) { | |
7517 | + | |
7518 | + // put views that have buttons first. there will be duplicates, but oh well | |
7519 | + viewTypes = this.header.getViewsWithButtons(); | |
7520 | + $.each(fc.views, function(viewType) { // all views | |
7521 | + viewTypes.push(viewType); | |
7522 | + }); | |
7523 | + | |
7524 | + for (i = 0; i < viewTypes.length; i++) { | |
7525 | + spec = this.getViewSpec(viewTypes[i]); | |
7526 | + if (spec) { | |
7527 | + if (spec.singleUnit == unit) { | |
7528 | + return spec; | |
7529 | + } | |
7530 | + } | |
7531 | + } | |
7532 | + } | |
7533 | + }, | |
7534 | + | |
7535 | + | |
7536 | + // Builds an object with information on how to create a given view | |
7537 | + buildViewSpec: function(requestedViewType) { | |
7538 | + var viewOverrides = this.overrides.views || {}; | |
7539 | + var defaultsChain = []; // for the view. lowest to highest priority | |
7540 | + var overridesChain = []; // for the view. lowest to highest priority | |
7541 | + var viewType = requestedViewType; | |
7542 | + var viewClass; | |
7543 | + var defaults; // for the view | |
7544 | + var overrides; // for the view | |
7545 | + var duration; | |
7546 | + var unit; | |
7547 | + var spec; | |
7548 | + | |
7549 | + // iterate from the specific view definition to a more general one until we hit an actual View class | |
7550 | + while (viewType && !viewClass) { | |
7551 | + defaults = fcViews[viewType] || {}; | |
7552 | + overrides = viewOverrides[viewType] || {}; | |
7553 | + duration = duration || overrides.duration || defaults.duration; | |
7554 | + viewType = overrides.type || defaults.type; // for next iteration | |
7555 | + | |
7556 | + if (typeof defaults === 'function') { // a class | |
7557 | + viewClass = defaults; | |
7558 | + defaultsChain.unshift(viewClass.defaults || {}); | |
7559 | + } | |
7560 | + else { // an options object | |
7561 | + defaultsChain.unshift(defaults); | |
7562 | + } | |
7563 | + overridesChain.unshift(overrides); | |
7564 | + } | |
7565 | + | |
7566 | + if (viewClass) { | |
7567 | + spec = { 'class': viewClass, type: requestedViewType }; | |
7568 | + | |
7569 | + if (duration) { | |
7570 | + duration = moment.duration(duration); | |
7571 | + if (!duration.valueOf()) { // invalid? | |
7572 | + duration = null; | |
7573 | + } | |
7574 | + } | |
7575 | + if (duration) { | |
7576 | + spec.duration = duration; | |
7577 | + unit = computeIntervalUnit(duration); | |
7578 | + | |
7579 | + // view is a single-unit duration, like "week" or "day" | |
7580 | + // incorporate options for this. lowest priority | |
7581 | + if (duration.as(unit) === 1) { | |
7582 | + spec.singleUnit = unit; | |
7583 | + overridesChain.unshift(viewOverrides[unit] || {}); | |
7584 | + } | |
7585 | + } | |
7586 | + | |
7587 | + // collapse into single objects | |
7588 | + spec.defaults = mergeOptions.apply(null, defaultsChain); | |
7589 | + spec.overrides = mergeOptions.apply(null, overridesChain); | |
7590 | + | |
7591 | + this.buildViewSpecOptions(spec); | |
7592 | + this.buildViewSpecButtonText(spec, requestedViewType); | |
7593 | + | |
7594 | + return spec; | |
7595 | + } | |
7596 | + }, | |
7597 | + | |
7598 | + | |
7599 | + // Builds and assigns a view spec's options object from its already-assigned defaults and overrides | |
7600 | + buildViewSpecOptions: function(spec) { | |
7601 | + spec.options = mergeOptions( // lowest to highest priority | |
7602 | + Calendar.defaults, // global defaults | |
7603 | + spec.defaults, // view's defaults (from ViewSubclass.defaults) | |
7604 | + this.dirDefaults, | |
7605 | + this.langDefaults, // locale and dir take precedence over view's defaults! | |
7606 | + this.overrides, // calendar's overrides (options given to constructor) | |
7607 | + spec.overrides // view's overrides (view-specific options) | |
7608 | + ); | |
7609 | + populateInstanceComputableOptions(spec.options); | |
7610 | + }, | |
7611 | + | |
7612 | + | |
7613 | + // Computes and assigns a view spec's buttonText-related options | |
7614 | + buildViewSpecButtonText: function(spec, requestedViewType) { | |
7615 | + | |
7616 | + // given an options object with a possible `buttonText` hash, lookup the buttonText for the | |
7617 | + // requested view, falling back to a generic unit entry like "week" or "day" | |
7618 | + function queryButtonText(options) { | |
7619 | + var buttonText = options.buttonText || {}; | |
7620 | + return buttonText[requestedViewType] || | |
7621 | + (spec.singleUnit ? buttonText[spec.singleUnit] : null); | |
7622 | + } | |
7623 | + | |
7624 | + // highest to lowest priority | |
7625 | + spec.buttonTextOverride = | |
7626 | + queryButtonText(this.overrides) || // constructor-specified buttonText lookup hash takes precedence | |
7627 | + spec.overrides.buttonText; // `buttonText` for view-specific options is a string | |
7628 | + | |
7629 | + // highest to lowest priority. mirrors buildViewSpecOptions | |
7630 | + spec.buttonTextDefault = | |
7631 | + queryButtonText(this.langDefaults) || | |
7632 | + queryButtonText(this.dirDefaults) || | |
7633 | + spec.defaults.buttonText || // a single string. from ViewSubclass.defaults | |
7634 | + queryButtonText(Calendar.defaults) || | |
7635 | + (spec.duration ? this.humanizeDuration(spec.duration) : null) || // like "3 days" | |
7636 | + requestedViewType; // fall back to given view name | |
7637 | + }, | |
7638 | + | |
7639 | + | |
7640 | + // Given a view name for a custom view or a standard view, creates a ready-to-go View object | |
7641 | + instantiateView: function(viewType) { | |
7642 | + var spec = this.getViewSpec(viewType); | |
7643 | + | |
7644 | + return new spec['class'](this, viewType, spec.options, spec.duration); | |
7645 | + }, | |
7646 | + | |
7647 | + | |
7648 | + // Returns a boolean about whether the view is okay to instantiate at some point | |
7649 | + isValidViewType: function(viewType) { | |
7650 | + return Boolean(this.getViewSpec(viewType)); | |
7651 | + } | |
7652 | + | |
7653 | +}); | |
7654 | + | |
7655 | + | |
7656 | +function Calendar_constructor(element, overrides) { | |
7657 | + var t = this; | |
7658 | + | |
7659 | + | |
7660 | + t.initOptions(overrides || {}); | |
7661 | + var options = this.options; | |
7662 | + | |
7663 | + | |
7664 | + // Exports | |
7665 | + // ----------------------------------------------------------------------------------- | |
7666 | + | |
7667 | + t.render = render; | |
7668 | + t.destroy = destroy; | |
7669 | + t.refetchEvents = refetchEvents; | |
7670 | + t.reportEvents = reportEvents; | |
7671 | + t.reportEventChange = reportEventChange; | |
7672 | + t.rerenderEvents = renderEvents; // `renderEvents` serves as a rerender. an API method | |
7673 | + t.changeView = renderView; // `renderView` will switch to another view | |
7674 | + t.select = select; | |
7675 | + t.unselect = unselect; | |
7676 | + t.prev = prev; | |
7677 | + t.next = next; | |
7678 | + t.prevYear = prevYear; | |
7679 | + t.nextYear = nextYear; | |
7680 | + t.today = today; | |
7681 | + t.gotoDate = gotoDate; | |
7682 | + t.incrementDate = incrementDate; | |
7683 | + t.zoomTo = zoomTo; | |
7684 | + t.getDate = getDate; | |
7685 | + t.getCalendar = getCalendar; | |
7686 | + t.getView = getView; | |
7687 | + t.option = option; | |
7688 | + t.trigger = trigger; | |
7689 | + | |
7690 | + | |
7691 | + | |
7692 | + // Language-data Internals | |
7693 | + // ----------------------------------------------------------------------------------- | |
7694 | + // Apply overrides to the current language's data | |
7695 | + | |
7696 | + | |
7697 | + var localeData = createObject( // make a cheap copy | |
7698 | + getMomentLocaleData(options.lang) // will fall back to en | |
7699 | + ); | |
7700 | + | |
7701 | + if (options.monthNames) { | |
7702 | + localeData._months = options.monthNames; | |
7703 | + } | |
7704 | + if (options.monthNamesShort) { | |
7705 | + localeData._monthsShort = options.monthNamesShort; | |
7706 | + } | |
7707 | + if (options.dayNames) { | |
7708 | + localeData._weekdays = options.dayNames; | |
7709 | + } | |
7710 | + if (options.dayNamesShort) { | |
7711 | + localeData._weekdaysShort = options.dayNamesShort; | |
7712 | + } | |
7713 | + if (options.firstDay != null) { | |
7714 | + var _week = createObject(localeData._week); // _week: { dow: # } | |
7715 | + _week.dow = options.firstDay; | |
7716 | + localeData._week = _week; | |
7717 | + } | |
7718 | + | |
7719 | + // assign a normalized value, to be used by our .week() moment extension | |
7720 | + localeData._fullCalendar_weekCalc = (function(weekCalc) { | |
7721 | + if (typeof weekCalc === 'function') { | |
7722 | + return weekCalc; | |
7723 | + } | |
7724 | + else if (weekCalc === 'local') { | |
7725 | + return weekCalc; | |
7726 | + } | |
7727 | + else if (weekCalc === 'iso' || weekCalc === 'ISO') { | |
7728 | + return 'ISO'; | |
7729 | + } | |
7730 | + })(options.weekNumberCalculation); | |
7731 | + | |
7732 | + | |
7733 | + | |
7734 | + // Calendar-specific Date Utilities | |
7735 | + // ----------------------------------------------------------------------------------- | |
7736 | + | |
7737 | + | |
7738 | + t.defaultAllDayEventDuration = moment.duration(options.defaultAllDayEventDuration); | |
7739 | + t.defaultTimedEventDuration = moment.duration(options.defaultTimedEventDuration); | |
7740 | + | |
7741 | + | |
7742 | + // Builds a moment using the settings of the current calendar: timezone and language. | |
7743 | + // Accepts anything the vanilla moment() constructor accepts. | |
7744 | + t.moment = function() { | |
7745 | + var mom; | |
7746 | + | |
7747 | + if (options.timezone === 'local') { | |
7748 | + mom = fc.moment.apply(null, arguments); | |
7749 | + | |
7750 | + // Force the moment to be local, because fc.moment doesn't guarantee it. | |
7751 | + if (mom.hasTime()) { // don't give ambiguously-timed moments a local zone | |
7752 | + mom.local(); | |
7753 | + } | |
7754 | + } | |
7755 | + else if (options.timezone === 'UTC') { | |
7756 | + mom = fc.moment.utc.apply(null, arguments); // process as UTC | |
7757 | + } | |
7758 | + else { | |
7759 | + mom = fc.moment.parseZone.apply(null, arguments); // let the input decide the zone | |
7760 | + } | |
7761 | + | |
7762 | + if ('_locale' in mom) { // moment 2.8 and above | |
7763 | + mom._locale = localeData; | |
7764 | + } | |
7765 | + else { // pre-moment-2.8 | |
7766 | + mom._lang = localeData; | |
7767 | + } | |
7768 | + | |
7769 | + return mom; | |
7770 | + }; | |
7771 | + | |
7772 | + | |
7773 | + // Returns a boolean about whether or not the calendar knows how to calculate | |
7774 | + // the timezone offset of arbitrary dates in the current timezone. | |
7775 | + t.getIsAmbigTimezone = function() { | |
7776 | + return options.timezone !== 'local' && options.timezone !== 'UTC'; | |
7777 | + }; | |
7778 | + | |
7779 | + | |
7780 | + // Returns a copy of the given date in the current timezone of it is ambiguously zoned. | |
7781 | + // This will also give the date an unambiguous time. | |
7782 | + t.rezoneDate = function(date) { | |
7783 | + return t.moment(date.toArray()); | |
7784 | + }; | |
7785 | + | |
7786 | + | |
7787 | + // Returns a moment for the current date, as defined by the client's computer, | |
7788 | + // or overridden by the `now` option. | |
7789 | + t.getNow = function() { | |
7790 | + var now = options.now; | |
7791 | + if (typeof now === 'function') { | |
7792 | + now = now(); | |
7793 | + } | |
7794 | + return t.moment(now); | |
7795 | + }; | |
7796 | + | |
7797 | + | |
7798 | + // Get an event's normalized end date. If not present, calculate it from the defaults. | |
7799 | + t.getEventEnd = function(event) { | |
7800 | + if (event.end) { | |
7801 | + return event.end.clone(); | |
7802 | + } | |
7803 | + else { | |
7804 | + return t.getDefaultEventEnd(event.allDay, event.start); | |
7805 | + } | |
7806 | + }; | |
7807 | + | |
7808 | + | |
7809 | + // Given an event's allDay status and start date, return swhat its fallback end date should be. | |
7810 | + t.getDefaultEventEnd = function(allDay, start) { // TODO: rename to computeDefaultEventEnd | |
7811 | + var end = start.clone(); | |
7812 | + | |
7813 | + if (allDay) { | |
7814 | + end.stripTime().add(t.defaultAllDayEventDuration); | |
7815 | + } | |
7816 | + else { | |
7817 | + end.add(t.defaultTimedEventDuration); | |
7818 | + } | |
7819 | + | |
7820 | + if (t.getIsAmbigTimezone()) { | |
7821 | + end.stripZone(); // we don't know what the tzo should be | |
7822 | + } | |
7823 | + | |
7824 | + return end; | |
7825 | + }; | |
7826 | + | |
7827 | + | |
7828 | + // Produces a human-readable string for the given duration. | |
7829 | + // Side-effect: changes the locale of the given duration. | |
7830 | + t.humanizeDuration = function(duration) { | |
7831 | + return (duration.locale || duration.lang).call(duration, options.lang) // works moment-pre-2.8 | |
7832 | + .humanize(); | |
7833 | + }; | |
7834 | + | |
7835 | + | |
7836 | + | |
7837 | + // Imports | |
7838 | + // ----------------------------------------------------------------------------------- | |
7839 | + | |
7840 | + | |
7841 | + EventManager.call(t, options); | |
7842 | + var isFetchNeeded = t.isFetchNeeded; | |
7843 | + var fetchEvents = t.fetchEvents; | |
7844 | + | |
7845 | + | |
7846 | + | |
7847 | + // Locals | |
7848 | + // ----------------------------------------------------------------------------------- | |
7849 | + | |
7850 | + | |
7851 | + var _element = element[0]; | |
7852 | + var header; | |
7853 | + var headerElement; | |
7854 | + var content; | |
7855 | + var tm; // for making theme classes | |
7856 | + var currentView; // NOTE: keep this in sync with this.view | |
7857 | + var viewsByType = {}; // holds all instantiated view instances, current or not | |
7858 | + var suggestedViewHeight; | |
7859 | + var windowResizeProxy; // wraps the windowResize function | |
7860 | + var ignoreWindowResize = 0; | |
7861 | + var date; | |
7862 | + var events = []; | |
7863 | + | |
7864 | + | |
7865 | + | |
7866 | + // Main Rendering | |
7867 | + // ----------------------------------------------------------------------------------- | |
7868 | + | |
7869 | + | |
7870 | + if (options.defaultDate != null) { | |
7871 | + date = t.moment(options.defaultDate); | |
7872 | + } | |
7873 | + else { | |
7874 | + date = t.getNow(); | |
7875 | + } | |
7876 | + | |
7877 | + | |
7878 | + function render() { | |
7879 | + if (!content) { | |
7880 | + initialRender(); | |
7881 | + } | |
7882 | + else if (elementVisible()) { | |
7883 | + // mainly for the public API | |
7884 | + calcSize(); | |
7885 | + renderView(); | |
7886 | + } | |
7887 | + } | |
7888 | + | |
7889 | + | |
7890 | + function initialRender() { | |
7891 | + tm = options.theme ? 'ui' : 'fc'; | |
7892 | + element.addClass('fc'); | |
7893 | + | |
7894 | + if (options.isRTL) { | |
7895 | + element.addClass('fc-rtl'); | |
7896 | + } | |
7897 | + else { | |
7898 | + element.addClass('fc-ltr'); | |
7899 | + } | |
7900 | + | |
7901 | + if (options.theme) { | |
7902 | + element.addClass('ui-widget'); | |
7903 | + } | |
7904 | + else { | |
7905 | + element.addClass('fc-unthemed'); | |
7906 | + } | |
7907 | + | |
7908 | + content = $("<div class='fc-view-container'/>").prependTo(element); | |
7909 | + | |
7910 | + header = t.header = new Header(t, options); | |
7911 | + headerElement = header.render(); | |
7912 | + if (headerElement) { | |
7913 | + element.prepend(headerElement); | |
7914 | + } | |
7915 | + | |
7916 | + renderView(options.defaultView); | |
7917 | + | |
7918 | + if (options.handleWindowResize) { | |
7919 | + windowResizeProxy = debounce(windowResize, options.windowResizeDelay); // prevents rapid calls | |
7920 | + $(window).resize(windowResizeProxy); | |
7921 | + } | |
7922 | + } | |
7923 | + | |
7924 | + | |
7925 | + function destroy() { | |
7926 | + | |
7927 | + if (currentView) { | |
7928 | + currentView.removeElement(); | |
7929 | + | |
7930 | + // NOTE: don't null-out currentView/t.view in case API methods are called after destroy. | |
7931 | + // It is still the "current" view, just not rendered. | |
7932 | + } | |
7933 | + | |
7934 | + header.destroy(); | |
7935 | + content.remove(); | |
7936 | + element.removeClass('fc fc-ltr fc-rtl fc-unthemed ui-widget'); | |
7937 | + | |
7938 | + if (windowResizeProxy) { | |
7939 | + $(window).unbind('resize', windowResizeProxy); | |
7940 | + } | |
7941 | + } | |
7942 | + | |
7943 | + | |
7944 | + function elementVisible() { | |
7945 | + return element.is(':visible'); | |
7946 | + } | |
7947 | + | |
7948 | + | |
7949 | + | |
7950 | + // View Rendering | |
7951 | + // ----------------------------------------------------------------------------------- | |
7952 | + | |
7953 | + | |
7954 | + // Renders a view because of a date change, view-type change, or for the first time. | |
7955 | + // If not given a viewType, keep the current view but render different dates. | |
7956 | + function renderView(viewType) { | |
7957 | + ignoreWindowResize++; | |
7958 | + | |
7959 | + // if viewType is changing, destroy the old view | |
7960 | + if (currentView && viewType && currentView.type !== viewType) { | |
7961 | + header.deactivateButton(currentView.type); | |
7962 | + freezeContentHeight(); // prevent a scroll jump when view element is removed | |
7963 | + currentView.removeElement(); | |
7964 | + currentView = t.view = null; | |
7965 | + } | |
7966 | + | |
7967 | + // if viewType changed, or the view was never created, create a fresh view | |
7968 | + if (!currentView && viewType) { | |
7969 | + currentView = t.view = | |
7970 | + viewsByType[viewType] || | |
7971 | + (viewsByType[viewType] = t.instantiateView(viewType)); | |
7972 | + | |
7973 | + currentView.setElement( | |
7974 | + $("<div class='fc-view fc-" + viewType + "-view' />").appendTo(content) | |
7975 | + ); | |
7976 | + header.activateButton(viewType); | |
7977 | + } | |
7978 | + | |
7979 | + if (currentView) { | |
7980 | + | |
7981 | + // in case the view should render a period of time that is completely hidden | |
7982 | + date = currentView.massageCurrentDate(date); | |
7983 | + | |
7984 | + // render or rerender the view | |
7985 | + if ( | |
7986 | + !currentView.isDisplayed || | |
7987 | + !date.isWithin(currentView.intervalStart, currentView.intervalEnd) // implicit date window change | |
7988 | + ) { | |
7989 | + if (elementVisible()) { | |
7990 | + | |
7991 | + freezeContentHeight(); | |
7992 | + currentView.display(date); | |
7993 | + unfreezeContentHeight(); | |
7994 | + | |
7995 | + // need to do this after View::render, so dates are calculated | |
7996 | + updateHeaderTitle(); | |
7997 | + updateTodayButton(); | |
7998 | + | |
7999 | + getAndRenderEvents(); | |
8000 | + } | |
8001 | + } | |
8002 | + } | |
8003 | + | |
8004 | + unfreezeContentHeight(); // undo any lone freezeContentHeight calls | |
8005 | + ignoreWindowResize--; | |
8006 | + } | |
8007 | + | |
8008 | + | |
8009 | + | |
8010 | + // Resizing | |
8011 | + // ----------------------------------------------------------------------------------- | |
8012 | + | |
8013 | + | |
8014 | + t.getSuggestedViewHeight = function() { | |
8015 | + if (suggestedViewHeight === undefined) { | |
8016 | + calcSize(); | |
8017 | + } | |
8018 | + return suggestedViewHeight; | |
8019 | + }; | |
8020 | + | |
8021 | + | |
8022 | + t.isHeightAuto = function() { | |
8023 | + return options.contentHeight === 'auto' || options.height === 'auto'; | |
8024 | + }; | |
8025 | + | |
8026 | + | |
8027 | + function updateSize(shouldRecalc) { | |
8028 | + if (elementVisible()) { | |
8029 | + | |
8030 | + if (shouldRecalc) { | |
8031 | + _calcSize(); | |
8032 | + } | |
8033 | + | |
8034 | + ignoreWindowResize++; | |
8035 | + currentView.updateSize(true); // isResize=true. will poll getSuggestedViewHeight() and isHeightAuto() | |
8036 | + ignoreWindowResize--; | |
8037 | + | |
8038 | + return true; // signal success | |
8039 | + } | |
8040 | + } | |
8041 | + | |
8042 | + | |
8043 | + function calcSize() { | |
8044 | + if (elementVisible()) { | |
8045 | + _calcSize(); | |
8046 | + } | |
8047 | + } | |
8048 | + | |
8049 | + | |
8050 | + function _calcSize() { // assumes elementVisible | |
8051 | + if (typeof options.contentHeight === 'number') { // exists and not 'auto' | |
8052 | + suggestedViewHeight = options.contentHeight; | |
8053 | + } | |
8054 | + else if (typeof options.height === 'number') { // exists and not 'auto' | |
8055 | + suggestedViewHeight = options.height - (headerElement ? headerElement.outerHeight(true) : 0); | |
8056 | + } | |
8057 | + else { | |
8058 | + suggestedViewHeight = Math.round(content.width() / Math.max(options.aspectRatio, .5)); | |
8059 | + } | |
8060 | + } | |
8061 | + | |
8062 | + | |
8063 | + function windowResize(ev) { | |
8064 | + if ( | |
8065 | + !ignoreWindowResize && | |
8066 | + ev.target === window && // so we don't process jqui "resize" events that have bubbled up | |
8067 | + currentView.start // view has already been rendered | |
8068 | + ) { | |
8069 | + if (updateSize(true)) { | |
8070 | + currentView.trigger('windowResize', _element); | |
8071 | + } | |
8072 | + } | |
8073 | + } | |
8074 | + | |
8075 | + | |
8076 | + | |
8077 | + /* Event Fetching/Rendering | |
8078 | + -----------------------------------------------------------------------------*/ | |
8079 | + // TODO: going forward, most of this stuff should be directly handled by the view | |
8080 | + | |
8081 | + | |
8082 | + function refetchEvents() { // can be called as an API method | |
8083 | + destroyEvents(); // so that events are cleared before user starts waiting for AJAX | |
8084 | + fetchAndRenderEvents(); | |
8085 | + } | |
8086 | + | |
8087 | + | |
8088 | + function renderEvents() { // destroys old events if previously rendered | |
8089 | + if (elementVisible()) { | |
8090 | + freezeContentHeight(); | |
8091 | + currentView.displayEvents(events); | |
8092 | + unfreezeContentHeight(); | |
8093 | + } | |
8094 | + } | |
8095 | + | |
8096 | + | |
8097 | + function destroyEvents() { | |
8098 | + freezeContentHeight(); | |
8099 | + currentView.clearEvents(); | |
8100 | + unfreezeContentHeight(); | |
8101 | + } | |
8102 | + | |
8103 | + | |
8104 | + function getAndRenderEvents() { | |
8105 | + if (!options.lazyFetching || isFetchNeeded(currentView.start, currentView.end)) { | |
8106 | + fetchAndRenderEvents(); | |
8107 | + } | |
8108 | + else { | |
8109 | + renderEvents(); | |
8110 | + } | |
8111 | + } | |
8112 | + | |
8113 | + | |
8114 | + function fetchAndRenderEvents() { | |
8115 | + fetchEvents(currentView.start, currentView.end); | |
8116 | + // ... will call reportEvents | |
8117 | + // ... which will call renderEvents | |
8118 | + } | |
8119 | + | |
8120 | + | |
8121 | + // called when event data arrives | |
8122 | + function reportEvents(_events) { | |
8123 | + events = _events; | |
8124 | + renderEvents(); | |
8125 | + } | |
8126 | + | |
8127 | + | |
8128 | + // called when a single event's data has been changed | |
8129 | + function reportEventChange() { | |
8130 | + renderEvents(); | |
8131 | + } | |
8132 | + | |
8133 | + | |
8134 | + | |
8135 | + /* Header Updating | |
8136 | + -----------------------------------------------------------------------------*/ | |
8137 | + | |
8138 | + | |
8139 | + function updateHeaderTitle() { | |
8140 | + header.updateTitle(currentView.title); | |
8141 | + } | |
8142 | + | |
8143 | + | |
8144 | + function updateTodayButton() { | |
8145 | + var now = t.getNow(); | |
8146 | + if (now.isWithin(currentView.intervalStart, currentView.intervalEnd)) { | |
8147 | + header.disableButton('today'); | |
8148 | + } | |
8149 | + else { | |
8150 | + header.enableButton('today'); | |
8151 | + } | |
8152 | + } | |
8153 | + | |
8154 | + | |
8155 | + | |
8156 | + /* Selection | |
8157 | + -----------------------------------------------------------------------------*/ | |
8158 | + | |
8159 | + | |
8160 | + function select(start, end) { | |
8161 | + | |
8162 | + start = t.moment(start); | |
8163 | + if (end) { | |
8164 | + end = t.moment(end); | |
8165 | + } | |
8166 | + else if (start.hasTime()) { | |
8167 | + end = start.clone().add(t.defaultTimedEventDuration); | |
8168 | + } | |
8169 | + else { | |
8170 | + end = start.clone().add(t.defaultAllDayEventDuration); | |
8171 | + } | |
8172 | + | |
8173 | + currentView.select({ start: start, end: end }); // accepts a range | |
8174 | + } | |
8175 | + | |
8176 | + | |
8177 | + function unselect() { // safe to be called before renderView | |
8178 | + if (currentView) { | |
8179 | + currentView.unselect(); | |
8180 | + } | |
8181 | + } | |
8182 | + | |
8183 | + | |
8184 | + | |
8185 | + /* Date | |
8186 | + -----------------------------------------------------------------------------*/ | |
8187 | + | |
8188 | + | |
8189 | + function prev() { | |
8190 | + date = currentView.computePrevDate(date); | |
8191 | + renderView(); | |
8192 | + } | |
8193 | + | |
8194 | + | |
8195 | + function next() { | |
8196 | + date = currentView.computeNextDate(date); | |
8197 | + renderView(); | |
8198 | + } | |
8199 | + | |
8200 | + | |
8201 | + function prevYear() { | |
8202 | + date.add(-1, 'years'); | |
8203 | + renderView(); | |
8204 | + } | |
8205 | + | |
8206 | + | |
8207 | + function nextYear() { | |
8208 | + date.add(1, 'years'); | |
8209 | + renderView(); | |
8210 | + } | |
8211 | + | |
8212 | + | |
8213 | + function today() { | |
8214 | + date = t.getNow(); | |
8215 | + renderView(); | |
8216 | + } | |
8217 | + | |
8218 | + | |
8219 | + function gotoDate(dateInput) { | |
8220 | + date = t.moment(dateInput); | |
8221 | + renderView(); | |
8222 | + } | |
8223 | + | |
8224 | + | |
8225 | + function incrementDate(delta) { | |
8226 | + date.add(moment.duration(delta)); | |
8227 | + renderView(); | |
8228 | + } | |
8229 | + | |
8230 | + | |
8231 | + // Forces navigation to a view for the given date. | |
8232 | + // `viewType` can be a specific view name or a generic one like "week" or "day". | |
8233 | + function zoomTo(newDate, viewType) { | |
8234 | + var spec; | |
8235 | + | |
8236 | + viewType = viewType || 'day'; // day is default zoom | |
8237 | + spec = t.getViewSpec(viewType) || t.getUnitViewSpec(viewType); | |
8238 | + | |
8239 | + date = newDate; | |
8240 | + renderView(spec ? spec.type : null); | |
8241 | + } | |
8242 | + | |
8243 | + | |
8244 | + function getDate() { | |
8245 | + return date.clone(); | |
8246 | + } | |
8247 | + | |
8248 | + | |
8249 | + | |
8250 | + /* Height "Freezing" | |
8251 | + -----------------------------------------------------------------------------*/ | |
8252 | + // TODO: move this into the view | |
8253 | + | |
8254 | + | |
8255 | + function freezeContentHeight() { | |
8256 | + content.css({ | |
8257 | + width: '100%', | |
8258 | + height: content.height(), | |
8259 | + overflow: 'hidden' | |
8260 | + }); | |
8261 | + } | |
8262 | + | |
8263 | + | |
8264 | + function unfreezeContentHeight() { | |
8265 | + content.css({ | |
8266 | + width: '', | |
8267 | + height: '', | |
8268 | + overflow: '' | |
8269 | + }); | |
8270 | + } | |
8271 | + | |
8272 | + | |
8273 | + | |
8274 | + /* Misc | |
8275 | + -----------------------------------------------------------------------------*/ | |
8276 | + | |
8277 | + | |
8278 | + function getCalendar() { | |
8279 | + return t; | |
8280 | + } | |
8281 | + | |
8282 | + | |
8283 | + function getView() { | |
8284 | + return currentView; | |
8285 | + } | |
8286 | + | |
8287 | + | |
8288 | + function option(name, value) { | |
8289 | + if (value === undefined) { | |
8290 | + return options[name]; | |
8291 | + } | |
8292 | + if (name == 'height' || name == 'contentHeight' || name == 'aspectRatio') { | |
8293 | + options[name] = value; | |
8294 | + updateSize(true); // true = allow recalculation of height | |
8295 | + } | |
8296 | + } | |
8297 | + | |
8298 | + | |
8299 | + function trigger(name, thisObj) { | |
8300 | + if (options[name]) { | |
8301 | + return options[name].apply( | |
8302 | + thisObj || _element, | |
8303 | + Array.prototype.slice.call(arguments, 2) | |
8304 | + ); | |
8305 | + } | |
8306 | + } | |
8307 | + | |
8308 | +} | |
8309 | + | |
8310 | +;; | |
8311 | + | |
8312 | +Calendar.defaults = { | |
8313 | + | |
8314 | + titleRangeSeparator: ' \u2014 ', // emphasized dash | |
8315 | + monthYearFormat: 'MMMM YYYY', // required for en. other languages rely on datepicker computable option | |
8316 | + | |
8317 | + defaultTimedEventDuration: '02:00:00', | |
8318 | + defaultAllDayEventDuration: { days: 1 }, | |
8319 | + forceEventDuration: false, | |
8320 | + nextDayThreshold: '09:00:00', // 9am | |
8321 | + | |
8322 | + // display | |
8323 | + defaultView: 'month', | |
8324 | + aspectRatio: 1.35, | |
8325 | + header: { | |
8326 | + left: 'title', | |
8327 | + center: '', | |
8328 | + right: 'today prev,next' | |
8329 | + }, | |
8330 | + weekends: true, | |
8331 | + weekNumbers: false, | |
8332 | + | |
8333 | + weekNumberTitle: 'W', | |
8334 | + weekNumberCalculation: 'local', | |
8335 | + | |
8336 | + //editable: false, | |
8337 | + | |
8338 | + // event ajax | |
8339 | + lazyFetching: true, | |
8340 | + startParam: 'start', | |
8341 | + endParam: 'end', | |
8342 | + timezoneParam: 'timezone', | |
8343 | + | |
8344 | + timezone: false, | |
8345 | + | |
8346 | + //allDayDefault: undefined, | |
8347 | + | |
8348 | + // locale | |
8349 | + isRTL: false, | |
8350 | + buttonText: { | |
8351 | + prev: "prev", | |
8352 | + next: "next", | |
8353 | + prevYear: "prev year", | |
8354 | + nextYear: "next year", | |
8355 | + year: 'year', // TODO: locale files need to specify this | |
8356 | + today: 'today', | |
8357 | + month: 'month', | |
8358 | + week: 'week', | |
8359 | + day: 'day' | |
8360 | + }, | |
8361 | + | |
8362 | + buttonIcons: { | |
8363 | + prev: 'left-single-arrow', | |
8364 | + next: 'right-single-arrow', | |
8365 | + prevYear: 'left-double-arrow', | |
8366 | + nextYear: 'right-double-arrow' | |
8367 | + }, | |
8368 | + | |
8369 | + // jquery-ui theming | |
8370 | + theme: false, | |
8371 | + themeButtonIcons: { | |
8372 | + prev: 'circle-triangle-w', | |
8373 | + next: 'circle-triangle-e', | |
8374 | + prevYear: 'seek-prev', | |
8375 | + nextYear: 'seek-next' | |
8376 | + }, | |
8377 | + | |
8378 | + //eventResizableFromStart: false, | |
8379 | + dragOpacity: .75, | |
8380 | + dragRevertDuration: 500, | |
8381 | + dragScroll: true, | |
8382 | + | |
8383 | + //selectable: false, | |
8384 | + unselectAuto: true, | |
8385 | + | |
8386 | + dropAccept: '*', | |
8387 | + | |
8388 | + eventLimit: false, | |
8389 | + eventLimitText: 'more', | |
8390 | + eventLimitClick: 'popover', | |
8391 | + dayPopoverFormat: 'LL', | |
8392 | + | |
8393 | + handleWindowResize: true, | |
8394 | + windowResizeDelay: 200 // milliseconds before an updateSize happens | |
8395 | + | |
8396 | +}; | |
8397 | + | |
8398 | + | |
8399 | +Calendar.englishDefaults = { // used by lang.js | |
8400 | + dayPopoverFormat: 'dddd, MMMM D' | |
8401 | +}; | |
8402 | + | |
8403 | + | |
8404 | +Calendar.rtlDefaults = { // right-to-left defaults | |
8405 | + header: { // TODO: smarter solution (first/center/last ?) | |
8406 | + left: 'next,prev today', | |
8407 | + center: '', | |
8408 | + right: 'title' | |
8409 | + }, | |
8410 | + buttonIcons: { | |
8411 | + prev: 'right-single-arrow', | |
8412 | + next: 'left-single-arrow', | |
8413 | + prevYear: 'right-double-arrow', | |
8414 | + nextYear: 'left-double-arrow' | |
8415 | + }, | |
8416 | + themeButtonIcons: { | |
8417 | + prev: 'circle-triangle-e', | |
8418 | + next: 'circle-triangle-w', | |
8419 | + nextYear: 'seek-prev', | |
8420 | + prevYear: 'seek-next' | |
8421 | + } | |
8422 | +}; | |
8423 | + | |
8424 | +;; | |
8425 | + | |
8426 | +var langOptionHash = fc.langs = {}; // initialize and expose | |
8427 | + | |
8428 | + | |
8429 | +// TODO: document the structure and ordering of a FullCalendar lang file | |
8430 | +// TODO: rename everything "lang" to "locale", like what the moment project did | |
8431 | + | |
8432 | + | |
8433 | +// Initialize jQuery UI datepicker translations while using some of the translations | |
8434 | +// Will set this as the default language for datepicker. | |
8435 | +fc.datepickerLang = function(langCode, dpLangCode, dpOptions) { | |
8436 | + | |
8437 | + // get the FullCalendar internal option hash for this language. create if necessary | |
8438 | + var fcOptions = langOptionHash[langCode] || (langOptionHash[langCode] = {}); | |
8439 | + | |
8440 | + // transfer some simple options from datepicker to fc | |
8441 | + fcOptions.isRTL = dpOptions.isRTL; | |
8442 | + fcOptions.weekNumberTitle = dpOptions.weekHeader; | |
8443 | + | |
8444 | + // compute some more complex options from datepicker | |
8445 | + $.each(dpComputableOptions, function(name, func) { | |
8446 | + fcOptions[name] = func(dpOptions); | |
8447 | + }); | |
8448 | + | |
8449 | + // is jQuery UI Datepicker is on the page? | |
8450 | + if ($.datepicker) { | |
8451 | + | |
8452 | + // Register the language data. | |
8453 | + // FullCalendar and MomentJS use language codes like "pt-br" but Datepicker | |
8454 | + // does it like "pt-BR" or if it doesn't have the language, maybe just "pt". | |
8455 | + // Make an alias so the language can be referenced either way. | |
8456 | + $.datepicker.regional[dpLangCode] = | |
8457 | + $.datepicker.regional[langCode] = // alias | |
8458 | + dpOptions; | |
8459 | + | |
8460 | + // Alias 'en' to the default language data. Do this every time. | |
8461 | + $.datepicker.regional.en = $.datepicker.regional['']; | |
8462 | + | |
8463 | + // Set as Datepicker's global defaults. | |
8464 | + $.datepicker.setDefaults(dpOptions); | |
8465 | + } | |
8466 | +}; | |
8467 | + | |
8468 | + | |
8469 | +// Sets FullCalendar-specific translations. Will set the language as the global default. | |
8470 | +fc.lang = function(langCode, newFcOptions) { | |
8471 | + var fcOptions; | |
8472 | + var momOptions; | |
8473 | + | |
8474 | + // get the FullCalendar internal option hash for this language. create if necessary | |
8475 | + fcOptions = langOptionHash[langCode] || (langOptionHash[langCode] = {}); | |
8476 | + | |
8477 | + // provided new options for this language? merge them in | |
8478 | + if (newFcOptions) { | |
8479 | + fcOptions = langOptionHash[langCode] = mergeOptions(fcOptions, newFcOptions); | |
8480 | + } | |
8481 | + | |
8482 | + // compute language options that weren't defined. | |
8483 | + // always do this. newFcOptions can be undefined when initializing from i18n file, | |
8484 | + // so no way to tell if this is an initialization or a default-setting. | |
8485 | + momOptions = getMomentLocaleData(langCode); // will fall back to en | |
8486 | + $.each(momComputableOptions, function(name, func) { | |
8487 | + if (fcOptions[name] == null) { | |
8488 | + fcOptions[name] = func(momOptions, fcOptions); | |
8489 | + } | |
8490 | + }); | |
8491 | + | |
8492 | + // set it as the default language for FullCalendar | |
8493 | + Calendar.defaults.lang = langCode; | |
8494 | +}; | |
8495 | + | |
8496 | + | |
8497 | +// NOTE: can't guarantee any of these computations will run because not every language has datepicker | |
8498 | +// configs, so make sure there are English fallbacks for these in the defaults file. | |
8499 | +var dpComputableOptions = { | |
8500 | + | |
8501 | + buttonText: function(dpOptions) { | |
8502 | + return { | |
8503 | + // the translations sometimes wrongly contain HTML entities | |
8504 | + prev: stripHtmlEntities(dpOptions.prevText), | |
8505 | + next: stripHtmlEntities(dpOptions.nextText), | |
8506 | + today: stripHtmlEntities(dpOptions.currentText) | |
8507 | + }; | |
8508 | + }, | |
8509 | + | |
8510 | + // Produces format strings like "MMMM YYYY" -> "September 2014" | |
8511 | + monthYearFormat: function(dpOptions) { | |
8512 | + return dpOptions.showMonthAfterYear ? | |
8513 | + 'YYYY[' + dpOptions.yearSuffix + '] MMMM' : | |
8514 | + 'MMMM YYYY[' + dpOptions.yearSuffix + ']'; | |
8515 | + } | |
8516 | + | |
8517 | +}; | |
8518 | + | |
8519 | +var momComputableOptions = { | |
8520 | + | |
8521 | + // Produces format strings like "ddd M/D" -> "Fri 9/15" | |
8522 | + dayOfMonthFormat: function(momOptions, fcOptions) { | |
8523 | + var format = momOptions.longDateFormat('l'); // for the format like "M/D/YYYY" | |
8524 | + | |
8525 | + // strip the year off the edge, as well as other misc non-whitespace chars | |
8526 | + format = format.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g, ''); | |
8527 | + | |
8528 | + if (fcOptions.isRTL) { | |
8529 | + format += ' ddd'; // for RTL, add day-of-week to end | |
8530 | + } | |
8531 | + else { | |
8532 | + format = 'ddd ' + format; // for LTR, add day-of-week to beginning | |
8533 | + } | |
8534 | + return format; | |
8535 | + }, | |
8536 | + | |
8537 | + // Produces format strings like "h:mma" -> "6:00pm" | |
8538 | + mediumTimeFormat: function(momOptions) { // can't be called `timeFormat` because collides with option | |
8539 | + return momOptions.longDateFormat('LT') | |
8540 | + .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand | |
8541 | + }, | |
8542 | + | |
8543 | + // Produces format strings like "h(:mm)a" -> "6pm" / "6:30pm" | |
8544 | + smallTimeFormat: function(momOptions) { | |
8545 | + return momOptions.longDateFormat('LT') | |
8546 | + .replace(':mm', '(:mm)') | |
8547 | + .replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs | |
8548 | + .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand | |
8549 | + }, | |
8550 | + | |
8551 | + // Produces format strings like "h(:mm)t" -> "6p" / "6:30p" | |
8552 | + extraSmallTimeFormat: function(momOptions) { | |
8553 | + return momOptions.longDateFormat('LT') | |
8554 | + .replace(':mm', '(:mm)') | |
8555 | + .replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs | |
8556 | + .replace(/\s*a$/i, 't'); // convert to AM/PM/am/pm to lowercase one-letter. remove any spaces beforehand | |
8557 | + }, | |
8558 | + | |
8559 | + // Produces format strings like "ha" / "H" -> "6pm" / "18" | |
8560 | + hourFormat: function(momOptions) { | |
8561 | + return momOptions.longDateFormat('LT') | |
8562 | + .replace(':mm', '') | |
8563 | + .replace(/(\Wmm)$/, '') // like above, but for foreign langs | |
8564 | + .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand | |
8565 | + }, | |
8566 | + | |
8567 | + // Produces format strings like "h:mm" -> "6:30" (with no AM/PM) | |
8568 | + noMeridiemTimeFormat: function(momOptions) { | |
8569 | + return momOptions.longDateFormat('LT') | |
8570 | + .replace(/\s*a$/i, ''); // remove trailing AM/PM | |
8571 | + } | |
8572 | + | |
8573 | +}; | |
8574 | + | |
8575 | + | |
8576 | +// options that should be computed off live calendar options (considers override options) | |
8577 | +var instanceComputableOptions = { // TODO: best place for this? related to lang? | |
8578 | + | |
8579 | + // Produces format strings for results like "Mo 16" | |
8580 | + smallDayDateFormat: function(options) { | |
8581 | + return options.isRTL ? | |
8582 | + 'D dd' : | |
8583 | + 'dd D'; | |
8584 | + }, | |
8585 | + | |
8586 | + // Produces format strings for results like "Wk 5" | |
8587 | + weekFormat: function(options) { | |
8588 | + return options.isRTL ? | |
8589 | + 'w[ ' + options.weekNumberTitle + ']' : | |
8590 | + '[' + options.weekNumberTitle + ' ]w'; | |
8591 | + }, | |
8592 | + | |
8593 | + // Produces format strings for results like "Wk5" | |
8594 | + smallWeekFormat: function(options) { | |
8595 | + return options.isRTL ? | |
8596 | + 'w[' + options.weekNumberTitle + ']' : | |
8597 | + '[' + options.weekNumberTitle + ']w'; | |
8598 | + } | |
8599 | + | |
8600 | +}; | |
8601 | + | |
8602 | +function populateInstanceComputableOptions(options) { | |
8603 | + $.each(instanceComputableOptions, function(name, func) { | |
8604 | + if (options[name] == null) { | |
8605 | + options[name] = func(options); | |
8606 | + } | |
8607 | + }); | |
8608 | +} | |
8609 | + | |
8610 | + | |
8611 | +// Returns moment's internal locale data. If doesn't exist, returns English. | |
8612 | +// Works with moment-pre-2.8 | |
8613 | +function getMomentLocaleData(langCode) { | |
8614 | + var func = moment.localeData || moment.langData; | |
8615 | + return func.call(moment, langCode) || | |
8616 | + func.call(moment, 'en'); // the newer localData could return null, so fall back to en | |
8617 | +} | |
8618 | + | |
8619 | + | |
8620 | +// Initialize English by forcing computation of moment-derived options. | |
8621 | +// Also, sets it as the default. | |
8622 | +fc.lang('en', Calendar.englishDefaults); | |
8623 | + | |
8624 | +;; | |
8625 | + | |
8626 | +/* Top toolbar area with buttons and title | |
8627 | +----------------------------------------------------------------------------------------------------------------------*/ | |
8628 | +// TODO: rename all header-related things to "toolbar" | |
8629 | + | |
8630 | +function Header(calendar, options) { | |
8631 | + var t = this; | |
8632 | + | |
8633 | + // exports | |
8634 | + t.render = render; | |
8635 | + t.destroy = destroy; | |
8636 | + t.updateTitle = updateTitle; | |
8637 | + t.activateButton = activateButton; | |
8638 | + t.deactivateButton = deactivateButton; | |
8639 | + t.disableButton = disableButton; | |
8640 | + t.enableButton = enableButton; | |
8641 | + t.getViewsWithButtons = getViewsWithButtons; | |
8642 | + | |
8643 | + // locals | |
8644 | + var el = $(); | |
8645 | + var viewsWithButtons = []; | |
8646 | + var tm; | |
8647 | + | |
8648 | + | |
8649 | + function render() { | |
8650 | + var sections = options.header; | |
8651 | + | |
8652 | + tm = options.theme ? 'ui' : 'fc'; | |
8653 | + | |
8654 | + if (sections) { | |
8655 | + el = $("<div class='fc-toolbar'/>") | |
8656 | + .append(renderSection('left')) | |
8657 | + .append(renderSection('right')) | |
8658 | + .append(renderSection('center')) | |
8659 | + .append('<div class="fc-clear"/>'); | |
8660 | + | |
8661 | + return el; | |
8662 | + } | |
8663 | + } | |
8664 | + | |
8665 | + | |
8666 | + function destroy() { | |
8667 | + el.remove(); | |
8668 | + } | |
8669 | + | |
8670 | + | |
8671 | + function renderSection(position) { | |
8672 | + var sectionEl = $('<div class="fc-' + position + '"/>'); | |
8673 | + var buttonStr = options.header[position]; | |
8674 | + | |
8675 | + if (buttonStr) { | |
8676 | + $.each(buttonStr.split(' '), function(i) { | |
8677 | + var groupChildren = $(); | |
8678 | + var isOnlyButtons = true; | |
8679 | + var groupEl; | |
8680 | + | |
8681 | + $.each(this.split(','), function(j, buttonName) { | |
8682 | + var viewSpec; | |
8683 | + var buttonClick; | |
8684 | + var overrideText; // text explicitly set by calendar's constructor options. overcomes icons | |
8685 | + var defaultText; | |
8686 | + var themeIcon; | |
8687 | + var normalIcon; | |
8688 | + var innerHtml; | |
8689 | + var classes; | |
8690 | + var button; | |
8691 | + | |
8692 | + if (buttonName == 'title') { | |
8693 | + groupChildren = groupChildren.add($('<h2> </h2>')); // we always want it to take up height | |
8694 | + isOnlyButtons = false; | |
8695 | + } | |
8696 | + else { | |
8697 | + viewSpec = calendar.getViewSpec(buttonName); | |
8698 | + | |
8699 | + if (viewSpec) { | |
8700 | + buttonClick = function() { | |
8701 | + calendar.changeView(buttonName); | |
8702 | + }; | |
8703 | + viewsWithButtons.push(buttonName); | |
8704 | + overrideText = viewSpec.buttonTextOverride; | |
8705 | + defaultText = viewSpec.buttonTextDefault; | |
8706 | + } | |
8707 | + else if (calendar[buttonName]) { // a calendar method | |
8708 | + buttonClick = function() { | |
8709 | + calendar[buttonName](); | |
8710 | + }; | |
8711 | + overrideText = (calendar.overrides.buttonText || {})[buttonName]; | |
8712 | + defaultText = options.buttonText[buttonName]; // everything else is considered default | |
8713 | + } | |
8714 | + | |
8715 | + if (buttonClick) { | |
8716 | + | |
8717 | + themeIcon = options.themeButtonIcons[buttonName]; | |
8718 | + normalIcon = options.buttonIcons[buttonName]; | |
8719 | + | |
8720 | + if (overrideText) { | |
8721 | + innerHtml = htmlEscape(overrideText); | |
8722 | + } | |
8723 | + else if (themeIcon && options.theme) { | |
8724 | + innerHtml = "<span class='ui-icon ui-icon-" + themeIcon + "'></span>"; | |
8725 | + } | |
8726 | + else if (normalIcon && !options.theme) { | |
8727 | + innerHtml = "<span class='fc-icon fc-icon-" + normalIcon + "'></span>"; | |
8728 | + } | |
8729 | + else { | |
8730 | + innerHtml = htmlEscape(defaultText); | |
8731 | + } | |
8732 | + | |
8733 | + classes = [ | |
8734 | + 'fc-' + buttonName + '-button', | |
8735 | + tm + '-button', | |
8736 | + tm + '-state-default' | |
8737 | + ]; | |
8738 | + | |
8739 | + button = $( // type="button" so that it doesn't submit a form | |
8740 | + '<button type="button" class="' + classes.join(' ') + '">' + | |
8741 | + innerHtml + | |
8742 | + '</button>' | |
8743 | + ) | |
8744 | + .click(function() { | |
8745 | + // don't process clicks for disabled buttons | |
8746 | + if (!button.hasClass(tm + '-state-disabled')) { | |
8747 | + | |
8748 | + buttonClick(); | |
8749 | + | |
8750 | + // after the click action, if the button becomes the "active" tab, or disabled, | |
8751 | + // it should never have a hover class, so remove it now. | |
8752 | + if ( | |
8753 | + button.hasClass(tm + '-state-active') || | |
8754 | + button.hasClass(tm + '-state-disabled') | |
8755 | + ) { | |
8756 | + button.removeClass(tm + '-state-hover'); | |
8757 | + } | |
8758 | + } | |
8759 | + }) | |
8760 | + .mousedown(function() { | |
8761 | + // the *down* effect (mouse pressed in). | |
8762 | + // only on buttons that are not the "active" tab, or disabled | |
8763 | + button | |
8764 | + .not('.' + tm + '-state-active') | |
8765 | + .not('.' + tm + '-state-disabled') | |
8766 | + .addClass(tm + '-state-down'); | |
8767 | + }) | |
8768 | + .mouseup(function() { | |
8769 | + // undo the *down* effect | |
8770 | + button.removeClass(tm + '-state-down'); | |
8771 | + }) | |
8772 | + .hover( | |
8773 | + function() { | |
8774 | + // the *hover* effect. | |
8775 | + // only on buttons that are not the "active" tab, or disabled | |
8776 | + button | |
8777 | + .not('.' + tm + '-state-active') | |
8778 | + .not('.' + tm + '-state-disabled') | |
8779 | + .addClass(tm + '-state-hover'); | |
8780 | + }, | |
8781 | + function() { | |
8782 | + // undo the *hover* effect | |
8783 | + button | |
8784 | + .removeClass(tm + '-state-hover') | |
8785 | + .removeClass(tm + '-state-down'); // if mouseleave happens before mouseup | |
8786 | + } | |
8787 | + ); | |
8788 | + | |
8789 | + groupChildren = groupChildren.add(button); | |
8790 | + } | |
8791 | + } | |
8792 | + }); | |
8793 | + | |
8794 | + if (isOnlyButtons) { | |
8795 | + groupChildren | |
8796 | + .first().addClass(tm + '-corner-left').end() | |
8797 | + .last().addClass(tm + '-corner-right').end(); | |
8798 | + } | |
8799 | + | |
8800 | + if (groupChildren.length > 1) { | |
8801 | + groupEl = $('<div/>'); | |
8802 | + if (isOnlyButtons) { | |
8803 | + groupEl.addClass('fc-button-group'); | |
8804 | + } | |
8805 | + groupEl.append(groupChildren); | |
8806 | + sectionEl.append(groupEl); | |
8807 | + } | |
8808 | + else { | |
8809 | + sectionEl.append(groupChildren); // 1 or 0 children | |
8810 | + } | |
8811 | + }); | |
8812 | + } | |
8813 | + | |
8814 | + return sectionEl; | |
8815 | + } | |
8816 | + | |
8817 | + | |
8818 | + function updateTitle(text) { | |
8819 | + el.find('h2').text(text); | |
8820 | + } | |
8821 | + | |
8822 | + | |
8823 | + function activateButton(buttonName) { | |
8824 | + el.find('.fc-' + buttonName + '-button') | |
8825 | + .addClass(tm + '-state-active'); | |
8826 | + } | |
8827 | + | |
8828 | + | |
8829 | + function deactivateButton(buttonName) { | |
8830 | + el.find('.fc-' + buttonName + '-button') | |
8831 | + .removeClass(tm + '-state-active'); | |
8832 | + } | |
8833 | + | |
8834 | + | |
8835 | + function disableButton(buttonName) { | |
8836 | + el.find('.fc-' + buttonName + '-button') | |
8837 | + .attr('disabled', 'disabled') | |
8838 | + .addClass(tm + '-state-disabled'); | |
8839 | + } | |
8840 | + | |
8841 | + | |
8842 | + function enableButton(buttonName) { | |
8843 | + el.find('.fc-' + buttonName + '-button') | |
8844 | + .removeAttr('disabled') | |
8845 | + .removeClass(tm + '-state-disabled'); | |
8846 | + } | |
8847 | + | |
8848 | + | |
8849 | + function getViewsWithButtons() { | |
8850 | + return viewsWithButtons; | |
8851 | + } | |
8852 | + | |
8853 | +} | |
8854 | + | |
8855 | +;; | |
8856 | + | |
8857 | +fc.sourceNormalizers = []; | |
8858 | +fc.sourceFetchers = []; | |
8859 | + | |
8860 | +var ajaxDefaults = { | |
8861 | + dataType: 'json', | |
8862 | + cache: false | |
8863 | +}; | |
8864 | + | |
8865 | +var eventGUID = 1; | |
8866 | + | |
8867 | + | |
8868 | +function EventManager(options) { // assumed to be a calendar | |
8869 | + var t = this; | |
8870 | + | |
8871 | + | |
8872 | + // exports | |
8873 | + t.isFetchNeeded = isFetchNeeded; | |
8874 | + t.fetchEvents = fetchEvents; | |
8875 | + t.addEventSource = addEventSource; | |
8876 | + t.removeEventSource = removeEventSource; | |
8877 | + t.updateEvent = updateEvent; | |
8878 | + t.renderEvent = renderEvent; | |
8879 | + t.removeEvents = removeEvents; | |
8880 | + t.clientEvents = clientEvents; | |
8881 | + t.mutateEvent = mutateEvent; | |
8882 | + t.normalizeEventRange = normalizeEventRange; | |
8883 | + t.normalizeEventRangeTimes = normalizeEventRangeTimes; | |
8884 | + t.ensureVisibleEventRange = ensureVisibleEventRange; | |
8885 | + | |
8886 | + | |
8887 | + // imports | |
8888 | + var trigger = t.trigger; | |
8889 | + var getView = t.getView; | |
8890 | + var reportEvents = t.reportEvents; | |
8891 | + | |
8892 | + | |
8893 | + // locals | |
8894 | + var stickySource = { events: [] }; | |
8895 | + var sources = [ stickySource ]; | |
8896 | + var rangeStart, rangeEnd; | |
8897 | + var currentFetchID = 0; | |
8898 | + var pendingSourceCnt = 0; | |
8899 | + var loadingLevel = 0; | |
8900 | + var cache = []; // holds events that have already been expanded | |
8901 | + | |
8902 | + | |
8903 | + $.each( | |
8904 | + (options.events ? [ options.events ] : []).concat(options.eventSources || []), | |
8905 | + function(i, sourceInput) { | |
8906 | + var source = buildEventSource(sourceInput); | |
8907 | + if (source) { | |
8908 | + sources.push(source); | |
8909 | + } | |
8910 | + } | |
8911 | + ); | |
8912 | + | |
8913 | + | |
8914 | + | |
8915 | + /* Fetching | |
8916 | + -----------------------------------------------------------------------------*/ | |
8917 | + | |
8918 | + | |
8919 | + function isFetchNeeded(start, end) { | |
8920 | + return !rangeStart || // nothing has been fetched yet? | |
8921 | + // or, a part of the new range is outside of the old range? (after normalizing) | |
8922 | + start.clone().stripZone() < rangeStart.clone().stripZone() || | |
8923 | + end.clone().stripZone() > rangeEnd.clone().stripZone(); | |
8924 | + } | |
8925 | + | |
8926 | + | |
8927 | + function fetchEvents(start, end) { | |
8928 | + rangeStart = start; | |
8929 | + rangeEnd = end; | |
8930 | + cache = []; | |
8931 | + var fetchID = ++currentFetchID; | |
8932 | + var len = sources.length; | |
8933 | + pendingSourceCnt = len; | |
8934 | + for (var i=0; i<len; i++) { | |
8935 | + fetchEventSource(sources[i], fetchID); | |
8936 | + } | |
8937 | + } | |
8938 | + | |
8939 | + | |
8940 | + function fetchEventSource(source, fetchID) { | |
8941 | + _fetchEventSource(source, function(eventInputs) { | |
8942 | + var isArraySource = $.isArray(source.events); | |
8943 | + var i, eventInput; | |
8944 | + var abstractEvent; | |
8945 | + | |
8946 | + if (fetchID == currentFetchID) { | |
8947 | + | |
8948 | + if (eventInputs) { | |
8949 | + for (i = 0; i < eventInputs.length; i++) { | |
8950 | + eventInput = eventInputs[i]; | |
8951 | + | |
8952 | + if (isArraySource) { // array sources have already been convert to Event Objects | |
8953 | + abstractEvent = eventInput; | |
8954 | + } | |
8955 | + else { | |
8956 | + abstractEvent = buildEventFromInput(eventInput, source); | |
8957 | + } | |
8958 | + | |
8959 | + if (abstractEvent) { // not false (an invalid event) | |
8960 | + cache.push.apply( | |
8961 | + cache, | |
8962 | + expandEvent(abstractEvent) // add individual expanded events to the cache | |
8963 | + ); | |
8964 | + } | |
8965 | + } | |
8966 | + } | |
8967 | + | |
8968 | + pendingSourceCnt--; | |
8969 | + if (!pendingSourceCnt) { | |
8970 | + reportEvents(cache); | |
8971 | + } | |
8972 | + } | |
8973 | + }); | |
8974 | + } | |
8975 | + | |
8976 | + | |
8977 | + function _fetchEventSource(source, callback) { | |
8978 | + var i; | |
8979 | + var fetchers = fc.sourceFetchers; | |
8980 | + var res; | |
8981 | + | |
8982 | + for (i=0; i<fetchers.length; i++) { | |
8983 | + res = fetchers[i].call( | |
8984 | + t, // this, the Calendar object | |
8985 | + source, | |
8986 | + rangeStart.clone(), | |
8987 | + rangeEnd.clone(), | |
8988 | + options.timezone, | |
8989 | + callback | |
8990 | + ); | |
8991 | + | |
8992 | + if (res === true) { | |
8993 | + // the fetcher is in charge. made its own async request | |
8994 | + return; | |
8995 | + } | |
8996 | + else if (typeof res == 'object') { | |
8997 | + // the fetcher returned a new source. process it | |
8998 | + _fetchEventSource(res, callback); | |
8999 | + return; | |
9000 | + } | |
9001 | + } | |
9002 | + | |
9003 | + var events = source.events; | |
9004 | + if (events) { | |
9005 | + if ($.isFunction(events)) { | |
9006 | + pushLoading(); | |
9007 | + events.call( | |
9008 | + t, // this, the Calendar object | |
9009 | + rangeStart.clone(), | |
9010 | + rangeEnd.clone(), | |
9011 | + options.timezone, | |
9012 | + function(events) { | |
9013 | + callback(events); | |
9014 | + popLoading(); | |
9015 | + } | |
9016 | + ); | |
9017 | + } | |
9018 | + else if ($.isArray(events)) { | |
9019 | + callback(events); | |
9020 | + } | |
9021 | + else { | |
9022 | + callback(); | |
9023 | + } | |
9024 | + }else{ | |
9025 | + var url = source.url; | |
9026 | + if (url) { | |
9027 | + var success = source.success; | |
9028 | + var error = source.error; | |
9029 | + var complete = source.complete; | |
9030 | + | |
9031 | + // retrieve any outbound GET/POST $.ajax data from the options | |
9032 | + var customData; | |
9033 | + if ($.isFunction(source.data)) { | |
9034 | + // supplied as a function that returns a key/value object | |
9035 | + customData = source.data(); | |
9036 | + } | |
9037 | + else { | |
9038 | + // supplied as a straight key/value object | |
9039 | + customData = source.data; | |
9040 | + } | |
9041 | + | |
9042 | + // use a copy of the custom data so we can modify the parameters | |
9043 | + // and not affect the passed-in object. | |
9044 | + var data = $.extend({}, customData || {}); | |
9045 | + | |
9046 | + var startParam = firstDefined(source.startParam, options.startParam); | |
9047 | + var endParam = firstDefined(source.endParam, options.endParam); | |
9048 | + var timezoneParam = firstDefined(source.timezoneParam, options.timezoneParam); | |
9049 | + | |
9050 | + if (startParam) { | |
9051 | + data[startParam] = rangeStart.format(); | |
9052 | + } | |
9053 | + if (endParam) { | |
9054 | + data[endParam] = rangeEnd.format(); | |
9055 | + } | |
9056 | + if (options.timezone && options.timezone != 'local') { | |
9057 | + data[timezoneParam] = options.timezone; | |
9058 | + } | |
9059 | + | |
9060 | + pushLoading(); | |
9061 | + $.ajax($.extend({}, ajaxDefaults, source, { | |
9062 | + data: data, | |
9063 | + success: function(events) { | |
9064 | + events = events || []; | |
9065 | + var res = applyAll(success, this, arguments); | |
9066 | + if ($.isArray(res)) { | |
9067 | + events = res; | |
9068 | + } | |
9069 | + callback(events); | |
9070 | + }, | |
9071 | + error: function() { | |
9072 | + applyAll(error, this, arguments); | |
9073 | + callback(); | |
9074 | + }, | |
9075 | + complete: function() { | |
9076 | + applyAll(complete, this, arguments); | |
9077 | + popLoading(); | |
9078 | + } | |
9079 | + })); | |
9080 | + }else{ | |
9081 | + callback(); | |
9082 | + } | |
9083 | + } | |
9084 | + } | |
9085 | + | |
9086 | + | |
9087 | + | |
9088 | + /* Sources | |
9089 | + -----------------------------------------------------------------------------*/ | |
9090 | + | |
9091 | + | |
9092 | + function addEventSource(sourceInput) { | |
9093 | + var source = buildEventSource(sourceInput); | |
9094 | + if (source) { | |
9095 | + sources.push(source); | |
9096 | + pendingSourceCnt++; | |
9097 | + fetchEventSource(source, currentFetchID); // will eventually call reportEvents | |
9098 | + } | |
9099 | + } | |
9100 | + | |
9101 | + | |
9102 | + function buildEventSource(sourceInput) { // will return undefined if invalid source | |
9103 | + var normalizers = fc.sourceNormalizers; | |
9104 | + var source; | |
9105 | + var i; | |
9106 | + | |
9107 | + if ($.isFunction(sourceInput) || $.isArray(sourceInput)) { | |
9108 | + source = { events: sourceInput }; | |
9109 | + } | |
9110 | + else if (typeof sourceInput === 'string') { | |
9111 | + source = { url: sourceInput }; | |
9112 | + } | |
9113 | + else if (typeof sourceInput === 'object') { | |
9114 | + source = $.extend({}, sourceInput); // shallow copy | |
9115 | + } | |
9116 | + | |
9117 | + if (source) { | |
9118 | + | |
9119 | + // TODO: repeat code, same code for event classNames | |
9120 | + if (source.className) { | |
9121 | + if (typeof source.className === 'string') { | |
9122 | + source.className = source.className.split(/\s+/); | |
9123 | + } | |
9124 | + // otherwise, assumed to be an array | |
9125 | + } | |
9126 | + else { | |
9127 | + source.className = []; | |
9128 | + } | |
9129 | + | |
9130 | + // for array sources, we convert to standard Event Objects up front | |
9131 | + if ($.isArray(source.events)) { | |
9132 | + source.origArray = source.events; // for removeEventSource | |
9133 | + source.events = $.map(source.events, function(eventInput) { | |
9134 | + return buildEventFromInput(eventInput, source); | |
9135 | + }); | |
9136 | + } | |
9137 | + | |
9138 | + for (i=0; i<normalizers.length; i++) { | |
9139 | + normalizers[i].call(t, source); | |
9140 | + } | |
9141 | + | |
9142 | + return source; | |
9143 | + } | |
9144 | + } | |
9145 | + | |
9146 | + | |
9147 | + function removeEventSource(source) { | |
9148 | + sources = $.grep(sources, function(src) { | |
9149 | + return !isSourcesEqual(src, source); | |
9150 | + }); | |
9151 | + // remove all client events from that source | |
9152 | + cache = $.grep(cache, function(e) { | |
9153 | + return !isSourcesEqual(e.source, source); | |
9154 | + }); | |
9155 | + reportEvents(cache); | |
9156 | + } | |
9157 | + | |
9158 | + | |
9159 | + function isSourcesEqual(source1, source2) { | |
9160 | + return source1 && source2 && getSourcePrimitive(source1) == getSourcePrimitive(source2); | |
9161 | + } | |
9162 | + | |
9163 | + | |
9164 | + function getSourcePrimitive(source) { | |
9165 | + return ( | |
9166 | + (typeof source === 'object') ? // a normalized event source? | |
9167 | + (source.origArray || source.googleCalendarId || source.url || source.events) : // get the primitive | |
9168 | + null | |
9169 | + ) || | |
9170 | + source; // the given argument *is* the primitive | |
9171 | + } | |
9172 | + | |
9173 | + | |
9174 | + | |
9175 | + /* Manipulation | |
9176 | + -----------------------------------------------------------------------------*/ | |
9177 | + | |
9178 | + | |
9179 | + // Only ever called from the externally-facing API | |
9180 | + function updateEvent(event) { | |
9181 | + | |
9182 | + // massage start/end values, even if date string values | |
9183 | + event.start = t.moment(event.start); | |
9184 | + if (event.end) { | |
9185 | + event.end = t.moment(event.end); | |
9186 | + } | |
9187 | + else { | |
9188 | + event.end = null; | |
9189 | + } | |
9190 | + | |
9191 | + mutateEvent(event, getMiscEventProps(event)); // will handle start/end/allDay normalization | |
9192 | + reportEvents(cache); // reports event modifications (so we can redraw) | |
9193 | + } | |
9194 | + | |
9195 | + | |
9196 | + // Returns a hash of misc event properties that should be copied over to related events. | |
9197 | + function getMiscEventProps(event) { | |
9198 | + var props = {}; | |
9199 | + | |
9200 | + $.each(event, function(name, val) { | |
9201 | + if (isMiscEventPropName(name)) { | |
9202 | + if (val !== undefined && isAtomic(val)) { // a defined non-object | |
9203 | + props[name] = val; | |
9204 | + } | |
9205 | + } | |
9206 | + }); | |
9207 | + | |
9208 | + return props; | |
9209 | + } | |
9210 | + | |
9211 | + // non-date-related, non-id-related, non-secret | |
9212 | + function isMiscEventPropName(name) { | |
9213 | + return !/^_|^(id|allDay|start|end)$/.test(name); | |
9214 | + } | |
9215 | + | |
9216 | + | |
9217 | + // returns the expanded events that were created | |
9218 | + function renderEvent(eventInput, stick) { | |
9219 | + var abstractEvent = buildEventFromInput(eventInput); | |
9220 | + var events; | |
9221 | + var i, event; | |
9222 | + | |
9223 | + if (abstractEvent) { // not false (a valid input) | |
9224 | + events = expandEvent(abstractEvent); | |
9225 | + | |
9226 | + for (i = 0; i < events.length; i++) { | |
9227 | + event = events[i]; | |
9228 | + | |
9229 | + if (!event.source) { | |
9230 | + if (stick) { | |
9231 | + stickySource.events.push(event); | |
9232 | + event.source = stickySource; | |
9233 | + } | |
9234 | + cache.push(event); | |
9235 | + } | |
9236 | + } | |
9237 | + | |
9238 | + reportEvents(cache); | |
9239 | + | |
9240 | + return events; | |
9241 | + } | |
9242 | + | |
9243 | + return []; | |
9244 | + } | |
9245 | + | |
9246 | + | |
9247 | + function removeEvents(filter) { | |
9248 | + var eventID; | |
9249 | + var i; | |
9250 | + | |
9251 | + if (filter == null) { // null or undefined. remove all events | |
9252 | + filter = function() { return true; }; // will always match | |
9253 | + } | |
9254 | + else if (!$.isFunction(filter)) { // an event ID | |
9255 | + eventID = filter + ''; | |
9256 | + filter = function(event) { | |
9257 | + return event._id == eventID; | |
9258 | + }; | |
9259 | + } | |
9260 | + | |
9261 | + // Purge event(s) from our local cache | |
9262 | + cache = $.grep(cache, filter, true); // inverse=true | |
9263 | + | |
9264 | + // Remove events from array sources. | |
9265 | + // This works because they have been converted to official Event Objects up front. | |
9266 | + // (and as a result, event._id has been calculated). | |
9267 | + for (i=0; i<sources.length; i++) { | |
9268 | + if ($.isArray(sources[i].events)) { | |
9269 | + sources[i].events = $.grep(sources[i].events, filter, true); | |
9270 | + } | |
9271 | + } | |
9272 | + | |
9273 | + reportEvents(cache); | |
9274 | + } | |
9275 | + | |
9276 | + | |
9277 | + function clientEvents(filter) { | |
9278 | + if ($.isFunction(filter)) { | |
9279 | + return $.grep(cache, filter); | |
9280 | + } | |
9281 | + else if (filter != null) { // not null, not undefined. an event ID | |
9282 | + filter += ''; | |
9283 | + return $.grep(cache, function(e) { | |
9284 | + return e._id == filter; | |
9285 | + }); | |
9286 | + } | |
9287 | + return cache; // else, return all | |
9288 | + } | |
9289 | + | |
9290 | + | |
9291 | + | |
9292 | + /* Loading State | |
9293 | + -----------------------------------------------------------------------------*/ | |
9294 | + | |
9295 | + | |
9296 | + function pushLoading() { | |
9297 | + if (!(loadingLevel++)) { | |
9298 | + trigger('loading', null, true, getView()); | |
9299 | + } | |
9300 | + } | |
9301 | + | |
9302 | + | |
9303 | + function popLoading() { | |
9304 | + if (!(--loadingLevel)) { | |
9305 | + trigger('loading', null, false, getView()); | |
9306 | + } | |
9307 | + } | |
9308 | + | |
9309 | + | |
9310 | + | |
9311 | + /* Event Normalization | |
9312 | + -----------------------------------------------------------------------------*/ | |
9313 | + | |
9314 | + | |
9315 | + // Given a raw object with key/value properties, returns an "abstract" Event object. | |
9316 | + // An "abstract" event is an event that, if recurring, will not have been expanded yet. | |
9317 | + // Will return `false` when input is invalid. | |
9318 | + // `source` is optional | |
9319 | + function buildEventFromInput(input, source) { | |
9320 | + var out = {}; | |
9321 | + var start, end; | |
9322 | + var allDay; | |
9323 | + | |
9324 | + if (options.eventDataTransform) { | |
9325 | + input = options.eventDataTransform(input); | |
9326 | + } | |
9327 | + if (source && source.eventDataTransform) { | |
9328 | + input = source.eventDataTransform(input); | |
9329 | + } | |
9330 | + | |
9331 | + // Copy all properties over to the resulting object. | |
9332 | + // The special-case properties will be copied over afterwards. | |
9333 | + $.extend(out, input); | |
9334 | + | |
9335 | + if (source) { | |
9336 | + out.source = source; | |
9337 | + } | |
9338 | + | |
9339 | + out._id = input._id || (input.id === undefined ? '_fc' + eventGUID++ : input.id + ''); | |
9340 | + | |
9341 | + if (input.className) { | |
9342 | + if (typeof input.className == 'string') { | |
9343 | + out.className = input.className.split(/\s+/); | |
9344 | + } | |
9345 | + else { // assumed to be an array | |
9346 | + out.className = input.className; | |
9347 | + } | |
9348 | + } | |
9349 | + else { | |
9350 | + out.className = []; | |
9351 | + } | |
9352 | + | |
9353 | + start = input.start || input.date; // "date" is an alias for "start" | |
9354 | + end = input.end; | |
9355 | + | |
9356 | + // parse as a time (Duration) if applicable | |
9357 | + if (isTimeString(start)) { | |
9358 | + start = moment.duration(start); | |
9359 | + } | |
9360 | + if (isTimeString(end)) { | |
9361 | + end = moment.duration(end); | |
9362 | + } | |
9363 | + | |
9364 | + if (input.dow || moment.isDuration(start) || moment.isDuration(end)) { | |
9365 | + | |
9366 | + // the event is "abstract" (recurring) so don't calculate exact start/end dates just yet | |
9367 | + out.start = start ? moment.duration(start) : null; // will be a Duration or null | |
9368 | + out.end = end ? moment.duration(end) : null; // will be a Duration or null | |
9369 | + out._recurring = true; // our internal marker | |
9370 | + } | |
9371 | + else { | |
9372 | + | |
9373 | + if (start) { | |
9374 | + start = t.moment(start); | |
9375 | + if (!start.isValid()) { | |
9376 | + return false; | |
9377 | + } | |
9378 | + } | |
9379 | + | |
9380 | + if (end) { | |
9381 | + end = t.moment(end); | |
9382 | + if (!end.isValid()) { | |
9383 | + end = null; // let defaults take over | |
9384 | + } | |
9385 | + } | |
9386 | + | |
9387 | + allDay = input.allDay; | |
9388 | + if (allDay === undefined) { // still undefined? fallback to default | |
9389 | + allDay = firstDefined( | |
9390 | + source ? source.allDayDefault : undefined, | |
9391 | + options.allDayDefault | |
9392 | + ); | |
9393 | + // still undefined? normalizeEventRange will calculate it | |
9394 | + } | |
9395 | + | |
9396 | + assignDatesToEvent(start, end, allDay, out); | |
9397 | + } | |
9398 | + | |
9399 | + return out; | |
9400 | + } | |
9401 | + | |
9402 | + | |
9403 | + // Normalizes and assigns the given dates to the given partially-formed event object. | |
9404 | + // NOTE: mutates the given start/end moments. does not make a copy. | |
9405 | + function assignDatesToEvent(start, end, allDay, event) { | |
9406 | + event.start = start; | |
9407 | + event.end = end; | |
9408 | + event.allDay = allDay; | |
9409 | + normalizeEventRange(event); | |
9410 | + backupEventDates(event); | |
9411 | + } | |
9412 | + | |
9413 | + | |
9414 | + // Ensures proper values for allDay/start/end. Accepts an Event object, or a plain object with event-ish properties. | |
9415 | + // NOTE: Will modify the given object. | |
9416 | + function normalizeEventRange(props) { | |
9417 | + | |
9418 | + normalizeEventRangeTimes(props); | |
9419 | + | |
9420 | + if (props.end && !props.end.isAfter(props.start)) { | |
9421 | + props.end = null; | |
9422 | + } | |
9423 | + | |
9424 | + if (!props.end) { | |
9425 | + if (options.forceEventDuration) { | |
9426 | + props.end = t.getDefaultEventEnd(props.allDay, props.start); | |
9427 | + } | |
9428 | + else { | |
9429 | + props.end = null; | |
9430 | + } | |
9431 | + } | |
9432 | + } | |
9433 | + | |
9434 | + | |
9435 | + // Ensures the allDay property exists and the timeliness of the start/end dates are consistent | |
9436 | + function normalizeEventRangeTimes(range) { | |
9437 | + if (range.allDay == null) { | |
9438 | + range.allDay = !(range.start.hasTime() || (range.end && range.end.hasTime())); | |
9439 | + } | |
9440 | + | |
9441 | + if (range.allDay) { | |
9442 | + range.start.stripTime(); | |
9443 | + if (range.end) { | |
9444 | + // TODO: consider nextDayThreshold here? If so, will require a lot of testing and adjustment | |
9445 | + range.end.stripTime(); | |
9446 | + } | |
9447 | + } | |
9448 | + else { | |
9449 | + if (!range.start.hasTime()) { | |
9450 | + range.start = t.rezoneDate(range.start); // will assign a 00:00 time | |
9451 | + } | |
9452 | + if (range.end && !range.end.hasTime()) { | |
9453 | + range.end = t.rezoneDate(range.end); // will assign a 00:00 time | |
9454 | + } | |
9455 | + } | |
9456 | + } | |
9457 | + | |
9458 | + | |
9459 | + // If `range` is a proper range with a start and end, returns the original object. | |
9460 | + // If missing an end, computes a new range with an end, computing it as if it were an event. | |
9461 | + // TODO: make this a part of the event -> eventRange system | |
9462 | + function ensureVisibleEventRange(range) { | |
9463 | + var allDay; | |
9464 | + | |
9465 | + if (!range.end) { | |
9466 | + | |
9467 | + allDay = range.allDay; // range might be more event-ish than we think | |
9468 | + if (allDay == null) { | |
9469 | + allDay = !range.start.hasTime(); | |
9470 | + } | |
9471 | + | |
9472 | + range = $.extend({}, range); // make a copy, copying over other misc properties | |
9473 | + range.end = t.getDefaultEventEnd(allDay, range.start); | |
9474 | + } | |
9475 | + return range; | |
9476 | + } | |
9477 | + | |
9478 | + | |
9479 | + // If the given event is a recurring event, break it down into an array of individual instances. | |
9480 | + // If not a recurring event, return an array with the single original event. | |
9481 | + // If given a falsy input (probably because of a failed buildEventFromInput call), returns an empty array. | |
9482 | + // HACK: can override the recurring window by providing custom rangeStart/rangeEnd (for businessHours). | |
9483 | + function expandEvent(abstractEvent, _rangeStart, _rangeEnd) { | |
9484 | + var events = []; | |
9485 | + var dowHash; | |
9486 | + var dow; | |
9487 | + var i; | |
9488 | + var date; | |
9489 | + var startTime, endTime; | |
9490 | + var start, end; | |
9491 | + var event; | |
9492 | + | |
9493 | + _rangeStart = _rangeStart || rangeStart; | |
9494 | + _rangeEnd = _rangeEnd || rangeEnd; | |
9495 | + | |
9496 | + if (abstractEvent) { | |
9497 | + if (abstractEvent._recurring) { | |
9498 | + | |
9499 | + // make a boolean hash as to whether the event occurs on each day-of-week | |
9500 | + if ((dow = abstractEvent.dow)) { | |
9501 | + dowHash = {}; | |
9502 | + for (i = 0; i < dow.length; i++) { | |
9503 | + dowHash[dow[i]] = true; | |
9504 | + } | |
9505 | + } | |
9506 | + | |
9507 | + // iterate through every day in the current range | |
9508 | + date = _rangeStart.clone().stripTime(); // holds the date of the current day | |
9509 | + while (date.isBefore(_rangeEnd)) { | |
9510 | + | |
9511 | + if (!dowHash || dowHash[date.day()]) { // if everyday, or this particular day-of-week | |
9512 | + | |
9513 | + startTime = abstractEvent.start; // the stored start and end properties are times (Durations) | |
9514 | + endTime = abstractEvent.end; // " | |
9515 | + start = date.clone(); | |
9516 | + end = null; | |
9517 | + | |
9518 | + if (startTime) { | |
9519 | + start = start.time(startTime); | |
9520 | + } | |
9521 | + if (endTime) { | |
9522 | + end = date.clone().time(endTime); | |
9523 | + } | |
9524 | + | |
9525 | + event = $.extend({}, abstractEvent); // make a copy of the original | |
9526 | + assignDatesToEvent( | |
9527 | + start, end, | |
9528 | + !startTime && !endTime, // allDay? | |
9529 | + event | |
9530 | + ); | |
9531 | + events.push(event); | |
9532 | + } | |
9533 | + | |
9534 | + date.add(1, 'days'); | |
9535 | + } | |
9536 | + } | |
9537 | + else { | |
9538 | + events.push(abstractEvent); // return the original event. will be a one-item array | |
9539 | + } | |
9540 | + } | |
9541 | + | |
9542 | + return events; | |
9543 | + } | |
9544 | + | |
9545 | + | |
9546 | + | |
9547 | + /* Event Modification Math | |
9548 | + -----------------------------------------------------------------------------------------*/ | |
9549 | + | |
9550 | + | |
9551 | + // Modifies an event and all related events by applying the given properties. | |
9552 | + // Special date-diffing logic is used for manipulation of dates. | |
9553 | + // If `props` does not contain start/end dates, the updated values are assumed to be the event's current start/end. | |
9554 | + // All date comparisons are done against the event's pristine _start and _end dates. | |
9555 | + // Returns an object with delta information and a function to undo all operations. | |
9556 | + // For making computations in a granularity greater than day/time, specify largeUnit. | |
9557 | + // NOTE: The given `newProps` might be mutated for normalization purposes. | |
9558 | + function mutateEvent(event, newProps, largeUnit) { | |
9559 | + var miscProps = {}; | |
9560 | + var oldProps; | |
9561 | + var clearEnd; | |
9562 | + var startDelta; | |
9563 | + var endDelta; | |
9564 | + var durationDelta; | |
9565 | + var undoFunc; | |
9566 | + | |
9567 | + // diffs the dates in the appropriate way, returning a duration | |
9568 | + function diffDates(date1, date0) { // date1 - date0 | |
9569 | + if (largeUnit) { | |
9570 | + return diffByUnit(date1, date0, largeUnit); | |
9571 | + } | |
9572 | + else if (newProps.allDay) { | |
9573 | + return diffDay(date1, date0); | |
9574 | + } | |
9575 | + else { | |
9576 | + return diffDayTime(date1, date0); | |
9577 | + } | |
9578 | + } | |
9579 | + | |
9580 | + newProps = newProps || {}; | |
9581 | + | |
9582 | + // normalize new date-related properties | |
9583 | + if (!newProps.start) { | |
9584 | + newProps.start = event.start.clone(); | |
9585 | + } | |
9586 | + if (newProps.end === undefined) { | |
9587 | + newProps.end = event.end ? event.end.clone() : null; | |
9588 | + } | |
9589 | + if (newProps.allDay == null) { // is null or undefined? | |
9590 | + newProps.allDay = event.allDay; | |
9591 | + } | |
9592 | + normalizeEventRange(newProps); | |
9593 | + | |
9594 | + // create normalized versions of the original props to compare against | |
9595 | + // need a real end value, for diffing | |
9596 | + oldProps = { | |
9597 | + start: event._start.clone(), | |
9598 | + end: event._end ? event._end.clone() : t.getDefaultEventEnd(event._allDay, event._start), | |
9599 | + allDay: newProps.allDay // normalize the dates in the same regard as the new properties | |
9600 | + }; | |
9601 | + normalizeEventRange(oldProps); | |
9602 | + | |
9603 | + // need to clear the end date if explicitly changed to null | |
9604 | + clearEnd = event._end !== null && newProps.end === null; | |
9605 | + | |
9606 | + // compute the delta for moving the start date | |
9607 | + startDelta = diffDates(newProps.start, oldProps.start); | |
9608 | + | |
9609 | + // compute the delta for moving the end date | |
9610 | + if (newProps.end) { | |
9611 | + endDelta = diffDates(newProps.end, oldProps.end); | |
9612 | + durationDelta = endDelta.subtract(startDelta); | |
9613 | + } | |
9614 | + else { | |
9615 | + durationDelta = null; | |
9616 | + } | |
9617 | + | |
9618 | + // gather all non-date-related properties | |
9619 | + $.each(newProps, function(name, val) { | |
9620 | + if (isMiscEventPropName(name)) { | |
9621 | + if (val !== undefined) { | |
9622 | + miscProps[name] = val; | |
9623 | + } | |
9624 | + } | |
9625 | + }); | |
9626 | + | |
9627 | + // apply the operations to the event and all related events | |
9628 | + undoFunc = mutateEvents( | |
9629 | + clientEvents(event._id), // get events with this ID | |
9630 | + clearEnd, | |
9631 | + newProps.allDay, | |
9632 | + startDelta, | |
9633 | + durationDelta, | |
9634 | + miscProps | |
9635 | + ); | |
9636 | + | |
9637 | + return { | |
9638 | + dateDelta: startDelta, | |
9639 | + durationDelta: durationDelta, | |
9640 | + undo: undoFunc | |
9641 | + }; | |
9642 | + } | |
9643 | + | |
9644 | + | |
9645 | + // Modifies an array of events in the following ways (operations are in order): | |
9646 | + // - clear the event's `end` | |
9647 | + // - convert the event to allDay | |
9648 | + // - add `dateDelta` to the start and end | |
9649 | + // - add `durationDelta` to the event's duration | |
9650 | + // - assign `miscProps` to the event | |
9651 | + // | |
9652 | + // Returns a function that can be called to undo all the operations. | |
9653 | + // | |
9654 | + // TODO: don't use so many closures. possible memory issues when lots of events with same ID. | |
9655 | + // | |
9656 | + function mutateEvents(events, clearEnd, allDay, dateDelta, durationDelta, miscProps) { | |
9657 | + var isAmbigTimezone = t.getIsAmbigTimezone(); | |
9658 | + var undoFunctions = []; | |
9659 | + | |
9660 | + // normalize zero-length deltas to be null | |
9661 | + if (dateDelta && !dateDelta.valueOf()) { dateDelta = null; } | |
9662 | + if (durationDelta && !durationDelta.valueOf()) { durationDelta = null; } | |
9663 | + | |
9664 | + $.each(events, function(i, event) { | |
9665 | + var oldProps; | |
9666 | + var newProps; | |
9667 | + | |
9668 | + // build an object holding all the old values, both date-related and misc. | |
9669 | + // for the undo function. | |
9670 | + oldProps = { | |
9671 | + start: event.start.clone(), | |
9672 | + end: event.end ? event.end.clone() : null, | |
9673 | + allDay: event.allDay | |
9674 | + }; | |
9675 | + $.each(miscProps, function(name) { | |
9676 | + oldProps[name] = event[name]; | |
9677 | + }); | |
9678 | + | |
9679 | + // new date-related properties. work off the original date snapshot. | |
9680 | + // ok to use references because they will be thrown away when backupEventDates is called. | |
9681 | + newProps = { | |
9682 | + start: event._start, | |
9683 | + end: event._end, | |
9684 | + allDay: allDay // normalize the dates in the same regard as the new properties | |
9685 | + }; | |
9686 | + normalizeEventRange(newProps); // massages start/end/allDay | |
9687 | + | |
9688 | + // strip or ensure the end date | |
9689 | + if (clearEnd) { | |
9690 | + newProps.end = null; | |
9691 | + } | |
9692 | + else if (durationDelta && !newProps.end) { // the duration translation requires an end date | |
9693 | + newProps.end = t.getDefaultEventEnd(newProps.allDay, newProps.start); | |
9694 | + } | |
9695 | + | |
9696 | + if (dateDelta) { | |
9697 | + newProps.start.add(dateDelta); | |
9698 | + if (newProps.end) { | |
9699 | + newProps.end.add(dateDelta); | |
9700 | + } | |
9701 | + } | |
9702 | + | |
9703 | + if (durationDelta) { | |
9704 | + newProps.end.add(durationDelta); // end already ensured above | |
9705 | + } | |
9706 | + | |
9707 | + // if the dates have changed, and we know it is impossible to recompute the | |
9708 | + // timezone offsets, strip the zone. | |
9709 | + if ( | |
9710 | + isAmbigTimezone && | |
9711 | + !newProps.allDay && | |
9712 | + (dateDelta || durationDelta) | |
9713 | + ) { | |
9714 | + newProps.start.stripZone(); | |
9715 | + if (newProps.end) { | |
9716 | + newProps.end.stripZone(); | |
9717 | + } | |
9718 | + } | |
9719 | + | |
9720 | + $.extend(event, miscProps, newProps); // copy over misc props, then date-related props | |
9721 | + backupEventDates(event); // regenerate internal _start/_end/_allDay | |
9722 | + | |
9723 | + undoFunctions.push(function() { | |
9724 | + $.extend(event, oldProps); | |
9725 | + backupEventDates(event); // regenerate internal _start/_end/_allDay | |
9726 | + }); | |
9727 | + }); | |
9728 | + | |
9729 | + return function() { | |
9730 | + for (var i = 0; i < undoFunctions.length; i++) { | |
9731 | + undoFunctions[i](); | |
9732 | + } | |
9733 | + }; | |
9734 | + } | |
9735 | + | |
9736 | + | |
9737 | + /* Business Hours | |
9738 | + -----------------------------------------------------------------------------------------*/ | |
9739 | + | |
9740 | + t.getBusinessHoursEvents = getBusinessHoursEvents; | |
9741 | + | |
9742 | + | |
9743 | + // Returns an array of events as to when the business hours occur in the given view. | |
9744 | + // Abuse of our event system :( | |
9745 | + function getBusinessHoursEvents(wholeDay) { | |
9746 | + var optionVal = options.businessHours; | |
9747 | + var defaultVal = { | |
9748 | + className: 'fc-nonbusiness', | |
9749 | + start: '09:00', | |
9750 | + end: '17:00', | |
9751 | + dow: [ 1, 2, 3, 4, 5 ], // monday - friday | |
9752 | + rendering: 'inverse-background' | |
9753 | + }; | |
9754 | + var view = t.getView(); | |
9755 | + var eventInput; | |
9756 | + | |
9757 | + if (optionVal) { // `true` (which means "use the defaults") or an override object | |
9758 | + eventInput = $.extend( | |
9759 | + {}, // copy to a new object in either case | |
9760 | + defaultVal, | |
9761 | + typeof optionVal === 'object' ? optionVal : {} // override the defaults | |
9762 | + ); | |
9763 | + } | |
9764 | + | |
9765 | + if (eventInput) { | |
9766 | + | |
9767 | + // if a whole-day series is requested, clear the start/end times | |
9768 | + if (wholeDay) { | |
9769 | + eventInput.start = null; | |
9770 | + eventInput.end = null; | |
9771 | + } | |
9772 | + | |
9773 | + return expandEvent( | |
9774 | + buildEventFromInput(eventInput), | |
9775 | + view.start, | |
9776 | + view.end | |
9777 | + ); | |
9778 | + } | |
9779 | + | |
9780 | + return []; | |
9781 | + } | |
9782 | + | |
9783 | + | |
9784 | + /* Overlapping / Constraining | |
9785 | + -----------------------------------------------------------------------------------------*/ | |
9786 | + | |
9787 | + t.isEventRangeAllowed = isEventRangeAllowed; | |
9788 | + t.isSelectionRangeAllowed = isSelectionRangeAllowed; | |
9789 | + t.isExternalDropRangeAllowed = isExternalDropRangeAllowed; | |
9790 | + | |
9791 | + | |
9792 | + function isEventRangeAllowed(range, event) { | |
9793 | + var source = event.source || {}; | |
9794 | + var constraint = firstDefined( | |
9795 | + event.constraint, | |
9796 | + source.constraint, | |
9797 | + options.eventConstraint | |
9798 | + ); | |
9799 | + var overlap = firstDefined( | |
9800 | + event.overlap, | |
9801 | + source.overlap, | |
9802 | + options.eventOverlap | |
9803 | + ); | |
9804 | + | |
9805 | + range = ensureVisibleEventRange(range); // ensure a proper range with an end for isRangeAllowed | |
9806 | + | |
9807 | + return isRangeAllowed(range, constraint, overlap, event); | |
9808 | + } | |
9809 | + | |
9810 | + | |
9811 | + function isSelectionRangeAllowed(range) { | |
9812 | + return isRangeAllowed(range, options.selectConstraint, options.selectOverlap); | |
9813 | + } | |
9814 | + | |
9815 | + | |
9816 | + // when `eventProps` is defined, consider this an event. | |
9817 | + // `eventProps` can contain misc non-date-related info about the event. | |
9818 | + function isExternalDropRangeAllowed(range, eventProps) { | |
9819 | + var eventInput; | |
9820 | + var event; | |
9821 | + | |
9822 | + // note: very similar logic is in View's reportExternalDrop | |
9823 | + if (eventProps) { | |
9824 | + eventInput = $.extend({}, eventProps, range); | |
9825 | + event = expandEvent(buildEventFromInput(eventInput))[0]; | |
9826 | + } | |
9827 | + | |
9828 | + if (event) { | |
9829 | + return isEventRangeAllowed(range, event); | |
9830 | + } | |
9831 | + else { // treat it as a selection | |
9832 | + | |
9833 | + range = ensureVisibleEventRange(range); // ensure a proper range with an end for isSelectionRangeAllowed | |
9834 | + | |
9835 | + return isSelectionRangeAllowed(range); | |
9836 | + } | |
9837 | + } | |
9838 | + | |
9839 | + | |
9840 | + // Returns true if the given range (caused by an event drop/resize or a selection) is allowed to exist | |
9841 | + // according to the constraint/overlap settings. | |
9842 | + // `event` is not required if checking a selection. | |
9843 | + function isRangeAllowed(range, constraint, overlap, event) { | |
9844 | + var constraintEvents; | |
9845 | + var anyContainment; | |
9846 | + var peerEvents; | |
9847 | + var i, peerEvent; | |
9848 | + var peerOverlap; | |
9849 | + | |
9850 | + // normalize. fyi, we're normalizing in too many places :( | |
9851 | + range = $.extend({}, range); // copy all properties in case there are misc non-date properties | |
9852 | + range.start = range.start.clone().stripZone(); | |
9853 | + range.end = range.end.clone().stripZone(); | |
9854 | + | |
9855 | + // the range must be fully contained by at least one of produced constraint events | |
9856 | + if (constraint != null) { | |
9857 | + | |
9858 | + // not treated as an event! intermediate data structure | |
9859 | + // TODO: use ranges in the future | |
9860 | + constraintEvents = constraintToEvents(constraint); | |
9861 | + | |
9862 | + anyContainment = false; | |
9863 | + for (i = 0; i < constraintEvents.length; i++) { | |
9864 | + if (eventContainsRange(constraintEvents[i], range)) { | |
9865 | + anyContainment = true; | |
9866 | + break; | |
9867 | + } | |
9868 | + } | |
9869 | + | |
9870 | + if (!anyContainment) { | |
9871 | + return false; | |
9872 | + } | |
9873 | + } | |
9874 | + | |
9875 | + peerEvents = t.getPeerEvents(event, range); | |
9876 | + | |
9877 | + for (i = 0; i < peerEvents.length; i++) { | |
9878 | + peerEvent = peerEvents[i]; | |
9879 | + | |
9880 | + // there needs to be an actual intersection before disallowing anything | |
9881 | + if (eventIntersectsRange(peerEvent, range)) { | |
9882 | + | |
9883 | + // evaluate overlap for the given range and short-circuit if necessary | |
9884 | + if (overlap === false) { | |
9885 | + return false; | |
9886 | + } | |
9887 | + // if the event's overlap is a test function, pass the peer event in question as the first param | |
9888 | + else if (typeof overlap === 'function' && !overlap(peerEvent, event)) { | |
9889 | + return false; | |
9890 | + } | |
9891 | + | |
9892 | + // if we are computing if the given range is allowable for an event, consider the other event's | |
9893 | + // EventObject-specific or Source-specific `overlap` property | |
9894 | + if (event) { | |
9895 | + peerOverlap = firstDefined( | |
9896 | + peerEvent.overlap, | |
9897 | + (peerEvent.source || {}).overlap | |
9898 | + // we already considered the global `eventOverlap` | |
9899 | + ); | |
9900 | + if (peerOverlap === false) { | |
9901 | + return false; | |
9902 | + } | |
9903 | + // if the peer event's overlap is a test function, pass the subject event as the first param | |
9904 | + if (typeof peerOverlap === 'function' && !peerOverlap(event, peerEvent)) { | |
9905 | + return false; | |
9906 | + } | |
9907 | + } | |
9908 | + } | |
9909 | + } | |
9910 | + | |
9911 | + return true; | |
9912 | + } | |
9913 | + | |
9914 | + | |
9915 | + // Given an event input from the API, produces an array of event objects. Possible event inputs: | |
9916 | + // 'businessHours' | |
9917 | + // An event ID (number or string) | |
9918 | + // An object with specific start/end dates or a recurring event (like what businessHours accepts) | |
9919 | + function constraintToEvents(constraintInput) { | |
9920 | + | |
9921 | + if (constraintInput === 'businessHours') { | |
9922 | + return getBusinessHoursEvents(); | |
9923 | + } | |
9924 | + | |
9925 | + if (typeof constraintInput === 'object') { | |
9926 | + return expandEvent(buildEventFromInput(constraintInput)); | |
9927 | + } | |
9928 | + | |
9929 | + return clientEvents(constraintInput); // probably an ID | |
9930 | + } | |
9931 | + | |
9932 | + | |
9933 | + // Does the event's date range fully contain the given range? | |
9934 | + // start/end already assumed to have stripped zones :( | |
9935 | + function eventContainsRange(event, range) { | |
9936 | + var eventStart = event.start.clone().stripZone(); | |
9937 | + var eventEnd = t.getEventEnd(event).stripZone(); | |
9938 | + | |
9939 | + return range.start >= eventStart && range.end <= eventEnd; | |
9940 | + } | |
9941 | + | |
9942 | + | |
9943 | + // Does the event's date range intersect with the given range? | |
9944 | + // start/end already assumed to have stripped zones :( | |
9945 | + function eventIntersectsRange(event, range) { | |
9946 | + var eventStart = event.start.clone().stripZone(); | |
9947 | + var eventEnd = t.getEventEnd(event).stripZone(); | |
9948 | + | |
9949 | + return range.start < eventEnd && range.end > eventStart; | |
9950 | + } | |
9951 | + | |
9952 | + | |
9953 | + t.getEventCache = function() { | |
9954 | + return cache; | |
9955 | + }; | |
9956 | + | |
9957 | +} | |
9958 | + | |
9959 | + | |
9960 | +// Returns a list of events that the given event should be compared against when being considered for a move to | |
9961 | +// the specified range. Attached to the Calendar's prototype because EventManager is a mixin for a Calendar. | |
9962 | +Calendar.prototype.getPeerEvents = function(event, range) { | |
9963 | + var cache = this.getEventCache(); | |
9964 | + var peerEvents = []; | |
9965 | + var i, otherEvent; | |
9966 | + | |
9967 | + for (i = 0; i < cache.length; i++) { | |
9968 | + otherEvent = cache[i]; | |
9969 | + if ( | |
9970 | + !event || | |
9971 | + event._id !== otherEvent._id // don't compare the event to itself or other related [repeating] events | |
9972 | + ) { | |
9973 | + peerEvents.push(otherEvent); | |
9974 | + } | |
9975 | + } | |
9976 | + | |
9977 | + return peerEvents; | |
9978 | +}; | |
9979 | + | |
9980 | + | |
9981 | +// updates the "backup" properties, which are preserved in order to compute diffs later on. | |
9982 | +function backupEventDates(event) { | |
9983 | + event._allDay = event.allDay; | |
9984 | + event._start = event.start.clone(); | |
9985 | + event._end = event.end ? event.end.clone() : null; | |
9986 | +} | |
9987 | + | |
9988 | +;; | |
9989 | + | |
9990 | +/* An abstract class for the "basic" views, as well as month view. Renders one or more rows of day cells. | |
9991 | +----------------------------------------------------------------------------------------------------------------------*/ | |
9992 | +// It is a manager for a DayGrid subcomponent, which does most of the heavy lifting. | |
9993 | +// It is responsible for managing width/height. | |
9994 | + | |
9995 | +var BasicView = fcViews.basic = View.extend({ | |
9996 | + | |
9997 | + dayGrid: null, // the main subcomponent that does most of the heavy lifting | |
9998 | + | |
9999 | + dayNumbersVisible: false, // display day numbers on each day cell? | |
10000 | + weekNumbersVisible: false, // display week numbers along the side? | |
10001 | + | |
10002 | + weekNumberWidth: null, // width of all the week-number cells running down the side | |
10003 | + | |
10004 | + headRowEl: null, // the fake row element of the day-of-week header | |
10005 | + | |
10006 | + | |
10007 | + initialize: function() { | |
10008 | + this.dayGrid = new DayGrid(this); | |
10009 | + this.coordMap = this.dayGrid.coordMap; // the view's date-to-cell mapping is identical to the subcomponent's | |
10010 | + }, | |
10011 | + | |
10012 | + | |
10013 | + // Sets the display range and computes all necessary dates | |
10014 | + setRange: function(range) { | |
10015 | + View.prototype.setRange.call(this, range); // call the super-method | |
10016 | + | |
10017 | + this.dayGrid.breakOnWeeks = /year|month|week/.test(this.intervalUnit); // do before setRange | |
10018 | + this.dayGrid.setRange(range); | |
10019 | + }, | |
10020 | + | |
10021 | + | |
10022 | + // Compute the value to feed into setRange. Overrides superclass. | |
10023 | + computeRange: function(date) { | |
10024 | + var range = View.prototype.computeRange.call(this, date); // get value from the super-method | |
10025 | + | |
10026 | + // year and month views should be aligned with weeks. this is already done for week | |
10027 | + if (/year|month/.test(range.intervalUnit)) { | |
10028 | + range.start.startOf('week'); | |
10029 | + range.start = this.skipHiddenDays(range.start); | |
10030 | + | |
10031 | + // make end-of-week if not already | |
10032 | + if (range.end.weekday()) { | |
10033 | + range.end.add(1, 'week').startOf('week'); | |
10034 | + range.end = this.skipHiddenDays(range.end, -1, true); // exclusively move backwards | |
10035 | + } | |
10036 | + } | |
10037 | + | |
10038 | + return range; | |
10039 | + }, | |
10040 | + | |
10041 | + | |
10042 | + // Renders the view into `this.el`, which should already be assigned | |
10043 | + render: function() { | |
10044 | + | |
10045 | + this.dayNumbersVisible = this.dayGrid.rowCnt > 1; // TODO: make grid responsible | |
10046 | + this.weekNumbersVisible = this.opt('weekNumbers'); | |
10047 | + this.dayGrid.numbersVisible = this.dayNumbersVisible || this.weekNumbersVisible; | |
10048 | + | |
10049 | + this.el.addClass('fc-basic-view').html(this.renderHtml()); | |
10050 | + | |
10051 | + this.headRowEl = this.el.find('thead .fc-row'); | |
10052 | + | |
10053 | + this.scrollerEl = this.el.find('.fc-day-grid-container'); | |
10054 | + this.dayGrid.coordMap.containerEl = this.scrollerEl; // constrain clicks/etc to the dimensions of the scroller | |
10055 | + | |
10056 | + this.dayGrid.setElement(this.el.find('.fc-day-grid')); | |
10057 | + this.dayGrid.renderDates(this.hasRigidRows()); | |
10058 | + }, | |
10059 | + | |
10060 | + | |
10061 | + // Unrenders the content of the view. Since we haven't separated skeleton rendering from date rendering, | |
10062 | + // always completely kill the dayGrid's rendering. | |
10063 | + destroy: function() { | |
10064 | + this.dayGrid.destroyDates(); | |
10065 | + this.dayGrid.removeElement(); | |
10066 | + }, | |
10067 | + | |
10068 | + | |
10069 | + renderBusinessHours: function() { | |
10070 | + this.dayGrid.renderBusinessHours(); | |
10071 | + }, | |
10072 | + | |
10073 | + | |
10074 | + // Builds the HTML skeleton for the view. | |
10075 | + // The day-grid component will render inside of a container defined by this HTML. | |
10076 | + renderHtml: function() { | |
10077 | + return '' + | |
10078 | + '<table>' + | |
10079 | + '<thead class="fc-head">' + | |
10080 | + '<tr>' + | |
10081 | + '<td class="' + this.widgetHeaderClass + '">' + | |
10082 | + this.dayGrid.headHtml() + // render the day-of-week headers | |
10083 | + '</td>' + | |
10084 | + '</tr>' + | |
10085 | + '</thead>' + | |
10086 | + '<tbody class="fc-body">' + | |
10087 | + '<tr>' + | |
10088 | + '<td class="' + this.widgetContentClass + '">' + | |
10089 | + '<div class="fc-day-grid-container">' + | |
10090 | + '<div class="fc-day-grid"/>' + | |
10091 | + '</div>' + | |
10092 | + '</td>' + | |
10093 | + '</tr>' + | |
10094 | + '</tbody>' + | |
10095 | + '</table>'; | |
10096 | + }, | |
10097 | + | |
10098 | + | |
10099 | + // Generates the HTML that will go before the day-of week header cells. | |
10100 | + // Queried by the DayGrid subcomponent when generating rows. Ordering depends on isRTL. | |
10101 | + headIntroHtml: function() { | |
10102 | + if (this.weekNumbersVisible) { | |
10103 | + return '' + | |
10104 | + '<th class="fc-week-number ' + this.widgetHeaderClass + '" ' + this.weekNumberStyleAttr() + '>' + | |
10105 | + '<span>' + // needed for matchCellWidths | |
10106 | + htmlEscape(this.opt('weekNumberTitle')) + | |
10107 | + '</span>' + | |
10108 | + '</th>'; | |
10109 | + } | |
10110 | + }, | |
10111 | + | |
10112 | + | |
10113 | + // Generates the HTML that will go before content-skeleton cells that display the day/week numbers. | |
10114 | + // Queried by the DayGrid subcomponent. Ordering depends on isRTL. | |
10115 | + numberIntroHtml: function(row) { | |
10116 | + if (this.weekNumbersVisible) { | |
10117 | + return '' + | |
10118 | + '<td class="fc-week-number" ' + this.weekNumberStyleAttr() + '>' + | |
10119 | + '<span>' + // needed for matchCellWidths | |
10120 | + this.dayGrid.getCell(row, 0).start.format('w') + | |
10121 | + '</span>' + | |
10122 | + '</td>'; | |
10123 | + } | |
10124 | + }, | |
10125 | + | |
10126 | + | |
10127 | + // Generates the HTML that goes before the day bg cells for each day-row. | |
10128 | + // Queried by the DayGrid subcomponent. Ordering depends on isRTL. | |
10129 | + dayIntroHtml: function() { | |
10130 | + if (this.weekNumbersVisible) { | |
10131 | + return '<td class="fc-week-number ' + this.widgetContentClass + '" ' + | |
10132 | + this.weekNumberStyleAttr() + '></td>'; | |
10133 | + } | |
10134 | + }, | |
10135 | + | |
10136 | + | |
10137 | + // Generates the HTML that goes before every other type of row generated by DayGrid. Ordering depends on isRTL. | |
10138 | + // Affects helper-skeleton and highlight-skeleton rows. | |
10139 | + introHtml: function() { | |
10140 | + if (this.weekNumbersVisible) { | |
10141 | + return '<td class="fc-week-number" ' + this.weekNumberStyleAttr() + '></td>'; | |
10142 | + } | |
10143 | + }, | |
10144 | + | |
10145 | + | |
10146 | + // Generates the HTML for the <td>s of the "number" row in the DayGrid's content skeleton. | |
10147 | + // The number row will only exist if either day numbers or week numbers are turned on. | |
10148 | + numberCellHtml: function(cell) { | |
10149 | + var date = cell.start; | |
10150 | + var classes; | |
10151 | + | |
10152 | + if (!this.dayNumbersVisible) { // if there are week numbers but not day numbers | |
10153 | + return '<td/>'; // will create an empty space above events :( | |
10154 | + } | |
10155 | + | |
10156 | + classes = this.dayGrid.getDayClasses(date); | |
10157 | + classes.unshift('fc-day-number'); | |
10158 | + | |
10159 | + return '' + | |
10160 | + '<td class="' + classes.join(' ') + '" data-date="' + date.format() + '">' + | |
10161 | + date.date() + | |
10162 | + '</td>'; | |
10163 | + }, | |
10164 | + | |
10165 | + | |
10166 | + // Generates an HTML attribute string for setting the width of the week number column, if it is known | |
10167 | + weekNumberStyleAttr: function() { | |
10168 | + if (this.weekNumberWidth !== null) { | |
10169 | + return 'style="width:' + this.weekNumberWidth + 'px"'; | |
10170 | + } | |
10171 | + return ''; | |
10172 | + }, | |
10173 | + | |
10174 | + | |
10175 | + // Determines whether each row should have a constant height | |
10176 | + hasRigidRows: function() { | |
10177 | + var eventLimit = this.opt('eventLimit'); | |
10178 | + return eventLimit && typeof eventLimit !== 'number'; | |
10179 | + }, | |
10180 | + | |
10181 | + | |
10182 | + /* Dimensions | |
10183 | + ------------------------------------------------------------------------------------------------------------------*/ | |
10184 | + | |
10185 | + | |
10186 | + // Refreshes the horizontal dimensions of the view | |
10187 | + updateWidth: function() { | |
10188 | + if (this.weekNumbersVisible) { | |
10189 | + // Make sure all week number cells running down the side have the same width. | |
10190 | + // Record the width for cells created later. | |
10191 | + this.weekNumberWidth = matchCellWidths( | |
10192 | + this.el.find('.fc-week-number') | |
10193 | + ); | |
10194 | + } | |
10195 | + }, | |
10196 | + | |
10197 | + | |
10198 | + // Adjusts the vertical dimensions of the view to the specified values | |
10199 | + setHeight: function(totalHeight, isAuto) { | |
10200 | + var eventLimit = this.opt('eventLimit'); | |
10201 | + var scrollerHeight; | |
10202 | + | |
10203 | + // reset all heights to be natural | |
10204 | + unsetScroller(this.scrollerEl); | |
10205 | + uncompensateScroll(this.headRowEl); | |
10206 | + | |
10207 | + this.dayGrid.destroySegPopover(); // kill the "more" popover if displayed | |
10208 | + | |
10209 | + // is the event limit a constant level number? | |
10210 | + if (eventLimit && typeof eventLimit === 'number') { | |
10211 | + this.dayGrid.limitRows(eventLimit); // limit the levels first so the height can redistribute after | |
10212 | + } | |
10213 | + | |
10214 | + scrollerHeight = this.computeScrollerHeight(totalHeight); | |
10215 | + this.setGridHeight(scrollerHeight, isAuto); | |
10216 | + | |
10217 | + // is the event limit dynamically calculated? | |
10218 | + if (eventLimit && typeof eventLimit !== 'number') { | |
10219 | + this.dayGrid.limitRows(eventLimit); // limit the levels after the grid's row heights have been set | |
10220 | + } | |
10221 | + | |
10222 | + if (!isAuto && setPotentialScroller(this.scrollerEl, scrollerHeight)) { // using scrollbars? | |
10223 | + | |
10224 | + compensateScroll(this.headRowEl, getScrollbarWidths(this.scrollerEl)); | |
10225 | + | |
10226 | + // doing the scrollbar compensation might have created text overflow which created more height. redo | |
10227 | + scrollerHeight = this.computeScrollerHeight(totalHeight); | |
10228 | + this.scrollerEl.height(scrollerHeight); | |
10229 | + } | |
10230 | + }, | |
10231 | + | |
10232 | + | |
10233 | + // Sets the height of just the DayGrid component in this view | |
10234 | + setGridHeight: function(height, isAuto) { | |
10235 | + if (isAuto) { | |
10236 | + undistributeHeight(this.dayGrid.rowEls); // let the rows be their natural height with no expanding | |
10237 | + } | |
10238 | + else { | |
10239 | + distributeHeight(this.dayGrid.rowEls, height, true); // true = compensate for height-hogging rows | |
10240 | + } | |
10241 | + }, | |
10242 | + | |
10243 | + | |
10244 | + /* Events | |
10245 | + ------------------------------------------------------------------------------------------------------------------*/ | |
10246 | + | |
10247 | + | |
10248 | + // Renders the given events onto the view and populates the segments array | |
10249 | + renderEvents: function(events) { | |
10250 | + this.dayGrid.renderEvents(events); | |
10251 | + | |
10252 | + this.updateHeight(); // must compensate for events that overflow the row | |
10253 | + }, | |
10254 | + | |
10255 | + | |
10256 | + // Retrieves all segment objects that are rendered in the view | |
10257 | + getEventSegs: function() { | |
10258 | + return this.dayGrid.getEventSegs(); | |
10259 | + }, | |
10260 | + | |
10261 | + | |
10262 | + // Unrenders all event elements and clears internal segment data | |
10263 | + destroyEvents: function() { | |
10264 | + this.dayGrid.destroyEvents(); | |
10265 | + | |
10266 | + // we DON'T need to call updateHeight() because: | |
10267 | + // A) a renderEvents() call always happens after this, which will eventually call updateHeight() | |
10268 | + // B) in IE8, this causes a flash whenever events are rerendered | |
10269 | + }, | |
10270 | + | |
10271 | + | |
10272 | + /* Dragging (for both events and external elements) | |
10273 | + ------------------------------------------------------------------------------------------------------------------*/ | |
10274 | + | |
10275 | + | |
10276 | + // A returned value of `true` signals that a mock "helper" event has been rendered. | |
10277 | + renderDrag: function(dropLocation, seg) { | |
10278 | + return this.dayGrid.renderDrag(dropLocation, seg); | |
10279 | + }, | |
10280 | + | |
10281 | + | |
10282 | + destroyDrag: function() { | |
10283 | + this.dayGrid.destroyDrag(); | |
10284 | + }, | |
10285 | + | |
10286 | + | |
10287 | + /* Selection | |
10288 | + ------------------------------------------------------------------------------------------------------------------*/ | |
10289 | + | |
10290 | + | |
10291 | + // Renders a visual indication of a selection | |
10292 | + renderSelection: function(range) { | |
10293 | + this.dayGrid.renderSelection(range); | |
10294 | + }, | |
10295 | + | |
10296 | + | |
10297 | + // Unrenders a visual indications of a selection | |
10298 | + destroySelection: function() { | |
10299 | + this.dayGrid.destroySelection(); | |
10300 | + } | |
10301 | + | |
10302 | +}); | |
10303 | + | |
10304 | +;; | |
10305 | + | |
10306 | +/* A month view with day cells running in rows (one-per-week) and columns | |
10307 | +----------------------------------------------------------------------------------------------------------------------*/ | |
10308 | + | |
10309 | +var MonthView = fcViews.month = BasicView.extend({ | |
10310 | + | |
10311 | + // Produces information about what range to display | |
10312 | + computeRange: function(date) { | |
10313 | + var range = BasicView.prototype.computeRange.call(this, date); // get value from super-method | |
10314 | + var rowCnt; | |
10315 | + | |
10316 | + // ensure 6 weeks | |
10317 | + if (this.isFixedWeeks()) { | |
10318 | + rowCnt = Math.ceil(range.end.diff(range.start, 'weeks', true)); // could be partial weeks due to hiddenDays | |
10319 | + range.end.add(6 - rowCnt, 'weeks'); | |
10320 | + } | |
10321 | + | |
10322 | + return range; | |
10323 | + }, | |
10324 | + | |
10325 | + | |
10326 | + // Overrides the default BasicView behavior to have special multi-week auto-height logic | |
10327 | + setGridHeight: function(height, isAuto) { | |
10328 | + | |
10329 | + isAuto = isAuto || this.opt('weekMode') === 'variable'; // LEGACY: weekMode is deprecated | |
10330 | + | |
10331 | + // if auto, make the height of each row the height that it would be if there were 6 weeks | |
10332 | + if (isAuto) { | |
10333 | + height *= this.rowCnt / 6; | |
10334 | + } | |
10335 | + | |
10336 | + distributeHeight(this.dayGrid.rowEls, height, !isAuto); // if auto, don't compensate for height-hogging rows | |
10337 | + }, | |
10338 | + | |
10339 | + | |
10340 | + isFixedWeeks: function() { | |
10341 | + var weekMode = this.opt('weekMode'); // LEGACY: weekMode is deprecated | |
10342 | + if (weekMode) { | |
10343 | + return weekMode === 'fixed'; // if any other type of weekMode, assume NOT fixed | |
10344 | + } | |
10345 | + | |
10346 | + return this.opt('fixedWeekCount'); | |
10347 | + } | |
10348 | + | |
10349 | +}); | |
10350 | + | |
10351 | +MonthView.duration = { months: 1 }; // important for prev/next | |
10352 | + | |
10353 | +MonthView.defaults = { | |
10354 | + fixedWeekCount: true | |
10355 | +}; | |
10356 | +;; | |
10357 | + | |
10358 | +/* A week view with simple day cells running horizontally | |
10359 | +----------------------------------------------------------------------------------------------------------------------*/ | |
10360 | + | |
10361 | +fcViews.basicWeek = { | |
10362 | + type: 'basic', | |
10363 | + duration: { weeks: 1 } | |
10364 | +}; | |
10365 | +;; | |
10366 | + | |
10367 | +/* A view with a single simple day cell | |
10368 | +----------------------------------------------------------------------------------------------------------------------*/ | |
10369 | + | |
10370 | +fcViews.basicDay = { | |
10371 | + type: 'basic', | |
10372 | + duration: { days: 1 } | |
10373 | +}; | |
10374 | +;; | |
10375 | + | |
10376 | +/* An abstract class for all agenda-related views. Displays one more columns with time slots running vertically. | |
10377 | +----------------------------------------------------------------------------------------------------------------------*/ | |
10378 | +// Is a manager for the TimeGrid subcomponent and possibly the DayGrid subcomponent (if allDaySlot is on). | |
10379 | +// Responsible for managing width/height. | |
10380 | + | |
10381 | +var AGENDA_DEFAULTS = { | |
10382 | + allDaySlot: true, | |
10383 | + allDayText: 'all-day', | |
10384 | + scrollTime: '06:00:00', | |
10385 | + slotDuration: '00:30:00', | |
10386 | + minTime: '00:00:00', | |
10387 | + maxTime: '24:00:00', | |
10388 | + slotEventOverlap: true // a bad name. confused with overlap/constraint system | |
10389 | +}; | |
10390 | + | |
10391 | +var AGENDA_ALL_DAY_EVENT_LIMIT = 5; | |
10392 | + | |
10393 | +var AgendaView = fcViews.agenda = View.extend({ | |
10394 | + | |
10395 | + timeGrid: null, // the main time-grid subcomponent of this view | |
10396 | + dayGrid: null, // the "all-day" subcomponent. if all-day is turned off, this will be null | |
10397 | + | |
10398 | + axisWidth: null, // the width of the time axis running down the side | |
10399 | + | |
10400 | + noScrollRowEls: null, // set of fake row elements that must compensate when scrollerEl has scrollbars | |
10401 | + | |
10402 | + // when the time-grid isn't tall enough to occupy the given height, we render an <hr> underneath | |
10403 | + bottomRuleEl: null, | |
10404 | + bottomRuleHeight: null, | |
10405 | + | |
10406 | + | |
10407 | + initialize: function() { | |
10408 | + this.timeGrid = new TimeGrid(this); | |
10409 | + | |
10410 | + if (this.opt('allDaySlot')) { // should we display the "all-day" area? | |
10411 | + this.dayGrid = new DayGrid(this); // the all-day subcomponent of this view | |
10412 | + | |
10413 | + // the coordinate grid will be a combination of both subcomponents' grids | |
10414 | + this.coordMap = new ComboCoordMap([ | |
10415 | + this.dayGrid.coordMap, | |
10416 | + this.timeGrid.coordMap | |
10417 | + ]); | |
10418 | + } | |
10419 | + else { | |
10420 | + this.coordMap = this.timeGrid.coordMap; | |
10421 | + } | |
10422 | + }, | |
10423 | + | |
10424 | + | |
10425 | + /* Rendering | |
10426 | + ------------------------------------------------------------------------------------------------------------------*/ | |
10427 | + | |
10428 | + | |
10429 | + // Sets the display range and computes all necessary dates | |
10430 | + setRange: function(range) { | |
10431 | + View.prototype.setRange.call(this, range); // call the super-method | |
10432 | + | |
10433 | + this.timeGrid.setRange(range); | |
10434 | + if (this.dayGrid) { | |
10435 | + this.dayGrid.setRange(range); | |
10436 | + } | |
10437 | + }, | |
10438 | + | |
10439 | + | |
10440 | + // Renders the view into `this.el`, which has already been assigned | |
10441 | + render: function() { | |
10442 | + | |
10443 | + this.el.addClass('fc-agenda-view').html(this.renderHtml()); | |
10444 | + | |
10445 | + // the element that wraps the time-grid that will probably scroll | |
10446 | + this.scrollerEl = this.el.find('.fc-time-grid-container'); | |
10447 | + this.timeGrid.coordMap.containerEl = this.scrollerEl; // don't accept clicks/etc outside of this | |
10448 | + | |
10449 | + this.timeGrid.setElement(this.el.find('.fc-time-grid')); | |
10450 | + this.timeGrid.renderDates(); | |
10451 | + | |
10452 | + // the <hr> that sometimes displays under the time-grid | |
10453 | + this.bottomRuleEl = $('<hr class="fc-divider ' + this.widgetHeaderClass + '"/>') | |
10454 | + .appendTo(this.timeGrid.el); // inject it into the time-grid | |
10455 | + | |
10456 | + if (this.dayGrid) { | |
10457 | + this.dayGrid.setElement(this.el.find('.fc-day-grid')); | |
10458 | + this.dayGrid.renderDates(); | |
10459 | + | |
10460 | + // have the day-grid extend it's coordinate area over the <hr> dividing the two grids | |
10461 | + this.dayGrid.bottomCoordPadding = this.dayGrid.el.next('hr').outerHeight(); | |
10462 | + } | |
10463 | + | |
10464 | + this.noScrollRowEls = this.el.find('.fc-row:not(.fc-scroller *)'); // fake rows not within the scroller | |
10465 | + }, | |
10466 | + | |
10467 | + | |
10468 | + // Unrenders the content of the view. Since we haven't separated skeleton rendering from date rendering, | |
10469 | + // always completely kill each grid's rendering. | |
10470 | + destroy: function() { | |
10471 | + this.timeGrid.destroyDates(); | |
10472 | + this.timeGrid.removeElement(); | |
10473 | + | |
10474 | + if (this.dayGrid) { | |
10475 | + this.dayGrid.destroyDates(); | |
10476 | + this.dayGrid.removeElement(); | |
10477 | + } | |
10478 | + }, | |
10479 | + | |
10480 | + | |
10481 | + renderBusinessHours: function() { | |
10482 | + this.timeGrid.renderBusinessHours(); | |
10483 | + | |
10484 | + if (this.dayGrid) { | |
10485 | + this.dayGrid.renderBusinessHours(); | |
10486 | + } | |
10487 | + }, | |
10488 | + | |
10489 | + | |
10490 | + // Builds the HTML skeleton for the view. | |
10491 | + // The day-grid and time-grid components will render inside containers defined by this HTML. | |
10492 | + renderHtml: function() { | |
10493 | + return '' + | |
10494 | + '<table>' + | |
10495 | + '<thead class="fc-head">' + | |
10496 | + '<tr>' + | |
10497 | + '<td class="' + this.widgetHeaderClass + '">' + | |
10498 | + this.timeGrid.headHtml() + // render the day-of-week headers | |
10499 | + '</td>' + | |
10500 | + '</tr>' + | |
10501 | + '</thead>' + | |
10502 | + '<tbody class="fc-body">' + | |
10503 | + '<tr>' + | |
10504 | + '<td class="' + this.widgetContentClass + '">' + | |
10505 | + (this.dayGrid ? | |
10506 | + '<div class="fc-day-grid"/>' + | |
10507 | + '<hr class="fc-divider ' + this.widgetHeaderClass + '"/>' : | |
10508 | + '' | |
10509 | + ) + | |
10510 | + '<div class="fc-time-grid-container">' + | |
10511 | + '<div class="fc-time-grid"/>' + | |
10512 | + '</div>' + | |
10513 | + '</td>' + | |
10514 | + '</tr>' + | |
10515 | + '</tbody>' + | |
10516 | + '</table>'; | |
10517 | + }, | |
10518 | + | |
10519 | + | |
10520 | + // Generates the HTML that will go before the day-of week header cells. | |
10521 | + // Queried by the TimeGrid subcomponent when generating rows. Ordering depends on isRTL. | |
10522 | + headIntroHtml: function() { | |
10523 | + var date; | |
10524 | + var weekText; | |
10525 | + | |
10526 | + if (this.opt('weekNumbers')) { | |
10527 | + date = this.timeGrid.getCell(0).start; | |
10528 | + weekText = date.format(this.opt('smallWeekFormat')); | |
10529 | + | |
10530 | + return '' + | |
10531 | + '<th class="fc-axis fc-week-number ' + this.widgetHeaderClass + '" ' + this.axisStyleAttr() + '>' + | |
10532 | + '<span>' + // needed for matchCellWidths | |
10533 | + htmlEscape(weekText) + | |
10534 | + '</span>' + | |
10535 | + '</th>'; | |
10536 | + } | |
10537 | + else { | |
10538 | + return '<th class="fc-axis ' + this.widgetHeaderClass + '" ' + this.axisStyleAttr() + '></th>'; | |
10539 | + } | |
10540 | + }, | |
10541 | + | |
10542 | + | |
10543 | + // Generates the HTML that goes before the all-day cells. | |
10544 | + // Queried by the DayGrid subcomponent when generating rows. Ordering depends on isRTL. | |
10545 | + dayIntroHtml: function() { | |
10546 | + return '' + | |
10547 | + '<td class="fc-axis ' + this.widgetContentClass + '" ' + this.axisStyleAttr() + '>' + | |
10548 | + '<span>' + // needed for matchCellWidths | |
10549 | + (this.opt('allDayHtml') || htmlEscape(this.opt('allDayText'))) + | |
10550 | + '</span>' + | |
10551 | + '</td>'; | |
10552 | + }, | |
10553 | + | |
10554 | + | |
10555 | + // Generates the HTML that goes before the bg of the TimeGrid slot area. Long vertical column. | |
10556 | + slotBgIntroHtml: function() { | |
10557 | + return '<td class="fc-axis ' + this.widgetContentClass + '" ' + this.axisStyleAttr() + '></td>'; | |
10558 | + }, | |
10559 | + | |
10560 | + | |
10561 | + // Generates the HTML that goes before all other types of cells. | |
10562 | + // Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid. | |
10563 | + // Queried by the TimeGrid and DayGrid subcomponents when generating rows. Ordering depends on isRTL. | |
10564 | + introHtml: function() { | |
10565 | + return '<td class="fc-axis" ' + this.axisStyleAttr() + '></td>'; | |
10566 | + }, | |
10567 | + | |
10568 | + | |
10569 | + // Generates an HTML attribute string for setting the width of the axis, if it is known | |
10570 | + axisStyleAttr: function() { | |
10571 | + if (this.axisWidth !== null) { | |
10572 | + return 'style="width:' + this.axisWidth + 'px"'; | |
10573 | + } | |
10574 | + return ''; | |
10575 | + }, | |
10576 | + | |
10577 | + | |
10578 | + /* Dimensions | |
10579 | + ------------------------------------------------------------------------------------------------------------------*/ | |
10580 | + | |
10581 | + | |
10582 | + updateSize: function(isResize) { | |
10583 | + this.timeGrid.updateSize(isResize); | |
10584 | + | |
10585 | + View.prototype.updateSize.call(this, isResize); // call the super-method | |
10586 | + }, | |
10587 | + | |
10588 | + | |
10589 | + // Refreshes the horizontal dimensions of the view | |
10590 | + updateWidth: function() { | |
10591 | + // make all axis cells line up, and record the width so newly created axis cells will have it | |
10592 | + this.axisWidth = matchCellWidths(this.el.find('.fc-axis')); | |
10593 | + }, | |
10594 | + | |
10595 | + | |
10596 | + // Adjusts the vertical dimensions of the view to the specified values | |
10597 | + setHeight: function(totalHeight, isAuto) { | |
10598 | + var eventLimit; | |
10599 | + var scrollerHeight; | |
10600 | + | |
10601 | + if (this.bottomRuleHeight === null) { | |
10602 | + // calculate the height of the rule the very first time | |
10603 | + this.bottomRuleHeight = this.bottomRuleEl.outerHeight(); | |
10604 | + } | |
10605 | + this.bottomRuleEl.hide(); // .show() will be called later if this <hr> is necessary | |
10606 | + | |
10607 | + // reset all dimensions back to the original state | |
10608 | + this.scrollerEl.css('overflow', ''); | |
10609 | + unsetScroller(this.scrollerEl); | |
10610 | + uncompensateScroll(this.noScrollRowEls); | |
10611 | + | |
10612 | + // limit number of events in the all-day area | |
10613 | + if (this.dayGrid) { | |
10614 | + this.dayGrid.destroySegPopover(); // kill the "more" popover if displayed | |
10615 | + | |
10616 | + eventLimit = this.opt('eventLimit'); | |
10617 | + if (eventLimit && typeof eventLimit !== 'number') { | |
10618 | + eventLimit = AGENDA_ALL_DAY_EVENT_LIMIT; // make sure "auto" goes to a real number | |
10619 | + } | |
10620 | + if (eventLimit) { | |
10621 | + this.dayGrid.limitRows(eventLimit); | |
10622 | + } | |
10623 | + } | |
10624 | + | |
10625 | + if (!isAuto) { // should we force dimensions of the scroll container, or let the contents be natural height? | |
10626 | + | |
10627 | + scrollerHeight = this.computeScrollerHeight(totalHeight); | |
10628 | + if (setPotentialScroller(this.scrollerEl, scrollerHeight)) { // using scrollbars? | |
10629 | + | |
10630 | + // make the all-day and header rows lines up | |
10631 | + compensateScroll(this.noScrollRowEls, getScrollbarWidths(this.scrollerEl)); | |
10632 | + | |
10633 | + // the scrollbar compensation might have changed text flow, which might affect height, so recalculate | |
10634 | + // and reapply the desired height to the scroller. | |
10635 | + scrollerHeight = this.computeScrollerHeight(totalHeight); | |
10636 | + this.scrollerEl.height(scrollerHeight); | |
10637 | + } | |
10638 | + else { // no scrollbars | |
10639 | + // still, force a height and display the bottom rule (marks the end of day) | |
10640 | + this.scrollerEl.height(scrollerHeight).css('overflow', 'hidden'); // in case <hr> goes outside | |
10641 | + this.bottomRuleEl.show(); | |
10642 | + } | |
10643 | + } | |
10644 | + }, | |
10645 | + | |
10646 | + | |
10647 | + // Computes the initial pre-configured scroll state prior to allowing the user to change it | |
10648 | + computeInitialScroll: function() { | |
10649 | + var scrollTime = moment.duration(this.opt('scrollTime')); | |
10650 | + var top = this.timeGrid.computeTimeTop(scrollTime); | |
10651 | + | |
10652 | + // zoom can give weird floating-point values. rather scroll a little bit further | |
10653 | + top = Math.ceil(top); | |
10654 | + | |
10655 | + if (top) { | |
10656 | + top++; // to overcome top border that slots beyond the first have. looks better | |
10657 | + } | |
10658 | + | |
10659 | + return top; | |
10660 | + }, | |
10661 | + | |
10662 | + | |
10663 | + /* Events | |
10664 | + ------------------------------------------------------------------------------------------------------------------*/ | |
10665 | + | |
10666 | + | |
10667 | + // Renders events onto the view and populates the View's segment array | |
10668 | + renderEvents: function(events) { | |
10669 | + var dayEvents = []; | |
10670 | + var timedEvents = []; | |
10671 | + var daySegs = []; | |
10672 | + var timedSegs; | |
10673 | + var i; | |
10674 | + | |
10675 | + // separate the events into all-day and timed | |
10676 | + for (i = 0; i < events.length; i++) { | |
10677 | + if (events[i].allDay) { | |
10678 | + dayEvents.push(events[i]); | |
10679 | + } | |
10680 | + else { | |
10681 | + timedEvents.push(events[i]); | |
10682 | + } | |
10683 | + } | |
10684 | + | |
10685 | + // render the events in the subcomponents | |
10686 | + timedSegs = this.timeGrid.renderEvents(timedEvents); | |
10687 | + if (this.dayGrid) { | |
10688 | + daySegs = this.dayGrid.renderEvents(dayEvents); | |
10689 | + } | |
10690 | + | |
10691 | + // the all-day area is flexible and might have a lot of events, so shift the height | |
10692 | + this.updateHeight(); | |
10693 | + }, | |
10694 | + | |
10695 | + | |
10696 | + // Retrieves all segment objects that are rendered in the view | |
10697 | + getEventSegs: function() { | |
10698 | + return this.timeGrid.getEventSegs().concat( | |
10699 | + this.dayGrid ? this.dayGrid.getEventSegs() : [] | |
10700 | + ); | |
10701 | + }, | |
10702 | + | |
10703 | + | |
10704 | + // Unrenders all event elements and clears internal segment data | |
10705 | + destroyEvents: function() { | |
10706 | + | |
10707 | + // destroy the events in the subcomponents | |
10708 | + this.timeGrid.destroyEvents(); | |
10709 | + if (this.dayGrid) { | |
10710 | + this.dayGrid.destroyEvents(); | |
10711 | + } | |
10712 | + | |
10713 | + // we DON'T need to call updateHeight() because: | |
10714 | + // A) a renderEvents() call always happens after this, which will eventually call updateHeight() | |
10715 | + // B) in IE8, this causes a flash whenever events are rerendered | |
10716 | + }, | |
10717 | + | |
10718 | + | |
10719 | + /* Dragging (for events and external elements) | |
10720 | + ------------------------------------------------------------------------------------------------------------------*/ | |
10721 | + | |
10722 | + | |
10723 | + // A returned value of `true` signals that a mock "helper" event has been rendered. | |
10724 | + renderDrag: function(dropLocation, seg) { | |
10725 | + if (dropLocation.start.hasTime()) { | |
10726 | + return this.timeGrid.renderDrag(dropLocation, seg); | |
10727 | + } | |
10728 | + else if (this.dayGrid) { | |
10729 | + return this.dayGrid.renderDrag(dropLocation, seg); | |
10730 | + } | |
10731 | + }, | |
10732 | + | |
10733 | + | |
10734 | + destroyDrag: function() { | |
10735 | + this.timeGrid.destroyDrag(); | |
10736 | + if (this.dayGrid) { | |
10737 | + this.dayGrid.destroyDrag(); | |
10738 | + } | |
10739 | + }, | |
10740 | + | |
10741 | + | |
10742 | + /* Selection | |
10743 | + ------------------------------------------------------------------------------------------------------------------*/ | |
10744 | + | |
10745 | + | |
10746 | + // Renders a visual indication of a selection | |
10747 | + renderSelection: function(range) { | |
10748 | + if (range.start.hasTime() || range.end.hasTime()) { | |
10749 | + this.timeGrid.renderSelection(range); | |
10750 | + } | |
10751 | + else if (this.dayGrid) { | |
10752 | + this.dayGrid.renderSelection(range); | |
10753 | + } | |
10754 | + }, | |
10755 | + | |
10756 | + | |
10757 | + // Unrenders a visual indications of a selection | |
10758 | + destroySelection: function() { | |
10759 | + this.timeGrid.destroySelection(); | |
10760 | + if (this.dayGrid) { | |
10761 | + this.dayGrid.destroySelection(); | |
10762 | + } | |
10763 | + } | |
10764 | + | |
10765 | +}); | |
10766 | + | |
10767 | +AgendaView.defaults = AGENDA_DEFAULTS; | |
10768 | + | |
10769 | +;; | |
10770 | + | |
10771 | +/* A week view with an all-day cell area at the top, and a time grid below | |
10772 | +----------------------------------------------------------------------------------------------------------------------*/ | |
10773 | + | |
10774 | +fcViews.agendaWeek = { | |
10775 | + type: 'agenda', | |
10776 | + duration: { weeks: 1 } | |
10777 | +}; | |
10778 | +;; | |
10779 | + | |
10780 | +/* A day view with an all-day cell area at the top, and a time grid below | |
10781 | +----------------------------------------------------------------------------------------------------------------------*/ | |
10782 | + | |
10783 | +fcViews.agendaDay = { | |
10784 | + type: 'agenda', | |
10785 | + duration: { days: 1 } | |
10786 | +}; | |
10787 | +;; | |
10788 | + | |
10789 | +return fc; // export for Node/CommonJS | |
10790 | +}); | |
12 | 10791 | \ No newline at end of file | ... | ... |
app/partials/scheduler/scheduler.controller.js
... | ... | @@ -3,80 +3,59 @@ |
3 | 3 | //Load controller |
4 | 4 | angular.module('acufuel') |
5 | 5 | |
6 | - .controller('schedulerController', ['$scope', function($scope) { | |
6 | + .controller('schedulerController', ['$scope','$compile', 'uiCalendarConfig', function($scope, $compile, uiCalendarConfig) { | |
7 | 7 | |
8 | 8 | $scope.test = "Testing..."; |
9 | + | |
10 | + var date = new Date(); | |
11 | + var d = date.getDate(); | |
12 | + var m = date.getMonth(); | |
13 | + var y = date.getFullYear(); | |
14 | + | |
9 | 15 | |
10 | - | |
11 | - /*$('#calendar').fullCalendar({ | |
12 | - header: { | |
13 | - left: 'prev,next today', | |
14 | - center: 'title', | |
15 | - right: 'month,agendaWeek,agendaDay,listWeek' | |
16 | - }, | |
17 | - defaultDate: '2017-05-12', | |
18 | - navLinks: true, // can click day/week names to navigate views | |
19 | - editable: true, | |
20 | - eventLimit: true, // allow "more" link when too many events | |
21 | - events: [ | |
22 | - { | |
23 | - title: 'All Day Event', | |
24 | - start: '2017-05-01' | |
25 | - }, | |
26 | - { | |
27 | - title: 'Long Event', | |
28 | - start: '2017-05-07', | |
29 | - end: '2017-05-10' | |
30 | - }, | |
31 | - { | |
32 | - id: 999, | |
33 | - title: 'Repeating Event', | |
34 | - start: '2017-05-09T16:00:00' | |
35 | - }, | |
36 | - { | |
37 | - id: 999, | |
38 | - title: 'Repeating Event', | |
39 | - start: '2017-05-16T16:00:00' | |
40 | - }, | |
41 | - { | |
42 | - title: 'Conference', | |
43 | - start: '2017-05-11', | |
44 | - end: '2017-05-13' | |
45 | - }, | |
46 | - { | |
47 | - title: 'Meeting', | |
48 | - start: '2017-05-12T10:30:00', | |
49 | - end: '2017-05-12T12:30:00' | |
50 | - }, | |
51 | - { | |
52 | - title: 'Lunch', | |
53 | - start: '2017-05-12T12:00:00' | |
54 | - }, | |
55 | - { | |
56 | - title: 'Meeting', | |
57 | - start: '2017-05-12T14:30:00' | |
58 | - }, | |
59 | - { | |
60 | - title: 'Happy Hour', | |
61 | - start: '2017-05-12T17:30:00' | |
62 | - }, | |
63 | - { | |
64 | - title: 'Dinner', | |
65 | - start: '2017-05-12T20:00:00' | |
66 | - }, | |
67 | - { | |
68 | - title: 'Birthday Party', | |
69 | - start: '2017-05-13T07:00:00' | |
70 | - }, | |
71 | - { | |
72 | - title: 'Click for Google', | |
73 | - start: '2017-05-28' | |
16 | + $scope.eventList=[ | |
17 | + {title:'Event 1'}, | |
18 | + {title:'Event 2'}, | |
19 | + {title:'Event 3'}, | |
20 | + {title:'Event 4'} | |
21 | + ]; | |
22 | + | |
23 | + $scope.eventSources=[]; | |
24 | + | |
25 | + $scope.events = [ | |
26 | + {title: 'All Day Event', start: new Date(y, m, 1)}, | |
27 | + {title: 'Birthday Party', start: new Date(y, m, d + 1, 19, 0), end: new Date(y, m, d + 1, 22, 30), allDay: false}, | |
28 | + {title: 'Click for Google', start: new Date(y, m, 28), end: new Date(y, m, 29)} | |
29 | + ]; | |
30 | + | |
31 | + $scope.uiConfig = { | |
32 | + calendar:{ | |
33 | + height: 450, | |
34 | + editable: true, | |
35 | + droppable: true, | |
36 | + drop: function (date, allDay, jsEvent, ui) { | |
37 | + console.log('Here ,but where is the object?'); | |
38 | + }, | |
39 | + header:{ | |
40 | + left: 'title', | |
41 | + center: '', | |
42 | + right: 'today prev,next' | |
43 | + }, | |
44 | + eventResize: true, | |
74 | 45 | } |
75 | - ] | |
76 | - }); | |
77 | - | |
78 | - });*/ | |
46 | + }; | |
79 | 47 | |
48 | + $scope.eventSources = [$scope.events]; | |
49 | + //$scope.eventSources = []; | |
50 | + //$scope.eventSources.push($scope.events); | |
51 | + | |
52 | + $scope.addEvent = function(index) { | |
53 | + console.log('INDEX', index); | |
54 | + console.log('EVENTS', $scope.eventSources); | |
55 | + //$scope.events.push($scope.eventList[index]); | |
56 | + } | |
57 | + | |
58 | + console.log($scope.eventSources); | |
80 | 59 | |
81 | 60 | }]); |
82 | 61 | ... | ... |
app/partials/scheduler/scheduler.html
... | ... | @@ -65,31 +65,34 @@ |
65 | 65 | <div class="container"> |
66 | 66 | <div class="row"> |
67 | 67 | <div class="col-xs-12"> |
68 | - <!-- <div id='wrap'> | |
68 | + <div id='wrap'> | |
69 | 69 | |
70 | - <div id='external-events'> | |
70 | + <div id='external-events' class="col-xs-12 col-md-3"> | |
71 | 71 | <h4>Draggable Events</h4> |
72 | - <div class='fc-event'>My Event 1</div> | |
73 | - <div class='fc-event'>My Event 2</div> | |
74 | - <div class='fc-event'>My Event 3</div> | |
75 | - <div class='fc-event'>My Event 4</div> | |
76 | - <div class='fc-event'>My Event 5</div> | |
77 | - <p> | |
72 | + <label ng-repeat="item in eventList" style="width: 100%;"> | |
73 | + <div class="fc-event" data-drag="true" data-jqyoui-options="{revert: 'invalid'}" jqyoui-draggable="{index: {{$index}},placeholder:true,animate:true}"> | |
74 | + {{item.title}} | |
75 | + </div> | |
76 | + </label> | |
77 | + <!-- <p> | |
78 | 78 | <input type='checkbox' id='drop-remove' /> |
79 | 79 | <label for='drop-remove'>remove after drop</label> |
80 | - </p> | |
80 | + </p> --> | |
81 | 81 | </div> |
82 | 82 | |
83 | - <div id='calendar'></div> | |
83 | + <div ui-calendar="uiConfig.calendar" ng-model="eventSources" class="col-xs-12 col-md-9" data-drop="true" jqyoui-droppable="{multiple:true, onDrop: 'addEvent($index)'}"></div> | |
84 | 84 | |
85 | 85 | <div style='clear:both'></div> |
86 | 86 | |
87 | - </div> --> | |
87 | + </div> | |
88 | + | |
89 | + | |
88 | 90 | |
89 | - | |
90 | 91 | </div> |
91 | 92 | |
92 | 93 | </div> |
94 | + <div> | |
95 | + </div> | |
93 | 96 | <!-- /row --> |
94 | 97 | </div> |
95 | 98 | <!-- /container --> | ... | ... |
app/partials/updateFuelManager/updateFuelManager.controller.js
... | ... | @@ -584,10 +584,11 @@ |
584 | 584 | $scope.newFuelPricing[i].futureFuelPricing.deployDate = $scope.newFuelPricing[i].futureFuelPricing.deployDate.getTime(); |
585 | 585 | } |
586 | 586 | |
587 | - $scope.newFuelPricing[i].futureFuelPricing.papTotal = parseFloat($scope.newFuelPricing[i].futureFuelPricing.cost) + parseFloat($scope.newFuelPricing[i].fuelPricing.papMargin); | |
587 | + $scope.newFuelPricing[i].futureFuelPricing.papTotal = parseFloat($scope.newFuelPricing[i].futureFuelPricing.cost) + parseFloat($scope.newFuelPricing[i].futureFuelPricing.papMargin); | |
588 | + //$scope.newFuelPricing[i].futureFuelPricing.papTotal; | |
588 | 589 | $scope.updateFutureFuelPricing.futureFuelPricingList.push({ |
589 | 590 | 'cost': $scope.newFuelPricing[i].futureFuelPricing.cost, |
590 | - 'papMargin': $scope.newFuelPricing[i].fuelPricing.papMargin, | |
591 | + 'papMargin': $scope.newFuelPricing[i].futureFuelPricing.papMargin, | |
591 | 592 | //'papTotal': $scope.newFuelPricing[i].futureFuelPricing.papTotal, |
592 | 593 | 'papTotal': $scope.newFuelPricing[i].futureFuelPricing.papTotal, |
593 | 594 | 'expirationDate': $scope.newFuelPricing[i].futureFuelPricing.nextExpiration, |
... | ... | @@ -604,6 +605,7 @@ |
604 | 605 | $scope.newFuelPricing[i].futureFuelPricing.deployDate = '';*/ |
605 | 606 | } |
606 | 607 | } |
608 | + //console.log('$scope.updateFutureFuelPricing', $scope.updateFutureFuelPricing); | |
607 | 609 | updateFuelManagerService.updateFutureFuelPricing($scope.updateFutureFuelPricing).then(function(result) { |
608 | 610 | toastr.success('Successfully Updated', { |
609 | 611 | closeButton: true | ... | ... |
app/partials/updateFuelManager/updateFuelManager.html
... | ... | @@ -8,7 +8,7 @@ |
8 | 8 | <div class="myLoader" ng-show="showLoader"> |
9 | 9 | <img src="../img/hourglass.gif" width="50px;"> |
10 | 10 | </div> |
11 | -<div style="width: 90%; margin-left: 5%;"> | |
11 | +<div style="width: 96%; margin-left: 2%;"> | |
12 | 12 | <div class="row"> |
13 | 13 | |
14 | 14 | <div class="col-md-6"> |
... | ... | @@ -53,7 +53,7 @@ |
53 | 53 | <input type="text" class="form-control" datepicker ng-disabled="fuelPricing.futureFuelPricing.cost == undefined || fuelPricing.futureFuelPricing.cost == null || fuelPricing.futureFuelPricing.cost == ''" ng-model="fuelPricing.futureFuelPricing.nextExpiration" style="height:31px; width: 80px; padding: 6px 6px;"> |
54 | 54 | </td> |
55 | 55 | <td> |
56 | - <span style="line-height: 31px; color: #1ab394;">$ {{fuelPricing.futureFuelPricing.cost -- fuelPricing.fuelPricing.papMargin | number : 2}}</span> | |
56 | + <span style="line-height: 31px; color: #1ab394;">$ {{fuelPricing.futureFuelPricing.cost -- fuelPricing.futureFuelPricing.papMargin | number : 2}}</span> | |
57 | 57 | </td> |
58 | 58 | </tr> |
59 | 59 | </tbody> |
... | ... | @@ -104,7 +104,7 @@ |
104 | 104 | <button class="btn btn-success" style="display: none; background-image: none; background-color: #f3f3f3; color: #333; border:0;" ng-click="closeAccordian(jets)">Close</button> |
105 | 105 | <button class="btn btn-success" style="display: none;" ng-click="saveJetAccordian(jets)">Save</button> |
106 | 106 | <button class="btn btn-danger" style="display: none;" ng-click="deleteJetAccordian(jets.id)">Delete</button> |
107 | - <button type="button" class="btn btn-primary" ng-model="" ng-click="emailPricingForMargin(jets.id)" style= "font-weight: normal; text-align: center; font-size:12px">Email Pricing for this Margin</button> | |
107 | + <button type="button" class="btn btn-primary" ng-click="emailPricingForMargin(jets.id)" style= "font-weight: normal; text-align: center; font-size:12px">Email Pricing for this Margin</button> | |
108 | 108 | <button class="btn btn-default" ng-click="toggleJestAccordian(jets.id, $index)" style= "text-align: center; font-size:12px">Edit</button> |
109 | 109 | </div> |
110 | 110 | <div class="clearfix"></div> | ... | ... |
bower.json