View.js 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602
  1. /* An abstract class from which other views inherit from
  2. ----------------------------------------------------------------------------------------------------------------------*/
  3. var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
  4. type: null, // subclass' view name (string)
  5. name: null, // deprecated. use `type` instead
  6. title: null, // the text that will be displayed in the header's title
  7. calendar: null, // owner Calendar object
  8. options: null, // hash containing all options. already merged with view-specific-options
  9. el: null, // the view's containing element. set by Calendar
  10. isDateSet: false,
  11. isDateRendered: false,
  12. dateRenderQueue: null,
  13. isEventsBound: false,
  14. isEventsSet: false,
  15. isEventsRendered: false,
  16. eventRenderQueue: null,
  17. // range the view is formally responsible for (moments)
  18. // may be different from start/end. for example, a month view might have 1st-31st, excluding padded dates
  19. intervalStart: null,
  20. intervalEnd: null, // exclusive
  21. intervalDuration: null,
  22. intervalUnit: null, // name of largest unit being displayed, like "month" or "week"
  23. // date range with a rendered skeleton
  24. // includes not-active days that need some sort of DOM
  25. renderStart: null,
  26. renderEnd: null,
  27. // active dates that display events and accept drag-nd-drop
  28. contentStart: null,
  29. contentEnd: null,
  30. // DEPRECATED: use contentStart/contentEnd instead
  31. start: null,
  32. end: null,
  33. // date constraints. defines the "valid range"
  34. // TODO: enforce this in prev/next/gotoDate
  35. minDate: null,
  36. maxDate: null,
  37. // for dates that are outside of minDate/maxDate
  38. // true = not rendered at all
  39. // false = rendered, but disabled
  40. isOutOfRangeHidden: false,
  41. isRTL: false,
  42. isSelected: false, // boolean whether a range of time is user-selected or not
  43. selectedEvent: null,
  44. eventOrderSpecs: null, // criteria for ordering events when they have same date/time
  45. // classNames styled by jqui themes
  46. widgetHeaderClass: null,
  47. widgetContentClass: null,
  48. highlightStateClass: null,
  49. // for date utils, computed from options
  50. nextDayThreshold: null,
  51. isHiddenDayHash: null,
  52. // now indicator
  53. isNowIndicatorRendered: null,
  54. initialNowDate: null, // result first getNow call
  55. initialNowQueriedMs: null, // ms time the getNow was called
  56. nowIndicatorTimeoutID: null, // for refresh timing of now indicator
  57. nowIndicatorIntervalID: null, // "
  58. constructor: function(calendar, type, options, intervalDuration) {
  59. this.calendar = calendar;
  60. this.type = this.name = type; // .name is deprecated
  61. this.options = options;
  62. this.intervalDuration = intervalDuration || moment.duration(1, 'day');
  63. this.intervalUnit = computeIntervalUnit(this.intervalDuration);
  64. this.nextDayThreshold = moment.duration(this.opt('nextDayThreshold'));
  65. this.initThemingProps();
  66. this.initHiddenDays();
  67. this.isRTL = this.opt('isRTL');
  68. this.eventOrderSpecs = parseFieldSpecs(this.opt('eventOrder'));
  69. this.dateRenderQueue = new TaskQueue();
  70. this.eventRenderQueue = new TaskQueue(this.opt('eventRenderWait'));
  71. this.initialize();
  72. },
  73. // A good place for subclasses to initialize member variables
  74. initialize: function() {
  75. // subclasses can implement
  76. },
  77. // Retrieves an option with the given name
  78. opt: function(name) {
  79. return this.options[name];
  80. },
  81. // Triggers handlers that are view-related. Modifies args before passing to calendar.
  82. publiclyTrigger: function(name, thisObj) { // arguments beyond thisObj are passed along
  83. var calendar = this.calendar;
  84. return calendar.publiclyTrigger.apply(
  85. calendar,
  86. [name, thisObj || this].concat(
  87. Array.prototype.slice.call(arguments, 2), // arguments beyond thisObj
  88. [ this ] // always make the last argument a reference to the view. TODO: deprecate
  89. )
  90. );
  91. },
  92. // Returns a proxy of the given promise that will be rejected if the given event fires
  93. // before the promise resolves.
  94. rejectOn: function(eventName, promise) {
  95. var _this = this;
  96. return new Promise(function(resolve, reject) {
  97. _this.one(eventName, reject);
  98. function cleanup() {
  99. _this.off(eventName, reject);
  100. }
  101. promise.then(function(res) { // success
  102. cleanup();
  103. resolve(res);
  104. }, function() { // failure
  105. cleanup();
  106. reject();
  107. });
  108. });
  109. },
  110. /* Date Computation
  111. ------------------------------------------------------------------------------------------------------------------*/
  112. // Updates all internal dates for displaying the given unzoned range.
  113. setRangeFromDate: function(date) {
  114. // best place for this?
  115. var minDateInput = this.opt('minDate');
  116. var maxDateInput = this.opt('maxDate');
  117. if (minDateInput) {
  118. this.minDate = this.calendar.moment(minDateInput);
  119. }
  120. if (maxDateInput) {
  121. this.maxDate = this.calendar.moment(maxDateInput);
  122. }
  123. var intervalRange = this.computeIntervalRange(date);
  124. var renderRange = this.computeRenderRange(intervalRange);
  125. var contentRange = this.computeContentRange(renderRange, intervalRange);
  126. this.intervalStart = intervalRange.start;
  127. this.intervalEnd = intervalRange.end;
  128. this.renderStart = renderRange.start;
  129. this.renderEnd = renderRange.end;
  130. this.contentStart = contentRange.start;
  131. this.contentEnd = contentRange.end;
  132. // DEPRECATED, but we need to keep it updated
  133. // TODO: run automated tests with this commented out
  134. this.start = this.contentStart;
  135. this.end = this.contentEnd;
  136. this.updateTitle();
  137. },
  138. computeIntervalRange: function(date) {
  139. var intervalStart = date.clone().startOf(this.intervalUnit);
  140. var intervalEnd = intervalStart.clone().add(this.intervalDuration);
  141. // normalize the range's time-ambiguity
  142. if (/^(year|month|week|day)$/.test(this.intervalUnit)) { // whole-days?
  143. intervalStart.stripTime();
  144. intervalEnd.stripTime();
  145. }
  146. else { // needs to have a time?
  147. if (!intervalStart.hasTime()) {
  148. intervalStart = this.calendar.time(0); // give 00:00 time
  149. }
  150. if (!intervalEnd.hasTime()) {
  151. intervalEnd = this.calendar.time(0); // give 00:00 time
  152. }
  153. }
  154. return { start: intervalStart, end: intervalEnd };
  155. },
  156. // Computes the date range that will be rendered.
  157. computeRenderRange: function(intervalRange) {
  158. return this.sanitizeRenderRange(
  159. cloneRange(intervalRange)
  160. );
  161. },
  162. sanitizeRenderRange: function(renderRange) {
  163. renderRange = this.trimHiddenDays(renderRange);
  164. if (this.isOutOfRangeHidden) {
  165. renderRange = this.trimToValidRange(renderRange);
  166. }
  167. return renderRange;
  168. },
  169. // Computes the date range that will be fully visible (not greyed out),
  170. // and that will contain events and allow drag-n-drop.
  171. computeContentRange: function(renderRange, intervalRange) {
  172. var contentRange = cloneRange(renderRange);
  173. if (this.opt('disableNonCurrentDates')) {
  174. contentRange = intersectRanges(contentRange, intervalRange);
  175. }
  176. // probably already done in sanitizeRenderRange,
  177. // but do again in case subclass added special behavior to computeRenderRange
  178. contentRange = this.trimToValidRange(contentRange);
  179. return contentRange;
  180. },
  181. trimToValidRange: function(inputRange) {
  182. var range = cloneRange(inputRange);
  183. if (this.minDate) {
  184. range.start = maxMoment(range.start, this.minDate);
  185. }
  186. if (this.maxDate) {
  187. range.end = minMoment(range.end, this.maxDate);
  188. }
  189. return range;
  190. },
  191. isRangeInValidRange: function(range) {
  192. return (!this.minDate || range.start >= this.minDate) &&
  193. (!this.maxDate || range.end <= this.maxDate);
  194. },
  195. isDateInContentRange: function(date) {
  196. return date >= this.contentStart && date < this.contentEnd;
  197. },
  198. isRangeInContentRange: function(range) {
  199. return range.start >= this.contentStart && range.end <= this.contentEnd;
  200. },
  201. // Computes the new date when the user hits the prev button, given the current date
  202. computePrevDate: function(date) {
  203. return this.massageCurrentDate(
  204. date.clone().startOf(this.intervalUnit).subtract(this.intervalDuration), -1
  205. );
  206. },
  207. // Computes the new date when the user hits the next button, given the current date
  208. computeNextDate: function(date) {
  209. return this.massageCurrentDate(
  210. date.clone().startOf(this.intervalUnit).add(this.intervalDuration)
  211. );
  212. },
  213. // Given an arbitrarily calculated current date of the calendar, returns a date that is ensured to be completely
  214. // visible. `direction` is optional and indicates which direction the current date was being
  215. // incremented or decremented (1 or -1).
  216. massageCurrentDate: function(date, direction) {
  217. if (this.intervalDuration.as('days') <= 1) { // if the view displays a single day or smaller
  218. if (this.isHiddenDay(date)) {
  219. date = this.skipHiddenDays(date, direction);
  220. date.startOf('day');
  221. }
  222. }
  223. return date;
  224. },
  225. trimHiddenDays: function(inputRange) {
  226. return {
  227. start: this.skipHiddenDays(inputRange.start),
  228. end: this.skipHiddenDays(inputRange.end, -1, true) // exclusively move backwards
  229. };
  230. },
  231. /* Title and Date Formatting
  232. ------------------------------------------------------------------------------------------------------------------*/
  233. // Sets the view's title property to the most updated computed value
  234. updateTitle: function() {
  235. this.title = this.computeTitle();
  236. this.calendar.setToolbarsTitle(this.title);
  237. },
  238. // Computes what the title at the top of the calendar should be for this view
  239. computeTitle: function() {
  240. var start, end;
  241. // for views that span a large unit of time, show the proper interval, ignoring stray days before and after
  242. if (/^(year|month)$/.test(this.intervalUnit)) {
  243. start = this.intervalStart;
  244. end = this.intervalEnd;
  245. }
  246. else { // for day units or smaller, use the actual day range
  247. start = this.contentStart;
  248. end = this.contentEnd;
  249. }
  250. return this.formatRange(
  251. {
  252. // in case intervalStart/End has a time, make sure timezone is correct
  253. start: this.calendar.applyTimezone(start),
  254. end: this.calendar.applyTimezone(end)
  255. },
  256. this.opt('titleFormat') || this.computeTitleFormat(),
  257. this.opt('titleRangeSeparator')
  258. );
  259. },
  260. // Generates the format string that should be used to generate the title for the current date range.
  261. // Attempts to compute the most appropriate format if not explicitly specified with `titleFormat`.
  262. computeTitleFormat: function() {
  263. if (this.intervalUnit == 'year') {
  264. return 'YYYY';
  265. }
  266. else if (this.intervalUnit == 'month') {
  267. return this.opt('monthYearFormat'); // like "September 2014"
  268. }
  269. else if (this.intervalDuration.as('days') > 1) {
  270. return 'll'; // multi-day range. shorter, like "Sep 9 - 10 2014"
  271. }
  272. else {
  273. return 'LL'; // one day. longer, like "September 9 2014"
  274. }
  275. },
  276. // Utility for formatting a range. Accepts a range object, formatting string, and optional separator.
  277. // Displays all-day ranges naturally, with an inclusive end. Takes the current isRTL into account.
  278. // The timezones of the dates within `range` will be respected.
  279. formatRange: function(range, formatStr, separator) {
  280. var end = range.end;
  281. if (!end.hasTime()) { // all-day?
  282. end = end.clone().subtract(1); // convert to inclusive. last ms of previous day
  283. }
  284. return formatRange(range.start, end, formatStr, separator, this.opt('isRTL'));
  285. },
  286. getAllDayHtml: function() {
  287. return this.opt('allDayHtml') || htmlEscape(this.opt('allDayText'));
  288. },
  289. /* Navigation
  290. ------------------------------------------------------------------------------------------------------------------*/
  291. // Generates HTML for an anchor to another view into the calendar.
  292. // Will either generate an <a> tag or a non-clickable <span> tag, depending on enabled settings.
  293. // `gotoOptions` can either be a moment input, or an object with the form:
  294. // { date, type, forceOff }
  295. // `type` is a view-type like "day" or "week". default value is "day".
  296. // `attrs` and `innerHtml` are use to generate the rest of the HTML tag.
  297. buildGotoAnchorHtml: function(gotoOptions, attrs, innerHtml) {
  298. var date, type, forceOff;
  299. var finalOptions;
  300. if ($.isPlainObject(gotoOptions)) {
  301. date = gotoOptions.date;
  302. type = gotoOptions.type;
  303. forceOff = gotoOptions.forceOff;
  304. }
  305. else {
  306. date = gotoOptions; // a single moment input
  307. }
  308. date = FC.moment(date); // if a string, parse it
  309. finalOptions = { // for serialization into the link
  310. date: date.format('YYYY-MM-DD'),
  311. type: type || 'day'
  312. };
  313. if (typeof attrs === 'string') {
  314. innerHtml = attrs;
  315. attrs = null;
  316. }
  317. attrs = attrs ? ' ' + attrsToStr(attrs) : ''; // will have a leading space
  318. innerHtml = innerHtml || '';
  319. if (!forceOff && this.opt('navLinks')) {
  320. return '<a' + attrs +
  321. ' data-goto="' + htmlEscape(JSON.stringify(finalOptions)) + '">' +
  322. innerHtml +
  323. '</a>';
  324. }
  325. else {
  326. return '<span' + attrs + '>' +
  327. innerHtml +
  328. '</span>';
  329. }
  330. },
  331. // Rendering Non-date-related Content
  332. // -----------------------------------------------------------------------------------------------------------------
  333. // Sets the container element that the view should render inside of, does global DOM-related initializations,
  334. // and renders all the non-date-related content inside.
  335. setElement: function(el) {
  336. this.el = el;
  337. this.bindGlobalHandlers();
  338. this.renderSkeleton();
  339. },
  340. // Removes the view's container element from the DOM, clearing any content beforehand.
  341. // Undoes any other DOM-related attachments.
  342. removeElement: function() {
  343. this.unsetDate();
  344. this.unrenderSkeleton();
  345. this.unbindGlobalHandlers();
  346. this.el.remove();
  347. // NOTE: don't null-out this.el in case the View was destroyed within an API callback.
  348. // We don't null-out the View's other jQuery element references upon destroy,
  349. // so we shouldn't kill this.el either.
  350. },
  351. // Renders the basic structure of the view before any content is rendered
  352. renderSkeleton: function() {
  353. // subclasses should implement
  354. },
  355. // Unrenders the basic structure of the view
  356. unrenderSkeleton: function() {
  357. // subclasses should implement
  358. },
  359. // Date Setting/Unsetting
  360. // -----------------------------------------------------------------------------------------------------------------
  361. setDate: function(date) {
  362. var isReset = this.isDateSet;
  363. this.isDateSet = true;
  364. this.handleDate(date, isReset);
  365. this.trigger(isReset ? 'dateReset' : 'dateSet', date);
  366. },
  367. unsetDate: function() {
  368. if (this.isDateSet) {
  369. this.isDateSet = false;
  370. this.handleDateUnset();
  371. this.trigger('dateUnset');
  372. }
  373. },
  374. // Date Handling
  375. // -----------------------------------------------------------------------------------------------------------------
  376. handleDate: function(date, isReset) {
  377. var _this = this;
  378. this.unbindEvents(); // will do nothing if not already bound
  379. this.requestDateRender(date).then(function() {
  380. // wish we could start earlier, but setRangeFromDate needs to execute first
  381. _this.bindEvents(); // will request events
  382. });
  383. },
  384. handleDateUnset: function() {
  385. this.unbindEvents();
  386. this.requestDateUnrender();
  387. },
  388. // Date Render Queuing
  389. // -----------------------------------------------------------------------------------------------------------------
  390. // if date not specified, uses current
  391. requestDateRender: function(date) {
  392. var _this = this;
  393. return this.dateRenderQueue.add(function() {
  394. return _this.executeDateRender(date);
  395. });
  396. },
  397. requestDateUnrender: function() {
  398. var _this = this;
  399. return this.dateRenderQueue.add(function() {
  400. return _this.executeDateUnrender();
  401. });
  402. },
  403. // Date High-level Rendering
  404. // -----------------------------------------------------------------------------------------------------------------
  405. // if date not specified, uses current
  406. executeDateRender: function(date) {
  407. var _this = this;
  408. // if rendering a new date, reset scroll to initial state (scrollTime)
  409. if (date) {
  410. this.captureInitialScroll();
  411. }
  412. else {
  413. this.captureScroll(); // a rerender of the current date
  414. }
  415. this.freezeHeight();
  416. return this.executeDateUnrender().then(function() {
  417. if (date) {
  418. _this.setRangeFromDate(date);
  419. }
  420. if (_this.render) {
  421. _this.render(); // TODO: deprecate
  422. }
  423. _this.renderDates();
  424. _this.updateSize();
  425. _this.renderBusinessHours(); // might need coordinates, so should go after updateSize()
  426. _this.startNowIndicator();
  427. _this.thawHeight();
  428. _this.releaseScroll();
  429. _this.isDateRendered = true;
  430. _this.onDateRender();
  431. _this.trigger('dateRender');
  432. });
  433. },
  434. executeDateUnrender: function() {
  435. var _this = this;
  436. if (_this.isDateRendered) {
  437. return this.requestEventsUnrender().then(function() {
  438. _this.unselect();
  439. _this.stopNowIndicator();
  440. _this.triggerUnrender();
  441. _this.unrenderBusinessHours();
  442. _this.unrenderDates();
  443. if (_this.destroy) {
  444. _this.destroy(); // TODO: deprecate
  445. }
  446. _this.isDateRendered = false;
  447. _this.trigger('dateUnrender');
  448. });
  449. }
  450. else {
  451. return Promise.resolve();
  452. }
  453. },
  454. // Date Rendering Triggers
  455. // -----------------------------------------------------------------------------------------------------------------
  456. onDateRender: function() {
  457. this.triggerRender();
  458. },
  459. // Date Low-level Rendering
  460. // -----------------------------------------------------------------------------------------------------------------
  461. // date-cell content only
  462. renderDates: function() {
  463. // subclasses should implement
  464. },
  465. // date-cell content only
  466. unrenderDates: function() {
  467. // subclasses should override
  468. },
  469. // Misc view rendering utils
  470. // -------------------------
  471. // Signals that the view's content has been rendered
  472. triggerRender: function() {
  473. this.publiclyTrigger('viewRender', this, this, this.el);
  474. },
  475. // Signals that the view's content is about to be unrendered
  476. triggerUnrender: function() {
  477. this.publiclyTrigger('viewDestroy', this, this, this.el);
  478. },
  479. // Binds DOM handlers to elements that reside outside the view container, such as the document
  480. bindGlobalHandlers: function() {
  481. this.listenTo(GlobalEmitter.get(), {
  482. touchstart: this.processUnselect,
  483. mousedown: this.handleDocumentMousedown
  484. });
  485. },
  486. // Unbinds DOM handlers from elements that reside outside the view container
  487. unbindGlobalHandlers: function() {
  488. this.stopListeningTo(GlobalEmitter.get());
  489. },
  490. // Initializes internal variables related to theming
  491. initThemingProps: function() {
  492. var tm = this.opt('theme') ? 'ui' : 'fc';
  493. this.widgetHeaderClass = tm + '-widget-header';
  494. this.widgetContentClass = tm + '-widget-content';
  495. this.highlightStateClass = tm + '-state-highlight';
  496. },
  497. /* Business Hours
  498. ------------------------------------------------------------------------------------------------------------------*/
  499. // Renders business-hours onto the view. Assumes updateSize has already been called.
  500. renderBusinessHours: function() {
  501. // subclasses should implement
  502. },
  503. // Unrenders previously-rendered business-hours
  504. unrenderBusinessHours: function() {
  505. // subclasses should implement
  506. },
  507. /* Now Indicator
  508. ------------------------------------------------------------------------------------------------------------------*/
  509. // Immediately render the current time indicator and begins re-rendering it at an interval,
  510. // which is defined by this.getNowIndicatorUnit().
  511. // TODO: somehow do this for the current whole day's background too
  512. startNowIndicator: function() {
  513. var _this = this;
  514. var unit;
  515. var update;
  516. var delay; // ms wait value
  517. if (this.opt('nowIndicator')) {
  518. unit = this.getNowIndicatorUnit();
  519. if (unit) {
  520. update = proxy(this, 'updateNowIndicator'); // bind to `this`
  521. this.initialNowDate = this.calendar.getNow();
  522. this.initialNowQueriedMs = +new Date();
  523. this.renderNowIndicator(this.initialNowDate);
  524. this.isNowIndicatorRendered = true;
  525. // wait until the beginning of the next interval
  526. delay = this.initialNowDate.clone().startOf(unit).add(1, unit) - this.initialNowDate;
  527. this.nowIndicatorTimeoutID = setTimeout(function() {
  528. _this.nowIndicatorTimeoutID = null;
  529. update();
  530. delay = +moment.duration(1, unit);
  531. delay = Math.max(100, delay); // prevent too frequent
  532. _this.nowIndicatorIntervalID = setInterval(update, delay); // update every interval
  533. }, delay);
  534. }
  535. }
  536. },
  537. // rerenders the now indicator, computing the new current time from the amount of time that has passed
  538. // since the initial getNow call.
  539. updateNowIndicator: function() {
  540. if (this.isNowIndicatorRendered) {
  541. this.unrenderNowIndicator();
  542. this.renderNowIndicator(
  543. this.initialNowDate.clone().add(new Date() - this.initialNowQueriedMs) // add ms
  544. );
  545. }
  546. },
  547. // Immediately unrenders the view's current time indicator and stops any re-rendering timers.
  548. // Won't cause side effects if indicator isn't rendered.
  549. stopNowIndicator: function() {
  550. if (this.isNowIndicatorRendered) {
  551. if (this.nowIndicatorTimeoutID) {
  552. clearTimeout(this.nowIndicatorTimeoutID);
  553. this.nowIndicatorTimeoutID = null;
  554. }
  555. if (this.nowIndicatorIntervalID) {
  556. clearTimeout(this.nowIndicatorIntervalID);
  557. this.nowIndicatorIntervalID = null;
  558. }
  559. this.unrenderNowIndicator();
  560. this.isNowIndicatorRendered = false;
  561. }
  562. },
  563. // Returns a string unit, like 'second' or 'minute' that defined how often the current time indicator
  564. // should be refreshed. If something falsy is returned, no time indicator is rendered at all.
  565. getNowIndicatorUnit: function() {
  566. // subclasses should implement
  567. },
  568. // Renders a current time indicator at the given datetime
  569. renderNowIndicator: function(date) {
  570. // subclasses should implement
  571. },
  572. // Undoes the rendering actions from renderNowIndicator
  573. unrenderNowIndicator: function() {
  574. // subclasses should implement
  575. },
  576. /* Dimensions
  577. ------------------------------------------------------------------------------------------------------------------*/
  578. // Refreshes anything dependant upon sizing of the container element of the grid
  579. updateSize: function(isResize) {
  580. if (isResize) {
  581. this.captureScroll();
  582. }
  583. this.updateHeight(isResize);
  584. this.updateWidth(isResize);
  585. this.updateNowIndicator();
  586. if (isResize) {
  587. this.releaseScroll();
  588. }
  589. },
  590. // Refreshes the horizontal dimensions of the calendar
  591. updateWidth: function(isResize) {
  592. // subclasses should implement
  593. },
  594. // Refreshes the vertical dimensions of the calendar
  595. updateHeight: function(isResize) {
  596. var calendar = this.calendar; // we poll the calendar for height information
  597. this.setHeight(
  598. calendar.getSuggestedViewHeight(),
  599. calendar.isHeightAuto()
  600. );
  601. },
  602. // Updates the vertical dimensions of the calendar to the specified height.
  603. // if `isAuto` is set to true, height becomes merely a suggestion and the view should use its "natural" height.
  604. setHeight: function(height, isAuto) {
  605. // subclasses should implement
  606. },
  607. /* Scroller
  608. ------------------------------------------------------------------------------------------------------------------*/
  609. capturedScroll: null,
  610. capturedScrollDepth: 0,
  611. captureScroll: function() {
  612. if (!(this.capturedScrollDepth++)) {
  613. this.capturedScroll = this.isDateRendered ? this.queryScroll() : {}; // require a render first
  614. return true; // root?
  615. }
  616. return false;
  617. },
  618. captureInitialScroll: function(forcedScroll) {
  619. if (this.captureScroll()) { // root?
  620. this.capturedScroll.isInitial = true;
  621. if (forcedScroll) {
  622. $.extend(this.capturedScroll, forcedScroll);
  623. }
  624. else {
  625. this.capturedScroll.isComputed = true;
  626. }
  627. }
  628. },
  629. releaseScroll: function() {
  630. var scroll = this.capturedScroll;
  631. var isRoot = this.discardScroll();
  632. if (scroll.isComputed) {
  633. if (isRoot) {
  634. // only compute initial scroll if it will actually be used (is the root capture)
  635. $.extend(scroll, this.computeInitialScroll());
  636. }
  637. else {
  638. scroll = null; // scroll couldn't be computed. don't apply it to the DOM
  639. }
  640. }
  641. if (scroll) {
  642. // we act immediately on a releaseScroll operation, as opposed to captureScroll.
  643. // if capture/release wraps a render operation that screws up the scroll,
  644. // we still want to restore it a good state after, regardless of depth.
  645. if (scroll.isInitial) {
  646. this.hardSetScroll(scroll); // outsmart how browsers set scroll on initial DOM
  647. }
  648. else {
  649. this.setScroll(scroll);
  650. }
  651. }
  652. },
  653. discardScroll: function() {
  654. if (!(--this.capturedScrollDepth)) {
  655. this.capturedScroll = null;
  656. return true; // root?
  657. }
  658. return false;
  659. },
  660. computeInitialScroll: function() {
  661. return {};
  662. },
  663. queryScroll: function() {
  664. return {};
  665. },
  666. hardSetScroll: function(scroll) {
  667. var _this = this;
  668. var exec = function() { _this.setScroll(scroll); };
  669. exec();
  670. setTimeout(exec, 0); // to surely clear the browser's initial scroll for the DOM
  671. },
  672. setScroll: function(scroll) {
  673. },
  674. /* Height Freezing
  675. ------------------------------------------------------------------------------------------------------------------*/
  676. freezeHeight: function() {
  677. this.calendar.freezeContentHeight();
  678. },
  679. thawHeight: function() {
  680. this.calendar.thawContentHeight();
  681. },
  682. // Event Binding/Unbinding
  683. // -----------------------------------------------------------------------------------------------------------------
  684. bindEvents: function() {
  685. var _this = this;
  686. if (!this.isEventsBound) {
  687. this.isEventsBound = true;
  688. this.rejectOn('eventsUnbind', this.requestEvents()).then(function(events) { // TODO: test rejection
  689. _this.listenTo(_this.calendar, 'eventsReset', _this.setEvents);
  690. _this.setEvents(events);
  691. });
  692. }
  693. },
  694. unbindEvents: function() {
  695. if (this.isEventsBound) {
  696. this.isEventsBound = false;
  697. this.stopListeningTo(this.calendar, 'eventsReset');
  698. this.unsetEvents();
  699. this.trigger('eventsUnbind');
  700. }
  701. },
  702. // Event Setting/Unsetting
  703. // -----------------------------------------------------------------------------------------------------------------
  704. setEvents: function(events) {
  705. var isReset = this.isEventSet;
  706. this.isEventsSet = true;
  707. this.handleEvents(events, isReset);
  708. this.trigger(isReset ? 'eventsReset' : 'eventsSet', events);
  709. },
  710. unsetEvents: function() {
  711. if (this.isEventsSet) {
  712. this.isEventsSet = false;
  713. this.handleEventsUnset();
  714. this.trigger('eventsUnset');
  715. }
  716. },
  717. whenEventsSet: function() {
  718. var _this = this;
  719. if (this.isEventsSet) {
  720. return Promise.resolve(this.getCurrentEvents());
  721. }
  722. else {
  723. return new Promise(function(resolve) {
  724. _this.one('eventsSet', resolve);
  725. });
  726. }
  727. },
  728. // Event Handling
  729. // -----------------------------------------------------------------------------------------------------------------
  730. handleEvents: function(events, isReset) {
  731. this.requestEventsRender(events);
  732. },
  733. handleEventsUnset: function() {
  734. this.requestEventsUnrender();
  735. },
  736. // Event Render Queuing
  737. // -----------------------------------------------------------------------------------------------------------------
  738. // assumes any previous event renders have been cleared already
  739. requestEventsRender: function(events) {
  740. var _this = this;
  741. return this.eventRenderQueue.add(function() { // might not return a promise if debounced!? bad
  742. return _this.executeEventsRender(events);
  743. });
  744. },
  745. requestEventsUnrender: function() {
  746. var _this = this;
  747. if (this.isEventsRendered) {
  748. return this.eventRenderQueue.addQuickly(function() {
  749. return _this.executeEventsUnrender();
  750. });
  751. }
  752. else {
  753. return Promise.resolve();
  754. }
  755. },
  756. requestCurrentEventsRender: function() {
  757. if (this.isEventsSet) {
  758. this.requestEventsRender(this.getCurrentEvents());
  759. }
  760. else {
  761. return Promise.reject();
  762. }
  763. },
  764. // Event High-level Rendering
  765. // -----------------------------------------------------------------------------------------------------------------
  766. executeEventsRender: function(events) {
  767. var _this = this;
  768. this.captureScroll();
  769. this.freezeHeight();
  770. return this.executeEventsUnrender().then(function() {
  771. _this.renderEvents(events);
  772. _this.thawHeight();
  773. _this.releaseScroll();
  774. _this.isEventsRendered = true;
  775. _this.onEventsRender();
  776. _this.trigger('eventsRender');
  777. });
  778. },
  779. executeEventsUnrender: function() {
  780. if (this.isEventsRendered) {
  781. this.onBeforeEventsUnrender();
  782. this.captureScroll();
  783. this.freezeHeight();
  784. if (this.destroyEvents) {
  785. this.destroyEvents(); // TODO: deprecate
  786. }
  787. this.unrenderEvents();
  788. this.thawHeight();
  789. this.releaseScroll();
  790. this.isEventsRendered = false;
  791. this.trigger('eventsUnrender');
  792. }
  793. return Promise.resolve(); // always synchronous
  794. },
  795. // Event Rendering Triggers
  796. // -----------------------------------------------------------------------------------------------------------------
  797. // Signals that all events have been rendered
  798. onEventsRender: function() {
  799. this.renderedEventSegEach(function(seg) {
  800. this.publiclyTrigger('eventAfterRender', seg.event, seg.event, seg.el);
  801. });
  802. this.publiclyTrigger('eventAfterAllRender');
  803. },
  804. // Signals that all event elements are about to be removed
  805. onBeforeEventsUnrender: function() {
  806. this.renderedEventSegEach(function(seg) {
  807. this.publiclyTrigger('eventDestroy', seg.event, seg.event, seg.el);
  808. });
  809. },
  810. // Event Low-level Rendering
  811. // -----------------------------------------------------------------------------------------------------------------
  812. // Renders the events onto the view.
  813. renderEvents: function(events) {
  814. // subclasses should implement
  815. },
  816. // Removes event elements from the view.
  817. unrenderEvents: function() {
  818. // subclasses should implement
  819. },
  820. // Event Data Access
  821. // -----------------------------------------------------------------------------------------------------------------
  822. requestEvents: function() {
  823. return this.calendar.requestEvents(this.contentStart, this.contentEnd);
  824. },
  825. getCurrentEvents: function() {
  826. return this.calendar.getPrunedEventCache();
  827. },
  828. // Event Rendering Utils
  829. // -----------------------------------------------------------------------------------------------------------------
  830. // Given an event and the default element used for rendering, returns the element that should actually be used.
  831. // Basically runs events and elements through the eventRender hook.
  832. resolveEventEl: function(event, el) {
  833. var custom = this.publiclyTrigger('eventRender', event, event, el);
  834. if (custom === false) { // means don't render at all
  835. el = null;
  836. }
  837. else if (custom && custom !== true) {
  838. el = $(custom);
  839. }
  840. return el;
  841. },
  842. // Hides all rendered event segments linked to the given event
  843. showEvent: function(event) {
  844. this.renderedEventSegEach(function(seg) {
  845. seg.el.css('visibility', '');
  846. }, event);
  847. },
  848. // Shows all rendered event segments linked to the given event
  849. hideEvent: function(event) {
  850. this.renderedEventSegEach(function(seg) {
  851. seg.el.css('visibility', 'hidden');
  852. }, event);
  853. },
  854. // Iterates through event segments that have been rendered (have an el). Goes through all by default.
  855. // If the optional `event` argument is specified, only iterates through segments linked to that event.
  856. // The `this` value of the callback function will be the view.
  857. renderedEventSegEach: function(func, event) {
  858. var segs = this.getEventSegs();
  859. var i;
  860. for (i = 0; i < segs.length; i++) {
  861. if (!event || segs[i].event._id === event._id) {
  862. if (segs[i].el) {
  863. func.call(this, segs[i]);
  864. }
  865. }
  866. }
  867. },
  868. // Retrieves all the rendered segment objects for the view
  869. getEventSegs: function() {
  870. // subclasses must implement
  871. return [];
  872. },
  873. /* Event Drag-n-Drop
  874. ------------------------------------------------------------------------------------------------------------------*/
  875. // Computes if the given event is allowed to be dragged by the user
  876. isEventDraggable: function(event) {
  877. return this.isEventStartEditable(event);
  878. },
  879. isEventStartEditable: function(event) {
  880. return firstDefined(
  881. event.startEditable,
  882. (event.source || {}).startEditable,
  883. this.opt('eventStartEditable'),
  884. this.isEventGenerallyEditable(event)
  885. );
  886. },
  887. isEventGenerallyEditable: function(event) {
  888. return firstDefined(
  889. event.editable,
  890. (event.source || {}).editable,
  891. this.opt('editable')
  892. );
  893. },
  894. // Must be called when an event in the view is dropped onto new location.
  895. // `dropLocation` is an object that contains the new zoned start/end/allDay values for the event.
  896. reportSegDrop: function(seg, dropLocation, largeUnit, el, ev) {
  897. var calendar = this.calendar;
  898. var mutateResult = calendar.mutateSeg(seg, dropLocation, largeUnit);
  899. var undoFunc = function() {
  900. mutateResult.undo();
  901. calendar.reportEventChange();
  902. };
  903. this.triggerEventDrop(seg.event, mutateResult.dateDelta, undoFunc, el, ev);
  904. calendar.reportEventChange(); // will rerender events
  905. },
  906. // Triggers event-drop handlers that have subscribed via the API
  907. triggerEventDrop: function(event, dateDelta, undoFunc, el, ev) {
  908. this.publiclyTrigger('eventDrop', el[0], event, dateDelta, undoFunc, ev, {}); // {} = jqui dummy
  909. },
  910. /* External Element Drag-n-Drop
  911. ------------------------------------------------------------------------------------------------------------------*/
  912. // Must be called when an external element, via jQuery UI, has been dropped onto the calendar.
  913. // `meta` is the parsed data that has been embedded into the dragging event.
  914. // `dropLocation` is an object that contains the new zoned start/end/allDay values for the event.
  915. reportExternalDrop: function(meta, dropLocation, el, ev, ui) {
  916. var eventProps = meta.eventProps;
  917. var eventInput;
  918. var event;
  919. // Try to build an event object and render it. TODO: decouple the two
  920. if (eventProps) {
  921. eventInput = $.extend({}, eventProps, dropLocation);
  922. event = this.calendar.renderEvent(eventInput, meta.stick)[0]; // renderEvent returns an array
  923. }
  924. this.triggerExternalDrop(event, dropLocation, el, ev, ui);
  925. },
  926. // Triggers external-drop handlers that have subscribed via the API
  927. triggerExternalDrop: function(event, dropLocation, el, ev, ui) {
  928. // trigger 'drop' regardless of whether element represents an event
  929. this.publiclyTrigger('drop', el[0], dropLocation.start, ev, ui);
  930. if (event) {
  931. this.publiclyTrigger('eventReceive', null, event); // signal an external event landed
  932. }
  933. },
  934. /* Drag-n-Drop Rendering (for both events and external elements)
  935. ------------------------------------------------------------------------------------------------------------------*/
  936. // Renders a visual indication of a event or external-element drag over the given drop zone.
  937. // If an external-element, seg will be `null`.
  938. // Must return elements used for any mock events.
  939. renderDrag: function(dropLocation, seg) {
  940. // subclasses must implement
  941. },
  942. // Unrenders a visual indication of an event or external-element being dragged.
  943. unrenderDrag: function() {
  944. // subclasses must implement
  945. },
  946. /* Event Resizing
  947. ------------------------------------------------------------------------------------------------------------------*/
  948. // Computes if the given event is allowed to be resized from its starting edge
  949. isEventResizableFromStart: function(event) {
  950. return this.opt('eventResizableFromStart') && this.isEventResizable(event);
  951. },
  952. // Computes if the given event is allowed to be resized from its ending edge
  953. isEventResizableFromEnd: function(event) {
  954. return this.isEventResizable(event);
  955. },
  956. // Computes if the given event is allowed to be resized by the user at all
  957. isEventResizable: function(event) {
  958. var source = event.source || {};
  959. return firstDefined(
  960. event.durationEditable,
  961. source.durationEditable,
  962. this.opt('eventDurationEditable'),
  963. event.editable,
  964. source.editable,
  965. this.opt('editable')
  966. );
  967. },
  968. // Must be called when an event in the view has been resized to a new length
  969. reportSegResize: function(seg, resizeLocation, largeUnit, el, ev) {
  970. var calendar = this.calendar;
  971. var mutateResult = calendar.mutateSeg(seg, resizeLocation, largeUnit);
  972. var undoFunc = function() {
  973. mutateResult.undo();
  974. calendar.reportEventChange();
  975. };
  976. this.triggerEventResize(seg.event, mutateResult.durationDelta, undoFunc, el, ev);
  977. calendar.reportEventChange(); // will rerender events
  978. },
  979. // Triggers event-resize handlers that have subscribed via the API
  980. triggerEventResize: function(event, durationDelta, undoFunc, el, ev) {
  981. this.publiclyTrigger('eventResize', el[0], event, durationDelta, undoFunc, ev, {}); // {} = jqui dummy
  982. },
  983. /* Selection (time range)
  984. ------------------------------------------------------------------------------------------------------------------*/
  985. // Selects a date span on the view. `start` and `end` are both Moments.
  986. // `ev` is the native mouse event that begin the interaction.
  987. select: function(span, ev) {
  988. this.unselect(ev);
  989. this.renderSelection(span);
  990. this.reportSelection(span, ev);
  991. },
  992. // Renders a visual indication of the selection
  993. renderSelection: function(span) {
  994. // subclasses should implement
  995. },
  996. // Called when a new selection is made. Updates internal state and triggers handlers.
  997. reportSelection: function(span, ev) {
  998. this.isSelected = true;
  999. this.triggerSelect(span, ev);
  1000. },
  1001. // Triggers handlers to 'select'
  1002. triggerSelect: function(span, ev) {
  1003. this.publiclyTrigger(
  1004. 'select',
  1005. null,
  1006. this.calendar.applyTimezone(span.start), // convert to calendar's tz for external API
  1007. this.calendar.applyTimezone(span.end), // "
  1008. ev
  1009. );
  1010. },
  1011. // Undoes a selection. updates in the internal state and triggers handlers.
  1012. // `ev` is the native mouse event that began the interaction.
  1013. unselect: function(ev) {
  1014. if (this.isSelected) {
  1015. this.isSelected = false;
  1016. if (this.destroySelection) {
  1017. this.destroySelection(); // TODO: deprecate
  1018. }
  1019. this.unrenderSelection();
  1020. this.publiclyTrigger('unselect', null, ev);
  1021. }
  1022. },
  1023. // Unrenders a visual indication of selection
  1024. unrenderSelection: function() {
  1025. // subclasses should implement
  1026. },
  1027. /* Event Selection
  1028. ------------------------------------------------------------------------------------------------------------------*/
  1029. selectEvent: function(event) {
  1030. if (!this.selectedEvent || this.selectedEvent !== event) {
  1031. this.unselectEvent();
  1032. this.renderedEventSegEach(function(seg) {
  1033. seg.el.addClass('fc-selected');
  1034. }, event);
  1035. this.selectedEvent = event;
  1036. }
  1037. },
  1038. unselectEvent: function() {
  1039. if (this.selectedEvent) {
  1040. this.renderedEventSegEach(function(seg) {
  1041. seg.el.removeClass('fc-selected');
  1042. }, this.selectedEvent);
  1043. this.selectedEvent = null;
  1044. }
  1045. },
  1046. isEventSelected: function(event) {
  1047. // event references might change on refetchEvents(), while selectedEvent doesn't,
  1048. // so compare IDs
  1049. return this.selectedEvent && this.selectedEvent._id === event._id;
  1050. },
  1051. /* Mouse / Touch Unselecting (time range & event unselection)
  1052. ------------------------------------------------------------------------------------------------------------------*/
  1053. // TODO: move consistently to down/start or up/end?
  1054. // TODO: don't kill previous selection if touch scrolling
  1055. handleDocumentMousedown: function(ev) {
  1056. if (isPrimaryMouseButton(ev)) {
  1057. this.processUnselect(ev);
  1058. }
  1059. },
  1060. processUnselect: function(ev) {
  1061. this.processRangeUnselect(ev);
  1062. this.processEventUnselect(ev);
  1063. },
  1064. processRangeUnselect: function(ev) {
  1065. var ignore;
  1066. // is there a time-range selection?
  1067. if (this.isSelected && this.opt('unselectAuto')) {
  1068. // only unselect if the clicked element is not identical to or inside of an 'unselectCancel' element
  1069. ignore = this.opt('unselectCancel');
  1070. if (!ignore || !$(ev.target).closest(ignore).length) {
  1071. this.unselect(ev);
  1072. }
  1073. }
  1074. },
  1075. processEventUnselect: function(ev) {
  1076. if (this.selectedEvent) {
  1077. if (!$(ev.target).closest('.fc-selected').length) {
  1078. this.unselectEvent();
  1079. }
  1080. }
  1081. },
  1082. /* Day Click
  1083. ------------------------------------------------------------------------------------------------------------------*/
  1084. // Triggers handlers to 'dayClick'
  1085. // Span has start/end of the clicked area. Only the start is useful.
  1086. triggerDayClick: function(span, dayEl, ev) {
  1087. this.publiclyTrigger(
  1088. 'dayClick',
  1089. dayEl,
  1090. this.calendar.applyTimezone(span.start), // convert to calendar's timezone for external API
  1091. ev
  1092. );
  1093. },
  1094. /* Date Utils
  1095. ------------------------------------------------------------------------------------------------------------------*/
  1096. // Initializes internal variables related to calculating hidden days-of-week
  1097. initHiddenDays: function() {
  1098. var hiddenDays = this.opt('hiddenDays') || []; // array of day-of-week indices that are hidden
  1099. var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool)
  1100. var dayCnt = 0;
  1101. var i;
  1102. if (this.opt('weekends') === false) {
  1103. hiddenDays.push(0, 6); // 0=sunday, 6=saturday
  1104. }
  1105. for (i = 0; i < 7; i++) {
  1106. if (
  1107. !(isHiddenDayHash[i] = $.inArray(i, hiddenDays) !== -1)
  1108. ) {
  1109. dayCnt++;
  1110. }
  1111. }
  1112. if (!dayCnt) {
  1113. throw 'invalid hiddenDays'; // all days were hidden? bad.
  1114. }
  1115. this.isHiddenDayHash = isHiddenDayHash;
  1116. },
  1117. // Is the current day hidden?
  1118. // `day` is a day-of-week index (0-6), or a Moment
  1119. isHiddenDay: function(day) {
  1120. if (moment.isMoment(day)) {
  1121. day = day.day();
  1122. }
  1123. return this.isHiddenDayHash[day];
  1124. },
  1125. // Incrementing the current day until it is no longer a hidden day, returning a copy.
  1126. // DOES NOT CONSIDER minDate/maxDate RANGE!
  1127. // If the initial value of `date` is not a hidden day, don't do anything.
  1128. // Pass `isExclusive` as `true` if you are dealing with an end date.
  1129. // `inc` defaults to `1` (increment one day forward each time)
  1130. skipHiddenDays: function(date, inc, isExclusive) {
  1131. var out = date.clone();
  1132. inc = inc || 1;
  1133. while (
  1134. this.isHiddenDayHash[(out.day() + (isExclusive ? inc : 0) + 7) % 7]
  1135. ) {
  1136. out.add(inc, 'days');
  1137. }
  1138. return out;
  1139. },
  1140. // Returns the date range of the full days the given range visually appears to occupy.
  1141. // Returns a new range object.
  1142. computeDayRange: function(range) {
  1143. var startDay = range.start.clone().stripTime(); // the beginning of the day the range starts
  1144. var end = range.end;
  1145. var endDay = null;
  1146. var endTimeMS;
  1147. if (end) {
  1148. endDay = end.clone().stripTime(); // the beginning of the day the range exclusively ends
  1149. endTimeMS = +end.time(); // # of milliseconds into `endDay`
  1150. // If the end time is actually inclusively part of the next day and is equal to or
  1151. // beyond the next day threshold, adjust the end to be the exclusive end of `endDay`.
  1152. // Otherwise, leaving it as inclusive will cause it to exclude `endDay`.
  1153. if (endTimeMS && endTimeMS >= this.nextDayThreshold) {
  1154. endDay.add(1, 'days');
  1155. }
  1156. }
  1157. // If no end was specified, or if it is within `startDay` but not past nextDayThreshold,
  1158. // assign the default duration of one day.
  1159. if (!end || endDay <= startDay) {
  1160. endDay = startDay.clone().add(1, 'days');
  1161. }
  1162. return { start: startDay, end: endDay };
  1163. },
  1164. // Does the given event visually appear to occupy more than one day?
  1165. isMultiDayEvent: function(event) {
  1166. var range = this.computeDayRange(event); // event is range-ish
  1167. return range.end.diff(range.start, 'days') > 1;
  1168. }
  1169. });