Não pode escolher mais do que 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 
 
 
 

11170 linhas
312 KiB

  1. /*!
  2. * FullCalendar v2.4.0
  3. * Docs & License: http://fullcalendar.io/
  4. * (c) 2015 Adam Shaw
  5. */
  6. (function(factory) {
  7. if (typeof define === 'function' && define.amd) {
  8. define([ 'jquery', 'moment' ], factory);
  9. }
  10. else if (typeof exports === 'object') { // Node/CommonJS
  11. module.exports = factory(require('jquery'), require('moment'));
  12. }
  13. else {
  14. factory(jQuery, moment);
  15. }
  16. })(function($, moment) {
  17. ;;
  18. var fc = $.fullCalendar = { version: "2.4.0" };
  19. var fcViews = fc.views = {};
  20. $.fn.fullCalendar = function(options) {
  21. var args = Array.prototype.slice.call(arguments, 1); // for a possible method call
  22. var res = this; // what this function will return (this jQuery object by default)
  23. this.each(function(i, _element) { // loop each DOM element involved
  24. var element = $(_element);
  25. var calendar = element.data('fullCalendar'); // get the existing calendar object (if any)
  26. var singleRes; // the returned value of this single method call
  27. // a method call
  28. if (typeof options === 'string') {
  29. if (calendar && $.isFunction(calendar[options])) {
  30. singleRes = calendar[options].apply(calendar, args);
  31. if (!i) {
  32. res = singleRes; // record the first method call result
  33. }
  34. if (options === 'destroy') { // for the destroy method, must remove Calendar object data
  35. element.removeData('fullCalendar');
  36. }
  37. }
  38. }
  39. // a new calendar initialization
  40. else if (!calendar) { // don't initialize twice
  41. calendar = new Calendar(element, options);
  42. element.data('fullCalendar', calendar);
  43. calendar.render();
  44. }
  45. });
  46. return res;
  47. };
  48. var complexOptions = [ // names of options that are objects whose properties should be combined
  49. 'header',
  50. 'buttonText',
  51. 'buttonIcons',
  52. 'themeButtonIcons'
  53. ];
  54. // Merges an array of option objects into a single object
  55. function mergeOptions(optionObjs) {
  56. return mergeProps(optionObjs, complexOptions);
  57. }
  58. // Given options specified for the calendar's constructor, massages any legacy options into a non-legacy form.
  59. // Converts View-Option-Hashes into the View-Specific-Options format.
  60. function massageOverrides(input) {
  61. var overrides = { views: input.views || {} }; // the output. ensure a `views` hash
  62. var subObj;
  63. // iterate through all option override properties (except `views`)
  64. $.each(input, function(name, val) {
  65. if (name != 'views') {
  66. // could the value be a legacy View-Option-Hash?
  67. if (
  68. $.isPlainObject(val) &&
  69. !/(time|duration|interval)$/i.test(name) && // exclude duration options. might be given as objects
  70. $.inArray(name, complexOptions) == -1 // complex options aren't allowed to be View-Option-Hashes
  71. ) {
  72. subObj = null;
  73. // iterate through the properties of this possible View-Option-Hash value
  74. $.each(val, function(subName, subVal) {
  75. // is the property targeting a view?
  76. if (/^(month|week|day|default|basic(Week|Day)?|agenda(Week|Day)?)$/.test(subName)) {
  77. if (!overrides.views[subName]) { // ensure the view-target entry exists
  78. overrides.views[subName] = {};
  79. }
  80. overrides.views[subName][name] = subVal; // record the value in the `views` object
  81. }
  82. else { // a non-View-Option-Hash property
  83. if (!subObj) {
  84. subObj = {};
  85. }
  86. subObj[subName] = subVal; // accumulate these unrelated values for later
  87. }
  88. });
  89. if (subObj) { // non-View-Option-Hash properties? transfer them as-is
  90. overrides[name] = subObj;
  91. }
  92. }
  93. else {
  94. overrides[name] = val; // transfer normal options as-is
  95. }
  96. }
  97. });
  98. return overrides;
  99. }
  100. ;;
  101. // exports
  102. fc.intersectionToSeg = intersectionToSeg;
  103. fc.applyAll = applyAll;
  104. fc.debounce = debounce;
  105. fc.isInt = isInt;
  106. fc.htmlEscape = htmlEscape;
  107. fc.cssToStr = cssToStr;
  108. fc.proxy = proxy;
  109. fc.capitaliseFirstLetter = capitaliseFirstLetter;
  110. /* FullCalendar-specific DOM Utilities
  111. ----------------------------------------------------------------------------------------------------------------------*/
  112. // Given the scrollbar widths of some other container, create borders/margins on rowEls in order to match the left
  113. // and right space that was offset by the scrollbars. A 1-pixel border first, then margin beyond that.
  114. function compensateScroll(rowEls, scrollbarWidths) {
  115. if (scrollbarWidths.left) {
  116. rowEls.css({
  117. 'border-left-width': 1,
  118. 'margin-left': scrollbarWidths.left - 1
  119. });
  120. }
  121. if (scrollbarWidths.right) {
  122. rowEls.css({
  123. 'border-right-width': 1,
  124. 'margin-right': scrollbarWidths.right - 1
  125. });
  126. }
  127. }
  128. // Undoes compensateScroll and restores all borders/margins
  129. function uncompensateScroll(rowEls) {
  130. rowEls.css({
  131. 'margin-left': '',
  132. 'margin-right': '',
  133. 'border-left-width': '',
  134. 'border-right-width': ''
  135. });
  136. }
  137. // Make the mouse cursor express that an event is not allowed in the current area
  138. function disableCursor() {
  139. $('body').addClass('fc-not-allowed');
  140. }
  141. // Returns the mouse cursor to its original look
  142. function enableCursor() {
  143. $('body').removeClass('fc-not-allowed');
  144. }
  145. // Given a total available height to fill, have `els` (essentially child rows) expand to accomodate.
  146. // By default, all elements that are shorter than the recommended height are expanded uniformly, not considering
  147. // any other els that are already too tall. if `shouldRedistribute` is on, it considers these tall rows and
  148. // reduces the available height.
  149. function distributeHeight(els, availableHeight, shouldRedistribute) {
  150. // *FLOORING NOTE*: we floor in certain places because zoom can give inaccurate floating-point dimensions,
  151. // and it is better to be shorter than taller, to avoid creating unnecessary scrollbars.
  152. var minOffset1 = Math.floor(availableHeight / els.length); // for non-last element
  153. var minOffset2 = Math.floor(availableHeight - minOffset1 * (els.length - 1)); // for last element *FLOORING NOTE*
  154. var flexEls = []; // elements that are allowed to expand. array of DOM nodes
  155. var flexOffsets = []; // amount of vertical space it takes up
  156. var flexHeights = []; // actual css height
  157. var usedHeight = 0;
  158. undistributeHeight(els); // give all elements their natural height
  159. // find elements that are below the recommended height (expandable).
  160. // important to query for heights in a single first pass (to avoid reflow oscillation).
  161. els.each(function(i, el) {
  162. var minOffset = i === els.length - 1 ? minOffset2 : minOffset1;
  163. var naturalOffset = $(el).outerHeight(true);
  164. if (naturalOffset < minOffset) {
  165. flexEls.push(el);
  166. flexOffsets.push(naturalOffset);
  167. flexHeights.push($(el).height());
  168. }
  169. else {
  170. // this element stretches past recommended height (non-expandable). mark the space as occupied.
  171. usedHeight += naturalOffset;
  172. }
  173. });
  174. // readjust the recommended height to only consider the height available to non-maxed-out rows.
  175. if (shouldRedistribute) {
  176. availableHeight -= usedHeight;
  177. minOffset1 = Math.floor(availableHeight / flexEls.length);
  178. minOffset2 = Math.floor(availableHeight - minOffset1 * (flexEls.length - 1)); // *FLOORING NOTE*
  179. }
  180. // assign heights to all expandable elements
  181. $(flexEls).each(function(i, el) {
  182. var minOffset = i === flexEls.length - 1 ? minOffset2 : minOffset1;
  183. var naturalOffset = flexOffsets[i];
  184. var naturalHeight = flexHeights[i];
  185. var newHeight = minOffset - (naturalOffset - naturalHeight); // subtract the margin/padding
  186. if (naturalOffset < minOffset) { // we check this again because redistribution might have changed things
  187. $(el).height(newHeight);
  188. }
  189. });
  190. }
  191. // Undoes distrubuteHeight, restoring all els to their natural height
  192. function undistributeHeight(els) {
  193. els.height('');
  194. }
  195. // Given `els`, a jQuery set of <td> cells, find the cell with the largest natural width and set the widths of all the
  196. // cells to be that width.
  197. // PREREQUISITE: if you want a cell to take up width, it needs to have a single inner element w/ display:inline
  198. function matchCellWidths(els) {
  199. var maxInnerWidth = 0;
  200. els.find('> *').each(function(i, innerEl) {
  201. var innerWidth = $(innerEl).outerWidth();
  202. if (innerWidth > maxInnerWidth) {
  203. maxInnerWidth = innerWidth;
  204. }
  205. });
  206. maxInnerWidth++; // sometimes not accurate of width the text needs to stay on one line. insurance
  207. els.width(maxInnerWidth);
  208. return maxInnerWidth;
  209. }
  210. // Turns a container element into a scroller if its contents is taller than the allotted height.
  211. // Returns true if the element is now a scroller, false otherwise.
  212. // NOTE: this method is best because it takes weird zooming dimensions into account
  213. function setPotentialScroller(containerEl, height) {
  214. containerEl.height(height).addClass('fc-scroller');
  215. // are scrollbars needed?
  216. if (containerEl[0].scrollHeight - 1 > containerEl[0].clientHeight) { // !!! -1 because IE is often off-by-one :(
  217. return true;
  218. }
  219. unsetScroller(containerEl); // undo
  220. return false;
  221. }
  222. // Takes an element that might have been a scroller, and turns it back into a normal element.
  223. function unsetScroller(containerEl) {
  224. containerEl.height('').removeClass('fc-scroller');
  225. }
  226. /* General DOM Utilities
  227. ----------------------------------------------------------------------------------------------------------------------*/
  228. fc.getClientRect = getClientRect;
  229. fc.getContentRect = getContentRect;
  230. fc.getScrollbarWidths = getScrollbarWidths;
  231. // borrowed from https://github.com/jquery/jquery-ui/blob/1.11.0/ui/core.js#L51
  232. function getScrollParent(el) {
  233. var position = el.css('position'),
  234. scrollParent = el.parents().filter(function() {
  235. var parent = $(this);
  236. return (/(auto|scroll)/).test(
  237. parent.css('overflow') + parent.css('overflow-y') + parent.css('overflow-x')
  238. );
  239. }).eq(0);
  240. return position === 'fixed' || !scrollParent.length ? $(el[0].ownerDocument || document) : scrollParent;
  241. }
  242. // Queries the outer bounding area of a jQuery element.
  243. // Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive).
  244. function getOuterRect(el) {
  245. var offset = el.offset();
  246. return {
  247. left: offset.left,
  248. right: offset.left + el.outerWidth(),
  249. top: offset.top,
  250. bottom: offset.top + el.outerHeight()
  251. };
  252. }
  253. // Queries the area within the margin/border/scrollbars of a jQuery element. Does not go within the padding.
  254. // Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive).
  255. // NOTE: should use clientLeft/clientTop, but very unreliable cross-browser.
  256. function getClientRect(el) {
  257. var offset = el.offset();
  258. var scrollbarWidths = getScrollbarWidths(el);
  259. var left = offset.left + getCssFloat(el, 'border-left-width') + scrollbarWidths.left;
  260. var top = offset.top + getCssFloat(el, 'border-top-width') + scrollbarWidths.top;
  261. return {
  262. left: left,
  263. right: left + el[0].clientWidth, // clientWidth includes padding but NOT scrollbars
  264. top: top,
  265. bottom: top + el[0].clientHeight // clientHeight includes padding but NOT scrollbars
  266. };
  267. }
  268. // Queries the area within the margin/border/padding of a jQuery element. Assumed not to have scrollbars.
  269. // Returns a rectangle with absolute coordinates: left, right (exclusive), top, bottom (exclusive).
  270. function getContentRect(el) {
  271. var offset = el.offset(); // just outside of border, margin not included
  272. var left = offset.left + getCssFloat(el, 'border-left-width') + getCssFloat(el, 'padding-left');
  273. var top = offset.top + getCssFloat(el, 'border-top-width') + getCssFloat(el, 'padding-top');
  274. return {
  275. left: left,
  276. right: left + el.width(),
  277. top: top,
  278. bottom: top + el.height()
  279. };
  280. }
  281. // Returns the computed left/right/top/bottom scrollbar widths for the given jQuery element.
  282. // NOTE: should use clientLeft/clientTop, but very unreliable cross-browser.
  283. function getScrollbarWidths(el) {
  284. var leftRightWidth = el.innerWidth() - el[0].clientWidth; // the paddings cancel out, leaving the scrollbars
  285. var widths = {
  286. left: 0,
  287. right: 0,
  288. top: 0,
  289. bottom: el.innerHeight() - el[0].clientHeight // the paddings cancel out, leaving the bottom scrollbar
  290. };
  291. if (getIsLeftRtlScrollbars() && el.css('direction') == 'rtl') { // is the scrollbar on the left side?
  292. widths.left = leftRightWidth;
  293. }
  294. else {
  295. widths.right = leftRightWidth;
  296. }
  297. return widths;
  298. }
  299. // Logic for determining if, when the element is right-to-left, the scrollbar appears on the left side
  300. var _isLeftRtlScrollbars = null;
  301. function getIsLeftRtlScrollbars() { // responsible for caching the computation
  302. if (_isLeftRtlScrollbars === null) {
  303. _isLeftRtlScrollbars = computeIsLeftRtlScrollbars();
  304. }
  305. return _isLeftRtlScrollbars;
  306. }
  307. function computeIsLeftRtlScrollbars() { // creates an offscreen test element, then removes it
  308. var el = $('<div><div/></div>')
  309. .css({
  310. position: 'absolute',
  311. top: -1000,
  312. left: 0,
  313. border: 0,
  314. padding: 0,
  315. overflow: 'scroll',
  316. direction: 'rtl'
  317. })
  318. .appendTo('body');
  319. var innerEl = el.children();
  320. var res = innerEl.offset().left > el.offset().left; // is the inner div shifted to accommodate a left scrollbar?
  321. el.remove();
  322. return res;
  323. }
  324. // Retrieves a jQuery element's computed CSS value as a floating-point number.
  325. // If the queried value is non-numeric (ex: IE can return "medium" for border width), will just return zero.
  326. function getCssFloat(el, prop) {
  327. return parseFloat(el.css(prop)) || 0;
  328. }
  329. // Returns a boolean whether this was a left mouse click and no ctrl key (which means right click on Mac)
  330. function isPrimaryMouseButton(ev) {
  331. return ev.which == 1 && !ev.ctrlKey;
  332. }
  333. /* Geometry
  334. ----------------------------------------------------------------------------------------------------------------------*/
  335. fc.intersectRects = intersectRects;
  336. // Returns a new rectangle that is the intersection of the two rectangles. If they don't intersect, returns false
  337. function intersectRects(rect1, rect2) {
  338. var res = {
  339. left: Math.max(rect1.left, rect2.left),
  340. right: Math.min(rect1.right, rect2.right),
  341. top: Math.max(rect1.top, rect2.top),
  342. bottom: Math.min(rect1.bottom, rect2.bottom)
  343. };
  344. if (res.left < res.right && res.top < res.bottom) {
  345. return res;
  346. }
  347. return false;
  348. }
  349. // Returns a new point that will have been moved to reside within the given rectangle
  350. function constrainPoint(point, rect) {
  351. return {
  352. left: Math.min(Math.max(point.left, rect.left), rect.right),
  353. top: Math.min(Math.max(point.top, rect.top), rect.bottom)
  354. };
  355. }
  356. // Returns a point that is the center of the given rectangle
  357. function getRectCenter(rect) {
  358. return {
  359. left: (rect.left + rect.right) / 2,
  360. top: (rect.top + rect.bottom) / 2
  361. };
  362. }
  363. // Subtracts point2's coordinates from point1's coordinates, returning a delta
  364. function diffPoints(point1, point2) {
  365. return {
  366. left: point1.left - point2.left,
  367. top: point1.top - point2.top
  368. };
  369. }
  370. /* Object Ordering by Field
  371. ----------------------------------------------------------------------------------------------------------------------*/
  372. fc.parseFieldSpecs = parseFieldSpecs;
  373. fc.compareByFieldSpecs = compareByFieldSpecs;
  374. fc.compareByFieldSpec = compareByFieldSpec;
  375. fc.flexibleCompare = flexibleCompare;
  376. function parseFieldSpecs(input) {
  377. var specs = [];
  378. var tokens = [];
  379. var i, token;
  380. if (typeof input === 'string') {
  381. tokens = input.split(/\s*,\s*/);
  382. }
  383. else if (typeof input === 'function') {
  384. tokens = [ input ];
  385. }
  386. else if ($.isArray(input)) {
  387. tokens = input;
  388. }
  389. for (i = 0; i < tokens.length; i++) {
  390. token = tokens[i];
  391. if (typeof token === 'string') {
  392. specs.push(
  393. token.charAt(0) == '-' ?
  394. { field: token.substring(1), order: -1 } :
  395. { field: token, order: 1 }
  396. );
  397. }
  398. else if (typeof token === 'function') {
  399. specs.push({ func: token });
  400. }
  401. }
  402. return specs;
  403. }
  404. function compareByFieldSpecs(obj1, obj2, fieldSpecs) {
  405. var i;
  406. var cmp;
  407. for (i = 0; i < fieldSpecs.length; i++) {
  408. cmp = compareByFieldSpec(obj1, obj2, fieldSpecs[i]);
  409. if (cmp) {
  410. return cmp;
  411. }
  412. }
  413. return 0;
  414. }
  415. function compareByFieldSpec(obj1, obj2, fieldSpec) {
  416. if (fieldSpec.func) {
  417. return fieldSpec.func(obj1, obj2);
  418. }
  419. return flexibleCompare(obj1[fieldSpec.field], obj2[fieldSpec.field]) *
  420. (fieldSpec.order || 1);
  421. }
  422. function flexibleCompare(a, b) {
  423. if (!a && !b) {
  424. return 0;
  425. }
  426. if (b == null) {
  427. return -1;
  428. }
  429. if (a == null) {
  430. return 1;
  431. }
  432. if ($.type(a) === 'string' || $.type(b) === 'string') {
  433. return String(a).localeCompare(String(b));
  434. }
  435. return a - b;
  436. }
  437. /* FullCalendar-specific Misc Utilities
  438. ----------------------------------------------------------------------------------------------------------------------*/
  439. // Creates a basic segment with the intersection of the two ranges. Returns undefined if no intersection.
  440. // Expects all dates to be normalized to the same timezone beforehand.
  441. // TODO: move to date section?
  442. function intersectionToSeg(subjectRange, constraintRange) {
  443. var subjectStart = subjectRange.start;
  444. var subjectEnd = subjectRange.end;
  445. var constraintStart = constraintRange.start;
  446. var constraintEnd = constraintRange.end;
  447. var segStart, segEnd;
  448. var isStart, isEnd;
  449. if (subjectEnd > constraintStart && subjectStart < constraintEnd) { // in bounds at all?
  450. if (subjectStart >= constraintStart) {
  451. segStart = subjectStart.clone();
  452. isStart = true;
  453. }
  454. else {
  455. segStart = constraintStart.clone();
  456. isStart = false;
  457. }
  458. if (subjectEnd <= constraintEnd) {
  459. segEnd = subjectEnd.clone();
  460. isEnd = true;
  461. }
  462. else {
  463. segEnd = constraintEnd.clone();
  464. isEnd = false;
  465. }
  466. return {
  467. start: segStart,
  468. end: segEnd,
  469. isStart: isStart,
  470. isEnd: isEnd
  471. };
  472. }
  473. }
  474. /* Date Utilities
  475. ----------------------------------------------------------------------------------------------------------------------*/
  476. fc.computeIntervalUnit = computeIntervalUnit;
  477. fc.divideRangeByDuration = divideRangeByDuration;
  478. fc.divideDurationByDuration = divideDurationByDuration;
  479. fc.multiplyDuration = multiplyDuration;
  480. fc.durationHasTime = durationHasTime;
  481. var dayIDs = [ 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat' ];
  482. var intervalUnits = [ 'year', 'month', 'week', 'day', 'hour', 'minute', 'second', 'millisecond' ];
  483. // Diffs the two moments into a Duration where full-days are recorded first, then the remaining time.
  484. // Moments will have their timezones normalized.
  485. function diffDayTime(a, b) {
  486. return moment.duration({
  487. days: a.clone().stripTime().diff(b.clone().stripTime(), 'days'),
  488. ms: a.time() - b.time() // time-of-day from day start. disregards timezone
  489. });
  490. }
  491. // Diffs the two moments via their start-of-day (regardless of timezone). Produces whole-day durations.
  492. function diffDay(a, b) {
  493. return moment.duration({
  494. days: a.clone().stripTime().diff(b.clone().stripTime(), 'days')
  495. });
  496. }
  497. // Diffs two moments, producing a duration, made of a whole-unit-increment of the given unit. Uses rounding.
  498. function diffByUnit(a, b, unit) {
  499. return moment.duration(
  500. Math.round(a.diff(b, unit, true)), // returnFloat=true
  501. unit
  502. );
  503. }
  504. // Computes the unit name of the largest whole-unit period of time.
  505. // For example, 48 hours will be "days" whereas 49 hours will be "hours".
  506. // Accepts start/end, a range object, or an original duration object.
  507. function computeIntervalUnit(start, end) {
  508. var i, unit;
  509. var val;
  510. for (i = 0; i < intervalUnits.length; i++) {
  511. unit = intervalUnits[i];
  512. val = computeRangeAs(unit, start, end);
  513. if (val >= 1 && isInt(val)) {
  514. break;
  515. }
  516. }
  517. return unit; // will be "milliseconds" if nothing else matches
  518. }
  519. // Computes the number of units (like "hours") in the given range.
  520. // Range can be a {start,end} object, separate start/end args, or a Duration.
  521. // Results are based on Moment's .as() and .diff() methods, so results can depend on internal handling
  522. // of month-diffing logic (which tends to vary from version to version).
  523. function computeRangeAs(unit, start, end) {
  524. if (end != null) { // given start, end
  525. return end.diff(start, unit, true);
  526. }
  527. else if (moment.isDuration(start)) { // given duration
  528. return start.as(unit);
  529. }
  530. else { // given { start, end } range object
  531. return start.end.diff(start.start, unit, true);
  532. }
  533. }
  534. // Intelligently divides a range (specified by a start/end params) by a duration
  535. function divideRangeByDuration(start, end, dur) {
  536. var months;
  537. if (durationHasTime(dur)) {
  538. return (end - start) / dur;
  539. }
  540. months = dur.asMonths();
  541. if (Math.abs(months) >= 1 && isInt(months)) {
  542. return end.diff(start, 'months', true) / months;
  543. }
  544. return end.diff(start, 'days', true) / dur.asDays();
  545. }
  546. // Intelligently divides one duration by another
  547. function divideDurationByDuration(dur1, dur2) {
  548. var months1, months2;
  549. if (durationHasTime(dur1) || durationHasTime(dur2)) {
  550. return dur1 / dur2;
  551. }
  552. months1 = dur1.asMonths();
  553. months2 = dur2.asMonths();
  554. if (
  555. Math.abs(months1) >= 1 && isInt(months1) &&
  556. Math.abs(months2) >= 1 && isInt(months2)
  557. ) {
  558. return months1 / months2;
  559. }
  560. return dur1.asDays() / dur2.asDays();
  561. }
  562. // Intelligently multiplies a duration by a number
  563. function multiplyDuration(dur, n) {
  564. var months;
  565. if (durationHasTime(dur)) {
  566. return moment.duration(dur * n);
  567. }
  568. months = dur.asMonths();
  569. if (Math.abs(months) >= 1 && isInt(months)) {
  570. return moment.duration({ months: months * n });
  571. }
  572. return moment.duration({ days: dur.asDays() * n });
  573. }
  574. // Returns a boolean about whether the given duration has any time parts (hours/minutes/seconds/ms)
  575. function durationHasTime(dur) {
  576. return Boolean(dur.hours() || dur.minutes() || dur.seconds() || dur.milliseconds());
  577. }
  578. function isNativeDate(input) {
  579. return Object.prototype.toString.call(input) === '[object Date]' || input instanceof Date;
  580. }
  581. // Returns a boolean about whether the given input is a time string, like "06:40:00" or "06:00"
  582. function isTimeString(str) {
  583. return /^\d+\:\d+(?:\:\d+\.?(?:\d{3})?)?$/.test(str);
  584. }
  585. /* Logging and Debug
  586. ----------------------------------------------------------------------------------------------------------------------*/
  587. fc.log = function() {
  588. var console = window.console;
  589. if (console && console.log) {
  590. return console.log.apply(console, arguments);
  591. }
  592. };
  593. fc.warn = function() {
  594. var console = window.console;
  595. if (console && console.warn) {
  596. return console.warn.apply(console, arguments);
  597. }
  598. else {
  599. return fc.log.apply(fc, arguments);
  600. }
  601. };
  602. /* General Utilities
  603. ----------------------------------------------------------------------------------------------------------------------*/
  604. var hasOwnPropMethod = {}.hasOwnProperty;
  605. // Merges an array of objects into a single object.
  606. // The second argument allows for an array of property names who's object values will be merged together.
  607. function mergeProps(propObjs, complexProps) {
  608. var dest = {};
  609. var i, name;
  610. var complexObjs;
  611. var j, val;
  612. var props;
  613. if (complexProps) {
  614. for (i = 0; i < complexProps.length; i++) {
  615. name = complexProps[i];
  616. complexObjs = [];
  617. // collect the trailing object values, stopping when a non-object is discovered
  618. for (j = propObjs.length - 1; j >= 0; j--) {
  619. val = propObjs[j][name];
  620. if (typeof val === 'object') {
  621. complexObjs.unshift(val);
  622. }
  623. else if (val !== undefined) {
  624. dest[name] = val; // if there were no objects, this value will be used
  625. break;
  626. }
  627. }
  628. // if the trailing values were objects, use the merged value
  629. if (complexObjs.length) {
  630. dest[name] = mergeProps(complexObjs);
  631. }
  632. }
  633. }
  634. // copy values into the destination, going from last to first
  635. for (i = propObjs.length - 1; i >= 0; i--) {
  636. props = propObjs[i];
  637. for (name in props) {
  638. if (!(name in dest)) { // if already assigned by previous props or complex props, don't reassign
  639. dest[name] = props[name];
  640. }
  641. }
  642. }
  643. return dest;
  644. }
  645. // Create an object that has the given prototype. Just like Object.create
  646. function createObject(proto) {
  647. var f = function() {};
  648. f.prototype = proto;
  649. return new f();
  650. }
  651. function copyOwnProps(src, dest) {
  652. for (var name in src) {
  653. if (hasOwnProp(src, name)) {
  654. dest[name] = src[name];
  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. for (i = 0; i < names.length; i++) {
  664. name = names[i];
  665. if (src[name] !== Object.prototype[name]) {
  666. dest[name] = src[name];
  667. }
  668. }
  669. }
  670. function hasOwnProp(obj, name) {
  671. return hasOwnPropMethod.call(obj, name);
  672. }
  673. // Is the given value a non-object non-function value?
  674. function isAtomic(val) {
  675. return /undefined|null|boolean|number|string/.test($.type(val));
  676. }
  677. function applyAll(functions, thisObj, args) {
  678. if ($.isFunction(functions)) {
  679. functions = [ functions ];
  680. }
  681. if (functions) {
  682. var i;
  683. var ret;
  684. for (i=0; i<functions.length; i++) {
  685. ret = functions[i].apply(thisObj, args) || ret;
  686. }
  687. return ret;
  688. }
  689. }
  690. function firstDefined() {
  691. for (var i=0; i<arguments.length; i++) {
  692. if (arguments[i] !== undefined) {
  693. return arguments[i];
  694. }
  695. }
  696. }
  697. function htmlEscape(s) {
  698. return (s + '').replace(/&/g, '&amp;')
  699. .replace(/</g, '&lt;')
  700. .replace(/>/g, '&gt;')
  701. .replace(/'/g, '&#039;')
  702. .replace(/"/g, '&quot;')
  703. .replace(/\n/g, '<br />');
  704. }
  705. function stripHtmlEntities(text) {
  706. return text.replace(/&.*?;/g, '');
  707. }
  708. // Given a hash of CSS properties, returns a string of CSS.
  709. // Uses property names as-is (no camel-case conversion). Will not make statements for null/undefined values.
  710. function cssToStr(cssProps) {
  711. var statements = [];
  712. $.each(cssProps, function(name, val) {
  713. if (val != null) {
  714. statements.push(name + ':' + val);
  715. }
  716. });
  717. return statements.join(';');
  718. }
  719. function capitaliseFirstLetter(str) {
  720. return str.charAt(0).toUpperCase() + str.slice(1);
  721. }
  722. function compareNumbers(a, b) { // for .sort()
  723. return a - b;
  724. }
  725. function isInt(n) {
  726. return n % 1 === 0;
  727. }
  728. // Returns a method bound to the given object context.
  729. // Just like one of the jQuery.proxy signatures, but without the undesired behavior of treating the same method with
  730. // different contexts as identical when binding/unbinding events.
  731. function proxy(obj, methodName) {
  732. var method = obj[methodName];
  733. return function() {
  734. return method.apply(obj, arguments);
  735. };
  736. }
  737. // Returns a function, that, as long as it continues to be invoked, will not
  738. // be triggered. The function will be called after it stops being called for
  739. // N milliseconds.
  740. // https://github.com/jashkenas/underscore/blob/1.6.0/underscore.js#L714
  741. function debounce(func, wait) {
  742. var timeoutId;
  743. var args;
  744. var context;
  745. var timestamp; // of most recent call
  746. var later = function() {
  747. var last = +new Date() - timestamp;
  748. if (last < wait && last > 0) {
  749. timeoutId = setTimeout(later, wait - last);
  750. }
  751. else {
  752. timeoutId = null;
  753. func.apply(context, args);
  754. if (!timeoutId) {
  755. context = args = null;
  756. }
  757. }
  758. };
  759. return function() {
  760. context = this;
  761. args = arguments;
  762. timestamp = +new Date();
  763. if (!timeoutId) {
  764. timeoutId = setTimeout(later, wait);
  765. }
  766. };
  767. }
  768. ;;
  769. var ambigDateOfMonthRegex = /^\s*\d{4}-\d\d$/;
  770. var ambigTimeOrZoneRegex =
  771. /^\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+)?)?)?)?)?$/;
  772. var newMomentProto = moment.fn; // where we will attach our new methods
  773. var oldMomentProto = $.extend({}, newMomentProto); // copy of original moment methods
  774. var allowValueOptimization;
  775. var setUTCValues; // function defined below
  776. var setLocalValues; // function defined below
  777. // Creating
  778. // -------------------------------------------------------------------------------------------------
  779. // Creates a new moment, similar to the vanilla moment(...) constructor, but with
  780. // extra features (ambiguous time, enhanced formatting). When given an existing moment,
  781. // it will function as a clone (and retain the zone of the moment). Anything else will
  782. // result in a moment in the local zone.
  783. fc.moment = function() {
  784. return makeMoment(arguments);
  785. };
  786. // Sames as fc.moment, but forces the resulting moment to be in the UTC timezone.
  787. fc.moment.utc = function() {
  788. var mom = makeMoment(arguments, true);
  789. // Force it into UTC because makeMoment doesn't guarantee it
  790. // (if given a pre-existing moment for example)
  791. if (mom.hasTime()) { // don't give ambiguously-timed moments a UTC zone
  792. mom.utc();
  793. }
  794. return mom;
  795. };
  796. // Same as fc.moment, but when given an ISO8601 string, the timezone offset is preserved.
  797. // ISO8601 strings with no timezone offset will become ambiguously zoned.
  798. fc.moment.parseZone = function() {
  799. return makeMoment(arguments, true, true);
  800. };
  801. // Builds an enhanced moment from args. When given an existing moment, it clones. When given a
  802. // native Date, or called with no arguments (the current time), the resulting moment will be local.
  803. // Anything else needs to be "parsed" (a string or an array), and will be affected by:
  804. // parseAsUTC - if there is no zone information, should we parse the input in UTC?
  805. // parseZone - if there is zone information, should we force the zone of the moment?
  806. function makeMoment(args, parseAsUTC, parseZone) {
  807. var input = args[0];
  808. var isSingleString = args.length == 1 && typeof input === 'string';
  809. var isAmbigTime;
  810. var isAmbigZone;
  811. var ambigMatch;
  812. var mom;
  813. if (moment.isMoment(input)) {
  814. mom = moment.apply(null, args); // clone it
  815. transferAmbigs(input, mom); // the ambig flags weren't transfered with the clone
  816. }
  817. else if (isNativeDate(input) || input === undefined) {
  818. mom = moment.apply(null, args); // will be local
  819. }
  820. else { // "parsing" is required
  821. isAmbigTime = false;
  822. isAmbigZone = false;
  823. if (isSingleString) {
  824. if (ambigDateOfMonthRegex.test(input)) {
  825. // accept strings like '2014-05', but convert to the first of the month
  826. input += '-01';
  827. args = [ input ]; // for when we pass it on to moment's constructor
  828. isAmbigTime = true;
  829. isAmbigZone = true;
  830. }
  831. else if ((ambigMatch = ambigTimeOrZoneRegex.exec(input))) {
  832. isAmbigTime = !ambigMatch[5]; // no time part?
  833. isAmbigZone = true;
  834. }
  835. }
  836. else if ($.isArray(input)) {
  837. // arrays have no timezone information, so assume ambiguous zone
  838. isAmbigZone = true;
  839. }
  840. // otherwise, probably a string with a format
  841. if (parseAsUTC || isAmbigTime) {
  842. mom = moment.utc.apply(moment, args);
  843. }
  844. else {
  845. mom = moment.apply(null, args);
  846. }
  847. if (isAmbigTime) {
  848. mom._ambigTime = true;
  849. mom._ambigZone = true; // ambiguous time always means ambiguous zone
  850. }
  851. else if (parseZone) { // let's record the inputted zone somehow
  852. if (isAmbigZone) {
  853. mom._ambigZone = true;
  854. }
  855. else if (isSingleString) {
  856. if (mom.utcOffset) {
  857. mom.utcOffset(input); // if not a valid zone, will assign UTC
  858. }
  859. else {
  860. mom.zone(input); // for moment-pre-2.9
  861. }
  862. }
  863. }
  864. }
  865. mom._fullCalendar = true; // flag for extended functionality
  866. return mom;
  867. }
  868. // A clone method that works with the flags related to our enhanced functionality.
  869. // In the future, use moment.momentProperties
  870. newMomentProto.clone = function() {
  871. var mom = oldMomentProto.clone.apply(this, arguments);
  872. // these flags weren't transfered with the clone
  873. transferAmbigs(this, mom);
  874. if (this._fullCalendar) {
  875. mom._fullCalendar = true;
  876. }
  877. return mom;
  878. };
  879. // Week Number
  880. // -------------------------------------------------------------------------------------------------
  881. // Returns the week number, considering the locale's custom week number calcuation
  882. // `weeks` is an alias for `week`
  883. newMomentProto.week = newMomentProto.weeks = function(input) {
  884. var weekCalc = (this._locale || this._lang) // works pre-moment-2.8
  885. ._fullCalendar_weekCalc;
  886. if (input == null && typeof weekCalc === 'function') { // custom function only works for getter
  887. return weekCalc(this);
  888. }
  889. else if (weekCalc === 'ISO') {
  890. return oldMomentProto.isoWeek.apply(this, arguments); // ISO getter/setter
  891. }
  892. return oldMomentProto.week.apply(this, arguments); // local getter/setter
  893. };
  894. // Time-of-day
  895. // -------------------------------------------------------------------------------------------------
  896. // GETTER
  897. // Returns a Duration with the hours/minutes/seconds/ms values of the moment.
  898. // If the moment has an ambiguous time, a duration of 00:00 will be returned.
  899. //
  900. // SETTER
  901. // You can supply a Duration, a Moment, or a Duration-like argument.
  902. // When setting the time, and the moment has an ambiguous time, it then becomes unambiguous.
  903. newMomentProto.time = function(time) {
  904. // Fallback to the original method (if there is one) if this moment wasn't created via FullCalendar.
  905. // `time` is a generic enough method name where this precaution is necessary to avoid collisions w/ other plugins.
  906. if (!this._fullCalendar) {
  907. return oldMomentProto.time.apply(this, arguments);
  908. }
  909. if (time == null) { // getter
  910. return moment.duration({
  911. hours: this.hours(),
  912. minutes: this.minutes(),
  913. seconds: this.seconds(),
  914. milliseconds: this.milliseconds()
  915. });
  916. }
  917. else { // setter
  918. this._ambigTime = false; // mark that the moment now has a time
  919. if (!moment.isDuration(time) && !moment.isMoment(time)) {
  920. time = moment.duration(time);
  921. }
  922. // The day value should cause overflow (so 24 hours becomes 00:00:00 of next day).
  923. // Only for Duration times, not Moment times.
  924. var dayHours = 0;
  925. if (moment.isDuration(time)) {
  926. dayHours = Math.floor(time.asDays()) * 24;
  927. }
  928. // We need to set the individual fields.
  929. // Can't use startOf('day') then add duration. In case of DST at start of day.
  930. return this.hours(dayHours + time.hours())
  931. .minutes(time.minutes())
  932. .seconds(time.seconds())
  933. .milliseconds(time.milliseconds());
  934. }
  935. };
  936. // Converts the moment to UTC, stripping out its time-of-day and timezone offset,
  937. // but preserving its YMD. A moment with a stripped time will display no time
  938. // nor timezone offset when .format() is called.
  939. newMomentProto.stripTime = function() {
  940. var a;
  941. if (!this._ambigTime) {
  942. // get the values before any conversion happens
  943. a = this.toArray(); // array of y/m/d/h/m/s/ms
  944. // TODO: use keepLocalTime in the future
  945. this.utc(); // set the internal UTC flag (will clear the ambig flags)
  946. setUTCValues(this, a.slice(0, 3)); // set the year/month/date. time will be zero
  947. // Mark the time as ambiguous. This needs to happen after the .utc() call, which might call .utcOffset(),
  948. // which clears all ambig flags. Same with setUTCValues with moment-timezone.
  949. this._ambigTime = true;
  950. this._ambigZone = true; // if ambiguous time, also ambiguous timezone offset
  951. }
  952. return this; // for chaining
  953. };
  954. // Returns if the moment has a non-ambiguous time (boolean)
  955. newMomentProto.hasTime = function() {
  956. return !this._ambigTime;
  957. };
  958. // Timezone
  959. // -------------------------------------------------------------------------------------------------
  960. // Converts the moment to UTC, stripping out its timezone offset, but preserving its
  961. // YMD and time-of-day. A moment with a stripped timezone offset will display no
  962. // timezone offset when .format() is called.
  963. // TODO: look into Moment's keepLocalTime functionality
  964. newMomentProto.stripZone = function() {
  965. var a, wasAmbigTime;
  966. if (!this._ambigZone) {
  967. // get the values before any conversion happens
  968. a = this.toArray(); // array of y/m/d/h/m/s/ms
  969. wasAmbigTime = this._ambigTime;
  970. this.utc(); // set the internal UTC flag (might clear the ambig flags, depending on Moment internals)
  971. setUTCValues(this, a); // will set the year/month/date/hours/minutes/seconds/ms
  972. // the above call to .utc()/.utcOffset() unfortunately might clear the ambig flags, so restore
  973. this._ambigTime = wasAmbigTime || false;
  974. // Mark the zone as ambiguous. This needs to happen after the .utc() call, which might call .utcOffset(),
  975. // which clears the ambig flags. Same with setUTCValues with moment-timezone.
  976. this._ambigZone = true;
  977. }
  978. return this; // for chaining
  979. };
  980. // Returns of the moment has a non-ambiguous timezone offset (boolean)
  981. newMomentProto.hasZone = function() {
  982. return !this._ambigZone;
  983. };
  984. // this method implicitly marks a zone
  985. newMomentProto.local = function() {
  986. var a = this.toArray(); // year,month,date,hours,minutes,seconds,ms as an array
  987. var wasAmbigZone = this._ambigZone;
  988. oldMomentProto.local.apply(this, arguments);
  989. // ensure non-ambiguous
  990. // this probably already happened via local() -> utcOffset(), but don't rely on Moment's internals
  991. this._ambigTime = false;
  992. this._ambigZone = false;
  993. if (wasAmbigZone) {
  994. // If the moment was ambiguously zoned, the date fields were stored as UTC.
  995. // We want to preserve these, but in local time.
  996. // TODO: look into Moment's keepLocalTime functionality
  997. setLocalValues(this, a);
  998. }
  999. return this; // for chaining
  1000. };
  1001. // implicitly marks a zone
  1002. newMomentProto.utc = function() {
  1003. oldMomentProto.utc.apply(this, arguments);
  1004. // ensure non-ambiguous
  1005. // this probably already happened via utc() -> utcOffset(), but don't rely on Moment's internals
  1006. this._ambigTime = false;
  1007. this._ambigZone = false;
  1008. return this;
  1009. };
  1010. // methods for arbitrarily manipulating timezone offset.
  1011. // should clear time/zone ambiguity when called.
  1012. $.each([
  1013. 'zone', // only in moment-pre-2.9. deprecated afterwards
  1014. 'utcOffset'
  1015. ], function(i, name) {
  1016. if (oldMomentProto[name]) { // original method exists?
  1017. // this method implicitly marks a zone (will probably get called upon .utc() and .local())
  1018. newMomentProto[name] = function(tzo) {
  1019. if (tzo != null) { // setter
  1020. // these assignments needs to happen before the original zone method is called.
  1021. // I forget why, something to do with a browser crash.
  1022. this._ambigTime = false;
  1023. this._ambigZone = false;
  1024. }
  1025. return oldMomentProto[name].apply(this, arguments);
  1026. };
  1027. }
  1028. });
  1029. // Formatting
  1030. // -------------------------------------------------------------------------------------------------
  1031. newMomentProto.format = function() {
  1032. if (this._fullCalendar && arguments[0]) { // an enhanced moment? and a format string provided?
  1033. return formatDate(this, arguments[0]); // our extended formatting
  1034. }
  1035. if (this._ambigTime) {
  1036. return oldMomentFormat(this, 'YYYY-MM-DD');
  1037. }
  1038. if (this._ambigZone) {
  1039. return oldMomentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss');
  1040. }
  1041. return oldMomentProto.format.apply(this, arguments);
  1042. };
  1043. newMomentProto.toISOString = function() {
  1044. if (this._ambigTime) {
  1045. return oldMomentFormat(this, 'YYYY-MM-DD');
  1046. }
  1047. if (this._ambigZone) {
  1048. return oldMomentFormat(this, 'YYYY-MM-DD[T]HH:mm:ss');
  1049. }
  1050. return oldMomentProto.toISOString.apply(this, arguments);
  1051. };
  1052. // Querying
  1053. // -------------------------------------------------------------------------------------------------
  1054. // Is the moment within the specified range? `end` is exclusive.
  1055. // FYI, this method is not a standard Moment method, so always do our enhanced logic.
  1056. newMomentProto.isWithin = function(start, end) {
  1057. var a = commonlyAmbiguate([ this, start, end ]);
  1058. return a[0] >= a[1] && a[0] < a[2];
  1059. };
  1060. // When isSame is called with units, timezone ambiguity is normalized before the comparison happens.
  1061. // If no units specified, the two moments must be identically the same, with matching ambig flags.
  1062. newMomentProto.isSame = function(input, units) {
  1063. var a;
  1064. // only do custom logic if this is an enhanced moment
  1065. if (!this._fullCalendar) {
  1066. return oldMomentProto.isSame.apply(this, arguments);
  1067. }
  1068. if (units) {
  1069. a = commonlyAmbiguate([ this, input ], true); // normalize timezones but don't erase times
  1070. return oldMomentProto.isSame.call(a[0], a[1], units);
  1071. }
  1072. else {
  1073. input = fc.moment.parseZone(input); // normalize input
  1074. return oldMomentProto.isSame.call(this, input) &&
  1075. Boolean(this._ambigTime) === Boolean(input._ambigTime) &&
  1076. Boolean(this._ambigZone) === Boolean(input._ambigZone);
  1077. }
  1078. };
  1079. // Make these query methods work with ambiguous moments
  1080. $.each([
  1081. 'isBefore',
  1082. 'isAfter'
  1083. ], function(i, methodName) {
  1084. newMomentProto[methodName] = function(input, units) {
  1085. var a;
  1086. // only do custom logic if this is an enhanced moment
  1087. if (!this._fullCalendar) {
  1088. return oldMomentProto[methodName].apply(this, arguments);
  1089. }
  1090. a = commonlyAmbiguate([ this, input ]);
  1091. return oldMomentProto[methodName].call(a[0], a[1], units);
  1092. };
  1093. });
  1094. // Misc Internals
  1095. // -------------------------------------------------------------------------------------------------
  1096. // given an array of moment-like inputs, return a parallel array w/ moments similarly ambiguated.
  1097. // for example, of one moment has ambig time, but not others, all moments will have their time stripped.
  1098. // set `preserveTime` to `true` to keep times, but only normalize zone ambiguity.
  1099. // returns the original moments if no modifications are necessary.
  1100. function commonlyAmbiguate(inputs, preserveTime) {
  1101. var anyAmbigTime = false;
  1102. var anyAmbigZone = false;
  1103. var len = inputs.length;
  1104. var moms = [];
  1105. var i, mom;
  1106. // parse inputs into real moments and query their ambig flags
  1107. for (i = 0; i < len; i++) {
  1108. mom = inputs[i];
  1109. if (!moment.isMoment(mom)) {
  1110. mom = fc.moment.parseZone(mom);
  1111. }
  1112. anyAmbigTime = anyAmbigTime || mom._ambigTime;
  1113. anyAmbigZone = anyAmbigZone || mom._ambigZone;
  1114. moms.push(mom);
  1115. }
  1116. // strip each moment down to lowest common ambiguity
  1117. // use clones to avoid modifying the original moments
  1118. for (i = 0; i < len; i++) {
  1119. mom = moms[i];
  1120. if (!preserveTime && anyAmbigTime && !mom._ambigTime) {
  1121. moms[i] = mom.clone().stripTime();
  1122. }
  1123. else if (anyAmbigZone && !mom._ambigZone) {
  1124. moms[i] = mom.clone().stripZone();
  1125. }
  1126. }
  1127. return moms;
  1128. }
  1129. // Transfers all the flags related to ambiguous time/zone from the `src` moment to the `dest` moment
  1130. // TODO: look into moment.momentProperties for this.
  1131. function transferAmbigs(src, dest) {
  1132. if (src._ambigTime) {
  1133. dest._ambigTime = true;
  1134. }
  1135. else if (dest._ambigTime) {
  1136. dest._ambigTime = false;
  1137. }
  1138. if (src._ambigZone) {
  1139. dest._ambigZone = true;
  1140. }
  1141. else if (dest._ambigZone) {
  1142. dest._ambigZone = false;
  1143. }
  1144. }
  1145. // Sets the year/month/date/etc values of the moment from the given array.
  1146. // Inefficient because it calls each individual setter.
  1147. function setMomentValues(mom, a) {
  1148. mom.year(a[0] || 0)
  1149. .month(a[1] || 0)
  1150. .date(a[2] || 0)
  1151. .hours(a[3] || 0)
  1152. .minutes(a[4] || 0)
  1153. .seconds(a[5] || 0)
  1154. .milliseconds(a[6] || 0);
  1155. }
  1156. // Can we set the moment's internal date directly?
  1157. allowValueOptimization = '_d' in moment() && 'updateOffset' in moment;
  1158. // Utility function. Accepts a moment and an array of the UTC year/month/date/etc values to set.
  1159. // Assumes the given moment is already in UTC mode.
  1160. setUTCValues = allowValueOptimization ? function(mom, a) {
  1161. // simlate what moment's accessors do
  1162. mom._d.setTime(Date.UTC.apply(Date, a));
  1163. moment.updateOffset(mom, false); // keepTime=false
  1164. } : setMomentValues;
  1165. // Utility function. Accepts a moment and an array of the local year/month/date/etc values to set.
  1166. // Assumes the given moment is already in local mode.
  1167. setLocalValues = allowValueOptimization ? function(mom, a) {
  1168. // simlate what moment's accessors do
  1169. mom._d.setTime(+new Date( // FYI, there is now way to apply an array of args to a constructor
  1170. a[0] || 0,
  1171. a[1] || 0,
  1172. a[2] || 0,
  1173. a[3] || 0,
  1174. a[4] || 0,
  1175. a[5] || 0,
  1176. a[6] || 0
  1177. ));
  1178. moment.updateOffset(mom, false); // keepTime=false
  1179. } : setMomentValues;
  1180. ;;
  1181. // Single Date Formatting
  1182. // -------------------------------------------------------------------------------------------------
  1183. // call this if you want Moment's original format method to be used
  1184. function oldMomentFormat(mom, formatStr) {
  1185. return oldMomentProto.format.call(mom, formatStr); // oldMomentProto defined in moment-ext.js
  1186. }
  1187. // Formats `date` with a Moment formatting string, but allow our non-zero areas and
  1188. // additional token.
  1189. function formatDate(date, formatStr) {
  1190. return formatDateWithChunks(date, getFormatStringChunks(formatStr));
  1191. }
  1192. function formatDateWithChunks(date, chunks) {
  1193. var s = '';
  1194. var i;
  1195. for (i=0; i<chunks.length; i++) {
  1196. s += formatDateWithChunk(date, chunks[i]);
  1197. }
  1198. return s;
  1199. }
  1200. // addition formatting tokens we want recognized
  1201. var tokenOverrides = {
  1202. t: function(date) { // "a" or "p"
  1203. return oldMomentFormat(date, 'a').charAt(0);
  1204. },
  1205. T: function(date) { // "A" or "P"
  1206. return oldMomentFormat(date, 'A').charAt(0);
  1207. }
  1208. };
  1209. function formatDateWithChunk(date, chunk) {
  1210. var token;
  1211. var maybeStr;
  1212. if (typeof chunk === 'string') { // a literal string
  1213. return chunk;
  1214. }
  1215. else if ((token = chunk.token)) { // a token, like "YYYY"
  1216. if (tokenOverrides[token]) {
  1217. return tokenOverrides[token](date); // use our custom token
  1218. }
  1219. return oldMomentFormat(date, token);
  1220. }
  1221. else if (chunk.maybe) { // a grouping of other chunks that must be non-zero
  1222. maybeStr = formatDateWithChunks(date, chunk.maybe);
  1223. if (maybeStr.match(/[1-9]/)) {
  1224. return maybeStr;
  1225. }
  1226. }
  1227. return '';
  1228. }
  1229. // Date Range Formatting
  1230. // -------------------------------------------------------------------------------------------------
  1231. // TODO: make it work with timezone offset
  1232. // Using a formatting string meant for a single date, generate a range string, like
  1233. // "Sep 2 - 9 2013", that intelligently inserts a separator where the dates differ.
  1234. // If the dates are the same as far as the format string is concerned, just return a single
  1235. // rendering of one date, without any separator.
  1236. function formatRange(date1, date2, formatStr, separator, isRTL) {
  1237. var localeData;
  1238. date1 = fc.moment.parseZone(date1);
  1239. date2 = fc.moment.parseZone(date2);
  1240. localeData = (date1.localeData || date1.lang).call(date1); // works with moment-pre-2.8
  1241. // Expand localized format strings, like "LL" -> "MMMM D YYYY"
  1242. formatStr = localeData.longDateFormat(formatStr) || formatStr;
  1243. // BTW, this is not important for `formatDate` because it is impossible to put custom tokens
  1244. // or non-zero areas in Moment's localized format strings.
  1245. separator = separator || ' - ';
  1246. return formatRangeWithChunks(
  1247. date1,
  1248. date2,
  1249. getFormatStringChunks(formatStr),
  1250. separator,
  1251. isRTL
  1252. );
  1253. }
  1254. fc.formatRange = formatRange; // expose
  1255. function formatRangeWithChunks(date1, date2, chunks, separator, isRTL) {
  1256. var chunkStr; // the rendering of the chunk
  1257. var leftI;
  1258. var leftStr = '';
  1259. var rightI;
  1260. var rightStr = '';
  1261. var middleI;
  1262. var middleStr1 = '';
  1263. var middleStr2 = '';
  1264. var middleStr = '';
  1265. // Start at the leftmost side of the formatting string and continue until you hit a token
  1266. // that is not the same between dates.
  1267. for (leftI=0; leftI<chunks.length; leftI++) {
  1268. chunkStr = formatSimilarChunk(date1, date2, chunks[leftI]);
  1269. if (chunkStr === false) {
  1270. break;
  1271. }
  1272. leftStr += chunkStr;
  1273. }
  1274. // Similarly, start at the rightmost side of the formatting string and move left
  1275. for (rightI=chunks.length-1; rightI>leftI; rightI--) {
  1276. chunkStr = formatSimilarChunk(date1, date2, chunks[rightI]);
  1277. if (chunkStr === false) {
  1278. break;
  1279. }
  1280. rightStr = chunkStr + rightStr;
  1281. }
  1282. // The area in the middle is different for both of the dates.
  1283. // Collect them distinctly so we can jam them together later.
  1284. for (middleI=leftI; middleI<=rightI; middleI++) {
  1285. middleStr1 += formatDateWithChunk(date1, chunks[middleI]);
  1286. middleStr2 += formatDateWithChunk(date2, chunks[middleI]);
  1287. }
  1288. if (middleStr1 || middleStr2) {
  1289. if (isRTL) {
  1290. middleStr = middleStr2 + separator + middleStr1;
  1291. }
  1292. else {
  1293. middleStr = middleStr1 + separator + middleStr2;
  1294. }
  1295. }
  1296. return leftStr + middleStr + rightStr;
  1297. }
  1298. var similarUnitMap = {
  1299. Y: 'year',
  1300. M: 'month',
  1301. D: 'day', // day of month
  1302. d: 'day', // day of week
  1303. // prevents a separator between anything time-related...
  1304. A: 'second', // AM/PM
  1305. a: 'second', // am/pm
  1306. T: 'second', // A/P
  1307. t: 'second', // a/p
  1308. H: 'second', // hour (24)
  1309. h: 'second', // hour (12)
  1310. m: 'second', // minute
  1311. s: 'second' // second
  1312. };
  1313. // TODO: week maybe?
  1314. // Given a formatting chunk, and given that both dates are similar in the regard the
  1315. // formatting chunk is concerned, format date1 against `chunk`. Otherwise, return `false`.
  1316. function formatSimilarChunk(date1, date2, chunk) {
  1317. var token;
  1318. var unit;
  1319. if (typeof chunk === 'string') { // a literal string
  1320. return chunk;
  1321. }
  1322. else if ((token = chunk.token)) {
  1323. unit = similarUnitMap[token.charAt(0)];
  1324. // are the dates the same for this unit of measurement?
  1325. if (unit && date1.isSame(date2, unit)) {
  1326. return oldMomentFormat(date1, token); // would be the same if we used `date2`
  1327. // BTW, don't support custom tokens
  1328. }
  1329. }
  1330. return false; // the chunk is NOT the same for the two dates
  1331. // BTW, don't support splitting on non-zero areas
  1332. }
  1333. // Chunking Utils
  1334. // -------------------------------------------------------------------------------------------------
  1335. var formatStringChunkCache = {};
  1336. function getFormatStringChunks(formatStr) {
  1337. if (formatStr in formatStringChunkCache) {
  1338. return formatStringChunkCache[formatStr];
  1339. }
  1340. return (formatStringChunkCache[formatStr] = chunkFormatString(formatStr));
  1341. }
  1342. // Break the formatting string into an array of chunks
  1343. function chunkFormatString(formatStr) {
  1344. var chunks = [];
  1345. var chunker = /\[([^\]]*)\]|\(([^\)]*)\)|(LTS|LT|(\w)\4*o?)|([^\w\[\(]+)/g; // TODO: more descrimination
  1346. var match;
  1347. while ((match = chunker.exec(formatStr))) {
  1348. if (match[1]) { // a literal string inside [ ... ]
  1349. chunks.push(match[1]);
  1350. }
  1351. else if (match[2]) { // non-zero formatting inside ( ... )
  1352. chunks.push({ maybe: chunkFormatString(match[2]) });
  1353. }
  1354. else if (match[3]) { // a formatting token
  1355. chunks.push({ token: match[3] });
  1356. }
  1357. else if (match[5]) { // an unenclosed literal string
  1358. chunks.push(match[5]);
  1359. }
  1360. }
  1361. return chunks;
  1362. }
  1363. ;;
  1364. fc.Class = Class; // export
  1365. // class that all other classes will inherit from
  1366. function Class() { }
  1367. // called upon a class to create a subclass
  1368. Class.extend = function(members) {
  1369. var superClass = this;
  1370. var subClass;
  1371. members = members || {};
  1372. // ensure a constructor for the subclass, forwarding all arguments to the super-constructor if it doesn't exist
  1373. if (hasOwnProp(members, 'constructor')) {
  1374. subClass = members.constructor;
  1375. }
  1376. if (typeof subClass !== 'function') {
  1377. subClass = members.constructor = function() {
  1378. superClass.apply(this, arguments);
  1379. };
  1380. }
  1381. // build the base prototype for the subclass, which is an new object chained to the superclass's prototype
  1382. subClass.prototype = createObject(superClass.prototype);
  1383. // copy each member variable/method onto the the subclass's prototype
  1384. copyOwnProps(members, subClass.prototype);
  1385. copyNativeMethods(members, subClass.prototype); // hack for IE8
  1386. // copy over all class variables/methods to the subclass, such as `extend` and `mixin`
  1387. copyOwnProps(superClass, subClass);
  1388. return subClass;
  1389. };
  1390. // adds new member variables/methods to the class's prototype.
  1391. // can be called with another class, or a plain object hash containing new members.
  1392. Class.mixin = function(members) {
  1393. copyOwnProps(members.prototype || members, this.prototype); // TODO: copyNativeMethods?
  1394. };
  1395. ;;
  1396. var Emitter = fc.Emitter = Class.extend({
  1397. callbackHash: null,
  1398. on: function(name, callback) {
  1399. this.getCallbacks(name).add(callback);
  1400. return this; // for chaining
  1401. },
  1402. off: function(name, callback) {
  1403. this.getCallbacks(name).remove(callback);
  1404. return this; // for chaining
  1405. },
  1406. trigger: function(name) { // args...
  1407. var args = Array.prototype.slice.call(arguments, 1);
  1408. this.triggerWith(name, this, args);
  1409. return this; // for chaining
  1410. },
  1411. triggerWith: function(name, context, args) {
  1412. var callbacks = this.getCallbacks(name);
  1413. callbacks.fireWith(context, args);
  1414. return this; // for chaining
  1415. },
  1416. getCallbacks: function(name) {
  1417. var callbacks;
  1418. if (!this.callbackHash) {
  1419. this.callbackHash = {};
  1420. }
  1421. callbacks = this.callbackHash[name];
  1422. if (!callbacks) {
  1423. callbacks = this.callbackHash[name] = $.Callbacks();
  1424. }
  1425. return callbacks;
  1426. }
  1427. });
  1428. ;;
  1429. /* A rectangular panel that is absolutely positioned over other content
  1430. ------------------------------------------------------------------------------------------------------------------------
  1431. Options:
  1432. - className (string)
  1433. - content (HTML string or jQuery element set)
  1434. - parentEl
  1435. - top
  1436. - left
  1437. - right (the x coord of where the right edge should be. not a "CSS" right)
  1438. - autoHide (boolean)
  1439. - show (callback)
  1440. - hide (callback)
  1441. */
  1442. var Popover = Class.extend({
  1443. isHidden: true,
  1444. options: null,
  1445. el: null, // the container element for the popover. generated by this object
  1446. documentMousedownProxy: null, // document mousedown handler bound to `this`
  1447. margin: 10, // the space required between the popover and the edges of the scroll container
  1448. constructor: function(options) {
  1449. this.options = options || {};
  1450. },
  1451. // Shows the popover on the specified position. Renders it if not already
  1452. show: function() {
  1453. if (this.isHidden) {
  1454. if (!this.el) {
  1455. this.render();
  1456. }
  1457. this.el.show();
  1458. this.position();
  1459. this.isHidden = false;
  1460. this.trigger('show');
  1461. }
  1462. },
  1463. // Hides the popover, through CSS, but does not remove it from the DOM
  1464. hide: function() {
  1465. if (!this.isHidden) {
  1466. this.el.hide();
  1467. this.isHidden = true;
  1468. this.trigger('hide');
  1469. }
  1470. },
  1471. // Creates `this.el` and renders content inside of it
  1472. render: function() {
  1473. var _this = this;
  1474. var options = this.options;
  1475. this.el = $('<div class="fc-popover"/>')
  1476. .addClass(options.className || '')
  1477. .css({
  1478. // position initially to the top left to avoid creating scrollbars
  1479. top: 0,
  1480. left: 0
  1481. })
  1482. .append(options.content)
  1483. .appendTo(options.parentEl);
  1484. // when a click happens on anything inside with a 'fc-close' className, hide the popover
  1485. this.el.on('click', '.fc-close', function() {
  1486. _this.hide();
  1487. });
  1488. if (options.autoHide) {
  1489. $(document).on('mousedown', this.documentMousedownProxy = proxy(this, 'documentMousedown'));
  1490. }
  1491. },
  1492. // Triggered when the user clicks *anywhere* in the document, for the autoHide feature
  1493. documentMousedown: function(ev) {
  1494. // only hide the popover if the click happened outside the popover
  1495. if (this.el && !$(ev.target).closest(this.el).length) {
  1496. this.hide();
  1497. }
  1498. },
  1499. // Hides and unregisters any handlers
  1500. removeElement: function() {
  1501. this.hide();
  1502. if (this.el) {
  1503. this.el.remove();
  1504. this.el = null;
  1505. }
  1506. $(document).off('mousedown', this.documentMousedownProxy);
  1507. },
  1508. // Positions the popover optimally, using the top/left/right options
  1509. position: function() {
  1510. var options = this.options;
  1511. var origin = this.el.offsetParent().offset();
  1512. var width = this.el.outerWidth();
  1513. var height = this.el.outerHeight();
  1514. var windowEl = $(window);
  1515. var viewportEl = getScrollParent(this.el);
  1516. var viewportTop;
  1517. var viewportLeft;
  1518. var viewportOffset;
  1519. var top; // the "position" (not "offset") values for the popover
  1520. var left; //
  1521. // compute top and left
  1522. top = options.top || 0;
  1523. if (options.left !== undefined) {
  1524. left = options.left;
  1525. }
  1526. else if (options.right !== undefined) {
  1527. left = options.right - width; // derive the left value from the right value
  1528. }
  1529. else {
  1530. left = 0;
  1531. }
  1532. if (viewportEl.is(window) || viewportEl.is(document)) { // normalize getScrollParent's result
  1533. viewportEl = windowEl;
  1534. viewportTop = 0; // the window is always at the top left
  1535. viewportLeft = 0; // (and .offset() won't work if called here)
  1536. }
  1537. else {
  1538. viewportOffset = viewportEl.offset();
  1539. viewportTop = viewportOffset.top;
  1540. viewportLeft = viewportOffset.left;
  1541. }
  1542. // if the window is scrolled, it causes the visible area to be further down
  1543. viewportTop += windowEl.scrollTop();
  1544. viewportLeft += windowEl.scrollLeft();
  1545. // constrain to the view port. if constrained by two edges, give precedence to top/left
  1546. if (options.viewportConstrain !== false) {
  1547. top = Math.min(top, viewportTop + viewportEl.outerHeight() - height - this.margin);
  1548. top = Math.max(top, viewportTop + this.margin);
  1549. left = Math.min(left, viewportLeft + viewportEl.outerWidth() - width - this.margin);
  1550. left = Math.max(left, viewportLeft + this.margin);
  1551. }
  1552. this.el.css({
  1553. top: top - origin.top,
  1554. left: left - origin.left
  1555. });
  1556. },
  1557. // Triggers a callback. Calls a function in the option hash of the same name.
  1558. // Arguments beyond the first `name` are forwarded on.
  1559. // TODO: better code reuse for this. Repeat code
  1560. trigger: function(name) {
  1561. if (this.options[name]) {
  1562. this.options[name].apply(this, Array.prototype.slice.call(arguments, 1));
  1563. }
  1564. }
  1565. });
  1566. ;;
  1567. /* A "coordinate map" converts pixel coordinates into an associated cell, which has an associated date
  1568. ------------------------------------------------------------------------------------------------------------------------
  1569. Common interface:
  1570. CoordMap.prototype = {
  1571. build: function() {},
  1572. getCell: function(x, y) {}
  1573. };
  1574. */
  1575. /* Coordinate map for a grid component
  1576. ----------------------------------------------------------------------------------------------------------------------*/
  1577. var GridCoordMap = Class.extend({
  1578. grid: null, // reference to the Grid
  1579. rowCoords: null, // array of {top,bottom} objects
  1580. colCoords: null, // array of {left,right} objects
  1581. containerEl: null, // container element that all coordinates are constrained to. optionally assigned
  1582. bounds: null,
  1583. constructor: function(grid) {
  1584. this.grid = grid;
  1585. },
  1586. // Queries the grid for the coordinates of all the cells
  1587. build: function() {
  1588. this.grid.build();
  1589. this.rowCoords = this.grid.computeRowCoords();
  1590. this.colCoords = this.grid.computeColCoords();
  1591. this.computeBounds();
  1592. },
  1593. // Clears the coordinates data to free up memory
  1594. clear: function() {
  1595. this.grid.clear();
  1596. this.rowCoords = null;
  1597. this.colCoords = null;
  1598. },
  1599. // Given a coordinate of the document, gets the associated cell. If no cell is underneath, returns null
  1600. getCell: function(x, y) {
  1601. var rowCoords = this.rowCoords;
  1602. var rowCnt = rowCoords.length;
  1603. var colCoords = this.colCoords;
  1604. var colCnt = colCoords.length;
  1605. var hitRow = null;
  1606. var hitCol = null;
  1607. var i, coords;
  1608. var cell;
  1609. if (this.inBounds(x, y)) {
  1610. for (i = 0; i < rowCnt; i++) {
  1611. coords = rowCoords[i];
  1612. if (y >= coords.top && y < coords.bottom) {
  1613. hitRow = i;
  1614. break;
  1615. }
  1616. }
  1617. for (i = 0; i < colCnt; i++) {
  1618. coords = colCoords[i];
  1619. if (x >= coords.left && x < coords.right) {
  1620. hitCol = i;
  1621. break;
  1622. }
  1623. }
  1624. if (hitRow !== null && hitCol !== null) {
  1625. cell = this.grid.getCell(hitRow, hitCol); // expected to return a fresh object we can modify
  1626. cell.grid = this.grid; // for CellDragListener's isCellsEqual. dragging between grids
  1627. // make the coordinates available on the cell object
  1628. $.extend(cell, rowCoords[hitRow], colCoords[hitCol]);
  1629. return cell;
  1630. }
  1631. }
  1632. return null;
  1633. },
  1634. // If there is a containerEl, compute the bounds into min/max values
  1635. computeBounds: function() {
  1636. this.bounds = this.containerEl ?
  1637. getClientRect(this.containerEl) : // area within scrollbars
  1638. null;
  1639. },
  1640. // Determines if the given coordinates are in bounds. If no `containerEl`, always true
  1641. inBounds: function(x, y) {
  1642. var bounds = this.bounds;
  1643. if (bounds) {
  1644. return x >= bounds.left && x < bounds.right && y >= bounds.top && y < bounds.bottom;
  1645. }
  1646. return true;
  1647. }
  1648. });
  1649. /* Coordinate map that is a combination of multiple other coordinate maps
  1650. ----------------------------------------------------------------------------------------------------------------------*/
  1651. var ComboCoordMap = Class.extend({
  1652. coordMaps: null, // an array of CoordMaps
  1653. constructor: function(coordMaps) {
  1654. this.coordMaps = coordMaps;
  1655. },
  1656. // Builds all coordMaps
  1657. build: function() {
  1658. var coordMaps = this.coordMaps;
  1659. var i;
  1660. for (i = 0; i < coordMaps.length; i++) {
  1661. coordMaps[i].build();
  1662. }
  1663. },
  1664. // Queries all coordMaps for the cell underneath the given coordinates, returning the first result
  1665. getCell: function(x, y) {
  1666. var coordMaps = this.coordMaps;
  1667. var cell = null;
  1668. var i;
  1669. for (i = 0; i < coordMaps.length && !cell; i++) {
  1670. cell = coordMaps[i].getCell(x, y);
  1671. }
  1672. return cell;
  1673. },
  1674. // Clears all coordMaps
  1675. clear: function() {
  1676. var coordMaps = this.coordMaps;
  1677. var i;
  1678. for (i = 0; i < coordMaps.length; i++) {
  1679. coordMaps[i].clear();
  1680. }
  1681. }
  1682. });
  1683. ;;
  1684. /* Tracks a drag's mouse movement, firing various handlers
  1685. ----------------------------------------------------------------------------------------------------------------------*/
  1686. var DragListener = fc.DragListener = Class.extend({
  1687. options: null,
  1688. isListening: false,
  1689. isDragging: false,
  1690. // coordinates of the initial mousedown
  1691. originX: null,
  1692. originY: null,
  1693. // handler attached to the document, bound to the DragListener's `this`
  1694. mousemoveProxy: null,
  1695. mouseupProxy: null,
  1696. // for IE8 bug-fighting behavior, for now
  1697. subjectEl: null, // the element being draged. optional
  1698. subjectHref: null,
  1699. scrollEl: null,
  1700. scrollBounds: null, // { top, bottom, left, right }
  1701. scrollTopVel: null, // pixels per second
  1702. scrollLeftVel: null, // pixels per second
  1703. scrollIntervalId: null, // ID of setTimeout for scrolling animation loop
  1704. scrollHandlerProxy: null, // this-scoped function for handling when scrollEl is scrolled
  1705. scrollSensitivity: 30, // pixels from edge for scrolling to start
  1706. scrollSpeed: 200, // pixels per second, at maximum speed
  1707. scrollIntervalMs: 50, // millisecond wait between scroll increment
  1708. constructor: function(options) {
  1709. options = options || {};
  1710. this.options = options;
  1711. this.subjectEl = options.subjectEl;
  1712. },
  1713. // Call this when the user does a mousedown. Will probably lead to startListening
  1714. mousedown: function(ev) {
  1715. if (isPrimaryMouseButton(ev)) {
  1716. ev.preventDefault(); // prevents native selection in most browsers
  1717. this.startListening(ev);
  1718. // start the drag immediately if there is no minimum distance for a drag start
  1719. if (!this.options.distance) {
  1720. this.startDrag(ev);
  1721. }
  1722. }
  1723. },
  1724. // Call this to start tracking mouse movements
  1725. startListening: function(ev) {
  1726. var scrollParent;
  1727. if (!this.isListening) {
  1728. // grab scroll container and attach handler
  1729. if (ev && this.options.scroll) {
  1730. scrollParent = getScrollParent($(ev.target));
  1731. if (!scrollParent.is(window) && !scrollParent.is(document)) {
  1732. this.scrollEl = scrollParent;
  1733. // scope to `this`, and use `debounce` to make sure rapid calls don't happen
  1734. this.scrollHandlerProxy = debounce(proxy(this, 'scrollHandler'), 100);
  1735. this.scrollEl.on('scroll', this.scrollHandlerProxy);
  1736. }
  1737. }
  1738. $(document)
  1739. .on('mousemove', this.mousemoveProxy = proxy(this, 'mousemove'))
  1740. .on('mouseup', this.mouseupProxy = proxy(this, 'mouseup'))
  1741. .on('selectstart', this.preventDefault); // prevents native selection in IE<=8
  1742. if (ev) {
  1743. this.originX = ev.pageX;
  1744. this.originY = ev.pageY;
  1745. }
  1746. else {
  1747. // if no starting information was given, origin will be the topleft corner of the screen.
  1748. // if so, dx/dy in the future will be the absolute coordinates.
  1749. this.originX = 0;
  1750. this.originY = 0;
  1751. }
  1752. this.isListening = true;
  1753. this.listenStart(ev);
  1754. }
  1755. },
  1756. // Called when drag listening has started (but a real drag has not necessarily began)
  1757. listenStart: function(ev) {
  1758. this.trigger('listenStart', ev);
  1759. },
  1760. // Called when the user moves the mouse
  1761. mousemove: function(ev) {
  1762. var dx = ev.pageX - this.originX;
  1763. var dy = ev.pageY - this.originY;
  1764. var minDistance;
  1765. var distanceSq; // current distance from the origin, squared
  1766. if (!this.isDragging) { // if not already dragging...
  1767. // then start the drag if the minimum distance criteria is met
  1768. minDistance = this.options.distance || 1;
  1769. distanceSq = dx * dx + dy * dy;
  1770. if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem
  1771. this.startDrag(ev);
  1772. }
  1773. }
  1774. if (this.isDragging) {
  1775. this.drag(dx, dy, ev); // report a drag, even if this mousemove initiated the drag
  1776. }
  1777. },
  1778. // Call this to initiate a legitimate drag.
  1779. // This function is called internally from this class, but can also be called explicitly from outside
  1780. startDrag: function(ev) {
  1781. if (!this.isListening) { // startDrag must have manually initiated
  1782. this.startListening();
  1783. }
  1784. if (!this.isDragging) {
  1785. this.isDragging = true;
  1786. this.dragStart(ev);
  1787. }
  1788. },
  1789. // Called when the actual drag has started (went beyond minDistance)
  1790. dragStart: function(ev) {
  1791. var subjectEl = this.subjectEl;
  1792. this.trigger('dragStart', ev);
  1793. // remove a mousedown'd <a>'s href so it is not visited (IE8 bug)
  1794. if ((this.subjectHref = subjectEl ? subjectEl.attr('href') : null)) {
  1795. subjectEl.removeAttr('href');
  1796. }
  1797. },
  1798. // Called while the mouse is being moved and when we know a legitimate drag is taking place
  1799. drag: function(dx, dy, ev) {
  1800. this.trigger('drag', dx, dy, ev);
  1801. this.updateScroll(ev); // will possibly cause scrolling
  1802. },
  1803. // Called when the user does a mouseup
  1804. mouseup: function(ev) {
  1805. this.stopListening(ev);
  1806. },
  1807. // Called when the drag is over. Will not cause listening to stop however.
  1808. // A concluding 'cellOut' event will NOT be triggered.
  1809. stopDrag: function(ev) {
  1810. if (this.isDragging) {
  1811. this.stopScrolling();
  1812. this.dragStop(ev);
  1813. this.isDragging = false;
  1814. }
  1815. },
  1816. // Called when dragging has been stopped
  1817. dragStop: function(ev) {
  1818. var _this = this;
  1819. this.trigger('dragStop', ev);
  1820. // restore a mousedown'd <a>'s href (for IE8 bug)
  1821. setTimeout(function() { // must be outside of the click's execution
  1822. if (_this.subjectHref) {
  1823. _this.subjectEl.attr('href', _this.subjectHref);
  1824. }
  1825. }, 0);
  1826. },
  1827. // Call this to stop listening to the user's mouse events
  1828. stopListening: function(ev) {
  1829. this.stopDrag(ev); // if there's a current drag, kill it
  1830. if (this.isListening) {
  1831. // remove the scroll handler if there is a scrollEl
  1832. if (this.scrollEl) {
  1833. this.scrollEl.off('scroll', this.scrollHandlerProxy);
  1834. this.scrollHandlerProxy = null;
  1835. }
  1836. $(document)
  1837. .off('mousemove', this.mousemoveProxy)
  1838. .off('mouseup', this.mouseupProxy)
  1839. .off('selectstart', this.preventDefault);
  1840. this.mousemoveProxy = null;
  1841. this.mouseupProxy = null;
  1842. this.isListening = false;
  1843. this.listenStop(ev);
  1844. }
  1845. },
  1846. // Called when drag listening has stopped
  1847. listenStop: function(ev) {
  1848. this.trigger('listenStop', ev);
  1849. },
  1850. // Triggers a callback. Calls a function in the option hash of the same name.
  1851. // Arguments beyond the first `name` are forwarded on.
  1852. trigger: function(name) {
  1853. if (this.options[name]) {
  1854. this.options[name].apply(this, Array.prototype.slice.call(arguments, 1));
  1855. }
  1856. },
  1857. // Stops a given mouse event from doing it's native browser action. In our case, text selection.
  1858. preventDefault: function(ev) {
  1859. ev.preventDefault();
  1860. },
  1861. /* Scrolling
  1862. ------------------------------------------------------------------------------------------------------------------*/
  1863. // Computes and stores the bounding rectangle of scrollEl
  1864. computeScrollBounds: function() {
  1865. var el = this.scrollEl;
  1866. this.scrollBounds = el ? getOuterRect(el) : null;
  1867. // TODO: use getClientRect in future. but prevents auto scrolling when on top of scrollbars
  1868. },
  1869. // Called when the dragging is in progress and scrolling should be updated
  1870. updateScroll: function(ev) {
  1871. var sensitivity = this.scrollSensitivity;
  1872. var bounds = this.scrollBounds;
  1873. var topCloseness, bottomCloseness;
  1874. var leftCloseness, rightCloseness;
  1875. var topVel = 0;
  1876. var leftVel = 0;
  1877. if (bounds) { // only scroll if scrollEl exists
  1878. // compute closeness to edges. valid range is from 0.0 - 1.0
  1879. topCloseness = (sensitivity - (ev.pageY - bounds.top)) / sensitivity;
  1880. bottomCloseness = (sensitivity - (bounds.bottom - ev.pageY)) / sensitivity;
  1881. leftCloseness = (sensitivity - (ev.pageX - bounds.left)) / sensitivity;
  1882. rightCloseness = (sensitivity - (bounds.right - ev.pageX)) / sensitivity;
  1883. // translate vertical closeness into velocity.
  1884. // mouse must be completely in bounds for velocity to happen.
  1885. if (topCloseness >= 0 && topCloseness <= 1) {
  1886. topVel = topCloseness * this.scrollSpeed * -1; // negative. for scrolling up
  1887. }
  1888. else if (bottomCloseness >= 0 && bottomCloseness <= 1) {
  1889. topVel = bottomCloseness * this.scrollSpeed;
  1890. }
  1891. // translate horizontal closeness into velocity
  1892. if (leftCloseness >= 0 && leftCloseness <= 1) {
  1893. leftVel = leftCloseness * this.scrollSpeed * -1; // negative. for scrolling left
  1894. }
  1895. else if (rightCloseness >= 0 && rightCloseness <= 1) {
  1896. leftVel = rightCloseness * this.scrollSpeed;
  1897. }
  1898. }
  1899. this.setScrollVel(topVel, leftVel);
  1900. },
  1901. // Sets the speed-of-scrolling for the scrollEl
  1902. setScrollVel: function(topVel, leftVel) {
  1903. this.scrollTopVel = topVel;
  1904. this.scrollLeftVel = leftVel;
  1905. this.constrainScrollVel(); // massages into realistic values
  1906. // if there is non-zero velocity, and an animation loop hasn't already started, then START
  1907. if ((this.scrollTopVel || this.scrollLeftVel) && !this.scrollIntervalId) {
  1908. this.scrollIntervalId = setInterval(
  1909. proxy(this, 'scrollIntervalFunc'), // scope to `this`
  1910. this.scrollIntervalMs
  1911. );
  1912. }
  1913. },
  1914. // Forces scrollTopVel and scrollLeftVel to be zero if scrolling has already gone all the way
  1915. constrainScrollVel: function() {
  1916. var el = this.scrollEl;
  1917. if (this.scrollTopVel < 0) { // scrolling up?
  1918. if (el.scrollTop() <= 0) { // already scrolled all the way up?
  1919. this.scrollTopVel = 0;
  1920. }
  1921. }
  1922. else if (this.scrollTopVel > 0) { // scrolling down?
  1923. if (el.scrollTop() + el[0].clientHeight >= el[0].scrollHeight) { // already scrolled all the way down?
  1924. this.scrollTopVel = 0;
  1925. }
  1926. }
  1927. if (this.scrollLeftVel < 0) { // scrolling left?
  1928. if (el.scrollLeft() <= 0) { // already scrolled all the left?
  1929. this.scrollLeftVel = 0;
  1930. }
  1931. }
  1932. else if (this.scrollLeftVel > 0) { // scrolling right?
  1933. if (el.scrollLeft() + el[0].clientWidth >= el[0].scrollWidth) { // already scrolled all the way right?
  1934. this.scrollLeftVel = 0;
  1935. }
  1936. }
  1937. },
  1938. // This function gets called during every iteration of the scrolling animation loop
  1939. scrollIntervalFunc: function() {
  1940. var el = this.scrollEl;
  1941. var frac = this.scrollIntervalMs / 1000; // considering animation frequency, what the vel should be mult'd by
  1942. // change the value of scrollEl's scroll
  1943. if (this.scrollTopVel) {
  1944. el.scrollTop(el.scrollTop() + this.scrollTopVel * frac);
  1945. }
  1946. if (this.scrollLeftVel) {
  1947. el.scrollLeft(el.scrollLeft() + this.scrollLeftVel * frac);
  1948. }
  1949. this.constrainScrollVel(); // since the scroll values changed, recompute the velocities
  1950. // if scrolled all the way, which causes the vels to be zero, stop the animation loop
  1951. if (!this.scrollTopVel && !this.scrollLeftVel) {
  1952. this.stopScrolling();
  1953. }
  1954. },
  1955. // Kills any existing scrolling animation loop
  1956. stopScrolling: function() {
  1957. if (this.scrollIntervalId) {
  1958. clearInterval(this.scrollIntervalId);
  1959. this.scrollIntervalId = null;
  1960. // when all done with scrolling, recompute positions since they probably changed
  1961. this.scrollStop();
  1962. }
  1963. },
  1964. // Get called when the scrollEl is scrolled (NOTE: this is delayed via debounce)
  1965. scrollHandler: function() {
  1966. // recompute all coordinates, but *only* if this is *not* part of our scrolling animation
  1967. if (!this.scrollIntervalId) {
  1968. this.scrollStop();
  1969. }
  1970. },
  1971. // Called when scrolling has stopped, whether through auto scroll, or the user scrolling
  1972. scrollStop: function() {
  1973. }
  1974. });
  1975. ;;
  1976. /* Tracks mouse movements over a CoordMap and raises events about which cell the mouse is over.
  1977. ------------------------------------------------------------------------------------------------------------------------
  1978. options:
  1979. - subjectEl
  1980. - subjectCenter
  1981. */
  1982. var CellDragListener = DragListener.extend({
  1983. coordMap: null, // converts coordinates to date cells
  1984. origCell: null, // the cell the mouse was over when listening started
  1985. cell: null, // the cell the mouse is over
  1986. coordAdjust: null, // delta that will be added to the mouse coordinates when computing collisions
  1987. constructor: function(coordMap, options) {
  1988. DragListener.prototype.constructor.call(this, options); // call the super-constructor
  1989. this.coordMap = coordMap;
  1990. },
  1991. // Called when drag listening starts (but a real drag has not necessarily began).
  1992. // ev might be undefined if dragging was started manually.
  1993. listenStart: function(ev) {
  1994. var subjectEl = this.subjectEl;
  1995. var subjectRect;
  1996. var origPoint;
  1997. var point;
  1998. DragListener.prototype.listenStart.apply(this, arguments); // call the super-method
  1999. this.computeCoords();
  2000. if (ev) {
  2001. origPoint = { left: ev.pageX, top: ev.pageY };
  2002. point = origPoint;
  2003. // constrain the point to bounds of the element being dragged
  2004. if (subjectEl) {
  2005. subjectRect = getOuterRect(subjectEl); // used for centering as well
  2006. point = constrainPoint(point, subjectRect);
  2007. }
  2008. this.origCell = this.getCell(point.left, point.top);
  2009. // treat the center of the subject as the collision point?
  2010. if (subjectEl && this.options.subjectCenter) {
  2011. // only consider the area the subject overlaps the cell. best for large subjects
  2012. if (this.origCell) {
  2013. subjectRect = intersectRects(this.origCell, subjectRect) ||
  2014. subjectRect; // in case there is no intersection
  2015. }
  2016. point = getRectCenter(subjectRect);
  2017. }
  2018. this.coordAdjust = diffPoints(point, origPoint); // point - origPoint
  2019. }
  2020. else {
  2021. this.origCell = null;
  2022. this.coordAdjust = null;
  2023. }
  2024. },
  2025. // Recomputes the drag-critical positions of elements
  2026. computeCoords: function() {
  2027. this.coordMap.build();
  2028. this.computeScrollBounds();
  2029. },
  2030. // Called when the actual drag has started
  2031. dragStart: function(ev) {
  2032. var cell;
  2033. DragListener.prototype.dragStart.apply(this, arguments); // call the super-method
  2034. cell = this.getCell(ev.pageX, ev.pageY); // might be different from this.origCell if the min-distance is large
  2035. // report the initial cell the mouse is over
  2036. // especially important if no min-distance and drag starts immediately
  2037. if (cell) {
  2038. this.cellOver(cell);
  2039. }
  2040. },
  2041. // Called when the drag moves
  2042. drag: function(dx, dy, ev) {
  2043. var cell;
  2044. DragListener.prototype.drag.apply(this, arguments); // call the super-method
  2045. cell = this.getCell(ev.pageX, ev.pageY);
  2046. if (!isCellsEqual(cell, this.cell)) { // a different cell than before?
  2047. if (this.cell) {
  2048. this.cellOut();
  2049. }
  2050. if (cell) {
  2051. this.cellOver(cell);
  2052. }
  2053. }
  2054. },
  2055. // Called when dragging has been stopped
  2056. dragStop: function() {
  2057. this.cellDone();
  2058. DragListener.prototype.dragStop.apply(this, arguments); // call the super-method
  2059. },
  2060. // Called when a the mouse has just moved over a new cell
  2061. cellOver: function(cell) {
  2062. this.cell = cell;
  2063. this.trigger('cellOver', cell, isCellsEqual(cell, this.origCell), this.origCell);
  2064. },
  2065. // Called when the mouse has just moved out of a cell
  2066. cellOut: function() {
  2067. if (this.cell) {
  2068. this.trigger('cellOut', this.cell);
  2069. this.cellDone();
  2070. this.cell = null;
  2071. }
  2072. },
  2073. // Called after a cellOut. Also called before a dragStop
  2074. cellDone: function() {
  2075. if (this.cell) {
  2076. this.trigger('cellDone', this.cell);
  2077. }
  2078. },
  2079. // Called when drag listening has stopped
  2080. listenStop: function() {
  2081. DragListener.prototype.listenStop.apply(this, arguments); // call the super-method
  2082. this.origCell = this.cell = null;
  2083. this.coordMap.clear();
  2084. },
  2085. // Called when scrolling has stopped, whether through auto scroll, or the user scrolling
  2086. scrollStop: function() {
  2087. DragListener.prototype.scrollStop.apply(this, arguments); // call the super-method
  2088. this.computeCoords(); // cells' absolute positions will be in new places. recompute
  2089. },
  2090. // Gets the cell underneath the coordinates for the given mouse event
  2091. getCell: function(left, top) {
  2092. if (this.coordAdjust) {
  2093. left += this.coordAdjust.left;
  2094. top += this.coordAdjust.top;
  2095. }
  2096. return this.coordMap.getCell(left, top);
  2097. }
  2098. });
  2099. // Returns `true` if the cells are identically equal. `false` otherwise.
  2100. // They must have the same row, col, and be from the same grid.
  2101. // Two null values will be considered equal, as two "out of the grid" states are the same.
  2102. function isCellsEqual(cell1, cell2) {
  2103. if (!cell1 && !cell2) {
  2104. return true;
  2105. }
  2106. if (cell1 && cell2) {
  2107. return cell1.grid === cell2.grid &&
  2108. cell1.row === cell2.row &&
  2109. cell1.col === cell2.col;
  2110. }
  2111. return false;
  2112. }
  2113. ;;
  2114. /* Creates a clone of an element and lets it track the mouse as it moves
  2115. ----------------------------------------------------------------------------------------------------------------------*/
  2116. var MouseFollower = Class.extend({
  2117. options: null,
  2118. sourceEl: null, // the element that will be cloned and made to look like it is dragging
  2119. el: null, // the clone of `sourceEl` that will track the mouse
  2120. parentEl: null, // the element that `el` (the clone) will be attached to
  2121. // the initial position of el, relative to the offset parent. made to match the initial offset of sourceEl
  2122. top0: null,
  2123. left0: null,
  2124. // the initial position of the mouse
  2125. mouseY0: null,
  2126. mouseX0: null,
  2127. // the number of pixels the mouse has moved from its initial position
  2128. topDelta: null,
  2129. leftDelta: null,
  2130. mousemoveProxy: null, // document mousemove handler, bound to the MouseFollower's `this`
  2131. isFollowing: false,
  2132. isHidden: false,
  2133. isAnimating: false, // doing the revert animation?
  2134. constructor: function(sourceEl, options) {
  2135. this.options = options = options || {};
  2136. this.sourceEl = sourceEl;
  2137. this.parentEl = options.parentEl ? $(options.parentEl) : sourceEl.parent(); // default to sourceEl's parent
  2138. },
  2139. // Causes the element to start following the mouse
  2140. start: function(ev) {
  2141. if (!this.isFollowing) {
  2142. this.isFollowing = true;
  2143. this.mouseY0 = ev.pageY;
  2144. this.mouseX0 = ev.pageX;
  2145. this.topDelta = 0;
  2146. this.leftDelta = 0;
  2147. if (!this.isHidden) {
  2148. this.updatePosition();
  2149. }
  2150. $(document).on('mousemove', this.mousemoveProxy = proxy(this, 'mousemove'));
  2151. }
  2152. },
  2153. // Causes the element to stop following the mouse. If shouldRevert is true, will animate back to original position.
  2154. // `callback` gets invoked when the animation is complete. If no animation, it is invoked immediately.
  2155. stop: function(shouldRevert, callback) {
  2156. var _this = this;
  2157. var revertDuration = this.options.revertDuration;
  2158. function complete() {
  2159. this.isAnimating = false;
  2160. _this.removeElement();
  2161. this.top0 = this.left0 = null; // reset state for future updatePosition calls
  2162. if (callback) {
  2163. callback();
  2164. }
  2165. }
  2166. if (this.isFollowing && !this.isAnimating) { // disallow more than one stop animation at a time
  2167. this.isFollowing = false;
  2168. $(document).off('mousemove', this.mousemoveProxy);
  2169. if (shouldRevert && revertDuration && !this.isHidden) { // do a revert animation?
  2170. this.isAnimating = true;
  2171. this.el.animate({
  2172. top: this.top0,
  2173. left: this.left0
  2174. }, {
  2175. duration: revertDuration,
  2176. complete: complete
  2177. });
  2178. }
  2179. else {
  2180. complete();
  2181. }
  2182. }
  2183. },
  2184. // Gets the tracking element. Create it if necessary
  2185. getEl: function() {
  2186. var el = this.el;
  2187. if (!el) {
  2188. this.sourceEl.width(); // hack to force IE8 to compute correct bounding box
  2189. el = this.el = this.sourceEl.clone()
  2190. .css({
  2191. position: 'absolute',
  2192. visibility: '', // in case original element was hidden (commonly through hideEvents())
  2193. display: this.isHidden ? 'none' : '', // for when initially hidden
  2194. margin: 0,
  2195. right: 'auto', // erase and set width instead
  2196. bottom: 'auto', // erase and set height instead
  2197. width: this.sourceEl.width(), // explicit height in case there was a 'right' value
  2198. height: this.sourceEl.height(), // explicit width in case there was a 'bottom' value
  2199. opacity: this.options.opacity || '',
  2200. zIndex: this.options.zIndex
  2201. })
  2202. .appendTo(this.parentEl);
  2203. }
  2204. return el;
  2205. },
  2206. // Removes the tracking element if it has already been created
  2207. removeElement: function() {
  2208. if (this.el) {
  2209. this.el.remove();
  2210. this.el = null;
  2211. }
  2212. },
  2213. // Update the CSS position of the tracking element
  2214. updatePosition: function() {
  2215. var sourceOffset;
  2216. var origin;
  2217. this.getEl(); // ensure this.el
  2218. // make sure origin info was computed
  2219. if (this.top0 === null) {
  2220. this.sourceEl.width(); // hack to force IE8 to compute correct bounding box
  2221. sourceOffset = this.sourceEl.offset();
  2222. origin = this.el.offsetParent().offset();
  2223. this.top0 = sourceOffset.top - origin.top;
  2224. this.left0 = sourceOffset.left - origin.left;
  2225. }
  2226. this.el.css({
  2227. top: this.top0 + this.topDelta,
  2228. left: this.left0 + this.leftDelta
  2229. });
  2230. },
  2231. // Gets called when the user moves the mouse
  2232. mousemove: function(ev) {
  2233. this.topDelta = ev.pageY - this.mouseY0;
  2234. this.leftDelta = ev.pageX - this.mouseX0;
  2235. if (!this.isHidden) {
  2236. this.updatePosition();
  2237. }
  2238. },
  2239. // Temporarily makes the tracking element invisible. Can be called before following starts
  2240. hide: function() {
  2241. if (!this.isHidden) {
  2242. this.isHidden = true;
  2243. if (this.el) {
  2244. this.el.hide();
  2245. }
  2246. }
  2247. },
  2248. // Show the tracking element after it has been temporarily hidden
  2249. show: function() {
  2250. if (this.isHidden) {
  2251. this.isHidden = false;
  2252. this.updatePosition();
  2253. this.getEl().show();
  2254. }
  2255. }
  2256. });
  2257. ;;
  2258. /* A utility class for rendering <tr> rows.
  2259. ----------------------------------------------------------------------------------------------------------------------*/
  2260. // It leverages methods of the subclass and the View to determine custom rendering behavior for each row "type"
  2261. // (such as highlight rows, day rows, helper rows, etc).
  2262. var RowRenderer = Class.extend({
  2263. view: null, // a View object
  2264. isRTL: null, // shortcut to the view's isRTL option
  2265. cellHtml: '<td/>', // plain default HTML used for a cell when no other is available
  2266. constructor: function(view) {
  2267. this.view = view;
  2268. this.isRTL = view.opt('isRTL');
  2269. },
  2270. // Renders the HTML for a row, leveraging custom cell-HTML-renderers based on the `rowType`.
  2271. // Also applies the "intro" and "outro" cells, which are specified by the subclass and views.
  2272. // `row` is an optional row number.
  2273. rowHtml: function(rowType, row) {
  2274. var renderCell = this.getHtmlRenderer('cell', rowType);
  2275. var rowCellHtml = '';
  2276. var col;
  2277. var cell;
  2278. row = row || 0;
  2279. for (col = 0; col < this.colCnt; col++) {
  2280. cell = this.getCell(row, col);
  2281. rowCellHtml += renderCell(cell);
  2282. }
  2283. rowCellHtml = this.bookendCells(rowCellHtml, rowType, row); // apply intro and outro
  2284. return '<tr>' + rowCellHtml + '</tr>';
  2285. },
  2286. // Applies the "intro" and "outro" HTML to the given cells.
  2287. // Intro means the leftmost cell when the calendar is LTR and the rightmost cell when RTL. Vice-versa for outro.
  2288. // `cells` can be an HTML string of <td>'s or a jQuery <tr> element
  2289. // `row` is an optional row number.
  2290. bookendCells: function(cells, rowType, row) {
  2291. var intro = this.getHtmlRenderer('intro', rowType)(row || 0);
  2292. var outro = this.getHtmlRenderer('outro', rowType)(row || 0);
  2293. var prependHtml = this.isRTL ? outro : intro;
  2294. var appendHtml = this.isRTL ? intro : outro;
  2295. if (typeof cells === 'string') {
  2296. return prependHtml + cells + appendHtml;
  2297. }
  2298. else { // a jQuery <tr> element
  2299. return cells.prepend(prependHtml).append(appendHtml);
  2300. }
  2301. },
  2302. // Returns an HTML-rendering function given a specific `rendererName` (like cell, intro, or outro) and a specific
  2303. // `rowType` (like day, eventSkeleton, helperSkeleton), which is optional.
  2304. // If a renderer for the specific rowType doesn't exist, it will fall back to a generic renderer.
  2305. // We will query the View object first for any custom rendering functions, then the methods of the subclass.
  2306. getHtmlRenderer: function(rendererName, rowType) {
  2307. var view = this.view;
  2308. var generalName; // like "cellHtml"
  2309. var specificName; // like "dayCellHtml". based on rowType
  2310. var provider; // either the View or the RowRenderer subclass, whichever provided the method
  2311. var renderer;
  2312. generalName = rendererName + 'Html';
  2313. if (rowType) {
  2314. specificName = rowType + capitaliseFirstLetter(rendererName) + 'Html';
  2315. }
  2316. if (specificName && (renderer = view[specificName])) {
  2317. provider = view;
  2318. }
  2319. else if (specificName && (renderer = this[specificName])) {
  2320. provider = this;
  2321. }
  2322. else if ((renderer = view[generalName])) {
  2323. provider = view;
  2324. }
  2325. else if ((renderer = this[generalName])) {
  2326. provider = this;
  2327. }
  2328. if (typeof renderer === 'function') {
  2329. return function() {
  2330. return renderer.apply(provider, arguments) || ''; // use correct `this` and always return a string
  2331. };
  2332. }
  2333. // the rendered can be a plain string as well. if not specified, always an empty string.
  2334. return function() {
  2335. return renderer || '';
  2336. };
  2337. }
  2338. });
  2339. ;;
  2340. /* An abstract class comprised of a "grid" of cells that each represent a specific datetime
  2341. ----------------------------------------------------------------------------------------------------------------------*/
  2342. var Grid = fc.Grid = RowRenderer.extend({
  2343. start: null, // the date of the first cell
  2344. end: null, // the date after the last cell
  2345. rowCnt: 0, // number of rows
  2346. colCnt: 0, // number of cols
  2347. el: null, // the containing element
  2348. coordMap: null, // a GridCoordMap that converts pixel values to datetimes
  2349. elsByFill: null, // a hash of jQuery element sets used for rendering each fill. Keyed by fill name.
  2350. externalDragStartProxy: null, // binds the Grid's scope to externalDragStart (in DayGrid.events)
  2351. // derived from options
  2352. colHeadFormat: null, // TODO: move to another class. not applicable to all Grids
  2353. eventTimeFormat: null,
  2354. displayEventTime: null,
  2355. displayEventEnd: null,
  2356. // if all cells are the same length of time, the duration they all share. optional.
  2357. // when defined, allows the computeCellRange shortcut, as well as improved resizing behavior.
  2358. cellDuration: null,
  2359. // if defined, holds the unit identified (ex: "year" or "month") that determines the level of granularity
  2360. // of the date cells. if not defined, assumes to be day and time granularity.
  2361. largeUnit: null,
  2362. constructor: function() {
  2363. RowRenderer.apply(this, arguments); // call the super-constructor
  2364. this.coordMap = new GridCoordMap(this);
  2365. this.elsByFill = {};
  2366. this.externalDragStartProxy = proxy(this, 'externalDragStart');
  2367. },
  2368. /* Options
  2369. ------------------------------------------------------------------------------------------------------------------*/
  2370. // Generates the format string used for the text in column headers, if not explicitly defined by 'columnFormat'
  2371. // TODO: move to another class. not applicable to all Grids
  2372. computeColHeadFormat: function() {
  2373. // subclasses must implement if they want to use headHtml()
  2374. },
  2375. // Generates the format string used for event time text, if not explicitly defined by 'timeFormat'
  2376. computeEventTimeFormat: function() {
  2377. return this.view.opt('smallTimeFormat');
  2378. },
  2379. // Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventTime'.
  2380. // Only applies to non-all-day events.
  2381. computeDisplayEventTime: function() {
  2382. return true;
  2383. },
  2384. // Determines whether events should have their end times displayed, if not explicitly defined by 'displayEventEnd'
  2385. computeDisplayEventEnd: function() {
  2386. return true;
  2387. },
  2388. /* Dates
  2389. ------------------------------------------------------------------------------------------------------------------*/
  2390. // Tells the grid about what period of time to display.
  2391. // Any date-related cell system internal data should be generated.
  2392. setRange: function(range) {
  2393. this.start = range.start.clone();
  2394. this.end = range.end.clone();
  2395. this.rangeUpdated();
  2396. this.processRangeOptions();
  2397. },
  2398. // Called when internal variables that rely on the range should be updated
  2399. rangeUpdated: function() {
  2400. },
  2401. // Updates values that rely on options and also relate to range
  2402. processRangeOptions: function() {
  2403. var view = this.view;
  2404. var displayEventTime;
  2405. var displayEventEnd;
  2406. // Populate option-derived settings. Look for override first, then compute if necessary.
  2407. this.colHeadFormat = view.opt('columnFormat') || this.computeColHeadFormat();
  2408. this.eventTimeFormat =
  2409. view.opt('eventTimeFormat') ||
  2410. view.opt('timeFormat') || // deprecated
  2411. this.computeEventTimeFormat();
  2412. displayEventTime = view.opt('displayEventTime');
  2413. if (displayEventTime == null) {
  2414. displayEventTime = this.computeDisplayEventTime(); // might be based off of range
  2415. }
  2416. displayEventEnd = view.opt('displayEventEnd');
  2417. if (displayEventEnd == null) {
  2418. displayEventEnd = this.computeDisplayEventEnd(); // might be based off of range
  2419. }
  2420. this.displayEventTime = displayEventTime;
  2421. this.displayEventEnd = displayEventEnd;
  2422. },
  2423. // Called before the grid's coordinates will need to be queried for cells.
  2424. // Any non-date-related cell system internal data should be built.
  2425. build: function() {
  2426. },
  2427. // Called after the grid's coordinates are done being relied upon.
  2428. // Any non-date-related cell system internal data should be cleared.
  2429. clear: function() {
  2430. },
  2431. // Converts a range with an inclusive `start` and an exclusive `end` into an array of segment objects
  2432. rangeToSegs: function(range) {
  2433. // subclasses must implement
  2434. },
  2435. // Diffs the two dates, returning a duration, based on granularity of the grid
  2436. diffDates: function(a, b) {
  2437. if (this.largeUnit) {
  2438. return diffByUnit(a, b, this.largeUnit);
  2439. }
  2440. else {
  2441. return diffDayTime(a, b);
  2442. }
  2443. },
  2444. /* Cells
  2445. ------------------------------------------------------------------------------------------------------------------*/
  2446. // NOTE: columns are ordered left-to-right
  2447. // Gets an object containing row/col number, misc data, and range information about the cell.
  2448. // Accepts row/col values, an object with row/col properties, or a single-number offset from the first cell.
  2449. getCell: function(row, col) {
  2450. var cell;
  2451. if (col == null) {
  2452. if (typeof row === 'number') { // a single-number offset
  2453. col = row % this.colCnt;
  2454. row = Math.floor(row / this.colCnt);
  2455. }
  2456. else { // an object with row/col properties
  2457. col = row.col;
  2458. row = row.row;
  2459. }
  2460. }
  2461. cell = { row: row, col: col };
  2462. $.extend(cell, this.getRowData(row), this.getColData(col));
  2463. $.extend(cell, this.computeCellRange(cell));
  2464. return cell;
  2465. },
  2466. // Given a cell object with index and misc data, generates a range object
  2467. // If the grid is leveraging cellDuration, this doesn't need to be defined. Only computeCellDate does.
  2468. // If being overridden, should return a range with reference-free date copies.
  2469. computeCellRange: function(cell) {
  2470. var date = this.computeCellDate(cell);
  2471. return {
  2472. start: date,
  2473. end: date.clone().add(this.cellDuration)
  2474. };
  2475. },
  2476. // Given a cell, returns its start date. Should return a reference-free date copy.
  2477. computeCellDate: function(cell) {
  2478. // subclasses can implement
  2479. },
  2480. // Retrieves misc data about the given row
  2481. getRowData: function(row) {
  2482. return {};
  2483. },
  2484. // Retrieves misc data baout the given column
  2485. getColData: function(col) {
  2486. return {};
  2487. },
  2488. // Retrieves the element representing the given row
  2489. getRowEl: function(row) {
  2490. // subclasses should implement if leveraging the default getCellDayEl() or computeRowCoords()
  2491. },
  2492. // Retrieves the element representing the given column
  2493. getColEl: function(col) {
  2494. // subclasses should implement if leveraging the default getCellDayEl() or computeColCoords()
  2495. },
  2496. // Given a cell object, returns the element that represents the cell's whole-day
  2497. getCellDayEl: function(cell) {
  2498. return this.getColEl(cell.col) || this.getRowEl(cell.row);
  2499. },
  2500. /* Cell Coordinates
  2501. ------------------------------------------------------------------------------------------------------------------*/
  2502. // Computes the top/bottom coordinates of all rows.
  2503. // By default, queries the dimensions of the element provided by getRowEl().
  2504. computeRowCoords: function() {
  2505. var items = [];
  2506. var i, el;
  2507. var top;
  2508. for (i = 0; i < this.rowCnt; i++) {
  2509. el = this.getRowEl(i);
  2510. top = el.offset().top;
  2511. items.push({
  2512. top: top,
  2513. bottom: top + el.outerHeight()
  2514. });
  2515. }
  2516. return items;
  2517. },
  2518. // Computes the left/right coordinates of all rows.
  2519. // By default, queries the dimensions of the element provided by getColEl(). Columns can be LTR or RTL.
  2520. computeColCoords: function() {
  2521. var items = [];
  2522. var i, el;
  2523. var left;
  2524. for (i = 0; i < this.colCnt; i++) {
  2525. el = this.getColEl(i);
  2526. left = el.offset().left;
  2527. items.push({
  2528. left: left,
  2529. right: left + el.outerWidth()
  2530. });
  2531. }
  2532. return items;
  2533. },
  2534. /* Rendering
  2535. ------------------------------------------------------------------------------------------------------------------*/
  2536. // Sets the container element that the grid should render inside of.
  2537. // Does other DOM-related initializations.
  2538. setElement: function(el) {
  2539. var _this = this;
  2540. this.el = el;
  2541. // attach a handler to the grid's root element.
  2542. // jQuery will take care of unregistering them when removeElement gets called.
  2543. el.on('mousedown', function(ev) {
  2544. if (
  2545. !$(ev.target).is('.fc-event-container *, .fc-more') && // not an an event element, or "more.." link
  2546. !$(ev.target).closest('.fc-popover').length // not on a popover (like the "more.." events one)
  2547. ) {
  2548. _this.dayMousedown(ev);
  2549. }
  2550. });
  2551. // attach event-element-related handlers. in Grid.events
  2552. // same garbage collection note as above.
  2553. this.bindSegHandlers();
  2554. this.bindGlobalHandlers();
  2555. },
  2556. // Removes the grid's container element from the DOM. Undoes any other DOM-related attachments.
  2557. // DOES NOT remove any content beforehand (doesn't clear events or call unrenderDates), unlike View
  2558. removeElement: function() {
  2559. this.unbindGlobalHandlers();
  2560. this.el.remove();
  2561. // NOTE: we don't null-out this.el for the same reasons we don't do it within View::removeElement
  2562. },
  2563. // Renders the basic structure of grid view before any content is rendered
  2564. renderSkeleton: function() {
  2565. // subclasses should implement
  2566. },
  2567. // Renders the grid's date-related content (like cells that represent days/times).
  2568. // Assumes setRange has already been called and the skeleton has already been rendered.
  2569. renderDates: function() {
  2570. // subclasses should implement
  2571. },
  2572. // Unrenders the grid's date-related content
  2573. unrenderDates: function() {
  2574. // subclasses should implement
  2575. },
  2576. /* Handlers
  2577. ------------------------------------------------------------------------------------------------------------------*/
  2578. // Binds DOM handlers to elements that reside outside the grid, such as the document
  2579. bindGlobalHandlers: function() {
  2580. $(document).on('dragstart sortstart', this.externalDragStartProxy); // jqui
  2581. },
  2582. // Unbinds DOM handlers from elements that reside outside the grid
  2583. unbindGlobalHandlers: function() {
  2584. $(document).off('dragstart sortstart', this.externalDragStartProxy); // jqui
  2585. },
  2586. // Process a mousedown on an element that represents a day. For day clicking and selecting.
  2587. dayMousedown: function(ev) {
  2588. var _this = this;
  2589. var view = this.view;
  2590. var isSelectable = view.opt('selectable');
  2591. var dayClickCell; // null if invalid dayClick
  2592. var selectionRange; // null if invalid selection
  2593. // this listener tracks a mousedown on a day element, and a subsequent drag.
  2594. // if the drag ends on the same day, it is a 'dayClick'.
  2595. // if 'selectable' is enabled, this listener also detects selections.
  2596. var dragListener = new CellDragListener(this.coordMap, {
  2597. //distance: 5, // needs more work if we want dayClick to fire correctly
  2598. scroll: view.opt('dragScroll'),
  2599. dragStart: function() {
  2600. view.unselect(); // since we could be rendering a new selection, we want to clear any old one
  2601. },
  2602. cellOver: function(cell, isOrig, origCell) {
  2603. if (origCell) { // click needs to have started on a cell
  2604. dayClickCell = isOrig ? cell : null; // single-cell selection is a day click
  2605. if (isSelectable) {
  2606. selectionRange = _this.computeSelection(origCell, cell);
  2607. if (selectionRange) {
  2608. _this.renderSelection(selectionRange);
  2609. }
  2610. else {
  2611. disableCursor();
  2612. }
  2613. }
  2614. }
  2615. },
  2616. cellOut: function(cell) {
  2617. dayClickCell = null;
  2618. selectionRange = null;
  2619. _this.unrenderSelection();
  2620. enableCursor();
  2621. },
  2622. listenStop: function(ev) {
  2623. if (dayClickCell) {
  2624. view.triggerDayClick(dayClickCell, _this.getCellDayEl(dayClickCell), ev);
  2625. }
  2626. if (selectionRange) {
  2627. // the selection will already have been rendered. just report it
  2628. view.reportSelection(selectionRange, ev);
  2629. }
  2630. enableCursor();
  2631. }
  2632. });
  2633. dragListener.mousedown(ev); // start listening, which will eventually initiate a dragStart
  2634. },
  2635. /* Event Helper
  2636. ------------------------------------------------------------------------------------------------------------------*/
  2637. // TODO: should probably move this to Grid.events, like we did event dragging / resizing
  2638. // Renders a mock event over the given range
  2639. renderRangeHelper: function(range, sourceSeg) {
  2640. var fakeEvent = this.fabricateHelperEvent(range, sourceSeg);
  2641. this.renderHelper(fakeEvent, sourceSeg); // do the actual rendering
  2642. },
  2643. // Builds a fake event given a date range it should cover, and a segment is should be inspired from.
  2644. // The range's end can be null, in which case the mock event that is rendered will have a null end time.
  2645. // `sourceSeg` is the internal segment object involved in the drag. If null, something external is dragging.
  2646. fabricateHelperEvent: function(range, sourceSeg) {
  2647. var fakeEvent = sourceSeg ? createObject(sourceSeg.event) : {}; // mask the original event object if possible
  2648. fakeEvent.start = range.start.clone();
  2649. fakeEvent.end = range.end ? range.end.clone() : null;
  2650. fakeEvent.allDay = null; // force it to be freshly computed by normalizeEventRange
  2651. this.view.calendar.normalizeEventRange(fakeEvent);
  2652. // this extra className will be useful for differentiating real events from mock events in CSS
  2653. fakeEvent.className = (fakeEvent.className || []).concat('fc-helper');
  2654. // if something external is being dragged in, don't render a resizer
  2655. if (!sourceSeg) {
  2656. fakeEvent.editable = false;
  2657. }
  2658. return fakeEvent;
  2659. },
  2660. // Renders a mock event
  2661. renderHelper: function(event, sourceSeg) {
  2662. // subclasses must implement
  2663. },
  2664. // Unrenders a mock event
  2665. unrenderHelper: function() {
  2666. // subclasses must implement
  2667. },
  2668. /* Selection
  2669. ------------------------------------------------------------------------------------------------------------------*/
  2670. // Renders a visual indication of a selection. Will highlight by default but can be overridden by subclasses.
  2671. renderSelection: function(range) {
  2672. this.renderHighlight(this.selectionRangeToSegs(range));
  2673. },
  2674. // Unrenders any visual indications of a selection. Will unrender a highlight by default.
  2675. unrenderSelection: function() {
  2676. this.unrenderHighlight();
  2677. },
  2678. // Given the first and last cells of a selection, returns a range object.
  2679. // Will return something falsy if the selection is invalid (when outside of selectionConstraint for example).
  2680. // Subclasses can override and provide additional data in the range object. Will be passed to renderSelection().
  2681. computeSelection: function(firstCell, lastCell) {
  2682. var dates = [
  2683. firstCell.start,
  2684. firstCell.end,
  2685. lastCell.start,
  2686. lastCell.end
  2687. ];
  2688. var range;
  2689. dates.sort(compareNumbers); // sorts chronologically. works with Moments
  2690. range = {
  2691. start: dates[0].clone(),
  2692. end: dates[3].clone()
  2693. };
  2694. if (!this.view.calendar.isSelectionRangeAllowed(range)) {
  2695. return null;
  2696. }
  2697. return range;
  2698. },
  2699. selectionRangeToSegs: function(range) {
  2700. return this.rangeToSegs(range);
  2701. },
  2702. /* Highlight
  2703. ------------------------------------------------------------------------------------------------------------------*/
  2704. // Renders an emphasis on the given date range. Given an array of segments.
  2705. renderHighlight: function(segs) {
  2706. this.renderFill('highlight', segs);
  2707. },
  2708. // Unrenders the emphasis on a date range
  2709. unrenderHighlight: function() {
  2710. this.unrenderFill('highlight');
  2711. },
  2712. // Generates an array of classNames for rendering the highlight. Used by the fill system.
  2713. highlightSegClasses: function() {
  2714. return [ 'fc-highlight' ];
  2715. },
  2716. /* Fill System (highlight, background events, business hours)
  2717. ------------------------------------------------------------------------------------------------------------------*/
  2718. // Renders a set of rectangles over the given segments of time.
  2719. // MUST RETURN a subset of segs, the segs that were actually rendered.
  2720. // Responsible for populating this.elsByFill. TODO: better API for expressing this requirement
  2721. renderFill: function(type, segs) {
  2722. // subclasses must implement
  2723. },
  2724. // Unrenders a specific type of fill that is currently rendered on the grid
  2725. unrenderFill: function(type) {
  2726. var el = this.elsByFill[type];
  2727. if (el) {
  2728. el.remove();
  2729. delete this.elsByFill[type];
  2730. }
  2731. },
  2732. // Renders and assigns an `el` property for each fill segment. Generic enough to work with different types.
  2733. // Only returns segments that successfully rendered.
  2734. // To be harnessed by renderFill (implemented by subclasses).
  2735. // Analagous to renderFgSegEls.
  2736. renderFillSegEls: function(type, segs) {
  2737. var _this = this;
  2738. var segElMethod = this[type + 'SegEl'];
  2739. var html = '';
  2740. var renderedSegs = [];
  2741. var i;
  2742. if (segs.length) {
  2743. // build a large concatenation of segment HTML
  2744. for (i = 0; i < segs.length; i++) {
  2745. html += this.fillSegHtml(type, segs[i]);
  2746. }
  2747. // Grab individual elements from the combined HTML string. Use each as the default rendering.
  2748. // Then, compute the 'el' for each segment.
  2749. $(html).each(function(i, node) {
  2750. var seg = segs[i];
  2751. var el = $(node);
  2752. // allow custom filter methods per-type
  2753. if (segElMethod) {
  2754. el = segElMethod.call(_this, seg, el);
  2755. }
  2756. if (el) { // custom filters did not cancel the render
  2757. el = $(el); // allow custom filter to return raw DOM node
  2758. // correct element type? (would be bad if a non-TD were inserted into a table for example)
  2759. if (el.is(_this.fillSegTag)) {
  2760. seg.el = el;
  2761. renderedSegs.push(seg);
  2762. }
  2763. }
  2764. });
  2765. }
  2766. return renderedSegs;
  2767. },
  2768. fillSegTag: 'div', // subclasses can override
  2769. // Builds the HTML needed for one fill segment. Generic enought o work with different types.
  2770. fillSegHtml: function(type, seg) {
  2771. // custom hooks per-type
  2772. var classesMethod = this[type + 'SegClasses'];
  2773. var cssMethod = this[type + 'SegCss'];
  2774. var classes = classesMethod ? classesMethod.call(this, seg) : [];
  2775. var css = cssToStr(cssMethod ? cssMethod.call(this, seg) : {});
  2776. return '<' + this.fillSegTag +
  2777. (classes.length ? ' class="' + classes.join(' ') + '"' : '') +
  2778. (css ? ' style="' + css + '"' : '') +
  2779. ' />';
  2780. },
  2781. /* Generic rendering utilities for subclasses
  2782. ------------------------------------------------------------------------------------------------------------------*/
  2783. // Renders a day-of-week header row.
  2784. // TODO: move to another class. not applicable to all Grids
  2785. headHtml: function() {
  2786. return '' +
  2787. '<div class="fc-row ' + this.view.widgetHeaderClass + '">' +
  2788. '<table>' +
  2789. '<thead>' +
  2790. this.rowHtml('head') + // leverages RowRenderer
  2791. '</thead>' +
  2792. '</table>' +
  2793. '</div>';
  2794. },
  2795. // Used by the `headHtml` method, via RowRenderer, for rendering the HTML of a day-of-week header cell
  2796. // TODO: move to another class. not applicable to all Grids
  2797. headCellHtml: function(cell) {
  2798. var view = this.view;
  2799. var date = cell.start;
  2800. return '' +
  2801. '<th class="fc-day-header ' + view.widgetHeaderClass + ' fc-' + dayIDs[date.day()] + '">' +
  2802. htmlEscape(date.format(this.colHeadFormat)) +
  2803. '</th>';
  2804. },
  2805. // Renders the HTML for a single-day background cell
  2806. bgCellHtml: function(cell) {
  2807. var view = this.view;
  2808. var date = cell.start;
  2809. var classes = this.getDayClasses(date);
  2810. classes.unshift('fc-day', view.widgetContentClass);
  2811. return '<td class="' + classes.join(' ') + '"' +
  2812. ' data-date="' + date.format('YYYY-MM-DD') + '"' + // if date has a time, won't format it
  2813. '></td>';
  2814. },
  2815. // Computes HTML classNames for a single-day cell
  2816. getDayClasses: function(date) {
  2817. var view = this.view;
  2818. var today = view.calendar.getNow().stripTime();
  2819. var classes = [ 'fc-' + dayIDs[date.day()] ];
  2820. if (
  2821. view.intervalDuration.as('months') == 1 &&
  2822. date.month() != view.intervalStart.month()
  2823. ) {
  2824. classes.push('fc-other-month');
  2825. }
  2826. if (date.isSame(today, 'day')) {
  2827. classes.push(
  2828. 'fc-today',
  2829. view.highlightStateClass
  2830. );
  2831. }
  2832. else if (date < today) {
  2833. classes.push('fc-past');
  2834. }
  2835. else {
  2836. classes.push('fc-future');
  2837. }
  2838. return classes;
  2839. }
  2840. });
  2841. ;;
  2842. /* Event-rendering and event-interaction methods for the abstract Grid class
  2843. ----------------------------------------------------------------------------------------------------------------------*/
  2844. Grid.mixin({
  2845. mousedOverSeg: null, // the segment object the user's mouse is over. null if over nothing
  2846. isDraggingSeg: false, // is a segment being dragged? boolean
  2847. isResizingSeg: false, // is a segment being resized? boolean
  2848. isDraggingExternal: false, // jqui-dragging an external element? boolean
  2849. segs: null, // the event segments currently rendered in the grid
  2850. // Renders the given events onto the grid
  2851. renderEvents: function(events) {
  2852. var segs = this.eventsToSegs(events);
  2853. var bgSegs = [];
  2854. var fgSegs = [];
  2855. var i, seg;
  2856. for (i = 0; i < segs.length; i++) {
  2857. seg = segs[i];
  2858. if (isBgEvent(seg.event)) {
  2859. bgSegs.push(seg);
  2860. }
  2861. else {
  2862. fgSegs.push(seg);
  2863. }
  2864. }
  2865. // Render each different type of segment.
  2866. // Each function may return a subset of the segs, segs that were actually rendered.
  2867. bgSegs = this.renderBgSegs(bgSegs) || bgSegs;
  2868. fgSegs = this.renderFgSegs(fgSegs) || fgSegs;
  2869. this.segs = bgSegs.concat(fgSegs);
  2870. },
  2871. // Unrenders all events currently rendered on the grid
  2872. unrenderEvents: function() {
  2873. this.triggerSegMouseout(); // trigger an eventMouseout if user's mouse is over an event
  2874. this.unrenderFgSegs();
  2875. this.unrenderBgSegs();
  2876. this.segs = null;
  2877. },
  2878. // Retrieves all rendered segment objects currently rendered on the grid
  2879. getEventSegs: function() {
  2880. return this.segs || [];
  2881. },
  2882. /* Foreground Segment Rendering
  2883. ------------------------------------------------------------------------------------------------------------------*/
  2884. // Renders foreground event segments onto the grid. May return a subset of segs that were rendered.
  2885. renderFgSegs: function(segs) {
  2886. // subclasses must implement
  2887. },
  2888. // Unrenders all currently rendered foreground segments
  2889. unrenderFgSegs: function() {
  2890. // subclasses must implement
  2891. },
  2892. // Renders and assigns an `el` property for each foreground event segment.
  2893. // Only returns segments that successfully rendered.
  2894. // A utility that subclasses may use.
  2895. renderFgSegEls: function(segs, disableResizing) {
  2896. var view = this.view;
  2897. var html = '';
  2898. var renderedSegs = [];
  2899. var i;
  2900. if (segs.length) { // don't build an empty html string
  2901. // build a large concatenation of event segment HTML
  2902. for (i = 0; i < segs.length; i++) {
  2903. html += this.fgSegHtml(segs[i], disableResizing);
  2904. }
  2905. // Grab individual elements from the combined HTML string. Use each as the default rendering.
  2906. // Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false.
  2907. $(html).each(function(i, node) {
  2908. var seg = segs[i];
  2909. var el = view.resolveEventEl(seg.event, $(node));
  2910. if (el) {
  2911. el.data('fc-seg', seg); // used by handlers
  2912. seg.el = el;
  2913. renderedSegs.push(seg);
  2914. }
  2915. });
  2916. }
  2917. return renderedSegs;
  2918. },
  2919. // Generates the HTML for the default rendering of a foreground event segment. Used by renderFgSegEls()
  2920. fgSegHtml: function(seg, disableResizing) {
  2921. // subclasses should implement
  2922. },
  2923. /* Background Segment Rendering
  2924. ------------------------------------------------------------------------------------------------------------------*/
  2925. // Renders the given background event segments onto the grid.
  2926. // Returns a subset of the segs that were actually rendered.
  2927. renderBgSegs: function(segs) {
  2928. return this.renderFill('bgEvent', segs);
  2929. },
  2930. // Unrenders all the currently rendered background event segments
  2931. unrenderBgSegs: function() {
  2932. this.unrenderFill('bgEvent');
  2933. },
  2934. // Renders a background event element, given the default rendering. Called by the fill system.
  2935. bgEventSegEl: function(seg, el) {
  2936. return this.view.resolveEventEl(seg.event, el); // will filter through eventRender
  2937. },
  2938. // Generates an array of classNames to be used for the default rendering of a background event.
  2939. // Called by the fill system.
  2940. bgEventSegClasses: function(seg) {
  2941. var event = seg.event;
  2942. var source = event.source || {};
  2943. return [ 'fc-bgevent' ].concat(
  2944. event.className,
  2945. source.className || []
  2946. );
  2947. },
  2948. // Generates a semicolon-separated CSS string to be used for the default rendering of a background event.
  2949. // Called by the fill system.
  2950. // TODO: consolidate with getEventSkinCss?
  2951. bgEventSegCss: function(seg) {
  2952. var view = this.view;
  2953. var event = seg.event;
  2954. var source = event.source || {};
  2955. return {
  2956. 'background-color':
  2957. event.backgroundColor ||
  2958. event.color ||
  2959. source.backgroundColor ||
  2960. source.color ||
  2961. view.opt('eventBackgroundColor') ||
  2962. view.opt('eventColor')
  2963. };
  2964. },
  2965. // Generates an array of classNames to be used for the rendering business hours overlay. Called by the fill system.
  2966. businessHoursSegClasses: function(seg) {
  2967. return [ 'fc-nonbusiness', 'fc-bgevent' ];
  2968. },
  2969. /* Handlers
  2970. ------------------------------------------------------------------------------------------------------------------*/
  2971. // Attaches event-element-related handlers to the container element and leverage bubbling
  2972. bindSegHandlers: function() {
  2973. var _this = this;
  2974. var view = this.view;
  2975. $.each(
  2976. {
  2977. mouseenter: function(seg, ev) {
  2978. _this.triggerSegMouseover(seg, ev);
  2979. },
  2980. mouseleave: function(seg, ev) {
  2981. _this.triggerSegMouseout(seg, ev);
  2982. },
  2983. click: function(seg, ev) {
  2984. return view.trigger('eventClick', this, seg.event, ev); // can return `false` to cancel
  2985. },
  2986. mousedown: function(seg, ev) {
  2987. if ($(ev.target).is('.fc-resizer') && view.isEventResizable(seg.event)) {
  2988. _this.segResizeMousedown(seg, ev, $(ev.target).is('.fc-start-resizer'));
  2989. }
  2990. else if (view.isEventDraggable(seg.event)) {
  2991. _this.segDragMousedown(seg, ev);
  2992. }
  2993. }
  2994. },
  2995. function(name, func) {
  2996. // attach the handler to the container element and only listen for real event elements via bubbling
  2997. _this.el.on(name, '.fc-event-container > *', function(ev) {
  2998. var seg = $(this).data('fc-seg'); // grab segment data. put there by View::renderEvents
  2999. // only call the handlers if there is not a drag/resize in progress
  3000. if (seg && !_this.isDraggingSeg && !_this.isResizingSeg) {
  3001. return func.call(this, seg, ev); // `this` will be the event element
  3002. }
  3003. });
  3004. }
  3005. );
  3006. },
  3007. // Updates internal state and triggers handlers for when an event element is moused over
  3008. triggerSegMouseover: function(seg, ev) {
  3009. if (!this.mousedOverSeg) {
  3010. this.mousedOverSeg = seg;
  3011. this.view.trigger('eventMouseover', seg.el[0], seg.event, ev);
  3012. }
  3013. },
  3014. // Updates internal state and triggers handlers for when an event element is moused out.
  3015. // Can be given no arguments, in which case it will mouseout the segment that was previously moused over.
  3016. triggerSegMouseout: function(seg, ev) {
  3017. ev = ev || {}; // if given no args, make a mock mouse event
  3018. if (this.mousedOverSeg) {
  3019. seg = seg || this.mousedOverSeg; // if given no args, use the currently moused-over segment
  3020. this.mousedOverSeg = null;
  3021. this.view.trigger('eventMouseout', seg.el[0], seg.event, ev);
  3022. }
  3023. },
  3024. /* Event Dragging
  3025. ------------------------------------------------------------------------------------------------------------------*/
  3026. // Called when the user does a mousedown on an event, which might lead to dragging.
  3027. // Generic enough to work with any type of Grid.
  3028. segDragMousedown: function(seg, ev) {
  3029. var _this = this;
  3030. var view = this.view;
  3031. var calendar = view.calendar;
  3032. var el = seg.el;
  3033. var event = seg.event;
  3034. var dropLocation;
  3035. // A clone of the original element that will move with the mouse
  3036. var mouseFollower = new MouseFollower(seg.el, {
  3037. parentEl: view.el,
  3038. opacity: view.opt('dragOpacity'),
  3039. revertDuration: view.opt('dragRevertDuration'),
  3040. zIndex: 2 // one above the .fc-view
  3041. });
  3042. // Tracks mouse movement over the *view's* coordinate map. Allows dragging and dropping between subcomponents
  3043. // of the view.
  3044. var dragListener = new CellDragListener(view.coordMap, {
  3045. distance: 5,
  3046. scroll: view.opt('dragScroll'),
  3047. subjectEl: el,
  3048. subjectCenter: true,
  3049. listenStart: function(ev) {
  3050. mouseFollower.hide(); // don't show until we know this is a real drag
  3051. mouseFollower.start(ev);
  3052. },
  3053. dragStart: function(ev) {
  3054. _this.triggerSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
  3055. _this.segDragStart(seg, ev);
  3056. view.hideEvent(event); // hide all event segments. our mouseFollower will take over
  3057. },
  3058. cellOver: function(cell, isOrig, origCell) {
  3059. // starting cell could be forced (DayGrid.limit)
  3060. if (seg.cell) {
  3061. origCell = seg.cell;
  3062. }
  3063. dropLocation = _this.computeEventDrop(origCell, cell, event);
  3064. if (dropLocation && !calendar.isEventRangeAllowed(dropLocation, event)) {
  3065. disableCursor();
  3066. dropLocation = null;
  3067. }
  3068. // if a valid drop location, have the subclass render a visual indication
  3069. if (dropLocation && view.renderDrag(dropLocation, seg)) {
  3070. mouseFollower.hide(); // if the subclass is already using a mock event "helper", hide our own
  3071. }
  3072. else {
  3073. mouseFollower.show(); // otherwise, have the helper follow the mouse (no snapping)
  3074. }
  3075. if (isOrig) {
  3076. dropLocation = null; // needs to have moved cells to be a valid drop
  3077. }
  3078. },
  3079. cellOut: function() { // called before mouse moves to a different cell OR moved out of all cells
  3080. view.unrenderDrag(); // unrender whatever was done in renderDrag
  3081. mouseFollower.show(); // show in case we are moving out of all cells
  3082. dropLocation = null;
  3083. },
  3084. cellDone: function() { // Called after a cellOut OR before a dragStop
  3085. enableCursor();
  3086. },
  3087. dragStop: function(ev) {
  3088. // do revert animation if hasn't changed. calls a callback when finished (whether animation or not)
  3089. mouseFollower.stop(!dropLocation, function() {
  3090. view.unrenderDrag();
  3091. view.showEvent(event);
  3092. _this.segDragStop(seg, ev);
  3093. if (dropLocation) {
  3094. view.reportEventDrop(event, dropLocation, this.largeUnit, el, ev);
  3095. }
  3096. });
  3097. },
  3098. listenStop: function() {
  3099. mouseFollower.stop(); // put in listenStop in case there was a mousedown but the drag never started
  3100. }
  3101. });
  3102. dragListener.mousedown(ev); // start listening, which will eventually lead to a dragStart
  3103. },
  3104. // Called before event segment dragging starts
  3105. segDragStart: function(seg, ev) {
  3106. this.isDraggingSeg = true;
  3107. this.view.trigger('eventDragStart', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
  3108. },
  3109. // Called after event segment dragging stops
  3110. segDragStop: function(seg, ev) {
  3111. this.isDraggingSeg = false;
  3112. this.view.trigger('eventDragStop', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
  3113. },
  3114. // Given the cell an event drag began, and the cell event was dropped, calculates the new start/end/allDay
  3115. // values for the event. Subclasses may override and set additional properties to be used by renderDrag.
  3116. // A falsy returned value indicates an invalid drop.
  3117. computeEventDrop: function(startCell, endCell, event) {
  3118. var calendar = this.view.calendar;
  3119. var dragStart = startCell.start;
  3120. var dragEnd = endCell.start;
  3121. var delta;
  3122. var dropLocation;
  3123. if (dragStart.hasTime() === dragEnd.hasTime()) {
  3124. delta = this.diffDates(dragEnd, dragStart);
  3125. // if an all-day event was in a timed area and it was dragged to a different time,
  3126. // guarantee an end and adjust start/end to have times
  3127. if (event.allDay && durationHasTime(delta)) {
  3128. dropLocation = {
  3129. start: event.start.clone(),
  3130. end: calendar.getEventEnd(event), // will be an ambig day
  3131. allDay: false // for normalizeEventRangeTimes
  3132. };
  3133. calendar.normalizeEventRangeTimes(dropLocation);
  3134. }
  3135. // othewise, work off existing values
  3136. else {
  3137. dropLocation = {
  3138. start: event.start.clone(),
  3139. end: event.end ? event.end.clone() : null,
  3140. allDay: event.allDay // keep it the same
  3141. };
  3142. }
  3143. dropLocation.start.add(delta);
  3144. if (dropLocation.end) {
  3145. dropLocation.end.add(delta);
  3146. }
  3147. }
  3148. else {
  3149. // if switching from day <-> timed, start should be reset to the dropped date, and the end cleared
  3150. dropLocation = {
  3151. start: dragEnd.clone(),
  3152. end: null, // end should be cleared
  3153. allDay: !dragEnd.hasTime()
  3154. };
  3155. }
  3156. return dropLocation;
  3157. },
  3158. // Utility for apply dragOpacity to a jQuery set
  3159. applyDragOpacity: function(els) {
  3160. var opacity = this.view.opt('dragOpacity');
  3161. if (opacity != null) {
  3162. els.each(function(i, node) {
  3163. // Don't use jQuery (will set an IE filter), do it the old fashioned way.
  3164. // In IE8, a helper element will disappears if there's a filter.
  3165. node.style.opacity = opacity;
  3166. });
  3167. }
  3168. },
  3169. /* External Element Dragging
  3170. ------------------------------------------------------------------------------------------------------------------*/
  3171. // Called when a jQuery UI drag is initiated anywhere in the DOM
  3172. externalDragStart: function(ev, ui) {
  3173. var view = this.view;
  3174. var el;
  3175. var accept;
  3176. if (view.opt('droppable')) { // only listen if this setting is on
  3177. el = $((ui ? ui.item : null) || ev.target);
  3178. // Test that the dragged element passes the dropAccept selector or filter function.
  3179. // FYI, the default is "*" (matches all)
  3180. accept = view.opt('dropAccept');
  3181. if ($.isFunction(accept) ? accept.call(el[0], el) : el.is(accept)) {
  3182. if (!this.isDraggingExternal) { // prevent double-listening if fired twice
  3183. this.listenToExternalDrag(el, ev, ui);
  3184. }
  3185. }
  3186. }
  3187. },
  3188. // Called when a jQuery UI drag starts and it needs to be monitored for cell dropping
  3189. listenToExternalDrag: function(el, ev, ui) {
  3190. var _this = this;
  3191. var meta = getDraggedElMeta(el); // extra data about event drop, including possible event to create
  3192. var dragListener;
  3193. var dropLocation; // a null value signals an unsuccessful drag
  3194. // listener that tracks mouse movement over date-associated pixel regions
  3195. dragListener = new CellDragListener(this.coordMap, {
  3196. listenStart: function() {
  3197. _this.isDraggingExternal = true;
  3198. },
  3199. cellOver: function(cell) {
  3200. dropLocation = _this.computeExternalDrop(cell, meta);
  3201. if (dropLocation) {
  3202. _this.renderDrag(dropLocation); // called without a seg parameter
  3203. }
  3204. else { // invalid drop cell
  3205. disableCursor();
  3206. }
  3207. },
  3208. cellOut: function() {
  3209. dropLocation = null; // signal unsuccessful
  3210. _this.unrenderDrag();
  3211. enableCursor();
  3212. },
  3213. dragStop: function() {
  3214. _this.unrenderDrag();
  3215. enableCursor();
  3216. if (dropLocation) { // element was dropped on a valid date/time cell
  3217. _this.view.reportExternalDrop(meta, dropLocation, el, ev, ui);
  3218. }
  3219. },
  3220. listenStop: function() {
  3221. _this.isDraggingExternal = false;
  3222. }
  3223. });
  3224. dragListener.startDrag(ev); // start listening immediately
  3225. },
  3226. // Given a cell to be dropped upon, and misc data associated with the jqui drag (guaranteed to be a plain object),
  3227. // returns start/end dates for the event that would result from the hypothetical drop. end might be null.
  3228. // Returning a null value signals an invalid drop cell.
  3229. computeExternalDrop: function(cell, meta) {
  3230. var dropLocation = {
  3231. start: cell.start.clone(),
  3232. end: null
  3233. };
  3234. // if dropped on an all-day cell, and element's metadata specified a time, set it
  3235. if (meta.startTime && !dropLocation.start.hasTime()) {
  3236. dropLocation.start.time(meta.startTime);
  3237. }
  3238. if (meta.duration) {
  3239. dropLocation.end = dropLocation.start.clone().add(meta.duration);
  3240. }
  3241. if (!this.view.calendar.isExternalDropRangeAllowed(dropLocation, meta.eventProps)) {
  3242. return null;
  3243. }
  3244. return dropLocation;
  3245. },
  3246. /* Drag Rendering (for both events and an external elements)
  3247. ------------------------------------------------------------------------------------------------------------------*/
  3248. // Renders a visual indication of an event or external element being dragged.
  3249. // `dropLocation` contains hypothetical start/end/allDay values the event would have if dropped. end can be null.
  3250. // `seg` is the internal segment object that is being dragged. If dragging an external element, `seg` is null.
  3251. // A truthy returned value indicates this method has rendered a helper element.
  3252. renderDrag: function(dropLocation, seg) {
  3253. // subclasses must implement
  3254. },
  3255. // Unrenders a visual indication of an event or external element being dragged
  3256. unrenderDrag: function() {
  3257. // subclasses must implement
  3258. },
  3259. /* Resizing
  3260. ------------------------------------------------------------------------------------------------------------------*/
  3261. // Called when the user does a mousedown on an event's resizer, which might lead to resizing.
  3262. // Generic enough to work with any type of Grid.
  3263. segResizeMousedown: function(seg, ev, isStart) {
  3264. var _this = this;
  3265. var view = this.view;
  3266. var calendar = view.calendar;
  3267. var el = seg.el;
  3268. var event = seg.event;
  3269. var eventEnd = calendar.getEventEnd(event);
  3270. var dragListener;
  3271. var resizeLocation; // falsy if invalid resize
  3272. // Tracks mouse movement over the *grid's* coordinate map
  3273. dragListener = new CellDragListener(this.coordMap, {
  3274. distance: 5,
  3275. scroll: view.opt('dragScroll'),
  3276. subjectEl: el,
  3277. dragStart: function(ev) {
  3278. _this.triggerSegMouseout(seg, ev); // ensure a mouseout on the manipulated event has been reported
  3279. _this.segResizeStart(seg, ev);
  3280. },
  3281. cellOver: function(cell, isOrig, origCell) {
  3282. resizeLocation = isStart ?
  3283. _this.computeEventStartResize(origCell, cell, event) :
  3284. _this.computeEventEndResize(origCell, cell, event);
  3285. if (resizeLocation) {
  3286. if (!calendar.isEventRangeAllowed(resizeLocation, event)) {
  3287. disableCursor();
  3288. resizeLocation = null;
  3289. }
  3290. // no change? (TODO: how does this work with timezones?)
  3291. else if (resizeLocation.start.isSame(event.start) && resizeLocation.end.isSame(eventEnd)) {
  3292. resizeLocation = null;
  3293. }
  3294. }
  3295. if (resizeLocation) {
  3296. view.hideEvent(event);
  3297. _this.renderEventResize(resizeLocation, seg);
  3298. }
  3299. },
  3300. cellOut: function() { // called before mouse moves to a different cell OR moved out of all cells
  3301. resizeLocation = null;
  3302. },
  3303. cellDone: function() { // resets the rendering to show the original event
  3304. _this.unrenderEventResize();
  3305. view.showEvent(event);
  3306. enableCursor();
  3307. },
  3308. dragStop: function(ev) {
  3309. _this.segResizeStop(seg, ev);
  3310. if (resizeLocation) { // valid date to resize to?
  3311. view.reportEventResize(event, resizeLocation, this.largeUnit, el, ev);
  3312. }
  3313. }
  3314. });
  3315. dragListener.mousedown(ev); // start listening, which will eventually lead to a dragStart
  3316. },
  3317. // Called before event segment resizing starts
  3318. segResizeStart: function(seg, ev) {
  3319. this.isResizingSeg = true;
  3320. this.view.trigger('eventResizeStart', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
  3321. },
  3322. // Called after event segment resizing stops
  3323. segResizeStop: function(seg, ev) {
  3324. this.isResizingSeg = false;
  3325. this.view.trigger('eventResizeStop', seg.el[0], seg.event, ev, {}); // last argument is jqui dummy
  3326. },
  3327. // Returns new date-information for an event segment being resized from its start
  3328. computeEventStartResize: function(startCell, endCell, event) {
  3329. return this.computeEventResize('start', startCell, endCell, event);
  3330. },
  3331. // Returns new date-information for an event segment being resized from its end
  3332. computeEventEndResize: function(startCell, endCell, event) {
  3333. return this.computeEventResize('end', startCell, endCell, event);
  3334. },
  3335. // Returns new date-information for an event segment being resized from its start OR end
  3336. // `type` is either 'start' or 'end'
  3337. computeEventResize: function(type, startCell, endCell, event) {
  3338. var calendar = this.view.calendar;
  3339. var delta = this.diffDates(endCell[type], startCell[type]);
  3340. var range;
  3341. var defaultDuration;
  3342. // build original values to work from, guaranteeing a start and end
  3343. range = {
  3344. start: event.start.clone(),
  3345. end: calendar.getEventEnd(event),
  3346. allDay: event.allDay
  3347. };
  3348. // if an all-day event was in a timed area and was resized to a time, adjust start/end to have times
  3349. if (range.allDay && durationHasTime(delta)) {
  3350. range.allDay = false;
  3351. calendar.normalizeEventRangeTimes(range);
  3352. }
  3353. range[type].add(delta); // apply delta to start or end
  3354. // if the event was compressed too small, find a new reasonable duration for it
  3355. if (!range.start.isBefore(range.end)) {
  3356. defaultDuration = event.allDay ?
  3357. calendar.defaultAllDayEventDuration :
  3358. calendar.defaultTimedEventDuration;
  3359. // between the cell's duration and the event's default duration, use the smaller of the two.
  3360. // example: if year-length slots, and compressed to one slot, we don't want the event to be a year long
  3361. if (this.cellDuration && this.cellDuration < defaultDuration) {
  3362. defaultDuration = this.cellDuration;
  3363. }
  3364. if (type == 'start') { // resizing the start?
  3365. range.start = range.end.clone().subtract(defaultDuration);
  3366. }
  3367. else { // resizing the end?
  3368. range.end = range.start.clone().add(defaultDuration);
  3369. }
  3370. }
  3371. return range;
  3372. },
  3373. // Renders a visual indication of an event being resized.
  3374. // `range` has the updated dates of the event. `seg` is the original segment object involved in the drag.
  3375. renderEventResize: function(range, seg) {
  3376. // subclasses must implement
  3377. },
  3378. // Unrenders a visual indication of an event being resized.
  3379. unrenderEventResize: function() {
  3380. // subclasses must implement
  3381. },
  3382. /* Rendering Utils
  3383. ------------------------------------------------------------------------------------------------------------------*/
  3384. // Compute the text that should be displayed on an event's element.
  3385. // `range` can be the Event object itself, or something range-like, with at least a `start`.
  3386. // If event times are disabled, or the event has no time, will return a blank string.
  3387. // If not specified, formatStr will default to the eventTimeFormat setting,
  3388. // and displayEnd will default to the displayEventEnd setting.
  3389. getEventTimeText: function(range, formatStr, displayEnd) {
  3390. if (formatStr == null) {
  3391. formatStr = this.eventTimeFormat;
  3392. }
  3393. if (displayEnd == null) {
  3394. displayEnd = this.displayEventEnd;
  3395. }
  3396. if (this.displayEventTime && range.start.hasTime()) {
  3397. if (displayEnd && range.end) {
  3398. return this.view.formatRange(range, formatStr);
  3399. }
  3400. else {
  3401. return range.start.format(formatStr);
  3402. }
  3403. }
  3404. return '';
  3405. },
  3406. // Generic utility for generating the HTML classNames for an event segment's element
  3407. getSegClasses: function(seg, isDraggable, isResizable) {
  3408. var event = seg.event;
  3409. var classes = [
  3410. 'fc-event',
  3411. seg.isStart ? 'fc-start' : 'fc-not-start',
  3412. seg.isEnd ? 'fc-end' : 'fc-not-end'
  3413. ].concat(
  3414. event.className,
  3415. event.source ? event.source.className : []
  3416. );
  3417. if (isDraggable) {
  3418. classes.push('fc-draggable');
  3419. }
  3420. if (isResizable) {
  3421. classes.push('fc-resizable');
  3422. }
  3423. return classes;
  3424. },
  3425. // Utility for generating event skin-related CSS properties
  3426. getEventSkinCss: function(event) {
  3427. var view = this.view;
  3428. var source = event.source || {};
  3429. var eventColor = event.color;
  3430. var sourceColor = source.color;
  3431. var optionColor = view.opt('eventColor');
  3432. return {
  3433. 'background-color':
  3434. event.backgroundColor ||
  3435. eventColor ||
  3436. source.backgroundColor ||
  3437. sourceColor ||
  3438. view.opt('eventBackgroundColor') ||
  3439. optionColor,
  3440. 'border-color':
  3441. event.borderColor ||
  3442. eventColor ||
  3443. source.borderColor ||
  3444. sourceColor ||
  3445. view.opt('eventBorderColor') ||
  3446. optionColor,
  3447. color:
  3448. event.textColor ||
  3449. source.textColor ||
  3450. view.opt('eventTextColor')
  3451. };
  3452. },
  3453. /* Converting events -> ranges -> segs
  3454. ------------------------------------------------------------------------------------------------------------------*/
  3455. // Converts an array of event objects into an array of event segment objects.
  3456. // A custom `rangeToSegsFunc` may be given for arbitrarily slicing up events.
  3457. // Doesn't guarantee an order for the resulting array.
  3458. eventsToSegs: function(events, rangeToSegsFunc) {
  3459. var eventRanges = this.eventsToRanges(events);
  3460. var segs = [];
  3461. var i;
  3462. for (i = 0; i < eventRanges.length; i++) {
  3463. segs.push.apply(
  3464. segs,
  3465. this.eventRangeToSegs(eventRanges[i], rangeToSegsFunc)
  3466. );
  3467. }
  3468. return segs;
  3469. },
  3470. // Converts an array of events into an array of "range" objects.
  3471. // A "range" object is a plain object with start/end properties denoting the time it covers. Also an event property.
  3472. // For "normal" events, this will be identical to the event's start/end, but for "inverse-background" events,
  3473. // will create an array of ranges that span the time *not* covered by the given event.
  3474. // Doesn't guarantee an order for the resulting array.
  3475. eventsToRanges: function(events) {
  3476. var _this = this;
  3477. var eventsById = groupEventsById(events);
  3478. var ranges = [];
  3479. // group by ID so that related inverse-background events can be rendered together
  3480. $.each(eventsById, function(id, eventGroup) {
  3481. if (eventGroup.length) {
  3482. ranges.push.apply(
  3483. ranges,
  3484. isInverseBgEvent(eventGroup[0]) ?
  3485. _this.eventsToInverseRanges(eventGroup) :
  3486. _this.eventsToNormalRanges(eventGroup)
  3487. );
  3488. }
  3489. });
  3490. return ranges;
  3491. },
  3492. // Converts an array of "normal" events (not inverted rendering) into a parallel array of ranges
  3493. eventsToNormalRanges: function(events) {
  3494. var calendar = this.view.calendar;
  3495. var ranges = [];
  3496. var i, event;
  3497. var eventStart, eventEnd;
  3498. for (i = 0; i < events.length; i++) {
  3499. event = events[i];
  3500. // make copies and normalize by stripping timezone
  3501. eventStart = event.start.clone().stripZone();
  3502. eventEnd = calendar.getEventEnd(event).stripZone();
  3503. ranges.push({
  3504. event: event,
  3505. start: eventStart,
  3506. end: eventEnd,
  3507. eventStartMS: +eventStart,
  3508. eventDurationMS: eventEnd - eventStart
  3509. });
  3510. }
  3511. return ranges;
  3512. },
  3513. // Converts an array of events, with inverse-background rendering, into an array of range objects.
  3514. // The range objects will cover all the time NOT covered by the events.
  3515. eventsToInverseRanges: function(events) {
  3516. var view = this.view;
  3517. var viewStart = view.start.clone().stripZone(); // normalize timezone
  3518. var viewEnd = view.end.clone().stripZone(); // normalize timezone
  3519. var normalRanges = this.eventsToNormalRanges(events); // will give us normalized dates we can use w/o copies
  3520. var inverseRanges = [];
  3521. var event0 = events[0]; // assign this to each range's `.event`
  3522. var start = viewStart; // the end of the previous range. the start of the new range
  3523. var i, normalRange;
  3524. // ranges need to be in order. required for our date-walking algorithm
  3525. normalRanges.sort(compareNormalRanges);
  3526. for (i = 0; i < normalRanges.length; i++) {
  3527. normalRange = normalRanges[i];
  3528. // add the span of time before the event (if there is any)
  3529. if (normalRange.start > start) { // compare millisecond time (skip any ambig logic)
  3530. inverseRanges.push({
  3531. event: event0,
  3532. start: start,
  3533. end: normalRange.start
  3534. });
  3535. }
  3536. start = normalRange.end;
  3537. }
  3538. // add the span of time after the last event (if there is any)
  3539. if (start < viewEnd) { // compare millisecond time (skip any ambig logic)
  3540. inverseRanges.push({
  3541. event: event0,
  3542. start: start,
  3543. end: viewEnd
  3544. });
  3545. }
  3546. return inverseRanges;
  3547. },
  3548. // Slices the given event range into one or more segment objects.
  3549. // A `rangeToSegsFunc` custom slicing function can be given.
  3550. eventRangeToSegs: function(eventRange, rangeToSegsFunc) {
  3551. var segs;
  3552. var i, seg;
  3553. eventRange = this.view.calendar.ensureVisibleEventRange(eventRange);
  3554. if (rangeToSegsFunc) {
  3555. segs = rangeToSegsFunc(eventRange);
  3556. }
  3557. else {
  3558. segs = this.rangeToSegs(eventRange); // defined by the subclass
  3559. }
  3560. for (i = 0; i < segs.length; i++) {
  3561. seg = segs[i];
  3562. seg.event = eventRange.event;
  3563. seg.eventStartMS = eventRange.eventStartMS;
  3564. seg.eventDurationMS = eventRange.eventDurationMS;
  3565. }
  3566. return segs;
  3567. },
  3568. sortSegs: function(segs) {
  3569. segs.sort(proxy(this, 'compareSegs'));
  3570. },
  3571. // A cmp function for determining which segments should take visual priority
  3572. // DOES NOT WORK ON INVERTED BACKGROUND EVENTS because they have no eventStartMS/eventDurationMS
  3573. compareSegs: function(seg1, seg2) {
  3574. return seg1.eventStartMS - seg2.eventStartMS || // earlier events go first
  3575. seg2.eventDurationMS - seg1.eventDurationMS || // tie? longer events go first
  3576. seg2.event.allDay - seg1.event.allDay || // tie? put all-day events first (booleans cast to 0/1)
  3577. compareByFieldSpecs(seg1.event, seg2.event, this.view.eventOrderSpecs);
  3578. }
  3579. });
  3580. /* Utilities
  3581. ----------------------------------------------------------------------------------------------------------------------*/
  3582. function isBgEvent(event) { // returns true if background OR inverse-background
  3583. var rendering = getEventRendering(event);
  3584. return rendering === 'background' || rendering === 'inverse-background';
  3585. }
  3586. function isInverseBgEvent(event) {
  3587. return getEventRendering(event) === 'inverse-background';
  3588. }
  3589. function getEventRendering(event) {
  3590. return firstDefined((event.source || {}).rendering, event.rendering);
  3591. }
  3592. function groupEventsById(events) {
  3593. var eventsById = {};
  3594. var i, event;
  3595. for (i = 0; i < events.length; i++) {
  3596. event = events[i];
  3597. (eventsById[event._id] || (eventsById[event._id] = [])).push(event);
  3598. }
  3599. return eventsById;
  3600. }
  3601. // A cmp function for determining which non-inverted "ranges" (see above) happen earlier
  3602. function compareNormalRanges(range1, range2) {
  3603. return range1.eventStartMS - range2.eventStartMS; // earlier ranges go first
  3604. }
  3605. /* External-Dragging-Element Data
  3606. ----------------------------------------------------------------------------------------------------------------------*/
  3607. // Require all HTML5 data-* attributes used by FullCalendar to have this prefix.
  3608. // A value of '' will query attributes like data-event. A value of 'fc' will query attributes like data-fc-event.
  3609. fc.dataAttrPrefix = '';
  3610. // Given a jQuery element that might represent a dragged FullCalendar event, returns an intermediate data structure
  3611. // to be used for Event Object creation.
  3612. // A defined `.eventProps`, even when empty, indicates that an event should be created.
  3613. function getDraggedElMeta(el) {
  3614. var prefix = fc.dataAttrPrefix;
  3615. var eventProps; // properties for creating the event, not related to date/time
  3616. var startTime; // a Duration
  3617. var duration;
  3618. var stick;
  3619. if (prefix) { prefix += '-'; }
  3620. eventProps = el.data(prefix + 'event') || null;
  3621. if (eventProps) {
  3622. if (typeof eventProps === 'object') {
  3623. eventProps = $.extend({}, eventProps); // make a copy
  3624. }
  3625. else { // something like 1 or true. still signal event creation
  3626. eventProps = {};
  3627. }
  3628. // pluck special-cased date/time properties
  3629. startTime = eventProps.start;
  3630. if (startTime == null) { startTime = eventProps.time; } // accept 'time' as well
  3631. duration = eventProps.duration;
  3632. stick = eventProps.stick;
  3633. delete eventProps.start;
  3634. delete eventProps.time;
  3635. delete eventProps.duration;
  3636. delete eventProps.stick;
  3637. }
  3638. // fallback to standalone attribute values for each of the date/time properties
  3639. if (startTime == null) { startTime = el.data(prefix + 'start'); }
  3640. if (startTime == null) { startTime = el.data(prefix + 'time'); } // accept 'time' as well
  3641. if (duration == null) { duration = el.data(prefix + 'duration'); }
  3642. if (stick == null) { stick = el.data(prefix + 'stick'); }
  3643. // massage into correct data types
  3644. startTime = startTime != null ? moment.duration(startTime) : null;
  3645. duration = duration != null ? moment.duration(duration) : null;
  3646. stick = Boolean(stick);
  3647. return { eventProps: eventProps, startTime: startTime, duration: duration, stick: stick };
  3648. }
  3649. ;;
  3650. /* A component that renders a grid of whole-days that runs horizontally. There can be multiple rows, one per week.
  3651. ----------------------------------------------------------------------------------------------------------------------*/
  3652. var DayGrid = Grid.extend({
  3653. numbersVisible: false, // should render a row for day/week numbers? set by outside view. TODO: make internal
  3654. bottomCoordPadding: 0, // hack for extending the hit area for the last row of the coordinate grid
  3655. breakOnWeeks: null, // should create a new row for each week? set by outside view
  3656. cellDates: null, // flat chronological array of each cell's dates
  3657. dayToCellOffsets: null, // maps days offsets from grid's start date, to cell offsets
  3658. rowEls: null, // set of fake row elements
  3659. dayEls: null, // set of whole-day elements comprising the row's background
  3660. helperEls: null, // set of cell skeleton elements for rendering the mock event "helper"
  3661. constructor: function() {
  3662. Grid.apply(this, arguments);
  3663. this.cellDuration = moment.duration(1, 'day'); // for Grid system
  3664. },
  3665. // Renders the rows and columns into the component's `this.el`, which should already be assigned.
  3666. // isRigid determins whether the individual rows should ignore the contents and be a constant height.
  3667. // Relies on the view's colCnt and rowCnt. In the future, this component should probably be self-sufficient.
  3668. renderDates: function(isRigid) {
  3669. var view = this.view;
  3670. var rowCnt = this.rowCnt;
  3671. var colCnt = this.colCnt;
  3672. var cellCnt = rowCnt * colCnt;
  3673. var html = '';
  3674. var row;
  3675. var i, cell;
  3676. for (row = 0; row < rowCnt; row++) {
  3677. html += this.dayRowHtml(row, isRigid);
  3678. }
  3679. this.el.html(html);
  3680. this.rowEls = this.el.find('.fc-row');
  3681. this.dayEls = this.el.find('.fc-day');
  3682. // trigger dayRender with each cell's element
  3683. for (i = 0; i < cellCnt; i++) {
  3684. cell = this.getCell(i);
  3685. view.trigger('dayRender', null, cell.start, this.dayEls.eq(i));
  3686. }
  3687. },
  3688. unrenderDates: function() {
  3689. this.removeSegPopover();
  3690. },
  3691. renderBusinessHours: function() {
  3692. var events = this.view.calendar.getBusinessHoursEvents(true); // wholeDay=true
  3693. var segs = this.eventsToSegs(events);
  3694. this.renderFill('businessHours', segs, 'bgevent');
  3695. },
  3696. // Generates the HTML for a single row. `row` is the row number.
  3697. dayRowHtml: function(row, isRigid) {
  3698. var view = this.view;
  3699. var classes = [ 'fc-row', 'fc-week', view.widgetContentClass ];
  3700. if (isRigid) {
  3701. classes.push('fc-rigid');
  3702. }
  3703. return '' +
  3704. '<div class="' + classes.join(' ') + '">' +
  3705. '<div class="fc-bg">' +
  3706. '<table>' +
  3707. this.rowHtml('day', row) + // leverages RowRenderer. calls dayCellHtml()
  3708. '</table>' +
  3709. '</div>' +
  3710. '<div class="fc-content-skeleton">' +
  3711. '<table>' +
  3712. (this.numbersVisible ?
  3713. '<thead>' +
  3714. this.rowHtml('number', row) + // leverages RowRenderer. View will define render method
  3715. '</thead>' :
  3716. ''
  3717. ) +
  3718. '</table>' +
  3719. '</div>' +
  3720. '</div>';
  3721. },
  3722. // Renders the HTML for a whole-day cell. Will eventually end up in the day-row's background.
  3723. // We go through a 'day' row type instead of just doing a 'bg' row type so that the View can do custom rendering
  3724. // specifically for whole-day rows, whereas a 'bg' might also be used for other purposes (TimeGrid bg for example).
  3725. dayCellHtml: function(cell) {
  3726. return this.bgCellHtml(cell);
  3727. },
  3728. /* Options
  3729. ------------------------------------------------------------------------------------------------------------------*/
  3730. // Computes a default column header formatting string if `colFormat` is not explicitly defined
  3731. computeColHeadFormat: function() {
  3732. if (this.rowCnt > 1) { // more than one week row. day numbers will be in each cell
  3733. return 'ddd'; // "Sat"
  3734. }
  3735. else if (this.colCnt > 1) { // multiple days, so full single date string WON'T be in title text
  3736. return this.view.opt('dayOfMonthFormat'); // "Sat 12/10"
  3737. }
  3738. else { // single day, so full single date string will probably be in title text
  3739. return 'dddd'; // "Saturday"
  3740. }
  3741. },
  3742. // Computes a default event time formatting string if `timeFormat` is not explicitly defined
  3743. computeEventTimeFormat: function() {
  3744. return this.view.opt('extraSmallTimeFormat'); // like "6p" or "6:30p"
  3745. },
  3746. // Computes a default `displayEventEnd` value if one is not expliclty defined
  3747. computeDisplayEventEnd: function() {
  3748. return this.colCnt == 1; // we'll likely have space if there's only one day
  3749. },
  3750. /* Cell System
  3751. ------------------------------------------------------------------------------------------------------------------*/
  3752. rangeUpdated: function() {
  3753. var cellDates;
  3754. var firstDay;
  3755. var rowCnt;
  3756. var colCnt;
  3757. this.updateCellDates(); // populates cellDates and dayToCellOffsets
  3758. cellDates = this.cellDates;
  3759. if (this.breakOnWeeks) {
  3760. // count columns until the day-of-week repeats
  3761. firstDay = cellDates[0].day();
  3762. for (colCnt = 1; colCnt < cellDates.length; colCnt++) {
  3763. if (cellDates[colCnt].day() == firstDay) {
  3764. break;
  3765. }
  3766. }
  3767. rowCnt = Math.ceil(cellDates.length / colCnt);
  3768. }
  3769. else {
  3770. rowCnt = 1;
  3771. colCnt = cellDates.length;
  3772. }
  3773. this.rowCnt = rowCnt;
  3774. this.colCnt = colCnt;
  3775. },
  3776. // Populates cellDates and dayToCellOffsets
  3777. updateCellDates: function() {
  3778. var view = this.view;
  3779. var date = this.start.clone();
  3780. var dates = [];
  3781. var offset = -1;
  3782. var offsets = [];
  3783. while (date.isBefore(this.end)) { // loop each day from start to end
  3784. if (view.isHiddenDay(date)) {
  3785. offsets.push(offset + 0.5); // mark that it's between offsets
  3786. }
  3787. else {
  3788. offset++;
  3789. offsets.push(offset);
  3790. dates.push(date.clone());
  3791. }
  3792. date.add(1, 'days');
  3793. }
  3794. this.cellDates = dates;
  3795. this.dayToCellOffsets = offsets;
  3796. },
  3797. // Given a cell object, generates its start date. Returns a reference-free copy.
  3798. computeCellDate: function(cell) {
  3799. var colCnt = this.colCnt;
  3800. var index = cell.row * colCnt + (this.isRTL ? colCnt - cell.col - 1 : cell.col);
  3801. return this.cellDates[index].clone();
  3802. },
  3803. // Retrieves the element representing the given row
  3804. getRowEl: function(row) {
  3805. return this.rowEls.eq(row);
  3806. },
  3807. // Retrieves the element representing the given column
  3808. getColEl: function(col) {
  3809. return this.dayEls.eq(col);
  3810. },
  3811. // Gets the whole-day element associated with the cell
  3812. getCellDayEl: function(cell) {
  3813. return this.dayEls.eq(cell.row * this.colCnt + cell.col);
  3814. },
  3815. // Overrides Grid's method for when row coordinates are computed
  3816. computeRowCoords: function() {
  3817. var rowCoords = Grid.prototype.computeRowCoords.call(this); // call the super-method
  3818. // hack for extending last row (used by AgendaView)
  3819. rowCoords[rowCoords.length - 1].bottom += this.bottomCoordPadding;
  3820. return rowCoords;
  3821. },
  3822. /* Dates
  3823. ------------------------------------------------------------------------------------------------------------------*/
  3824. // Slices up a date range by row into an array of segments
  3825. rangeToSegs: function(range) {
  3826. var isRTL = this.isRTL;
  3827. var rowCnt = this.rowCnt;
  3828. var colCnt = this.colCnt;
  3829. var segs = [];
  3830. var first, last; // inclusive cell-offset range for given range
  3831. var row;
  3832. var rowFirst, rowLast; // inclusive cell-offset range for current row
  3833. var isStart, isEnd;
  3834. var segFirst, segLast; // inclusive cell-offset range for segment
  3835. var seg;
  3836. range = this.view.computeDayRange(range); // make whole-day range, considering nextDayThreshold
  3837. first = this.dateToCellOffset(range.start);
  3838. last = this.dateToCellOffset(range.end.subtract(1, 'days')); // offset of inclusive end date
  3839. for (row = 0; row < rowCnt; row++) {
  3840. rowFirst = row * colCnt;
  3841. rowLast = rowFirst + colCnt - 1;
  3842. // intersect segment's offset range with the row's
  3843. segFirst = Math.max(rowFirst, first);
  3844. segLast = Math.min(rowLast, last);
  3845. // deal with in-between indices
  3846. segFirst = Math.ceil(segFirst); // in-between starts round to next cell
  3847. segLast = Math.floor(segLast); // in-between ends round to prev cell
  3848. if (segFirst <= segLast) { // was there any intersection with the current row?
  3849. // must be matching integers to be the segment's start/end
  3850. isStart = segFirst === first;
  3851. isEnd = segLast === last;
  3852. // translate offsets to be relative to start-of-row
  3853. segFirst -= rowFirst;
  3854. segLast -= rowFirst;
  3855. seg = { row: row, isStart: isStart, isEnd: isEnd };
  3856. if (isRTL) {
  3857. seg.leftCol = colCnt - segLast - 1;
  3858. seg.rightCol = colCnt - segFirst - 1;
  3859. }
  3860. else {
  3861. seg.leftCol = segFirst;
  3862. seg.rightCol = segLast;
  3863. }
  3864. segs.push(seg);
  3865. }
  3866. }
  3867. return segs;
  3868. },
  3869. // Given a date, returns its chronolocial cell-offset from the first cell of the grid.
  3870. // If the date lies between cells (because of hiddenDays), returns a floating-point value between offsets.
  3871. // If before the first offset, returns a negative number.
  3872. // If after the last offset, returns an offset past the last cell offset.
  3873. // Only works for *start* dates of cells. Will not work for exclusive end dates for cells.
  3874. dateToCellOffset: function(date) {
  3875. var offsets = this.dayToCellOffsets;
  3876. var day = date.diff(this.start, 'days');
  3877. if (day < 0) {
  3878. return offsets[0] - 1;
  3879. }
  3880. else if (day >= offsets.length) {
  3881. return offsets[offsets.length - 1] + 1;
  3882. }
  3883. else {
  3884. return offsets[day];
  3885. }
  3886. },
  3887. /* Event Drag Visualization
  3888. ------------------------------------------------------------------------------------------------------------------*/
  3889. // TODO: move to DayGrid.event, similar to what we did with Grid's drag methods
  3890. // Renders a visual indication of an event or external element being dragged.
  3891. // The dropLocation's end can be null. seg can be null. See Grid::renderDrag for more info.
  3892. renderDrag: function(dropLocation, seg) {
  3893. // always render a highlight underneath
  3894. this.renderHighlight(this.eventRangeToSegs(dropLocation));
  3895. // if a segment from the same calendar but another component is being dragged, render a helper event
  3896. if (seg && !seg.el.closest(this.el).length) {
  3897. this.renderRangeHelper(dropLocation, seg);
  3898. this.applyDragOpacity(this.helperEls);
  3899. return true; // a helper has been rendered
  3900. }
  3901. },
  3902. // Unrenders any visual indication of a hovering event
  3903. unrenderDrag: function() {
  3904. this.unrenderHighlight();
  3905. this.unrenderHelper();
  3906. },
  3907. /* Event Resize Visualization
  3908. ------------------------------------------------------------------------------------------------------------------*/
  3909. // Renders a visual indication of an event being resized
  3910. renderEventResize: function(range, seg) {
  3911. this.renderHighlight(this.eventRangeToSegs(range));
  3912. this.renderRangeHelper(range, seg);
  3913. },
  3914. // Unrenders a visual indication of an event being resized
  3915. unrenderEventResize: function() {
  3916. this.unrenderHighlight();
  3917. this.unrenderHelper();
  3918. },
  3919. /* Event Helper
  3920. ------------------------------------------------------------------------------------------------------------------*/
  3921. // Renders a mock "helper" event. `sourceSeg` is the associated internal segment object. It can be null.
  3922. renderHelper: function(event, sourceSeg) {
  3923. var helperNodes = [];
  3924. var segs = this.eventsToSegs([ event ]);
  3925. var rowStructs;
  3926. segs = this.renderFgSegEls(segs); // assigns each seg's el and returns a subset of segs that were rendered
  3927. rowStructs = this.renderSegRows(segs);
  3928. // inject each new event skeleton into each associated row
  3929. this.rowEls.each(function(row, rowNode) {
  3930. var rowEl = $(rowNode); // the .fc-row
  3931. var skeletonEl = $('<div class="fc-helper-skeleton"><table/></div>'); // will be absolutely positioned
  3932. var skeletonTop;
  3933. // If there is an original segment, match the top position. Otherwise, put it at the row's top level
  3934. if (sourceSeg && sourceSeg.row === row) {
  3935. skeletonTop = sourceSeg.el.position().top;
  3936. }
  3937. else {
  3938. skeletonTop = rowEl.find('.fc-content-skeleton tbody').position().top;
  3939. }
  3940. skeletonEl.css('top', skeletonTop)
  3941. .find('table')
  3942. .append(rowStructs[row].tbodyEl);
  3943. rowEl.append(skeletonEl);
  3944. helperNodes.push(skeletonEl[0]);
  3945. });
  3946. this.helperEls = $(helperNodes); // array -> jQuery set
  3947. },
  3948. // Unrenders any visual indication of a mock helper event
  3949. unrenderHelper: function() {
  3950. if (this.helperEls) {
  3951. this.helperEls.remove();
  3952. this.helperEls = null;
  3953. }
  3954. },
  3955. /* Fill System (highlight, background events, business hours)
  3956. ------------------------------------------------------------------------------------------------------------------*/
  3957. fillSegTag: 'td', // override the default tag name
  3958. // Renders a set of rectangles over the given segments of days.
  3959. // Only returns segments that successfully rendered.
  3960. renderFill: function(type, segs, className) {
  3961. var nodes = [];
  3962. var i, seg;
  3963. var skeletonEl;
  3964. segs = this.renderFillSegEls(type, segs); // assignes `.el` to each seg. returns successfully rendered segs
  3965. for (i = 0; i < segs.length; i++) {
  3966. seg = segs[i];
  3967. skeletonEl = this.renderFillRow(type, seg, className);
  3968. this.rowEls.eq(seg.row).append(skeletonEl);
  3969. nodes.push(skeletonEl[0]);
  3970. }
  3971. this.elsByFill[type] = $(nodes);
  3972. return segs;
  3973. },
  3974. // Generates the HTML needed for one row of a fill. Requires the seg's el to be rendered.
  3975. renderFillRow: function(type, seg, className) {
  3976. var colCnt = this.colCnt;
  3977. var startCol = seg.leftCol;
  3978. var endCol = seg.rightCol + 1;
  3979. var skeletonEl;
  3980. var trEl;
  3981. className = className || type.toLowerCase();
  3982. skeletonEl = $(
  3983. '<div class="fc-' + className + '-skeleton">' +
  3984. '<table><tr/></table>' +
  3985. '</div>'
  3986. );
  3987. trEl = skeletonEl.find('tr');
  3988. if (startCol > 0) {
  3989. trEl.append('<td colspan="' + startCol + '"/>');
  3990. }
  3991. trEl.append(
  3992. seg.el.attr('colspan', endCol - startCol)
  3993. );
  3994. if (endCol < colCnt) {
  3995. trEl.append('<td colspan="' + (colCnt - endCol) + '"/>');
  3996. }
  3997. this.bookendCells(trEl, type);
  3998. return skeletonEl;
  3999. }
  4000. });
  4001. ;;
  4002. /* Event-rendering methods for the DayGrid class
  4003. ----------------------------------------------------------------------------------------------------------------------*/
  4004. DayGrid.mixin({
  4005. rowStructs: null, // an array of objects, each holding information about a row's foreground event-rendering
  4006. // Unrenders all events currently rendered on the grid
  4007. unrenderEvents: function() {
  4008. this.removeSegPopover(); // removes the "more.." events popover
  4009. Grid.prototype.unrenderEvents.apply(this, arguments); // calls the super-method
  4010. },
  4011. // Retrieves all rendered segment objects currently rendered on the grid
  4012. getEventSegs: function() {
  4013. return Grid.prototype.getEventSegs.call(this) // get the segments from the super-method
  4014. .concat(this.popoverSegs || []); // append the segments from the "more..." popover
  4015. },
  4016. // Renders the given background event segments onto the grid
  4017. renderBgSegs: function(segs) {
  4018. // don't render timed background events
  4019. var allDaySegs = $.grep(segs, function(seg) {
  4020. return seg.event.allDay;
  4021. });
  4022. return Grid.prototype.renderBgSegs.call(this, allDaySegs); // call the super-method
  4023. },
  4024. // Renders the given foreground event segments onto the grid
  4025. renderFgSegs: function(segs) {
  4026. var rowStructs;
  4027. // render an `.el` on each seg
  4028. // returns a subset of the segs. segs that were actually rendered
  4029. segs = this.renderFgSegEls(segs);
  4030. rowStructs = this.rowStructs = this.renderSegRows(segs);
  4031. // append to each row's content skeleton
  4032. this.rowEls.each(function(i, rowNode) {
  4033. $(rowNode).find('.fc-content-skeleton > table').append(
  4034. rowStructs[i].tbodyEl
  4035. );
  4036. });
  4037. return segs; // return only the segs that were actually rendered
  4038. },
  4039. // Unrenders all currently rendered foreground event segments
  4040. unrenderFgSegs: function() {
  4041. var rowStructs = this.rowStructs || [];
  4042. var rowStruct;
  4043. while ((rowStruct = rowStructs.pop())) {
  4044. rowStruct.tbodyEl.remove();
  4045. }
  4046. this.rowStructs = null;
  4047. },
  4048. // Uses the given events array to generate <tbody> elements that should be appended to each row's content skeleton.
  4049. // Returns an array of rowStruct objects (see the bottom of `renderSegRow`).
  4050. // PRECONDITION: each segment shoud already have a rendered and assigned `.el`
  4051. renderSegRows: function(segs) {
  4052. var rowStructs = [];
  4053. var segRows;
  4054. var row;
  4055. segRows = this.groupSegRows(segs); // group into nested arrays
  4056. // iterate each row of segment groupings
  4057. for (row = 0; row < segRows.length; row++) {
  4058. rowStructs.push(
  4059. this.renderSegRow(row, segRows[row])
  4060. );
  4061. }
  4062. return rowStructs;
  4063. },
  4064. // Builds the HTML to be used for the default element for an individual segment
  4065. fgSegHtml: function(seg, disableResizing) {
  4066. var view = this.view;
  4067. var event = seg.event;
  4068. var isDraggable = view.isEventDraggable(event);
  4069. var isResizableFromStart = !disableResizing && event.allDay &&
  4070. seg.isStart && view.isEventResizableFromStart(event);
  4071. var isResizableFromEnd = !disableResizing && event.allDay &&
  4072. seg.isEnd && view.isEventResizableFromEnd(event);
  4073. var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd);
  4074. var skinCss = cssToStr(this.getEventSkinCss(event));
  4075. var timeHtml = '';
  4076. var timeText;
  4077. var titleHtml;
  4078. classes.unshift('fc-day-grid-event', 'fc-h-event');
  4079. // Only display a timed events time if it is the starting segment
  4080. if (seg.isStart) {
  4081. timeText = this.getEventTimeText(event);
  4082. if (timeText) {
  4083. timeHtml = '<span class="fc-time">' + htmlEscape(timeText) + '</span>';
  4084. }
  4085. }
  4086. titleHtml =
  4087. '<span class="fc-title">' +
  4088. (htmlEscape(event.title || '') || '&nbsp;') + // we always want one line of height
  4089. '</span>';
  4090. return '<a class="' + classes.join(' ') + '"' +
  4091. (event.url ?
  4092. ' href="' + htmlEscape(event.url) + '"' :
  4093. ''
  4094. ) +
  4095. (skinCss ?
  4096. ' style="' + skinCss + '"' :
  4097. ''
  4098. ) +
  4099. '>' +
  4100. '<div class="fc-content">' +
  4101. (this.isRTL ?
  4102. titleHtml + ' ' + timeHtml : // put a natural space in between
  4103. timeHtml + ' ' + titleHtml //
  4104. ) +
  4105. '</div>' +
  4106. (isResizableFromStart ?
  4107. '<div class="fc-resizer fc-start-resizer" />' :
  4108. ''
  4109. ) +
  4110. (isResizableFromEnd ?
  4111. '<div class="fc-resizer fc-end-resizer" />' :
  4112. ''
  4113. ) +
  4114. '</a>';
  4115. },
  4116. // Given a row # and an array of segments all in the same row, render a <tbody> element, a skeleton that contains
  4117. // the segments. Returns object with a bunch of internal data about how the render was calculated.
  4118. // NOTE: modifies rowSegs
  4119. renderSegRow: function(row, rowSegs) {
  4120. var colCnt = this.colCnt;
  4121. var segLevels = this.buildSegLevels(rowSegs); // group into sub-arrays of levels
  4122. var levelCnt = Math.max(1, segLevels.length); // ensure at least one level
  4123. var tbody = $('<tbody/>');
  4124. var segMatrix = []; // lookup for which segments are rendered into which level+col cells
  4125. var cellMatrix = []; // lookup for all <td> elements of the level+col matrix
  4126. var loneCellMatrix = []; // lookup for <td> elements that only take up a single column
  4127. var i, levelSegs;
  4128. var col;
  4129. var tr;
  4130. var j, seg;
  4131. var td;
  4132. // populates empty cells from the current column (`col`) to `endCol`
  4133. function emptyCellsUntil(endCol) {
  4134. while (col < endCol) {
  4135. // try to grab a cell from the level above and extend its rowspan. otherwise, create a fresh cell
  4136. td = (loneCellMatrix[i - 1] || [])[col];
  4137. if (td) {
  4138. td.attr(
  4139. 'rowspan',
  4140. parseInt(td.attr('rowspan') || 1, 10) + 1
  4141. );
  4142. }
  4143. else {
  4144. td = $('<td/>');
  4145. tr.append(td);
  4146. }
  4147. cellMatrix[i][col] = td;
  4148. loneCellMatrix[i][col] = td;
  4149. col++;
  4150. }
  4151. }
  4152. for (i = 0; i < levelCnt; i++) { // iterate through all levels
  4153. levelSegs = segLevels[i];
  4154. col = 0;
  4155. tr = $('<tr/>');
  4156. segMatrix.push([]);
  4157. cellMatrix.push([]);
  4158. loneCellMatrix.push([]);
  4159. // levelCnt might be 1 even though there are no actual levels. protect against this.
  4160. // this single empty row is useful for styling.
  4161. if (levelSegs) {
  4162. for (j = 0; j < levelSegs.length; j++) { // iterate through segments in level
  4163. seg = levelSegs[j];
  4164. emptyCellsUntil(seg.leftCol);
  4165. // create a container that occupies or more columns. append the event element.
  4166. td = $('<td class="fc-event-container"/>').append(seg.el);
  4167. if (seg.leftCol != seg.rightCol) {
  4168. td.attr('colspan', seg.rightCol - seg.leftCol + 1);
  4169. }
  4170. else { // a single-column segment
  4171. loneCellMatrix[i][col] = td;
  4172. }
  4173. while (col <= seg.rightCol) {
  4174. cellMatrix[i][col] = td;
  4175. segMatrix[i][col] = seg;
  4176. col++;
  4177. }
  4178. tr.append(td);
  4179. }
  4180. }
  4181. emptyCellsUntil(colCnt); // finish off the row
  4182. this.bookendCells(tr, 'eventSkeleton');
  4183. tbody.append(tr);
  4184. }
  4185. return { // a "rowStruct"
  4186. row: row, // the row number
  4187. tbodyEl: tbody,
  4188. cellMatrix: cellMatrix,
  4189. segMatrix: segMatrix,
  4190. segLevels: segLevels,
  4191. segs: rowSegs
  4192. };
  4193. },
  4194. // Stacks a flat array of segments, which are all assumed to be in the same row, into subarrays of vertical levels.
  4195. // NOTE: modifies segs
  4196. buildSegLevels: function(segs) {
  4197. var levels = [];
  4198. var i, seg;
  4199. var j;
  4200. // Give preference to elements with certain criteria, so they have
  4201. // a chance to be closer to the top.
  4202. this.sortSegs(segs);
  4203. for (i = 0; i < segs.length; i++) {
  4204. seg = segs[i];
  4205. // loop through levels, starting with the topmost, until the segment doesn't collide with other segments
  4206. for (j = 0; j < levels.length; j++) {
  4207. if (!isDaySegCollision(seg, levels[j])) {
  4208. break;
  4209. }
  4210. }
  4211. // `j` now holds the desired subrow index
  4212. seg.level = j;
  4213. // create new level array if needed and append segment
  4214. (levels[j] || (levels[j] = [])).push(seg);
  4215. }
  4216. // order segments left-to-right. very important if calendar is RTL
  4217. for (j = 0; j < levels.length; j++) {
  4218. levels[j].sort(compareDaySegCols);
  4219. }
  4220. return levels;
  4221. },
  4222. // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's row
  4223. groupSegRows: function(segs) {
  4224. var segRows = [];
  4225. var i;
  4226. for (i = 0; i < this.rowCnt; i++) {
  4227. segRows.push([]);
  4228. }
  4229. for (i = 0; i < segs.length; i++) {
  4230. segRows[segs[i].row].push(segs[i]);
  4231. }
  4232. return segRows;
  4233. }
  4234. });
  4235. // Computes whether two segments' columns collide. They are assumed to be in the same row.
  4236. function isDaySegCollision(seg, otherSegs) {
  4237. var i, otherSeg;
  4238. for (i = 0; i < otherSegs.length; i++) {
  4239. otherSeg = otherSegs[i];
  4240. if (
  4241. otherSeg.leftCol <= seg.rightCol &&
  4242. otherSeg.rightCol >= seg.leftCol
  4243. ) {
  4244. return true;
  4245. }
  4246. }
  4247. return false;
  4248. }
  4249. // A cmp function for determining the leftmost event
  4250. function compareDaySegCols(a, b) {
  4251. return a.leftCol - b.leftCol;
  4252. }
  4253. ;;
  4254. /* Methods relate to limiting the number events for a given day on a DayGrid
  4255. ----------------------------------------------------------------------------------------------------------------------*/
  4256. // NOTE: all the segs being passed around in here are foreground segs
  4257. DayGrid.mixin({
  4258. segPopover: null, // the Popover that holds events that can't fit in a cell. null when not visible
  4259. popoverSegs: null, // an array of segment objects that the segPopover holds. null when not visible
  4260. removeSegPopover: function() {
  4261. if (this.segPopover) {
  4262. this.segPopover.hide(); // in handler, will call segPopover's removeElement
  4263. }
  4264. },
  4265. // Limits the number of "levels" (vertically stacking layers of events) for each row of the grid.
  4266. // `levelLimit` can be false (don't limit), a number, or true (should be computed).
  4267. limitRows: function(levelLimit) {
  4268. var rowStructs = this.rowStructs || [];
  4269. var row; // row #
  4270. var rowLevelLimit;
  4271. for (row = 0; row < rowStructs.length; row++) {
  4272. this.unlimitRow(row);
  4273. if (!levelLimit) {
  4274. rowLevelLimit = false;
  4275. }
  4276. else if (typeof levelLimit === 'number') {
  4277. rowLevelLimit = levelLimit;
  4278. }
  4279. else {
  4280. rowLevelLimit = this.computeRowLevelLimit(row);
  4281. }
  4282. if (rowLevelLimit !== false) {
  4283. this.limitRow(row, rowLevelLimit);
  4284. }
  4285. }
  4286. },
  4287. // Computes the number of levels a row will accomodate without going outside its bounds.
  4288. // Assumes the row is "rigid" (maintains a constant height regardless of what is inside).
  4289. // `row` is the row number.
  4290. computeRowLevelLimit: function(row) {
  4291. var rowEl = this.rowEls.eq(row); // the containing "fake" row div
  4292. var rowHeight = rowEl.height(); // TODO: cache somehow?
  4293. var trEls = this.rowStructs[row].tbodyEl.children();
  4294. var i, trEl;
  4295. var trHeight;
  4296. function iterInnerHeights(i, childNode) {
  4297. trHeight = Math.max(trHeight, $(childNode).outerHeight());
  4298. }
  4299. // Reveal one level <tr> at a time and stop when we find one out of bounds
  4300. for (i = 0; i < trEls.length; i++) {
  4301. trEl = trEls.eq(i).removeClass('fc-limited'); // reset to original state (reveal)
  4302. // with rowspans>1 and IE8, trEl.outerHeight() would return the height of the largest cell,
  4303. // so instead, find the tallest inner content element.
  4304. trHeight = 0;
  4305. trEl.find('> td > :first-child').each(iterInnerHeights);
  4306. if (trEl.position().top + trHeight > rowHeight) {
  4307. return i;
  4308. }
  4309. }
  4310. return false; // should not limit at all
  4311. },
  4312. // Limits the given grid row to the maximum number of levels and injects "more" links if necessary.
  4313. // `row` is the row number.
  4314. // `levelLimit` is a number for the maximum (inclusive) number of levels allowed.
  4315. limitRow: function(row, levelLimit) {
  4316. var _this = this;
  4317. var rowStruct = this.rowStructs[row];
  4318. var moreNodes = []; // array of "more" <a> links and <td> DOM nodes
  4319. var col = 0; // col #, left-to-right (not chronologically)
  4320. var cell;
  4321. var levelSegs; // array of segment objects in the last allowable level, ordered left-to-right
  4322. var cellMatrix; // a matrix (by level, then column) of all <td> jQuery elements in the row
  4323. var limitedNodes; // array of temporarily hidden level <tr> and segment <td> DOM nodes
  4324. var i, seg;
  4325. var segsBelow; // array of segment objects below `seg` in the current `col`
  4326. var totalSegsBelow; // total number of segments below `seg` in any of the columns `seg` occupies
  4327. var colSegsBelow; // array of segment arrays, below seg, one for each column (offset from segs's first column)
  4328. var td, rowspan;
  4329. var segMoreNodes; // array of "more" <td> cells that will stand-in for the current seg's cell
  4330. var j;
  4331. var moreTd, moreWrap, moreLink;
  4332. // Iterates through empty level cells and places "more" links inside if need be
  4333. function emptyCellsUntil(endCol) { // goes from current `col` to `endCol`
  4334. while (col < endCol) {
  4335. cell = _this.getCell(row, col);
  4336. segsBelow = _this.getCellSegs(cell, levelLimit);
  4337. if (segsBelow.length) {
  4338. td = cellMatrix[levelLimit - 1][col];
  4339. moreLink = _this.renderMoreLink(cell, segsBelow);
  4340. moreWrap = $('<div/>').append(moreLink);
  4341. td.append(moreWrap);
  4342. moreNodes.push(moreWrap[0]);
  4343. }
  4344. col++;
  4345. }
  4346. }
  4347. if (levelLimit && levelLimit < rowStruct.segLevels.length) { // is it actually over the limit?
  4348. levelSegs = rowStruct.segLevels[levelLimit - 1];
  4349. cellMatrix = rowStruct.cellMatrix;
  4350. limitedNodes = rowStruct.tbodyEl.children().slice(levelLimit) // get level <tr> elements past the limit
  4351. .addClass('fc-limited').get(); // hide elements and get a simple DOM-nodes array
  4352. // iterate though segments in the last allowable level
  4353. for (i = 0; i < levelSegs.length; i++) {
  4354. seg = levelSegs[i];
  4355. emptyCellsUntil(seg.leftCol); // process empty cells before the segment
  4356. // determine *all* segments below `seg` that occupy the same columns
  4357. colSegsBelow = [];
  4358. totalSegsBelow = 0;
  4359. while (col <= seg.rightCol) {
  4360. cell = this.getCell(row, col);
  4361. segsBelow = this.getCellSegs(cell, levelLimit);
  4362. colSegsBelow.push(segsBelow);
  4363. totalSegsBelow += segsBelow.length;
  4364. col++;
  4365. }
  4366. if (totalSegsBelow) { // do we need to replace this segment with one or many "more" links?
  4367. td = cellMatrix[levelLimit - 1][seg.leftCol]; // the segment's parent cell
  4368. rowspan = td.attr('rowspan') || 1;
  4369. segMoreNodes = [];
  4370. // make a replacement <td> for each column the segment occupies. will be one for each colspan
  4371. for (j = 0; j < colSegsBelow.length; j++) {
  4372. moreTd = $('<td class="fc-more-cell"/>').attr('rowspan', rowspan);
  4373. segsBelow = colSegsBelow[j];
  4374. cell = this.getCell(row, seg.leftCol + j);
  4375. moreLink = this.renderMoreLink(cell, [ seg ].concat(segsBelow)); // count seg as hidden too
  4376. moreWrap = $('<div/>').append(moreLink);
  4377. moreTd.append(moreWrap);
  4378. segMoreNodes.push(moreTd[0]);
  4379. moreNodes.push(moreTd[0]);
  4380. }
  4381. td.addClass('fc-limited').after($(segMoreNodes)); // hide original <td> and inject replacements
  4382. limitedNodes.push(td[0]);
  4383. }
  4384. }
  4385. emptyCellsUntil(this.colCnt); // finish off the level
  4386. rowStruct.moreEls = $(moreNodes); // for easy undoing later
  4387. rowStruct.limitedEls = $(limitedNodes); // for easy undoing later
  4388. }
  4389. },
  4390. // Reveals all levels and removes all "more"-related elements for a grid's row.
  4391. // `row` is a row number.
  4392. unlimitRow: function(row) {
  4393. var rowStruct = this.rowStructs[row];
  4394. if (rowStruct.moreEls) {
  4395. rowStruct.moreEls.remove();
  4396. rowStruct.moreEls = null;
  4397. }
  4398. if (rowStruct.limitedEls) {
  4399. rowStruct.limitedEls.removeClass('fc-limited');
  4400. rowStruct.limitedEls = null;
  4401. }
  4402. },
  4403. // Renders an <a> element that represents hidden event element for a cell.
  4404. // Responsible for attaching click handler as well.
  4405. renderMoreLink: function(cell, hiddenSegs) {
  4406. var _this = this;
  4407. var view = this.view;
  4408. return $('<a class="fc-more"/>')
  4409. .text(
  4410. this.getMoreLinkText(hiddenSegs.length)
  4411. )
  4412. .on('click', function(ev) {
  4413. var clickOption = view.opt('eventLimitClick');
  4414. var date = cell.start;
  4415. var moreEl = $(this);
  4416. var dayEl = _this.getCellDayEl(cell);
  4417. var allSegs = _this.getCellSegs(cell);
  4418. // rescope the segments to be within the cell's date
  4419. var reslicedAllSegs = _this.resliceDaySegs(allSegs, date);
  4420. var reslicedHiddenSegs = _this.resliceDaySegs(hiddenSegs, date);
  4421. if (typeof clickOption === 'function') {
  4422. // the returned value can be an atomic option
  4423. clickOption = view.trigger('eventLimitClick', null, {
  4424. date: date,
  4425. dayEl: dayEl,
  4426. moreEl: moreEl,
  4427. segs: reslicedAllSegs,
  4428. hiddenSegs: reslicedHiddenSegs
  4429. }, ev);
  4430. }
  4431. if (clickOption === 'popover') {
  4432. _this.showSegPopover(cell, moreEl, reslicedAllSegs);
  4433. }
  4434. else if (typeof clickOption === 'string') { // a view name
  4435. view.calendar.zoomTo(date, clickOption);
  4436. }
  4437. });
  4438. },
  4439. // Reveals the popover that displays all events within a cell
  4440. showSegPopover: function(cell, moreLink, segs) {
  4441. var _this = this;
  4442. var view = this.view;
  4443. var moreWrap = moreLink.parent(); // the <div> wrapper around the <a>
  4444. var topEl; // the element we want to match the top coordinate of
  4445. var options;
  4446. if (this.rowCnt == 1) {
  4447. topEl = view.el; // will cause the popover to cover any sort of header
  4448. }
  4449. else {
  4450. topEl = this.rowEls.eq(cell.row); // will align with top of row
  4451. }
  4452. options = {
  4453. className: 'fc-more-popover',
  4454. content: this.renderSegPopoverContent(cell, segs),
  4455. parentEl: this.el,
  4456. top: topEl.offset().top,
  4457. autoHide: true, // when the user clicks elsewhere, hide the popover
  4458. viewportConstrain: view.opt('popoverViewportConstrain'),
  4459. hide: function() {
  4460. // kill everything when the popover is hidden
  4461. _this.segPopover.removeElement();
  4462. _this.segPopover = null;
  4463. _this.popoverSegs = null;
  4464. }
  4465. };
  4466. // Determine horizontal coordinate.
  4467. // We use the moreWrap instead of the <td> to avoid border confusion.
  4468. if (this.isRTL) {
  4469. options.right = moreWrap.offset().left + moreWrap.outerWidth() + 1; // +1 to be over cell border
  4470. }
  4471. else {
  4472. options.left = moreWrap.offset().left - 1; // -1 to be over cell border
  4473. }
  4474. this.segPopover = new Popover(options);
  4475. this.segPopover.show();
  4476. },
  4477. // Builds the inner DOM contents of the segment popover
  4478. renderSegPopoverContent: function(cell, segs) {
  4479. var view = this.view;
  4480. var isTheme = view.opt('theme');
  4481. var title = cell.start.format(view.opt('dayPopoverFormat'));
  4482. var content = $(
  4483. '<div class="fc-header ' + view.widgetHeaderClass + '">' +
  4484. '<span class="fc-close ' +
  4485. (isTheme ? 'ui-icon ui-icon-closethick' : 'fc-icon fc-icon-x') +
  4486. '"></span>' +
  4487. '<span class="fc-title">' +
  4488. htmlEscape(title) +
  4489. '</span>' +
  4490. '<div class="fc-clear"/>' +
  4491. '</div>' +
  4492. '<div class="fc-body ' + view.widgetContentClass + '">' +
  4493. '<div class="fc-event-container"></div>' +
  4494. '</div>'
  4495. );
  4496. var segContainer = content.find('.fc-event-container');
  4497. var i;
  4498. // render each seg's `el` and only return the visible segs
  4499. segs = this.renderFgSegEls(segs, true); // disableResizing=true
  4500. this.popoverSegs = segs;
  4501. for (i = 0; i < segs.length; i++) {
  4502. // because segments in the popover are not part of a grid coordinate system, provide a hint to any
  4503. // grids that want to do drag-n-drop about which cell it came from
  4504. segs[i].cell = cell;
  4505. segContainer.append(segs[i].el);
  4506. }
  4507. return content;
  4508. },
  4509. // Given the events within an array of segment objects, reslice them to be in a single day
  4510. resliceDaySegs: function(segs, dayDate) {
  4511. // build an array of the original events
  4512. var events = $.map(segs, function(seg) {
  4513. return seg.event;
  4514. });
  4515. var dayStart = dayDate.clone().stripTime();
  4516. var dayEnd = dayStart.clone().add(1, 'days');
  4517. var dayRange = { start: dayStart, end: dayEnd };
  4518. // slice the events with a custom slicing function
  4519. segs = this.eventsToSegs(
  4520. events,
  4521. function(range) {
  4522. var seg = intersectionToSeg(range, dayRange); // undefind if no intersection
  4523. return seg ? [ seg ] : []; // must return an array of segments
  4524. }
  4525. );
  4526. // force an order because eventsToSegs doesn't guarantee one
  4527. this.sortSegs(segs);
  4528. return segs;
  4529. },
  4530. // Generates the text that should be inside a "more" link, given the number of events it represents
  4531. getMoreLinkText: function(num) {
  4532. var opt = this.view.opt('eventLimitText');
  4533. if (typeof opt === 'function') {
  4534. return opt(num);
  4535. }
  4536. else {
  4537. return '+' + num + ' ' + opt;
  4538. }
  4539. },
  4540. // Returns segments within a given cell.
  4541. // If `startLevel` is specified, returns only events including and below that level. Otherwise returns all segs.
  4542. getCellSegs: function(cell, startLevel) {
  4543. var segMatrix = this.rowStructs[cell.row].segMatrix;
  4544. var level = startLevel || 0;
  4545. var segs = [];
  4546. var seg;
  4547. while (level < segMatrix.length) {
  4548. seg = segMatrix[level][cell.col];
  4549. if (seg) {
  4550. segs.push(seg);
  4551. }
  4552. level++;
  4553. }
  4554. return segs;
  4555. }
  4556. });
  4557. ;;
  4558. /* A component that renders one or more columns of vertical time slots
  4559. ----------------------------------------------------------------------------------------------------------------------*/
  4560. var TimeGrid = Grid.extend({
  4561. slotDuration: null, // duration of a "slot", a distinct time segment on given day, visualized by lines
  4562. snapDuration: null, // granularity of time for dragging and selecting
  4563. minTime: null, // Duration object that denotes the first visible time of any given day
  4564. maxTime: null, // Duration object that denotes the exclusive visible end time of any given day
  4565. colDates: null, // whole-day dates for each column. left to right
  4566. labelFormat: null, // formatting string for times running along vertical axis
  4567. labelInterval: null, // duration of how often a label should be displayed for a slot
  4568. dayEls: null, // cells elements in the day-row background
  4569. slatEls: null, // elements running horizontally across all columns
  4570. slatTops: null, // an array of top positions, relative to the container. last item holds bottom of last slot
  4571. helperEl: null, // cell skeleton element for rendering the mock event "helper"
  4572. businessHourSegs: null,
  4573. constructor: function() {
  4574. Grid.apply(this, arguments); // call the super-constructor
  4575. this.processOptions();
  4576. },
  4577. // Renders the time grid into `this.el`, which should already be assigned.
  4578. // Relies on the view's colCnt. In the future, this component should probably be self-sufficient.
  4579. renderDates: function() {
  4580. this.el.html(this.renderHtml());
  4581. this.dayEls = this.el.find('.fc-day');
  4582. this.slatEls = this.el.find('.fc-slats tr');
  4583. },
  4584. renderBusinessHours: function() {
  4585. var events = this.view.calendar.getBusinessHoursEvents();
  4586. this.businessHourSegs = this.renderFill('businessHours', this.eventsToSegs(events), 'bgevent');
  4587. },
  4588. // Renders the basic HTML skeleton for the grid
  4589. renderHtml: function() {
  4590. return '' +
  4591. '<div class="fc-bg">' +
  4592. '<table>' +
  4593. this.rowHtml('slotBg') + // leverages RowRenderer, which will call slotBgCellHtml
  4594. '</table>' +
  4595. '</div>' +
  4596. '<div class="fc-slats">' +
  4597. '<table>' +
  4598. this.slatRowHtml() +
  4599. '</table>' +
  4600. '</div>';
  4601. },
  4602. // Renders the HTML for a vertical background cell behind the slots.
  4603. // This method is distinct from 'bg' because we wanted a new `rowType` so the View could customize the rendering.
  4604. slotBgCellHtml: function(cell) {
  4605. return this.bgCellHtml(cell);
  4606. },
  4607. // Generates the HTML for the horizontal "slats" that run width-wise. Has a time axis on a side. Depends on RTL.
  4608. slatRowHtml: function() {
  4609. var view = this.view;
  4610. var isRTL = this.isRTL;
  4611. var html = '';
  4612. var slotTime = moment.duration(+this.minTime); // wish there was .clone() for durations
  4613. var slotDate; // will be on the view's first day, but we only care about its time
  4614. var isLabeled;
  4615. var axisHtml;
  4616. // Calculate the time for each slot
  4617. while (slotTime < this.maxTime) {
  4618. slotDate = this.start.clone().time(slotTime); // after .time() will be in UTC. but that's good, avoids DST issues
  4619. isLabeled = isInt(divideDurationByDuration(slotTime, this.labelInterval));
  4620. axisHtml =
  4621. '<td class="fc-axis fc-time ' + view.widgetContentClass + '" ' + view.axisStyleAttr() + '>' +
  4622. (isLabeled ?
  4623. '<span>' + // for matchCellWidths
  4624. htmlEscape(slotDate.format(this.labelFormat)) +
  4625. '</span>' :
  4626. ''
  4627. ) +
  4628. '</td>';
  4629. html +=
  4630. '<tr ' + (isLabeled ? '' : 'class="fc-minor"') + '>' +
  4631. (!isRTL ? axisHtml : '') +
  4632. '<td class="' + view.widgetContentClass + '"/>' +
  4633. (isRTL ? axisHtml : '') +
  4634. "</tr>";
  4635. slotTime.add(this.slotDuration);
  4636. }
  4637. return html;
  4638. },
  4639. /* Options
  4640. ------------------------------------------------------------------------------------------------------------------*/
  4641. // Parses various options into properties of this object
  4642. processOptions: function() {
  4643. var view = this.view;
  4644. var slotDuration = view.opt('slotDuration');
  4645. var snapDuration = view.opt('snapDuration');
  4646. var input;
  4647. slotDuration = moment.duration(slotDuration);
  4648. snapDuration = snapDuration ? moment.duration(snapDuration) : slotDuration;
  4649. this.slotDuration = slotDuration;
  4650. this.snapDuration = snapDuration;
  4651. this.cellDuration = snapDuration; // for Grid system
  4652. this.minTime = moment.duration(view.opt('minTime'));
  4653. this.maxTime = moment.duration(view.opt('maxTime'));
  4654. // might be an array value (for TimelineView).
  4655. // if so, getting the most granular entry (the last one probably).
  4656. input = view.opt('slotLabelFormat');
  4657. if ($.isArray(input)) {
  4658. input = input[input.length - 1];
  4659. }
  4660. this.labelFormat =
  4661. input ||
  4662. view.opt('axisFormat') || // deprecated
  4663. view.opt('smallTimeFormat'); // the computed default
  4664. input = view.opt('slotLabelInterval');
  4665. this.labelInterval = input ?
  4666. moment.duration(input) :
  4667. this.computeLabelInterval(slotDuration);
  4668. },
  4669. // Computes an automatic value for slotLabelInterval
  4670. computeLabelInterval: function(slotDuration) {
  4671. var i;
  4672. var labelInterval;
  4673. var slotsPerLabel;
  4674. // find the smallest stock label interval that results in more than one slots-per-label
  4675. for (i = AGENDA_STOCK_SUB_DURATIONS.length - 1; i >= 0; i--) {
  4676. labelInterval = moment.duration(AGENDA_STOCK_SUB_DURATIONS[i]);
  4677. slotsPerLabel = divideDurationByDuration(labelInterval, slotDuration);
  4678. if (isInt(slotsPerLabel) && slotsPerLabel > 1) {
  4679. return labelInterval;
  4680. }
  4681. }
  4682. return moment.duration(slotDuration); // fall back. clone
  4683. },
  4684. // Computes a default column header formatting string if `colFormat` is not explicitly defined
  4685. computeColHeadFormat: function() {
  4686. if (this.colCnt > 1) { // multiple days, so full single date string WON'T be in title text
  4687. return this.view.opt('dayOfMonthFormat'); // "Sat 12/10"
  4688. }
  4689. else { // single day, so full single date string will probably be in title text
  4690. return 'dddd'; // "Saturday"
  4691. }
  4692. },
  4693. // Computes a default event time formatting string if `timeFormat` is not explicitly defined
  4694. computeEventTimeFormat: function() {
  4695. return this.view.opt('noMeridiemTimeFormat'); // like "6:30" (no AM/PM)
  4696. },
  4697. // Computes a default `displayEventEnd` value if one is not expliclty defined
  4698. computeDisplayEventEnd: function() {
  4699. return true;
  4700. },
  4701. /* Cell System
  4702. ------------------------------------------------------------------------------------------------------------------*/
  4703. rangeUpdated: function() {
  4704. var view = this.view;
  4705. var colDates = [];
  4706. var date;
  4707. date = this.start.clone();
  4708. while (date.isBefore(this.end)) {
  4709. colDates.push(date.clone());
  4710. date.add(1, 'day');
  4711. date = view.skipHiddenDays(date);
  4712. }
  4713. if (this.isRTL) {
  4714. colDates.reverse();
  4715. }
  4716. this.colDates = colDates;
  4717. this.colCnt = colDates.length;
  4718. this.rowCnt = Math.ceil((this.maxTime - this.minTime) / this.snapDuration); // # of vertical snaps
  4719. },
  4720. // Given a cell object, generates its start date. Returns a reference-free copy.
  4721. computeCellDate: function(cell) {
  4722. var date = this.colDates[cell.col];
  4723. var time = this.computeSnapTime(cell.row);
  4724. date = this.view.calendar.rezoneDate(date); // give it a 00:00 time
  4725. date.time(time);
  4726. return date;
  4727. },
  4728. // Retrieves the element representing the given column
  4729. getColEl: function(col) {
  4730. return this.dayEls.eq(col);
  4731. },
  4732. /* Dates
  4733. ------------------------------------------------------------------------------------------------------------------*/
  4734. // Given a row number of the grid, representing a "snap", returns a time (Duration) from its start-of-day
  4735. computeSnapTime: function(row) {
  4736. return moment.duration(this.minTime + this.snapDuration * row);
  4737. },
  4738. // Slices up a date range by column into an array of segments
  4739. rangeToSegs: function(range) {
  4740. var colCnt = this.colCnt;
  4741. var segs = [];
  4742. var seg;
  4743. var col;
  4744. var colDate;
  4745. var colRange;
  4746. // normalize :(
  4747. range = {
  4748. start: range.start.clone().stripZone(),
  4749. end: range.end.clone().stripZone()
  4750. };
  4751. for (col = 0; col < colCnt; col++) {
  4752. colDate = this.colDates[col]; // will be ambig time/timezone
  4753. colRange = {
  4754. start: colDate.clone().time(this.minTime),
  4755. end: colDate.clone().time(this.maxTime)
  4756. };
  4757. seg = intersectionToSeg(range, colRange); // both will be ambig timezone
  4758. if (seg) {
  4759. seg.col = col;
  4760. segs.push(seg);
  4761. }
  4762. }
  4763. return segs;
  4764. },
  4765. /* Coordinates
  4766. ------------------------------------------------------------------------------------------------------------------*/
  4767. updateSize: function(isResize) { // NOT a standard Grid method
  4768. this.computeSlatTops();
  4769. if (isResize) {
  4770. this.updateSegVerticals();
  4771. }
  4772. },
  4773. // Computes the top/bottom coordinates of each "snap" rows
  4774. computeRowCoords: function() {
  4775. var originTop = this.el.offset().top;
  4776. var items = [];
  4777. var i;
  4778. var item;
  4779. for (i = 0; i < this.rowCnt; i++) {
  4780. item = {
  4781. top: originTop + this.computeTimeTop(this.computeSnapTime(i))
  4782. };
  4783. if (i > 0) {
  4784. items[i - 1].bottom = item.top;
  4785. }
  4786. items.push(item);
  4787. }
  4788. item.bottom = item.top + this.computeTimeTop(this.computeSnapTime(i));
  4789. return items;
  4790. },
  4791. // Computes the top coordinate, relative to the bounds of the grid, of the given date.
  4792. // A `startOfDayDate` must be given for avoiding ambiguity over how to treat midnight.
  4793. computeDateTop: function(date, startOfDayDate) {
  4794. return this.computeTimeTop(
  4795. moment.duration(
  4796. date.clone().stripZone() - startOfDayDate.clone().stripTime()
  4797. )
  4798. );
  4799. },
  4800. // Computes the top coordinate, relative to the bounds of the grid, of the given time (a Duration).
  4801. computeTimeTop: function(time) {
  4802. var slatCoverage = (time - this.minTime) / this.slotDuration; // floating-point value of # of slots covered
  4803. var slatIndex;
  4804. var slatRemainder;
  4805. var slatTop;
  4806. var slatBottom;
  4807. // constrain. because minTime/maxTime might be customized
  4808. slatCoverage = Math.max(0, slatCoverage);
  4809. slatCoverage = Math.min(this.slatEls.length, slatCoverage);
  4810. slatIndex = Math.floor(slatCoverage); // an integer index of the furthest whole slot
  4811. slatRemainder = slatCoverage - slatIndex;
  4812. slatTop = this.slatTops[slatIndex]; // the top position of the furthest whole slot
  4813. if (slatRemainder) { // time spans part-way into the slot
  4814. slatBottom = this.slatTops[slatIndex + 1];
  4815. return slatTop + (slatBottom - slatTop) * slatRemainder; // part-way between slots
  4816. }
  4817. else {
  4818. return slatTop;
  4819. }
  4820. },
  4821. // Queries each `slatEl` for its position relative to the grid's container and stores it in `slatTops`.
  4822. // Includes the the bottom of the last slat as the last item in the array.
  4823. computeSlatTops: function() {
  4824. var tops = [];
  4825. var top;
  4826. this.slatEls.each(function(i, node) {
  4827. top = $(node).position().top;
  4828. tops.push(top);
  4829. });
  4830. tops.push(top + this.slatEls.last().outerHeight()); // bottom of the last slat
  4831. this.slatTops = tops;
  4832. },
  4833. /* Event Drag Visualization
  4834. ------------------------------------------------------------------------------------------------------------------*/
  4835. // Renders a visual indication of an event being dragged over the specified date(s).
  4836. // dropLocation's end might be null, as well as `seg`. See Grid::renderDrag for more info.
  4837. // A returned value of `true` signals that a mock "helper" event has been rendered.
  4838. renderDrag: function(dropLocation, seg) {
  4839. if (seg) { // if there is event information for this drag, render a helper event
  4840. this.renderRangeHelper(dropLocation, seg);
  4841. this.applyDragOpacity(this.helperEl);
  4842. return true; // signal that a helper has been rendered
  4843. }
  4844. else {
  4845. // otherwise, just render a highlight
  4846. this.renderHighlight(this.eventRangeToSegs(dropLocation));
  4847. }
  4848. },
  4849. // Unrenders any visual indication of an event being dragged
  4850. unrenderDrag: function() {
  4851. this.unrenderHelper();
  4852. this.unrenderHighlight();
  4853. },
  4854. /* Event Resize Visualization
  4855. ------------------------------------------------------------------------------------------------------------------*/
  4856. // Renders a visual indication of an event being resized
  4857. renderEventResize: function(range, seg) {
  4858. this.renderRangeHelper(range, seg);
  4859. },
  4860. // Unrenders any visual indication of an event being resized
  4861. unrenderEventResize: function() {
  4862. this.unrenderHelper();
  4863. },
  4864. /* Event Helper
  4865. ------------------------------------------------------------------------------------------------------------------*/
  4866. // Renders a mock "helper" event. `sourceSeg` is the original segment object and might be null (an external drag)
  4867. renderHelper: function(event, sourceSeg) {
  4868. var segs = this.eventsToSegs([ event ]);
  4869. var tableEl;
  4870. var i, seg;
  4871. var sourceEl;
  4872. segs = this.renderFgSegEls(segs); // assigns each seg's el and returns a subset of segs that were rendered
  4873. tableEl = this.renderSegTable(segs);
  4874. // Try to make the segment that is in the same row as sourceSeg look the same
  4875. for (i = 0; i < segs.length; i++) {
  4876. seg = segs[i];
  4877. if (sourceSeg && sourceSeg.col === seg.col) {
  4878. sourceEl = sourceSeg.el;
  4879. seg.el.css({
  4880. left: sourceEl.css('left'),
  4881. right: sourceEl.css('right'),
  4882. 'margin-left': sourceEl.css('margin-left'),
  4883. 'margin-right': sourceEl.css('margin-right')
  4884. });
  4885. }
  4886. }
  4887. this.helperEl = $('<div class="fc-helper-skeleton"/>')
  4888. .append(tableEl)
  4889. .appendTo(this.el);
  4890. },
  4891. // Unrenders any mock helper event
  4892. unrenderHelper: function() {
  4893. if (this.helperEl) {
  4894. this.helperEl.remove();
  4895. this.helperEl = null;
  4896. }
  4897. },
  4898. /* Selection
  4899. ------------------------------------------------------------------------------------------------------------------*/
  4900. // Renders a visual indication of a selection. Overrides the default, which was to simply render a highlight.
  4901. renderSelection: function(range) {
  4902. if (this.view.opt('selectHelper')) { // this setting signals that a mock helper event should be rendered
  4903. this.renderRangeHelper(range);
  4904. }
  4905. else {
  4906. this.renderHighlight(this.selectionRangeToSegs(range));
  4907. }
  4908. },
  4909. // Unrenders any visual indication of a selection
  4910. unrenderSelection: function() {
  4911. this.unrenderHelper();
  4912. this.unrenderHighlight();
  4913. },
  4914. /* Fill System (highlight, background events, business hours)
  4915. ------------------------------------------------------------------------------------------------------------------*/
  4916. // Renders a set of rectangles over the given time segments.
  4917. // Only returns segments that successfully rendered.
  4918. renderFill: function(type, segs, className) {
  4919. var segCols;
  4920. var skeletonEl;
  4921. var trEl;
  4922. var col, colSegs;
  4923. var tdEl;
  4924. var containerEl;
  4925. var dayDate;
  4926. var i, seg;
  4927. if (segs.length) {
  4928. segs = this.renderFillSegEls(type, segs); // assignes `.el` to each seg. returns successfully rendered segs
  4929. segCols = this.groupSegCols(segs); // group into sub-arrays, and assigns 'col' to each seg
  4930. className = className || type.toLowerCase();
  4931. skeletonEl = $(
  4932. '<div class="fc-' + className + '-skeleton">' +
  4933. '<table><tr/></table>' +
  4934. '</div>'
  4935. );
  4936. trEl = skeletonEl.find('tr');
  4937. for (col = 0; col < segCols.length; col++) {
  4938. colSegs = segCols[col];
  4939. tdEl = $('<td/>').appendTo(trEl);
  4940. if (colSegs.length) {
  4941. containerEl = $('<div class="fc-' + className + '-container"/>').appendTo(tdEl);
  4942. dayDate = this.colDates[col];
  4943. for (i = 0; i < colSegs.length; i++) {
  4944. seg = colSegs[i];
  4945. containerEl.append(
  4946. seg.el.css({
  4947. top: this.computeDateTop(seg.start, dayDate),
  4948. bottom: -this.computeDateTop(seg.end, dayDate) // the y position of the bottom edge
  4949. })
  4950. );
  4951. }
  4952. }
  4953. }
  4954. this.bookendCells(trEl, type);
  4955. this.el.append(skeletonEl);
  4956. this.elsByFill[type] = skeletonEl;
  4957. }
  4958. return segs;
  4959. }
  4960. });
  4961. ;;
  4962. /* Event-rendering methods for the TimeGrid class
  4963. ----------------------------------------------------------------------------------------------------------------------*/
  4964. TimeGrid.mixin({
  4965. eventSkeletonEl: null, // has cells with event-containers, which contain absolutely positioned event elements
  4966. // Renders the given foreground event segments onto the grid
  4967. renderFgSegs: function(segs) {
  4968. segs = this.renderFgSegEls(segs); // returns a subset of the segs. segs that were actually rendered
  4969. this.el.append(
  4970. this.eventSkeletonEl = $('<div class="fc-content-skeleton"/>')
  4971. .append(this.renderSegTable(segs))
  4972. );
  4973. return segs; // return only the segs that were actually rendered
  4974. },
  4975. // Unrenders all currently rendered foreground event segments
  4976. unrenderFgSegs: function(segs) {
  4977. if (this.eventSkeletonEl) {
  4978. this.eventSkeletonEl.remove();
  4979. this.eventSkeletonEl = null;
  4980. }
  4981. },
  4982. // Renders and returns the <table> portion of the event-skeleton.
  4983. // Returns an object with properties 'tbodyEl' and 'segs'.
  4984. renderSegTable: function(segs) {
  4985. var tableEl = $('<table><tr/></table>');
  4986. var trEl = tableEl.find('tr');
  4987. var segCols;
  4988. var i, seg;
  4989. var col, colSegs;
  4990. var containerEl;
  4991. segCols = this.groupSegCols(segs); // group into sub-arrays, and assigns 'col' to each seg
  4992. this.computeSegVerticals(segs); // compute and assign top/bottom
  4993. for (col = 0; col < segCols.length; col++) { // iterate each column grouping
  4994. colSegs = segCols[col];
  4995. this.placeSlotSegs(colSegs); // compute horizontal coordinates, z-index's, and reorder the array
  4996. containerEl = $('<div class="fc-event-container"/>');
  4997. // assign positioning CSS and insert into container
  4998. for (i = 0; i < colSegs.length; i++) {
  4999. seg = colSegs[i];
  5000. seg.el.css(this.generateSegPositionCss(seg));
  5001. // if the height is short, add a className for alternate styling
  5002. if (seg.bottom - seg.top < 30) {
  5003. seg.el.addClass('fc-short');
  5004. }
  5005. containerEl.append(seg.el);
  5006. }
  5007. trEl.append($('<td/>').append(containerEl));
  5008. }
  5009. this.bookendCells(trEl, 'eventSkeleton');
  5010. return tableEl;
  5011. },
  5012. // Given an array of segments that are all in the same column, sets the backwardCoord and forwardCoord on each.
  5013. // NOTE: Also reorders the given array by date!
  5014. placeSlotSegs: function(segs) {
  5015. var levels;
  5016. var level0;
  5017. var i;
  5018. this.sortSegs(segs); // order by date
  5019. levels = buildSlotSegLevels(segs);
  5020. computeForwardSlotSegs(levels);
  5021. if ((level0 = levels[0])) {
  5022. for (i = 0; i < level0.length; i++) {
  5023. computeSlotSegPressures(level0[i]);
  5024. }
  5025. for (i = 0; i < level0.length; i++) {
  5026. this.computeSlotSegCoords(level0[i], 0, 0);
  5027. }
  5028. }
  5029. },
  5030. // Calculate seg.forwardCoord and seg.backwardCoord for the segment, where both values range
  5031. // from 0 to 1. If the calendar is left-to-right, the seg.backwardCoord maps to "left" and
  5032. // seg.forwardCoord maps to "right" (via percentage). Vice-versa if the calendar is right-to-left.
  5033. //
  5034. // The segment might be part of a "series", which means consecutive segments with the same pressure
  5035. // who's width is unknown until an edge has been hit. `seriesBackwardPressure` is the number of
  5036. // segments behind this one in the current series, and `seriesBackwardCoord` is the starting
  5037. // coordinate of the first segment in the series.
  5038. computeSlotSegCoords: function(seg, seriesBackwardPressure, seriesBackwardCoord) {
  5039. var forwardSegs = seg.forwardSegs;
  5040. var i;
  5041. if (seg.forwardCoord === undefined) { // not already computed
  5042. if (!forwardSegs.length) {
  5043. // if there are no forward segments, this segment should butt up against the edge
  5044. seg.forwardCoord = 1;
  5045. }
  5046. else {
  5047. // sort highest pressure first
  5048. this.sortForwardSlotSegs(forwardSegs);
  5049. // this segment's forwardCoord will be calculated from the backwardCoord of the
  5050. // highest-pressure forward segment.
  5051. this.computeSlotSegCoords(forwardSegs[0], seriesBackwardPressure + 1, seriesBackwardCoord);
  5052. seg.forwardCoord = forwardSegs[0].backwardCoord;
  5053. }
  5054. // calculate the backwardCoord from the forwardCoord. consider the series
  5055. seg.backwardCoord = seg.forwardCoord -
  5056. (seg.forwardCoord - seriesBackwardCoord) / // available width for series
  5057. (seriesBackwardPressure + 1); // # of segments in the series
  5058. // use this segment's coordinates to computed the coordinates of the less-pressurized
  5059. // forward segments
  5060. for (i=0; i<forwardSegs.length; i++) {
  5061. this.computeSlotSegCoords(forwardSegs[i], 0, seg.forwardCoord);
  5062. }
  5063. }
  5064. },
  5065. // Refreshes the CSS top/bottom coordinates for each segment element. Probably after a window resize/zoom.
  5066. // Repositions business hours segs too, so not just for events. Maybe shouldn't be here.
  5067. updateSegVerticals: function() {
  5068. var allSegs = (this.segs || []).concat(this.businessHourSegs || []);
  5069. var i;
  5070. this.computeSegVerticals(allSegs);
  5071. for (i = 0; i < allSegs.length; i++) {
  5072. allSegs[i].el.css(
  5073. this.generateSegVerticalCss(allSegs[i])
  5074. );
  5075. }
  5076. },
  5077. // For each segment in an array, computes and assigns its top and bottom properties
  5078. computeSegVerticals: function(segs) {
  5079. var i, seg;
  5080. for (i = 0; i < segs.length; i++) {
  5081. seg = segs[i];
  5082. seg.top = this.computeDateTop(seg.start, seg.start);
  5083. seg.bottom = this.computeDateTop(seg.end, seg.start);
  5084. }
  5085. },
  5086. // Renders the HTML for a single event segment's default rendering
  5087. fgSegHtml: function(seg, disableResizing) {
  5088. var view = this.view;
  5089. var event = seg.event;
  5090. var isDraggable = view.isEventDraggable(event);
  5091. var isResizableFromStart = !disableResizing && seg.isStart && view.isEventResizableFromStart(event);
  5092. var isResizableFromEnd = !disableResizing && seg.isEnd && view.isEventResizableFromEnd(event);
  5093. var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd);
  5094. var skinCss = cssToStr(this.getEventSkinCss(event));
  5095. var timeText;
  5096. var fullTimeText; // more verbose time text. for the print stylesheet
  5097. var startTimeText; // just the start time text
  5098. classes.unshift('fc-time-grid-event', 'fc-v-event');
  5099. if (view.isMultiDayEvent(event)) { // if the event appears to span more than one day...
  5100. // Don't display time text on segments that run entirely through a day.
  5101. // That would appear as midnight-midnight and would look dumb.
  5102. // Otherwise, display the time text for the *segment's* times (like 6pm-midnight or midnight-10am)
  5103. if (seg.isStart || seg.isEnd) {
  5104. timeText = this.getEventTimeText(seg);
  5105. fullTimeText = this.getEventTimeText(seg, 'LT');
  5106. startTimeText = this.getEventTimeText(seg, null, false); // displayEnd=false
  5107. }
  5108. } else {
  5109. // Display the normal time text for the *event's* times
  5110. timeText = this.getEventTimeText(event);
  5111. fullTimeText = this.getEventTimeText(event, 'LT');
  5112. startTimeText = this.getEventTimeText(event, null, false); // displayEnd=false
  5113. }
  5114. return '<a class="' + classes.join(' ') + '"' +
  5115. (event.url ?
  5116. ' href="' + htmlEscape(event.url) + '"' :
  5117. ''
  5118. ) +
  5119. (skinCss ?
  5120. ' style="' + skinCss + '"' :
  5121. ''
  5122. ) +
  5123. '>' +
  5124. '<div class="fc-content">' +
  5125. (timeText ?
  5126. '<div class="fc-time"' +
  5127. ' data-start="' + htmlEscape(startTimeText) + '"' +
  5128. ' data-full="' + htmlEscape(fullTimeText) + '"' +
  5129. '>' +
  5130. '<span>' + htmlEscape(timeText) + '</span>' +
  5131. '</div>' :
  5132. ''
  5133. ) +
  5134. (event.title ?
  5135. '<div class="fc-title">' +
  5136. htmlEscape(event.title) +
  5137. '</div>' :
  5138. ''
  5139. ) +
  5140. '</div>' +
  5141. '<div class="fc-bg"/>' +
  5142. /* TODO: write CSS for this
  5143. (isResizableFromStart ?
  5144. '<div class="fc-resizer fc-start-resizer" />' :
  5145. ''
  5146. ) +
  5147. */
  5148. (isResizableFromEnd ?
  5149. '<div class="fc-resizer fc-end-resizer" />' :
  5150. ''
  5151. ) +
  5152. '</a>';
  5153. },
  5154. // Generates an object with CSS properties/values that should be applied to an event segment element.
  5155. // Contains important positioning-related properties that should be applied to any event element, customized or not.
  5156. generateSegPositionCss: function(seg) {
  5157. var shouldOverlap = this.view.opt('slotEventOverlap');
  5158. var backwardCoord = seg.backwardCoord; // the left side if LTR. the right side if RTL. floating-point
  5159. var forwardCoord = seg.forwardCoord; // the right side if LTR. the left side if RTL. floating-point
  5160. var props = this.generateSegVerticalCss(seg); // get top/bottom first
  5161. var left; // amount of space from left edge, a fraction of the total width
  5162. var right; // amount of space from right edge, a fraction of the total width
  5163. if (shouldOverlap) {
  5164. // double the width, but don't go beyond the maximum forward coordinate (1.0)
  5165. forwardCoord = Math.min(1, backwardCoord + (forwardCoord - backwardCoord) * 2);
  5166. }
  5167. if (this.isRTL) {
  5168. left = 1 - forwardCoord;
  5169. right = backwardCoord;
  5170. }
  5171. else {
  5172. left = backwardCoord;
  5173. right = 1 - forwardCoord;
  5174. }
  5175. props.zIndex = seg.level + 1; // convert from 0-base to 1-based
  5176. props.left = left * 100 + '%';
  5177. props.right = right * 100 + '%';
  5178. if (shouldOverlap && seg.forwardPressure) {
  5179. // add padding to the edge so that forward stacked events don't cover the resizer's icon
  5180. props[this.isRTL ? 'marginLeft' : 'marginRight'] = 10 * 2; // 10 is a guesstimate of the icon's width
  5181. }
  5182. return props;
  5183. },
  5184. // Generates an object with CSS properties for the top/bottom coordinates of a segment element
  5185. generateSegVerticalCss: function(seg) {
  5186. return {
  5187. top: seg.top,
  5188. bottom: -seg.bottom // flipped because needs to be space beyond bottom edge of event container
  5189. };
  5190. },
  5191. // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's col
  5192. groupSegCols: function(segs) {
  5193. var segCols = [];
  5194. var i;
  5195. for (i = 0; i < this.colCnt; i++) {
  5196. segCols.push([]);
  5197. }
  5198. for (i = 0; i < segs.length; i++) {
  5199. segCols[segs[i].col].push(segs[i]);
  5200. }
  5201. return segCols;
  5202. },
  5203. sortForwardSlotSegs: function(forwardSegs) {
  5204. forwardSegs.sort(proxy(this, 'compareForwardSlotSegs'));
  5205. },
  5206. // A cmp function for determining which forward segment to rely on more when computing coordinates.
  5207. compareForwardSlotSegs: function(seg1, seg2) {
  5208. // put higher-pressure first
  5209. return seg2.forwardPressure - seg1.forwardPressure ||
  5210. // put segments that are closer to initial edge first (and favor ones with no coords yet)
  5211. (seg1.backwardCoord || 0) - (seg2.backwardCoord || 0) ||
  5212. // do normal sorting...
  5213. this.compareSegs(seg1, seg2);
  5214. }
  5215. });
  5216. // Builds an array of segments "levels". The first level will be the leftmost tier of segments if the calendar is
  5217. // left-to-right, or the rightmost if the calendar is right-to-left. Assumes the segments are already ordered by date.
  5218. function buildSlotSegLevels(segs) {
  5219. var levels = [];
  5220. var i, seg;
  5221. var j;
  5222. for (i=0; i<segs.length; i++) {
  5223. seg = segs[i];
  5224. // go through all the levels and stop on the first level where there are no collisions
  5225. for (j=0; j<levels.length; j++) {
  5226. if (!computeSlotSegCollisions(seg, levels[j]).length) {
  5227. break;
  5228. }
  5229. }
  5230. seg.level = j;
  5231. (levels[j] || (levels[j] = [])).push(seg);
  5232. }
  5233. return levels;
  5234. }
  5235. // For every segment, figure out the other segments that are in subsequent
  5236. // levels that also occupy the same vertical space. Accumulate in seg.forwardSegs
  5237. function computeForwardSlotSegs(levels) {
  5238. var i, level;
  5239. var j, seg;
  5240. var k;
  5241. for (i=0; i<levels.length; i++) {
  5242. level = levels[i];
  5243. for (j=0; j<level.length; j++) {
  5244. seg = level[j];
  5245. seg.forwardSegs = [];
  5246. for (k=i+1; k<levels.length; k++) {
  5247. computeSlotSegCollisions(seg, levels[k], seg.forwardSegs);
  5248. }
  5249. }
  5250. }
  5251. }
  5252. // Figure out which path forward (via seg.forwardSegs) results in the longest path until
  5253. // the furthest edge is reached. The number of segments in this path will be seg.forwardPressure
  5254. function computeSlotSegPressures(seg) {
  5255. var forwardSegs = seg.forwardSegs;
  5256. var forwardPressure = 0;
  5257. var i, forwardSeg;
  5258. if (seg.forwardPressure === undefined) { // not already computed
  5259. for (i=0; i<forwardSegs.length; i++) {
  5260. forwardSeg = forwardSegs[i];
  5261. // figure out the child's maximum forward path
  5262. computeSlotSegPressures(forwardSeg);
  5263. // either use the existing maximum, or use the child's forward pressure
  5264. // plus one (for the forwardSeg itself)
  5265. forwardPressure = Math.max(
  5266. forwardPressure,
  5267. 1 + forwardSeg.forwardPressure
  5268. );
  5269. }
  5270. seg.forwardPressure = forwardPressure;
  5271. }
  5272. }
  5273. // Find all the segments in `otherSegs` that vertically collide with `seg`.
  5274. // Append into an optionally-supplied `results` array and return.
  5275. function computeSlotSegCollisions(seg, otherSegs, results) {
  5276. results = results || [];
  5277. for (var i=0; i<otherSegs.length; i++) {
  5278. if (isSlotSegCollision(seg, otherSegs[i])) {
  5279. results.push(otherSegs[i]);
  5280. }
  5281. }
  5282. return results;
  5283. }
  5284. // Do these segments occupy the same vertical space?
  5285. function isSlotSegCollision(seg1, seg2) {
  5286. return seg1.bottom > seg2.top && seg1.top < seg2.bottom;
  5287. }
  5288. ;;
  5289. /* An abstract class from which other views inherit from
  5290. ----------------------------------------------------------------------------------------------------------------------*/
  5291. var View = fc.View = Class.extend({
  5292. type: null, // subclass' view name (string)
  5293. name: null, // deprecated. use `type` instead
  5294. title: null, // the text that will be displayed in the header's title
  5295. calendar: null, // owner Calendar object
  5296. options: null, // hash containing all options. already merged with view-specific-options
  5297. coordMap: null, // a CoordMap object for converting pixel regions to dates
  5298. el: null, // the view's containing element. set by Calendar
  5299. displaying: null, // a promise representing the state of rendering. null if no render requested
  5300. isSkeletonRendered: false,
  5301. isEventsRendered: false,
  5302. // range the view is actually displaying (moments)
  5303. start: null,
  5304. end: null, // exclusive
  5305. // range the view is formally responsible for (moments)
  5306. // may be different from start/end. for example, a month view might have 1st-31st, excluding padded dates
  5307. intervalStart: null,
  5308. intervalEnd: null, // exclusive
  5309. intervalDuration: null,
  5310. intervalUnit: null, // name of largest unit being displayed, like "month" or "week"
  5311. isRTL: false,
  5312. isSelected: false, // boolean whether a range of time is user-selected or not
  5313. eventOrderSpecs: null, // criteria for ordering events when they have same date/time
  5314. // subclasses can optionally use a scroll container
  5315. scrollerEl: null, // the element that will most likely scroll when content is too tall
  5316. scrollTop: null, // cached vertical scroll value
  5317. // classNames styled by jqui themes
  5318. widgetHeaderClass: null,
  5319. widgetContentClass: null,
  5320. highlightStateClass: null,
  5321. // for date utils, computed from options
  5322. nextDayThreshold: null,
  5323. isHiddenDayHash: null,
  5324. // document handlers, bound to `this` object
  5325. documentMousedownProxy: null, // TODO: doesn't work with touch
  5326. constructor: function(calendar, type, options, intervalDuration) {
  5327. this.calendar = calendar;
  5328. this.type = this.name = type; // .name is deprecated
  5329. this.options = options;
  5330. this.intervalDuration = intervalDuration || moment.duration(1, 'day');
  5331. this.nextDayThreshold = moment.duration(this.opt('nextDayThreshold'));
  5332. this.initThemingProps();
  5333. this.initHiddenDays();
  5334. this.isRTL = this.opt('isRTL');
  5335. this.eventOrderSpecs = parseFieldSpecs(this.opt('eventOrder'));
  5336. this.documentMousedownProxy = proxy(this, 'documentMousedown');
  5337. this.initialize();
  5338. },
  5339. // A good place for subclasses to initialize member variables
  5340. initialize: function() {
  5341. // subclasses can implement
  5342. },
  5343. // Retrieves an option with the given name
  5344. opt: function(name) {
  5345. return this.options[name];
  5346. },
  5347. // Triggers handlers that are view-related. Modifies args before passing to calendar.
  5348. trigger: function(name, thisObj) { // arguments beyond thisObj are passed along
  5349. var calendar = this.calendar;
  5350. return calendar.trigger.apply(
  5351. calendar,
  5352. [name, thisObj || this].concat(
  5353. Array.prototype.slice.call(arguments, 2), // arguments beyond thisObj
  5354. [ this ] // always make the last argument a reference to the view. TODO: deprecate
  5355. )
  5356. );
  5357. },
  5358. /* Dates
  5359. ------------------------------------------------------------------------------------------------------------------*/
  5360. // Updates all internal dates to center around the given current date
  5361. setDate: function(date) {
  5362. this.setRange(this.computeRange(date));
  5363. },
  5364. // Updates all internal dates for displaying the given range.
  5365. // Expects all values to be normalized (like what computeRange does).
  5366. setRange: function(range) {
  5367. $.extend(this, range);
  5368. this.updateTitle();
  5369. },
  5370. // Given a single current date, produce information about what range to display.
  5371. // Subclasses can override. Must return all properties.
  5372. computeRange: function(date) {
  5373. var intervalUnit = computeIntervalUnit(this.intervalDuration);
  5374. var intervalStart = date.clone().startOf(intervalUnit);
  5375. var intervalEnd = intervalStart.clone().add(this.intervalDuration);
  5376. var start, end;
  5377. // normalize the range's time-ambiguity
  5378. if (/year|month|week|day/.test(intervalUnit)) { // whole-days?
  5379. intervalStart.stripTime();
  5380. intervalEnd.stripTime();
  5381. }
  5382. else { // needs to have a time?
  5383. if (!intervalStart.hasTime()) {
  5384. intervalStart = this.calendar.rezoneDate(intervalStart); // convert to current timezone, with 00:00
  5385. }
  5386. if (!intervalEnd.hasTime()) {
  5387. intervalEnd = this.calendar.rezoneDate(intervalEnd); // convert to current timezone, with 00:00
  5388. }
  5389. }
  5390. start = intervalStart.clone();
  5391. start = this.skipHiddenDays(start);
  5392. end = intervalEnd.clone();
  5393. end = this.skipHiddenDays(end, -1, true); // exclusively move backwards
  5394. return {
  5395. intervalUnit: intervalUnit,
  5396. intervalStart: intervalStart,
  5397. intervalEnd: intervalEnd,
  5398. start: start,
  5399. end: end
  5400. };
  5401. },
  5402. // Computes the new date when the user hits the prev button, given the current date
  5403. computePrevDate: function(date) {
  5404. return this.massageCurrentDate(
  5405. date.clone().startOf(this.intervalUnit).subtract(this.intervalDuration), -1
  5406. );
  5407. },
  5408. // Computes the new date when the user hits the next button, given the current date
  5409. computeNextDate: function(date) {
  5410. return this.massageCurrentDate(
  5411. date.clone().startOf(this.intervalUnit).add(this.intervalDuration)
  5412. );
  5413. },
  5414. // Given an arbitrarily calculated current date of the calendar, returns a date that is ensured to be completely
  5415. // visible. `direction` is optional and indicates which direction the current date was being
  5416. // incremented or decremented (1 or -1).
  5417. massageCurrentDate: function(date, direction) {
  5418. if (this.intervalDuration.as('days') <= 1) { // if the view displays a single day or smaller
  5419. if (this.isHiddenDay(date)) {
  5420. date = this.skipHiddenDays(date, direction);
  5421. date.startOf('day');
  5422. }
  5423. }
  5424. return date;
  5425. },
  5426. /* Title and Date Formatting
  5427. ------------------------------------------------------------------------------------------------------------------*/
  5428. // Sets the view's title property to the most updated computed value
  5429. updateTitle: function() {
  5430. this.title = this.computeTitle();
  5431. },
  5432. // Computes what the title at the top of the calendar should be for this view
  5433. computeTitle: function() {
  5434. return this.formatRange(
  5435. { start: this.intervalStart, end: this.intervalEnd },
  5436. this.opt('titleFormat') || this.computeTitleFormat(),
  5437. this.opt('titleRangeSeparator')
  5438. );
  5439. },
  5440. // Generates the format string that should be used to generate the title for the current date range.
  5441. // Attempts to compute the most appropriate format if not explicitly specified with `titleFormat`.
  5442. computeTitleFormat: function() {
  5443. if (this.intervalUnit == 'year') {
  5444. return 'YYYY';
  5445. }
  5446. else if (this.intervalUnit == 'month') {
  5447. return this.opt('monthYearFormat'); // like "September 2014"
  5448. }
  5449. else if (this.intervalDuration.as('days') > 1) {
  5450. return 'll'; // multi-day range. shorter, like "Sep 9 - 10 2014"
  5451. }
  5452. else {
  5453. return 'LL'; // one day. longer, like "September 9 2014"
  5454. }
  5455. },
  5456. // Utility for formatting a range. Accepts a range object, formatting string, and optional separator.
  5457. // Displays all-day ranges naturally, with an inclusive end. Takes the current isRTL into account.
  5458. formatRange: function(range, formatStr, separator) {
  5459. var end = range.end;
  5460. if (!end.hasTime()) { // all-day?
  5461. end = end.clone().subtract(1); // convert to inclusive. last ms of previous day
  5462. }
  5463. return formatRange(range.start, end, formatStr, separator, this.opt('isRTL'));
  5464. },
  5465. /* Rendering
  5466. ------------------------------------------------------------------------------------------------------------------*/
  5467. // Sets the container element that the view should render inside of.
  5468. // Does other DOM-related initializations.
  5469. setElement: function(el) {
  5470. this.el = el;
  5471. this.bindGlobalHandlers();
  5472. },
  5473. // Removes the view's container element from the DOM, clearing any content beforehand.
  5474. // Undoes any other DOM-related attachments.
  5475. removeElement: function() {
  5476. this.clear(); // clears all content
  5477. // clean up the skeleton
  5478. if (this.isSkeletonRendered) {
  5479. this.unrenderSkeleton();
  5480. this.isSkeletonRendered = false;
  5481. }
  5482. this.unbindGlobalHandlers();
  5483. this.el.remove();
  5484. // NOTE: don't null-out this.el in case the View was destroyed within an API callback.
  5485. // We don't null-out the View's other jQuery element references upon destroy,
  5486. // so we shouldn't kill this.el either.
  5487. },
  5488. // Does everything necessary to display the view centered around the given date.
  5489. // Does every type of rendering EXCEPT rendering events.
  5490. // Is asychronous and returns a promise.
  5491. display: function(date) {
  5492. var _this = this;
  5493. var scrollState = null;
  5494. if (this.displaying) {
  5495. scrollState = this.queryScroll();
  5496. }
  5497. return this.clear().then(function() { // clear the content first (async)
  5498. return (
  5499. _this.displaying =
  5500. $.when(_this.displayView(date)) // displayView might return a promise
  5501. .then(function() {
  5502. _this.forceScroll(_this.computeInitialScroll(scrollState));
  5503. _this.triggerRender();
  5504. })
  5505. );
  5506. });
  5507. },
  5508. // Does everything necessary to clear the content of the view.
  5509. // Clears dates and events. Does not clear the skeleton.
  5510. // Is asychronous and returns a promise.
  5511. clear: function() {
  5512. var _this = this;
  5513. var displaying = this.displaying;
  5514. if (displaying) { // previously displayed, or in the process of being displayed?
  5515. return displaying.then(function() { // wait for the display to finish
  5516. _this.displaying = null;
  5517. _this.clearEvents();
  5518. return _this.clearView(); // might return a promise. chain it
  5519. });
  5520. }
  5521. else {
  5522. return $.when(); // an immediately-resolved promise
  5523. }
  5524. },
  5525. // Displays the view's non-event content, such as date-related content or anything required by events.
  5526. // Renders the view's non-content skeleton if necessary.
  5527. // Can be asynchronous and return a promise.
  5528. displayView: function(date) {
  5529. if (!this.isSkeletonRendered) {
  5530. this.renderSkeleton();
  5531. this.isSkeletonRendered = true;
  5532. }
  5533. this.setDate(date);
  5534. if (this.render) {
  5535. this.render(); // TODO: deprecate
  5536. }
  5537. this.renderDates();
  5538. this.updateSize();
  5539. this.renderBusinessHours(); // might need coordinates, so should go after updateSize()
  5540. },
  5541. // Unrenders the view content that was rendered in displayView.
  5542. // Can be asynchronous and return a promise.
  5543. clearView: function() {
  5544. this.unselect();
  5545. this.triggerUnrender();
  5546. this.unrenderBusinessHours();
  5547. this.unrenderDates();
  5548. if (this.destroy) {
  5549. this.destroy(); // TODO: deprecate
  5550. }
  5551. },
  5552. // Renders the basic structure of the view before any content is rendered
  5553. renderSkeleton: function() {
  5554. // subclasses should implement
  5555. },
  5556. // Unrenders the basic structure of the view
  5557. unrenderSkeleton: function() {
  5558. // subclasses should implement
  5559. },
  5560. // Renders the view's date-related content (like cells that represent days/times).
  5561. // Assumes setRange has already been called and the skeleton has already been rendered.
  5562. renderDates: function() {
  5563. // subclasses should implement
  5564. },
  5565. // Unrenders the view's date-related content
  5566. unrenderDates: function() {
  5567. // subclasses should override
  5568. },
  5569. // Renders business-hours onto the view. Assumes updateSize has already been called.
  5570. renderBusinessHours: function() {
  5571. // subclasses should implement
  5572. },
  5573. // Unrenders previously-rendered business-hours
  5574. unrenderBusinessHours: function() {
  5575. // subclasses should implement
  5576. },
  5577. // Signals that the view's content has been rendered
  5578. triggerRender: function() {
  5579. this.trigger('viewRender', this, this, this.el);
  5580. },
  5581. // Signals that the view's content is about to be unrendered
  5582. triggerUnrender: function() {
  5583. this.trigger('viewDestroy', this, this, this.el);
  5584. },
  5585. // Binds DOM handlers to elements that reside outside the view container, such as the document
  5586. bindGlobalHandlers: function() {
  5587. $(document).on('mousedown', this.documentMousedownProxy);
  5588. },
  5589. // Unbinds DOM handlers from elements that reside outside the view container
  5590. unbindGlobalHandlers: function() {
  5591. $(document).off('mousedown', this.documentMousedownProxy);
  5592. },
  5593. // Initializes internal variables related to theming
  5594. initThemingProps: function() {
  5595. var tm = this.opt('theme') ? 'ui' : 'fc';
  5596. this.widgetHeaderClass = tm + '-widget-header';
  5597. this.widgetContentClass = tm + '-widget-content';
  5598. this.highlightStateClass = tm + '-state-highlight';
  5599. },
  5600. /* Dimensions
  5601. ------------------------------------------------------------------------------------------------------------------*/
  5602. // Refreshes anything dependant upon sizing of the container element of the grid
  5603. updateSize: function(isResize) {
  5604. var scrollState;
  5605. if (isResize) {
  5606. scrollState = this.queryScroll();
  5607. }
  5608. this.updateHeight(isResize);
  5609. this.updateWidth(isResize);
  5610. if (isResize) {
  5611. this.setScroll(scrollState);
  5612. }
  5613. },
  5614. // Refreshes the horizontal dimensions of the calendar
  5615. updateWidth: function(isResize) {
  5616. // subclasses should implement
  5617. },
  5618. // Refreshes the vertical dimensions of the calendar
  5619. updateHeight: function(isResize) {
  5620. var calendar = this.calendar; // we poll the calendar for height information
  5621. this.setHeight(
  5622. calendar.getSuggestedViewHeight(),
  5623. calendar.isHeightAuto()
  5624. );
  5625. },
  5626. // Updates the vertical dimensions of the calendar to the specified height.
  5627. // if `isAuto` is set to true, height becomes merely a suggestion and the view should use its "natural" height.
  5628. setHeight: function(height, isAuto) {
  5629. // subclasses should implement
  5630. },
  5631. /* Scroller
  5632. ------------------------------------------------------------------------------------------------------------------*/
  5633. // Given the total height of the view, return the number of pixels that should be used for the scroller.
  5634. // Utility for subclasses.
  5635. computeScrollerHeight: function(totalHeight) {
  5636. var scrollerEl = this.scrollerEl;
  5637. var both;
  5638. var otherHeight; // cumulative height of everything that is not the scrollerEl in the view (header+borders)
  5639. both = this.el.add(scrollerEl);
  5640. // fuckin IE8/9/10/11 sometimes returns 0 for dimensions. this weird hack was the only thing that worked
  5641. both.css({
  5642. position: 'relative', // cause a reflow, which will force fresh dimension recalculation
  5643. left: -1 // ensure reflow in case the el was already relative. negative is less likely to cause new scroll
  5644. });
  5645. otherHeight = this.el.outerHeight() - scrollerEl.height(); // grab the dimensions
  5646. both.css({ position: '', left: '' }); // undo hack
  5647. return totalHeight - otherHeight;
  5648. },
  5649. // Computes the initial pre-configured scroll state prior to allowing the user to change it.
  5650. // Given the scroll state from the previous rendering. If first time rendering, given null.
  5651. computeInitialScroll: function(previousScrollState) {
  5652. return 0;
  5653. },
  5654. // Retrieves the view's current natural scroll state. Can return an arbitrary format.
  5655. queryScroll: function() {
  5656. if (this.scrollerEl) {
  5657. return this.scrollerEl.scrollTop(); // operates on scrollerEl by default
  5658. }
  5659. },
  5660. // Sets the view's scroll state. Will accept the same format computeInitialScroll and queryScroll produce.
  5661. setScroll: function(scrollState) {
  5662. if (this.scrollerEl) {
  5663. return this.scrollerEl.scrollTop(scrollState); // operates on scrollerEl by default
  5664. }
  5665. },
  5666. // Sets the scroll state, making sure to overcome any predefined scroll value the browser has in mind
  5667. forceScroll: function(scrollState) {
  5668. var _this = this;
  5669. this.setScroll(scrollState);
  5670. setTimeout(function() {
  5671. _this.setScroll(scrollState);
  5672. }, 0);
  5673. },
  5674. /* Event Elements / Segments
  5675. ------------------------------------------------------------------------------------------------------------------*/
  5676. // Does everything necessary to display the given events onto the current view
  5677. displayEvents: function(events) {
  5678. var scrollState = this.queryScroll();
  5679. this.clearEvents();
  5680. this.renderEvents(events);
  5681. this.isEventsRendered = true;
  5682. this.setScroll(scrollState);
  5683. this.triggerEventRender();
  5684. },
  5685. // Does everything necessary to clear the view's currently-rendered events
  5686. clearEvents: function() {
  5687. if (this.isEventsRendered) {
  5688. this.triggerEventUnrender();
  5689. if (this.destroyEvents) {
  5690. this.destroyEvents(); // TODO: deprecate
  5691. }
  5692. this.unrenderEvents();
  5693. this.isEventsRendered = false;
  5694. }
  5695. },
  5696. // Renders the events onto the view.
  5697. renderEvents: function(events) {
  5698. // subclasses should implement
  5699. },
  5700. // Removes event elements from the view.
  5701. unrenderEvents: function() {
  5702. // subclasses should implement
  5703. },
  5704. // Signals that all events have been rendered
  5705. triggerEventRender: function() {
  5706. this.renderedEventSegEach(function(seg) {
  5707. this.trigger('eventAfterRender', seg.event, seg.event, seg.el);
  5708. });
  5709. this.trigger('eventAfterAllRender');
  5710. },
  5711. // Signals that all event elements are about to be removed
  5712. triggerEventUnrender: function() {
  5713. this.renderedEventSegEach(function(seg) {
  5714. this.trigger('eventDestroy', seg.event, seg.event, seg.el);
  5715. });
  5716. },
  5717. // Given an event and the default element used for rendering, returns the element that should actually be used.
  5718. // Basically runs events and elements through the eventRender hook.
  5719. resolveEventEl: function(event, el) {
  5720. var custom = this.trigger('eventRender', event, event, el);
  5721. if (custom === false) { // means don't render at all
  5722. el = null;
  5723. }
  5724. else if (custom && custom !== true) {
  5725. el = $(custom);
  5726. }
  5727. return el;
  5728. },
  5729. // Hides all rendered event segments linked to the given event
  5730. showEvent: function(event) {
  5731. this.renderedEventSegEach(function(seg) {
  5732. seg.el.css('visibility', '');
  5733. }, event);
  5734. },
  5735. // Shows all rendered event segments linked to the given event
  5736. hideEvent: function(event) {
  5737. this.renderedEventSegEach(function(seg) {
  5738. seg.el.css('visibility', 'hidden');
  5739. }, event);
  5740. },
  5741. // Iterates through event segments that have been rendered (have an el). Goes through all by default.
  5742. // If the optional `event` argument is specified, only iterates through segments linked to that event.
  5743. // The `this` value of the callback function will be the view.
  5744. renderedEventSegEach: function(func, event) {
  5745. var segs = this.getEventSegs();
  5746. var i;
  5747. for (i = 0; i < segs.length; i++) {
  5748. if (!event || segs[i].event._id === event._id) {
  5749. if (segs[i].el) {
  5750. func.call(this, segs[i]);
  5751. }
  5752. }
  5753. }
  5754. },
  5755. // Retrieves all the rendered segment objects for the view
  5756. getEventSegs: function() {
  5757. // subclasses must implement
  5758. return [];
  5759. },
  5760. /* Event Drag-n-Drop
  5761. ------------------------------------------------------------------------------------------------------------------*/
  5762. // Computes if the given event is allowed to be dragged by the user
  5763. isEventDraggable: function(event) {
  5764. var source = event.source || {};
  5765. return firstDefined(
  5766. event.startEditable,
  5767. source.startEditable,
  5768. this.opt('eventStartEditable'),
  5769. event.editable,
  5770. source.editable,
  5771. this.opt('editable')
  5772. );
  5773. },
  5774. // Must be called when an event in the view is dropped onto new location.
  5775. // `dropLocation` is an object that contains the new start/end/allDay values for the event.
  5776. reportEventDrop: function(event, dropLocation, largeUnit, el, ev) {
  5777. var calendar = this.calendar;
  5778. var mutateResult = calendar.mutateEvent(event, dropLocation, largeUnit);
  5779. var undoFunc = function() {
  5780. mutateResult.undo();
  5781. calendar.reportEventChange();
  5782. };
  5783. this.triggerEventDrop(event, mutateResult.dateDelta, undoFunc, el, ev);
  5784. calendar.reportEventChange(); // will rerender events
  5785. },
  5786. // Triggers event-drop handlers that have subscribed via the API
  5787. triggerEventDrop: function(event, dateDelta, undoFunc, el, ev) {
  5788. this.trigger('eventDrop', el[0], event, dateDelta, undoFunc, ev, {}); // {} = jqui dummy
  5789. },
  5790. /* External Element Drag-n-Drop
  5791. ------------------------------------------------------------------------------------------------------------------*/
  5792. // Must be called when an external element, via jQuery UI, has been dropped onto the calendar.
  5793. // `meta` is the parsed data that has been embedded into the dragging event.
  5794. // `dropLocation` is an object that contains the new start/end/allDay values for the event.
  5795. reportExternalDrop: function(meta, dropLocation, el, ev, ui) {
  5796. var eventProps = meta.eventProps;
  5797. var eventInput;
  5798. var event;
  5799. // Try to build an event object and render it. TODO: decouple the two
  5800. if (eventProps) {
  5801. eventInput = $.extend({}, eventProps, dropLocation);
  5802. event = this.calendar.renderEvent(eventInput, meta.stick)[0]; // renderEvent returns an array
  5803. }
  5804. this.triggerExternalDrop(event, dropLocation, el, ev, ui);
  5805. },
  5806. // Triggers external-drop handlers that have subscribed via the API
  5807. triggerExternalDrop: function(event, dropLocation, el, ev, ui) {
  5808. // trigger 'drop' regardless of whether element represents an event
  5809. this.trigger('drop', el[0], dropLocation.start, ev, ui);
  5810. if (event) {
  5811. this.trigger('eventReceive', null, event); // signal an external event landed
  5812. }
  5813. },
  5814. /* Drag-n-Drop Rendering (for both events and external elements)
  5815. ------------------------------------------------------------------------------------------------------------------*/
  5816. // Renders a visual indication of a event or external-element drag over the given drop zone.
  5817. // If an external-element, seg will be `null`
  5818. renderDrag: function(dropLocation, seg) {
  5819. // subclasses must implement
  5820. },
  5821. // Unrenders a visual indication of an event or external-element being dragged.
  5822. unrenderDrag: function() {
  5823. // subclasses must implement
  5824. },
  5825. /* Event Resizing
  5826. ------------------------------------------------------------------------------------------------------------------*/
  5827. // Computes if the given event is allowed to be resized from its starting edge
  5828. isEventResizableFromStart: function(event) {
  5829. return this.opt('eventResizableFromStart') && this.isEventResizable(event);
  5830. },
  5831. // Computes if the given event is allowed to be resized from its ending edge
  5832. isEventResizableFromEnd: function(event) {
  5833. return this.isEventResizable(event);
  5834. },
  5835. // Computes if the given event is allowed to be resized by the user at all
  5836. isEventResizable: function(event) {
  5837. var source = event.source || {};
  5838. return firstDefined(
  5839. event.durationEditable,
  5840. source.durationEditable,
  5841. this.opt('eventDurationEditable'),
  5842. event.editable,
  5843. source.editable,
  5844. this.opt('editable')
  5845. );
  5846. },
  5847. // Must be called when an event in the view has been resized to a new length
  5848. reportEventResize: function(event, resizeLocation, largeUnit, el, ev) {
  5849. var calendar = this.calendar;
  5850. var mutateResult = calendar.mutateEvent(event, resizeLocation, largeUnit);
  5851. var undoFunc = function() {
  5852. mutateResult.undo();
  5853. calendar.reportEventChange();
  5854. };
  5855. this.triggerEventResize(event, mutateResult.durationDelta, undoFunc, el, ev);
  5856. calendar.reportEventChange(); // will rerender events
  5857. },
  5858. // Triggers event-resize handlers that have subscribed via the API
  5859. triggerEventResize: function(event, durationDelta, undoFunc, el, ev) {
  5860. this.trigger('eventResize', el[0], event, durationDelta, undoFunc, ev, {}); // {} = jqui dummy
  5861. },
  5862. /* Selection
  5863. ------------------------------------------------------------------------------------------------------------------*/
  5864. // Selects a date range on the view. `start` and `end` are both Moments.
  5865. // `ev` is the native mouse event that begin the interaction.
  5866. select: function(range, ev) {
  5867. this.unselect(ev);
  5868. this.renderSelection(range);
  5869. this.reportSelection(range, ev);
  5870. },
  5871. // Renders a visual indication of the selection
  5872. renderSelection: function(range) {
  5873. // subclasses should implement
  5874. },
  5875. // Called when a new selection is made. Updates internal state and triggers handlers.
  5876. reportSelection: function(range, ev) {
  5877. this.isSelected = true;
  5878. this.triggerSelect(range, ev);
  5879. },
  5880. // Triggers handlers to 'select'
  5881. triggerSelect: function(range, ev) {
  5882. this.trigger('select', null, range.start, range.end, ev);
  5883. },
  5884. // Undoes a selection. updates in the internal state and triggers handlers.
  5885. // `ev` is the native mouse event that began the interaction.
  5886. unselect: function(ev) {
  5887. if (this.isSelected) {
  5888. this.isSelected = false;
  5889. if (this.destroySelection) {
  5890. this.destroySelection(); // TODO: deprecate
  5891. }
  5892. this.unrenderSelection();
  5893. this.trigger('unselect', null, ev);
  5894. }
  5895. },
  5896. // Unrenders a visual indication of selection
  5897. unrenderSelection: function() {
  5898. // subclasses should implement
  5899. },
  5900. // Handler for unselecting when the user clicks something and the 'unselectAuto' setting is on
  5901. documentMousedown: function(ev) {
  5902. var ignore;
  5903. // is there a selection, and has the user made a proper left click?
  5904. if (this.isSelected && this.opt('unselectAuto') && isPrimaryMouseButton(ev)) {
  5905. // only unselect if the clicked element is not identical to or inside of an 'unselectCancel' element
  5906. ignore = this.opt('unselectCancel');
  5907. if (!ignore || !$(ev.target).closest(ignore).length) {
  5908. this.unselect(ev);
  5909. }
  5910. }
  5911. },
  5912. /* Day Click
  5913. ------------------------------------------------------------------------------------------------------------------*/
  5914. // Triggers handlers to 'dayClick'
  5915. triggerDayClick: function(cell, dayEl, ev) {
  5916. this.trigger('dayClick', dayEl, cell.start, ev);
  5917. },
  5918. /* Date Utils
  5919. ------------------------------------------------------------------------------------------------------------------*/
  5920. // Initializes internal variables related to calculating hidden days-of-week
  5921. initHiddenDays: function() {
  5922. var hiddenDays = this.opt('hiddenDays') || []; // array of day-of-week indices that are hidden
  5923. var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool)
  5924. var dayCnt = 0;
  5925. var i;
  5926. if (this.opt('weekends') === false) {
  5927. hiddenDays.push(0, 6); // 0=sunday, 6=saturday
  5928. }
  5929. for (i = 0; i < 7; i++) {
  5930. if (
  5931. !(isHiddenDayHash[i] = $.inArray(i, hiddenDays) !== -1)
  5932. ) {
  5933. dayCnt++;
  5934. }
  5935. }
  5936. if (!dayCnt) {
  5937. throw 'invalid hiddenDays'; // all days were hidden? bad.
  5938. }
  5939. this.isHiddenDayHash = isHiddenDayHash;
  5940. },
  5941. // Is the current day hidden?
  5942. // `day` is a day-of-week index (0-6), or a Moment
  5943. isHiddenDay: function(day) {
  5944. if (moment.isMoment(day)) {
  5945. day = day.day();
  5946. }
  5947. return this.isHiddenDayHash[day];
  5948. },
  5949. // Incrementing the current day until it is no longer a hidden day, returning a copy.
  5950. // If the initial value of `date` is not a hidden day, don't do anything.
  5951. // Pass `isExclusive` as `true` if you are dealing with an end date.
  5952. // `inc` defaults to `1` (increment one day forward each time)
  5953. skipHiddenDays: function(date, inc, isExclusive) {
  5954. var out = date.clone();
  5955. inc = inc || 1;
  5956. while (
  5957. this.isHiddenDayHash[(out.day() + (isExclusive ? inc : 0) + 7) % 7]
  5958. ) {
  5959. out.add(inc, 'days');
  5960. }
  5961. return out;
  5962. },
  5963. // Returns the date range of the full days the given range visually appears to occupy.
  5964. // Returns a new range object.
  5965. computeDayRange: function(range) {
  5966. var startDay = range.start.clone().stripTime(); // the beginning of the day the range starts
  5967. var end = range.end;
  5968. var endDay = null;
  5969. var endTimeMS;
  5970. if (end) {
  5971. endDay = end.clone().stripTime(); // the beginning of the day the range exclusively ends
  5972. endTimeMS = +end.time(); // # of milliseconds into `endDay`
  5973. // If the end time is actually inclusively part of the next day and is equal to or
  5974. // beyond the next day threshold, adjust the end to be the exclusive end of `endDay`.
  5975. // Otherwise, leaving it as inclusive will cause it to exclude `endDay`.
  5976. if (endTimeMS && endTimeMS >= this.nextDayThreshold) {
  5977. endDay.add(1, 'days');
  5978. }
  5979. }
  5980. // If no end was specified, or if it is within `startDay` but not past nextDayThreshold,
  5981. // assign the default duration of one day.
  5982. if (!end || endDay <= startDay) {
  5983. endDay = startDay.clone().add(1, 'days');
  5984. }
  5985. return { start: startDay, end: endDay };
  5986. },
  5987. // Does the given event visually appear to occupy more than one day?
  5988. isMultiDayEvent: function(event) {
  5989. var range = this.computeDayRange(event); // event is range-ish
  5990. return range.end.diff(range.start, 'days') > 1;
  5991. }
  5992. });
  5993. ;;
  5994. var Calendar = fc.Calendar = Class.extend({
  5995. dirDefaults: null, // option defaults related to LTR or RTL
  5996. langDefaults: null, // option defaults related to current locale
  5997. overrides: null, // option overrides given to the fullCalendar constructor
  5998. options: null, // all defaults combined with overrides
  5999. viewSpecCache: null, // cache of view definitions
  6000. view: null, // current View object
  6001. header: null,
  6002. loadingLevel: 0, // number of simultaneous loading tasks
  6003. // a lot of this class' OOP logic is scoped within this constructor function,
  6004. // but in the future, write individual methods on the prototype.
  6005. constructor: Calendar_constructor,
  6006. // Subclasses can override this for initialization logic after the constructor has been called
  6007. initialize: function() {
  6008. },
  6009. // Initializes `this.options` and other important options-related objects
  6010. initOptions: function(overrides) {
  6011. var lang, langDefaults;
  6012. var isRTL, dirDefaults;
  6013. // converts legacy options into non-legacy ones.
  6014. // in the future, when this is removed, don't use `overrides` reference. make a copy.
  6015. overrides = massageOverrides(overrides);
  6016. lang = overrides.lang;
  6017. langDefaults = langOptionHash[lang];
  6018. if (!langDefaults) {
  6019. lang = Calendar.defaults.lang;
  6020. langDefaults = langOptionHash[lang] || {};
  6021. }
  6022. isRTL = firstDefined(
  6023. overrides.isRTL,
  6024. langDefaults.isRTL,
  6025. Calendar.defaults.isRTL
  6026. );
  6027. dirDefaults = isRTL ? Calendar.rtlDefaults : {};
  6028. this.dirDefaults = dirDefaults;
  6029. this.langDefaults = langDefaults;
  6030. this.overrides = overrides;
  6031. this.options = mergeOptions([ // merge defaults and overrides. lowest to highest precedence
  6032. Calendar.defaults, // global defaults
  6033. dirDefaults,
  6034. langDefaults,
  6035. overrides
  6036. ]);
  6037. populateInstanceComputableOptions(this.options);
  6038. this.viewSpecCache = {}; // somewhat unrelated
  6039. },
  6040. // Gets information about how to create a view. Will use a cache.
  6041. getViewSpec: function(viewType) {
  6042. var cache = this.viewSpecCache;
  6043. return cache[viewType] || (cache[viewType] = this.buildViewSpec(viewType));
  6044. },
  6045. // Given a duration singular unit, like "week" or "day", finds a matching view spec.
  6046. // Preference is given to views that have corresponding buttons.
  6047. getUnitViewSpec: function(unit) {
  6048. var viewTypes;
  6049. var i;
  6050. var spec;
  6051. if ($.inArray(unit, intervalUnits) != -1) {
  6052. // put views that have buttons first. there will be duplicates, but oh well
  6053. viewTypes = this.header.getViewsWithButtons();
  6054. $.each(fc.views, function(viewType) { // all views
  6055. viewTypes.push(viewType);
  6056. });
  6057. for (i = 0; i < viewTypes.length; i++) {
  6058. spec = this.getViewSpec(viewTypes[i]);
  6059. if (spec) {
  6060. if (spec.singleUnit == unit) {
  6061. return spec;
  6062. }
  6063. }
  6064. }
  6065. }
  6066. },
  6067. // Builds an object with information on how to create a given view
  6068. buildViewSpec: function(requestedViewType) {
  6069. var viewOverrides = this.overrides.views || {};
  6070. var specChain = []; // for the view. lowest to highest priority
  6071. var defaultsChain = []; // for the view. lowest to highest priority
  6072. var overridesChain = []; // for the view. lowest to highest priority
  6073. var viewType = requestedViewType;
  6074. var spec; // for the view
  6075. var overrides; // for the view
  6076. var duration;
  6077. var unit;
  6078. // iterate from the specific view definition to a more general one until we hit an actual View class
  6079. while (viewType) {
  6080. spec = fcViews[viewType];
  6081. overrides = viewOverrides[viewType];
  6082. viewType = null; // clear. might repopulate for another iteration
  6083. if (typeof spec === 'function') { // TODO: deprecate
  6084. spec = { 'class': spec };
  6085. }
  6086. if (spec) {
  6087. specChain.unshift(spec);
  6088. defaultsChain.unshift(spec.defaults || {});
  6089. duration = duration || spec.duration;
  6090. viewType = viewType || spec.type;
  6091. }
  6092. if (overrides) {
  6093. overridesChain.unshift(overrides); // view-specific option hashes have options at zero-level
  6094. duration = duration || overrides.duration;
  6095. viewType = viewType || overrides.type;
  6096. }
  6097. }
  6098. spec = mergeProps(specChain);
  6099. spec.type = requestedViewType;
  6100. if (!spec['class']) {
  6101. return false;
  6102. }
  6103. if (duration) {
  6104. duration = moment.duration(duration);
  6105. if (duration.valueOf()) { // valid?
  6106. spec.duration = duration;
  6107. unit = computeIntervalUnit(duration);
  6108. // view is a single-unit duration, like "week" or "day"
  6109. // incorporate options for this. lowest priority
  6110. if (duration.as(unit) === 1) {
  6111. spec.singleUnit = unit;
  6112. overridesChain.unshift(viewOverrides[unit] || {});
  6113. }
  6114. }
  6115. }
  6116. spec.defaults = mergeOptions(defaultsChain);
  6117. spec.overrides = mergeOptions(overridesChain);
  6118. this.buildViewSpecOptions(spec);
  6119. this.buildViewSpecButtonText(spec, requestedViewType);
  6120. return spec;
  6121. },
  6122. // Builds and assigns a view spec's options object from its already-assigned defaults and overrides
  6123. buildViewSpecOptions: function(spec) {
  6124. spec.options = mergeOptions([ // lowest to highest priority
  6125. Calendar.defaults, // global defaults
  6126. spec.defaults, // view's defaults (from ViewSubclass.defaults)
  6127. this.dirDefaults,
  6128. this.langDefaults, // locale and dir take precedence over view's defaults!
  6129. this.overrides, // calendar's overrides (options given to constructor)
  6130. spec.overrides // view's overrides (view-specific options)
  6131. ]);
  6132. populateInstanceComputableOptions(spec.options);
  6133. },
  6134. // Computes and assigns a view spec's buttonText-related options
  6135. buildViewSpecButtonText: function(spec, requestedViewType) {
  6136. // given an options object with a possible `buttonText` hash, lookup the buttonText for the
  6137. // requested view, falling back to a generic unit entry like "week" or "day"
  6138. function queryButtonText(options) {
  6139. var buttonText = options.buttonText || {};
  6140. return buttonText[requestedViewType] ||
  6141. (spec.singleUnit ? buttonText[spec.singleUnit] : null);
  6142. }
  6143. // highest to lowest priority
  6144. spec.buttonTextOverride =
  6145. queryButtonText(this.overrides) || // constructor-specified buttonText lookup hash takes precedence
  6146. spec.overrides.buttonText; // `buttonText` for view-specific options is a string
  6147. // highest to lowest priority. mirrors buildViewSpecOptions
  6148. spec.buttonTextDefault =
  6149. queryButtonText(this.langDefaults) ||
  6150. queryButtonText(this.dirDefaults) ||
  6151. spec.defaults.buttonText || // a single string. from ViewSubclass.defaults
  6152. queryButtonText(Calendar.defaults) ||
  6153. (spec.duration ? this.humanizeDuration(spec.duration) : null) || // like "3 days"
  6154. requestedViewType; // fall back to given view name
  6155. },
  6156. // Given a view name for a custom view or a standard view, creates a ready-to-go View object
  6157. instantiateView: function(viewType) {
  6158. var spec = this.getViewSpec(viewType);
  6159. return new spec['class'](this, viewType, spec.options, spec.duration);
  6160. },
  6161. // Returns a boolean about whether the view is okay to instantiate at some point
  6162. isValidViewType: function(viewType) {
  6163. return Boolean(this.getViewSpec(viewType));
  6164. },
  6165. // Should be called when any type of async data fetching begins
  6166. pushLoading: function() {
  6167. if (!(this.loadingLevel++)) {
  6168. this.trigger('loading', null, true, this.view);
  6169. }
  6170. },
  6171. // Should be called when any type of async data fetching completes
  6172. popLoading: function() {
  6173. if (!(--this.loadingLevel)) {
  6174. this.trigger('loading', null, false, this.view);
  6175. }
  6176. },
  6177. // Given arguments to the select method in the API, returns a range
  6178. buildSelectRange: function(start, end) {
  6179. start = this.moment(start);
  6180. if (end) {
  6181. end = this.moment(end);
  6182. }
  6183. else if (start.hasTime()) {
  6184. end = start.clone().add(this.defaultTimedEventDuration);
  6185. }
  6186. else {
  6187. end = start.clone().add(this.defaultAllDayEventDuration);
  6188. }
  6189. return { start: start, end: end };
  6190. }
  6191. });
  6192. Calendar.mixin(Emitter);
  6193. function Calendar_constructor(element, overrides) {
  6194. var t = this;
  6195. t.initOptions(overrides || {});
  6196. var options = this.options;
  6197. // Exports
  6198. // -----------------------------------------------------------------------------------
  6199. t.render = render;
  6200. t.destroy = destroy;
  6201. t.refetchEvents = refetchEvents;
  6202. t.reportEvents = reportEvents;
  6203. t.reportEventChange = reportEventChange;
  6204. t.rerenderEvents = renderEvents; // `renderEvents` serves as a rerender. an API method
  6205. t.changeView = renderView; // `renderView` will switch to another view
  6206. t.select = select;
  6207. t.unselect = unselect;
  6208. t.prev = prev;
  6209. t.next = next;
  6210. t.prevYear = prevYear;
  6211. t.nextYear = nextYear;
  6212. t.today = today;
  6213. t.gotoDate = gotoDate;
  6214. t.incrementDate = incrementDate;
  6215. t.zoomTo = zoomTo;
  6216. t.getDate = getDate;
  6217. t.getCalendar = getCalendar;
  6218. t.getView = getView;
  6219. t.option = option;
  6220. t.trigger = trigger;
  6221. // Language-data Internals
  6222. // -----------------------------------------------------------------------------------
  6223. // Apply overrides to the current language's data
  6224. var localeData = createObject( // make a cheap copy
  6225. getMomentLocaleData(options.lang) // will fall back to en
  6226. );
  6227. if (options.monthNames) {
  6228. localeData._months = options.monthNames;
  6229. }
  6230. if (options.monthNamesShort) {
  6231. localeData._monthsShort = options.monthNamesShort;
  6232. }
  6233. if (options.dayNames) {
  6234. localeData._weekdays = options.dayNames;
  6235. }
  6236. if (options.dayNamesShort) {
  6237. localeData._weekdaysShort = options.dayNamesShort;
  6238. }
  6239. if (options.firstDay != null) {
  6240. var _week = createObject(localeData._week); // _week: { dow: # }
  6241. _week.dow = options.firstDay;
  6242. localeData._week = _week;
  6243. }
  6244. // assign a normalized value, to be used by our .week() moment extension
  6245. localeData._fullCalendar_weekCalc = (function(weekCalc) {
  6246. if (typeof weekCalc === 'function') {
  6247. return weekCalc;
  6248. }
  6249. else if (weekCalc === 'local') {
  6250. return weekCalc;
  6251. }
  6252. else if (weekCalc === 'iso' || weekCalc === 'ISO') {
  6253. return 'ISO';
  6254. }
  6255. })(options.weekNumberCalculation);
  6256. // Calendar-specific Date Utilities
  6257. // -----------------------------------------------------------------------------------
  6258. t.defaultAllDayEventDuration = moment.duration(options.defaultAllDayEventDuration);
  6259. t.defaultTimedEventDuration = moment.duration(options.defaultTimedEventDuration);
  6260. // Builds a moment using the settings of the current calendar: timezone and language.
  6261. // Accepts anything the vanilla moment() constructor accepts.
  6262. t.moment = function() {
  6263. var mom;
  6264. if (options.timezone === 'local') {
  6265. mom = fc.moment.apply(null, arguments);
  6266. // Force the moment to be local, because fc.moment doesn't guarantee it.
  6267. if (mom.hasTime()) { // don't give ambiguously-timed moments a local zone
  6268. mom.local();
  6269. }
  6270. }
  6271. else if (options.timezone === 'UTC') {
  6272. mom = fc.moment.utc.apply(null, arguments); // process as UTC
  6273. }
  6274. else {
  6275. mom = fc.moment.parseZone.apply(null, arguments); // let the input decide the zone
  6276. }
  6277. if ('_locale' in mom) { // moment 2.8 and above
  6278. mom._locale = localeData;
  6279. }
  6280. else { // pre-moment-2.8
  6281. mom._lang = localeData;
  6282. }
  6283. return mom;
  6284. };
  6285. // Returns a boolean about whether or not the calendar knows how to calculate
  6286. // the timezone offset of arbitrary dates in the current timezone.
  6287. t.getIsAmbigTimezone = function() {
  6288. return options.timezone !== 'local' && options.timezone !== 'UTC';
  6289. };
  6290. // Returns a copy of the given date in the current timezone of it is ambiguously zoned.
  6291. // This will also give the date an unambiguous time.
  6292. t.rezoneDate = function(date) {
  6293. return t.moment(date.toArray());
  6294. };
  6295. // Returns a moment for the current date, as defined by the client's computer,
  6296. // or overridden by the `now` option.
  6297. t.getNow = function() {
  6298. var now = options.now;
  6299. if (typeof now === 'function') {
  6300. now = now();
  6301. }
  6302. return t.moment(now);
  6303. };
  6304. // Get an event's normalized end date. If not present, calculate it from the defaults.
  6305. t.getEventEnd = function(event) {
  6306. if (event.end) {
  6307. return event.end.clone();
  6308. }
  6309. else {
  6310. return t.getDefaultEventEnd(event.allDay, event.start);
  6311. }
  6312. };
  6313. // Given an event's allDay status and start date, return swhat its fallback end date should be.
  6314. t.getDefaultEventEnd = function(allDay, start) { // TODO: rename to computeDefaultEventEnd
  6315. var end = start.clone();
  6316. if (allDay) {
  6317. end.stripTime().add(t.defaultAllDayEventDuration);
  6318. }
  6319. else {
  6320. end.add(t.defaultTimedEventDuration);
  6321. }
  6322. if (t.getIsAmbigTimezone()) {
  6323. end.stripZone(); // we don't know what the tzo should be
  6324. }
  6325. return end;
  6326. };
  6327. // Produces a human-readable string for the given duration.
  6328. // Side-effect: changes the locale of the given duration.
  6329. t.humanizeDuration = function(duration) {
  6330. return (duration.locale || duration.lang).call(duration, options.lang) // works moment-pre-2.8
  6331. .humanize();
  6332. };
  6333. // Imports
  6334. // -----------------------------------------------------------------------------------
  6335. EventManager.call(t, options);
  6336. var isFetchNeeded = t.isFetchNeeded;
  6337. var fetchEvents = t.fetchEvents;
  6338. // Locals
  6339. // -----------------------------------------------------------------------------------
  6340. var _element = element[0];
  6341. var header;
  6342. var headerElement;
  6343. var content;
  6344. var tm; // for making theme classes
  6345. var currentView; // NOTE: keep this in sync with this.view
  6346. var viewsByType = {}; // holds all instantiated view instances, current or not
  6347. var suggestedViewHeight;
  6348. var windowResizeProxy; // wraps the windowResize function
  6349. var ignoreWindowResize = 0;
  6350. var date;
  6351. var events = [];
  6352. // Main Rendering
  6353. // -----------------------------------------------------------------------------------
  6354. if (options.defaultDate != null) {
  6355. date = t.moment(options.defaultDate);
  6356. }
  6357. else {
  6358. date = t.getNow();
  6359. }
  6360. function render() {
  6361. if (!content) {
  6362. initialRender();
  6363. }
  6364. else if (elementVisible()) {
  6365. // mainly for the public API
  6366. calcSize();
  6367. renderView();
  6368. }
  6369. }
  6370. function initialRender() {
  6371. tm = options.theme ? 'ui' : 'fc';
  6372. element.addClass('fc');
  6373. if (options.isRTL) {
  6374. element.addClass('fc-rtl');
  6375. }
  6376. else {
  6377. element.addClass('fc-ltr');
  6378. }
  6379. if (options.theme) {
  6380. element.addClass('ui-widget');
  6381. }
  6382. else {
  6383. element.addClass('fc-unthemed');
  6384. }
  6385. content = $("<div class='fc-view-container'/>").prependTo(element);
  6386. header = t.header = new Header(t, options);
  6387. headerElement = header.render();
  6388. if (headerElement) {
  6389. element.prepend(headerElement);
  6390. }
  6391. renderView(options.defaultView);
  6392. if (options.handleWindowResize) {
  6393. windowResizeProxy = debounce(windowResize, options.windowResizeDelay); // prevents rapid calls
  6394. $(window).resize(windowResizeProxy);
  6395. }
  6396. }
  6397. function destroy() {
  6398. if (currentView) {
  6399. currentView.removeElement();
  6400. // NOTE: don't null-out currentView/t.view in case API methods are called after destroy.
  6401. // It is still the "current" view, just not rendered.
  6402. }
  6403. header.removeElement();
  6404. content.remove();
  6405. element.removeClass('fc fc-ltr fc-rtl fc-unthemed ui-widget');
  6406. if (windowResizeProxy) {
  6407. $(window).unbind('resize', windowResizeProxy);
  6408. }
  6409. }
  6410. function elementVisible() {
  6411. return element.is(':visible');
  6412. }
  6413. // View Rendering
  6414. // -----------------------------------------------------------------------------------
  6415. // Renders a view because of a date change, view-type change, or for the first time.
  6416. // If not given a viewType, keep the current view but render different dates.
  6417. function renderView(viewType) {
  6418. ignoreWindowResize++;
  6419. // if viewType is changing, remove the old view's rendering
  6420. if (currentView && viewType && currentView.type !== viewType) {
  6421. header.deactivateButton(currentView.type);
  6422. freezeContentHeight(); // prevent a scroll jump when view element is removed
  6423. currentView.removeElement();
  6424. currentView = t.view = null;
  6425. }
  6426. // if viewType changed, or the view was never created, create a fresh view
  6427. if (!currentView && viewType) {
  6428. currentView = t.view =
  6429. viewsByType[viewType] ||
  6430. (viewsByType[viewType] = t.instantiateView(viewType));
  6431. currentView.setElement(
  6432. $("<div class='fc-view fc-" + viewType + "-view' />").appendTo(content)
  6433. );
  6434. header.activateButton(viewType);
  6435. }
  6436. if (currentView) {
  6437. // in case the view should render a period of time that is completely hidden
  6438. date = currentView.massageCurrentDate(date);
  6439. // render or rerender the view
  6440. if (
  6441. !currentView.displaying ||
  6442. !date.isWithin(currentView.intervalStart, currentView.intervalEnd) // implicit date window change
  6443. ) {
  6444. if (elementVisible()) {
  6445. freezeContentHeight();
  6446. currentView.display(date);
  6447. unfreezeContentHeight(); // immediately unfreeze regardless of whether display is async
  6448. // need to do this after View::render, so dates are calculated
  6449. updateHeaderTitle();
  6450. updateTodayButton();
  6451. getAndRenderEvents();
  6452. }
  6453. }
  6454. }
  6455. unfreezeContentHeight(); // undo any lone freezeContentHeight calls
  6456. ignoreWindowResize--;
  6457. }
  6458. // Resizing
  6459. // -----------------------------------------------------------------------------------
  6460. t.getSuggestedViewHeight = function() {
  6461. if (suggestedViewHeight === undefined) {
  6462. calcSize();
  6463. }
  6464. return suggestedViewHeight;
  6465. };
  6466. t.isHeightAuto = function() {
  6467. return options.contentHeight === 'auto' || options.height === 'auto';
  6468. };
  6469. function updateSize(shouldRecalc) {
  6470. if (elementVisible()) {
  6471. if (shouldRecalc) {
  6472. _calcSize();
  6473. }
  6474. ignoreWindowResize++;
  6475. currentView.updateSize(true); // isResize=true. will poll getSuggestedViewHeight() and isHeightAuto()
  6476. ignoreWindowResize--;
  6477. return true; // signal success
  6478. }
  6479. }
  6480. function calcSize() {
  6481. if (elementVisible()) {
  6482. _calcSize();
  6483. }
  6484. }
  6485. function _calcSize() { // assumes elementVisible
  6486. if (typeof options.contentHeight === 'number') { // exists and not 'auto'
  6487. suggestedViewHeight = options.contentHeight;
  6488. }
  6489. else if (typeof options.height === 'number') { // exists and not 'auto'
  6490. suggestedViewHeight = options.height - (headerElement ? headerElement.outerHeight(true) : 0);
  6491. }
  6492. else {
  6493. suggestedViewHeight = Math.round(content.width() / Math.max(options.aspectRatio, .5));
  6494. }
  6495. }
  6496. function windowResize(ev) {
  6497. if (
  6498. !ignoreWindowResize &&
  6499. ev.target === window && // so we don't process jqui "resize" events that have bubbled up
  6500. currentView.start // view has already been rendered
  6501. ) {
  6502. if (updateSize(true)) {
  6503. currentView.trigger('windowResize', _element);
  6504. }
  6505. }
  6506. }
  6507. /* Event Fetching/Rendering
  6508. -----------------------------------------------------------------------------*/
  6509. // TODO: going forward, most of this stuff should be directly handled by the view
  6510. function refetchEvents() { // can be called as an API method
  6511. destroyEvents(); // so that events are cleared before user starts waiting for AJAX
  6512. fetchAndRenderEvents();
  6513. }
  6514. function renderEvents() { // destroys old events if previously rendered
  6515. if (elementVisible()) {
  6516. freezeContentHeight();
  6517. currentView.displayEvents(events);
  6518. unfreezeContentHeight();
  6519. }
  6520. }
  6521. function destroyEvents() {
  6522. freezeContentHeight();
  6523. currentView.clearEvents();
  6524. unfreezeContentHeight();
  6525. }
  6526. function getAndRenderEvents() {
  6527. if (!options.lazyFetching || isFetchNeeded(currentView.start, currentView.end)) {
  6528. fetchAndRenderEvents();
  6529. }
  6530. else {
  6531. renderEvents();
  6532. }
  6533. }
  6534. function fetchAndRenderEvents() {
  6535. fetchEvents(currentView.start, currentView.end);
  6536. // ... will call reportEvents
  6537. // ... which will call renderEvents
  6538. }
  6539. // called when event data arrives
  6540. function reportEvents(_events) {
  6541. events = _events;
  6542. renderEvents();
  6543. }
  6544. // called when a single event's data has been changed
  6545. function reportEventChange() {
  6546. renderEvents();
  6547. }
  6548. /* Header Updating
  6549. -----------------------------------------------------------------------------*/
  6550. function updateHeaderTitle() {
  6551. header.updateTitle(currentView.title);
  6552. }
  6553. function updateTodayButton() {
  6554. var now = t.getNow();
  6555. if (now.isWithin(currentView.intervalStart, currentView.intervalEnd)) {
  6556. header.disableButton('today');
  6557. }
  6558. else {
  6559. header.enableButton('today');
  6560. }
  6561. }
  6562. /* Selection
  6563. -----------------------------------------------------------------------------*/
  6564. function select(start, end) {
  6565. currentView.select(
  6566. t.buildSelectRange.apply(t, arguments)
  6567. );
  6568. }
  6569. function unselect() { // safe to be called before renderView
  6570. if (currentView) {
  6571. currentView.unselect();
  6572. }
  6573. }
  6574. /* Date
  6575. -----------------------------------------------------------------------------*/
  6576. function prev() {
  6577. date = currentView.computePrevDate(date);
  6578. renderView();
  6579. }
  6580. function next() {
  6581. date = currentView.computeNextDate(date);
  6582. renderView();
  6583. }
  6584. function prevYear() {
  6585. date.add(-1, 'years');
  6586. renderView();
  6587. }
  6588. function nextYear() {
  6589. date.add(1, 'years');
  6590. renderView();
  6591. }
  6592. function today() {
  6593. date = t.getNow();
  6594. renderView();
  6595. }
  6596. function gotoDate(dateInput) {
  6597. date = t.moment(dateInput);
  6598. renderView();
  6599. }
  6600. function incrementDate(delta) {
  6601. date.add(moment.duration(delta));
  6602. renderView();
  6603. }
  6604. // Forces navigation to a view for the given date.
  6605. // `viewType` can be a specific view name or a generic one like "week" or "day".
  6606. function zoomTo(newDate, viewType) {
  6607. var spec;
  6608. viewType = viewType || 'day'; // day is default zoom
  6609. spec = t.getViewSpec(viewType) || t.getUnitViewSpec(viewType);
  6610. date = newDate;
  6611. renderView(spec ? spec.type : null);
  6612. }
  6613. function getDate() {
  6614. return date.clone();
  6615. }
  6616. /* Height "Freezing"
  6617. -----------------------------------------------------------------------------*/
  6618. // TODO: move this into the view
  6619. function freezeContentHeight() {
  6620. content.css({
  6621. width: '100%',
  6622. height: content.height(),
  6623. overflow: 'hidden'
  6624. });
  6625. }
  6626. function unfreezeContentHeight() {
  6627. content.css({
  6628. width: '',
  6629. height: '',
  6630. overflow: ''
  6631. });
  6632. }
  6633. /* Misc
  6634. -----------------------------------------------------------------------------*/
  6635. function getCalendar() {
  6636. return t;
  6637. }
  6638. function getView() {
  6639. return currentView;
  6640. }
  6641. function option(name, value) {
  6642. if (value === undefined) {
  6643. return options[name];
  6644. }
  6645. if (name == 'height' || name == 'contentHeight' || name == 'aspectRatio') {
  6646. options[name] = value;
  6647. updateSize(true); // true = allow recalculation of height
  6648. }
  6649. }
  6650. function trigger(name, thisObj) { // overrides the Emitter's trigger method :(
  6651. var args = Array.prototype.slice.call(arguments, 2);
  6652. thisObj = thisObj || _element;
  6653. this.triggerWith(name, thisObj, args); // Emitter's method
  6654. if (options[name]) {
  6655. return options[name].apply(thisObj, args);
  6656. }
  6657. }
  6658. t.initialize();
  6659. }
  6660. ;;
  6661. Calendar.defaults = {
  6662. titleRangeSeparator: ' \u2014 ', // emphasized dash
  6663. monthYearFormat: 'MMMM YYYY', // required for en. other languages rely on datepicker computable option
  6664. defaultTimedEventDuration: '02:00:00',
  6665. defaultAllDayEventDuration: { days: 1 },
  6666. forceEventDuration: false,
  6667. nextDayThreshold: '09:00:00', // 9am
  6668. // display
  6669. defaultView: 'month',
  6670. aspectRatio: 1.35,
  6671. header: {
  6672. left: 'title',
  6673. center: '',
  6674. right: 'today prev,next'
  6675. },
  6676. weekends: true,
  6677. weekNumbers: false,
  6678. weekNumberTitle: 'W',
  6679. weekNumberCalculation: 'local',
  6680. //editable: false,
  6681. scrollTime: '06:00:00',
  6682. // event ajax
  6683. lazyFetching: true,
  6684. startParam: 'start',
  6685. endParam: 'end',
  6686. timezoneParam: 'timezone',
  6687. timezone: false,
  6688. //allDayDefault: undefined,
  6689. // locale
  6690. isRTL: false,
  6691. buttonText: {
  6692. prev: "prev",
  6693. next: "next",
  6694. prevYear: "prev year",
  6695. nextYear: "next year",
  6696. year: 'year', // TODO: locale files need to specify this
  6697. today: 'today',
  6698. month: 'month',
  6699. week: 'week',
  6700. day: 'day'
  6701. },
  6702. buttonIcons: {
  6703. prev: 'left-single-arrow',
  6704. next: 'right-single-arrow',
  6705. prevYear: 'left-double-arrow',
  6706. nextYear: 'right-double-arrow'
  6707. },
  6708. // jquery-ui theming
  6709. theme: false,
  6710. themeButtonIcons: {
  6711. prev: 'circle-triangle-w',
  6712. next: 'circle-triangle-e',
  6713. prevYear: 'seek-prev',
  6714. nextYear: 'seek-next'
  6715. },
  6716. //eventResizableFromStart: false,
  6717. dragOpacity: .75,
  6718. dragRevertDuration: 500,
  6719. dragScroll: true,
  6720. //selectable: false,
  6721. unselectAuto: true,
  6722. dropAccept: '*',
  6723. eventOrder: 'title',
  6724. eventLimit: false,
  6725. eventLimitText: 'more',
  6726. eventLimitClick: 'popover',
  6727. dayPopoverFormat: 'LL',
  6728. handleWindowResize: true,
  6729. windowResizeDelay: 200 // milliseconds before an updateSize happens
  6730. };
  6731. Calendar.englishDefaults = { // used by lang.js
  6732. dayPopoverFormat: 'dddd, MMMM D'
  6733. };
  6734. Calendar.rtlDefaults = { // right-to-left defaults
  6735. header: { // TODO: smarter solution (first/center/last ?)
  6736. left: 'next,prev today',
  6737. center: '',
  6738. right: 'title'
  6739. },
  6740. buttonIcons: {
  6741. prev: 'right-single-arrow',
  6742. next: 'left-single-arrow',
  6743. prevYear: 'right-double-arrow',
  6744. nextYear: 'left-double-arrow'
  6745. },
  6746. themeButtonIcons: {
  6747. prev: 'circle-triangle-e',
  6748. next: 'circle-triangle-w',
  6749. nextYear: 'seek-prev',
  6750. prevYear: 'seek-next'
  6751. }
  6752. };
  6753. ;;
  6754. var langOptionHash = fc.langs = {}; // initialize and expose
  6755. // TODO: document the structure and ordering of a FullCalendar lang file
  6756. // TODO: rename everything "lang" to "locale", like what the moment project did
  6757. // Initialize jQuery UI datepicker translations while using some of the translations
  6758. // Will set this as the default language for datepicker.
  6759. fc.datepickerLang = function(langCode, dpLangCode, dpOptions) {
  6760. // get the FullCalendar internal option hash for this language. create if necessary
  6761. var fcOptions = langOptionHash[langCode] || (langOptionHash[langCode] = {});
  6762. // transfer some simple options from datepicker to fc
  6763. fcOptions.isRTL = dpOptions.isRTL;
  6764. fcOptions.weekNumberTitle = dpOptions.weekHeader;
  6765. // compute some more complex options from datepicker
  6766. $.each(dpComputableOptions, function(name, func) {
  6767. fcOptions[name] = func(dpOptions);
  6768. });
  6769. // is jQuery UI Datepicker is on the page?
  6770. if ($.datepicker) {
  6771. // Register the language data.
  6772. // FullCalendar and MomentJS use language codes like "pt-br" but Datepicker
  6773. // does it like "pt-BR" or if it doesn't have the language, maybe just "pt".
  6774. // Make an alias so the language can be referenced either way.
  6775. $.datepicker.regional[dpLangCode] =
  6776. $.datepicker.regional[langCode] = // alias
  6777. dpOptions;
  6778. // Alias 'en' to the default language data. Do this every time.
  6779. $.datepicker.regional.en = $.datepicker.regional[''];
  6780. // Set as Datepicker's global defaults.
  6781. $.datepicker.setDefaults(dpOptions);
  6782. }
  6783. };
  6784. // Sets FullCalendar-specific translations. Will set the language as the global default.
  6785. fc.lang = function(langCode, newFcOptions) {
  6786. var fcOptions;
  6787. var momOptions;
  6788. // get the FullCalendar internal option hash for this language. create if necessary
  6789. fcOptions = langOptionHash[langCode] || (langOptionHash[langCode] = {});
  6790. // provided new options for this language? merge them in
  6791. if (newFcOptions) {
  6792. fcOptions = langOptionHash[langCode] = mergeOptions([ fcOptions, newFcOptions ]);
  6793. }
  6794. // compute language options that weren't defined.
  6795. // always do this. newFcOptions can be undefined when initializing from i18n file,
  6796. // so no way to tell if this is an initialization or a default-setting.
  6797. momOptions = getMomentLocaleData(langCode); // will fall back to en
  6798. $.each(momComputableOptions, function(name, func) {
  6799. if (fcOptions[name] == null) {
  6800. fcOptions[name] = func(momOptions, fcOptions);
  6801. }
  6802. });
  6803. // set it as the default language for FullCalendar
  6804. Calendar.defaults.lang = langCode;
  6805. };
  6806. // NOTE: can't guarantee any of these computations will run because not every language has datepicker
  6807. // configs, so make sure there are English fallbacks for these in the defaults file.
  6808. var dpComputableOptions = {
  6809. buttonText: function(dpOptions) {
  6810. return {
  6811. // the translations sometimes wrongly contain HTML entities
  6812. prev: stripHtmlEntities(dpOptions.prevText),
  6813. next: stripHtmlEntities(dpOptions.nextText),
  6814. today: stripHtmlEntities(dpOptions.currentText)
  6815. };
  6816. },
  6817. // Produces format strings like "MMMM YYYY" -> "September 2014"
  6818. monthYearFormat: function(dpOptions) {
  6819. return dpOptions.showMonthAfterYear ?
  6820. 'YYYY[' + dpOptions.yearSuffix + '] MMMM' :
  6821. 'MMMM YYYY[' + dpOptions.yearSuffix + ']';
  6822. }
  6823. };
  6824. var momComputableOptions = {
  6825. // Produces format strings like "ddd M/D" -> "Fri 9/15"
  6826. dayOfMonthFormat: function(momOptions, fcOptions) {
  6827. var format = momOptions.longDateFormat('l'); // for the format like "M/D/YYYY"
  6828. // strip the year off the edge, as well as other misc non-whitespace chars
  6829. format = format.replace(/^Y+[^\w\s]*|[^\w\s]*Y+$/g, '');
  6830. if (fcOptions.isRTL) {
  6831. format += ' ddd'; // for RTL, add day-of-week to end
  6832. }
  6833. else {
  6834. format = 'ddd ' + format; // for LTR, add day-of-week to beginning
  6835. }
  6836. return format;
  6837. },
  6838. // Produces format strings like "h:mma" -> "6:00pm"
  6839. mediumTimeFormat: function(momOptions) { // can't be called `timeFormat` because collides with option
  6840. return momOptions.longDateFormat('LT')
  6841. .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
  6842. },
  6843. // Produces format strings like "h(:mm)a" -> "6pm" / "6:30pm"
  6844. smallTimeFormat: function(momOptions) {
  6845. return momOptions.longDateFormat('LT')
  6846. .replace(':mm', '(:mm)')
  6847. .replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs
  6848. .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
  6849. },
  6850. // Produces format strings like "h(:mm)t" -> "6p" / "6:30p"
  6851. extraSmallTimeFormat: function(momOptions) {
  6852. return momOptions.longDateFormat('LT')
  6853. .replace(':mm', '(:mm)')
  6854. .replace(/(\Wmm)$/, '($1)') // like above, but for foreign langs
  6855. .replace(/\s*a$/i, 't'); // convert to AM/PM/am/pm to lowercase one-letter. remove any spaces beforehand
  6856. },
  6857. // Produces format strings like "ha" / "H" -> "6pm" / "18"
  6858. hourFormat: function(momOptions) {
  6859. return momOptions.longDateFormat('LT')
  6860. .replace(':mm', '')
  6861. .replace(/(\Wmm)$/, '') // like above, but for foreign langs
  6862. .replace(/\s*a$/i, 'a'); // convert AM/PM/am/pm to lowercase. remove any spaces beforehand
  6863. },
  6864. // Produces format strings like "h:mm" -> "6:30" (with no AM/PM)
  6865. noMeridiemTimeFormat: function(momOptions) {
  6866. return momOptions.longDateFormat('LT')
  6867. .replace(/\s*a$/i, ''); // remove trailing AM/PM
  6868. }
  6869. };
  6870. // options that should be computed off live calendar options (considers override options)
  6871. var instanceComputableOptions = { // TODO: best place for this? related to lang?
  6872. // Produces format strings for results like "Mo 16"
  6873. smallDayDateFormat: function(options) {
  6874. return options.isRTL ?
  6875. 'D dd' :
  6876. 'dd D';
  6877. },
  6878. // Produces format strings for results like "Wk 5"
  6879. weekFormat: function(options) {
  6880. return options.isRTL ?
  6881. 'w[ ' + options.weekNumberTitle + ']' :
  6882. '[' + options.weekNumberTitle + ' ]w';
  6883. },
  6884. // Produces format strings for results like "Wk5"
  6885. smallWeekFormat: function(options) {
  6886. return options.isRTL ?
  6887. 'w[' + options.weekNumberTitle + ']' :
  6888. '[' + options.weekNumberTitle + ']w';
  6889. }
  6890. };
  6891. function populateInstanceComputableOptions(options) {
  6892. $.each(instanceComputableOptions, function(name, func) {
  6893. if (options[name] == null) {
  6894. options[name] = func(options);
  6895. }
  6896. });
  6897. }
  6898. // Returns moment's internal locale data. If doesn't exist, returns English.
  6899. // Works with moment-pre-2.8
  6900. function getMomentLocaleData(langCode) {
  6901. var func = moment.localeData || moment.langData;
  6902. return func.call(moment, langCode) ||
  6903. func.call(moment, 'en'); // the newer localData could return null, so fall back to en
  6904. }
  6905. // Initialize English by forcing computation of moment-derived options.
  6906. // Also, sets it as the default.
  6907. fc.lang('en', Calendar.englishDefaults);
  6908. ;;
  6909. /* Top toolbar area with buttons and title
  6910. ----------------------------------------------------------------------------------------------------------------------*/
  6911. // TODO: rename all header-related things to "toolbar"
  6912. function Header(calendar, options) {
  6913. var t = this;
  6914. // exports
  6915. t.render = render;
  6916. t.removeElement = removeElement;
  6917. t.updateTitle = updateTitle;
  6918. t.activateButton = activateButton;
  6919. t.deactivateButton = deactivateButton;
  6920. t.disableButton = disableButton;
  6921. t.enableButton = enableButton;
  6922. t.getViewsWithButtons = getViewsWithButtons;
  6923. // locals
  6924. var el = $();
  6925. var viewsWithButtons = [];
  6926. var tm;
  6927. function render() {
  6928. var sections = options.header;
  6929. tm = options.theme ? 'ui' : 'fc';
  6930. if (sections) {
  6931. el = $("<div class='fc-toolbar'/>")
  6932. .append(renderSection('left'))
  6933. .append(renderSection('right'))
  6934. .append(renderSection('center'))
  6935. .append('<div class="fc-clear"/>');
  6936. return el;
  6937. }
  6938. }
  6939. function removeElement() {
  6940. el.remove();
  6941. el = $();
  6942. }
  6943. function renderSection(position) {
  6944. var sectionEl = $('<div class="fc-' + position + '"/>');
  6945. var buttonStr = options.header[position];
  6946. if (buttonStr) {
  6947. $.each(buttonStr.split(' '), function(i) {
  6948. var groupChildren = $();
  6949. var isOnlyButtons = true;
  6950. var groupEl;
  6951. $.each(this.split(','), function(j, buttonName) {
  6952. var customButtonProps;
  6953. var viewSpec;
  6954. var buttonClick;
  6955. var overrideText; // text explicitly set by calendar's constructor options. overcomes icons
  6956. var defaultText;
  6957. var themeIcon;
  6958. var normalIcon;
  6959. var innerHtml;
  6960. var classes;
  6961. var button; // the element
  6962. if (buttonName == 'title') {
  6963. groupChildren = groupChildren.add($('<h2>&nbsp;</h2>')); // we always want it to take up height
  6964. isOnlyButtons = false;
  6965. }
  6966. else {
  6967. if ((customButtonProps = (calendar.options.customButtons || {})[buttonName])) {
  6968. buttonClick = function(ev) {
  6969. if (customButtonProps.click) {
  6970. customButtonProps.click.call(button[0], ev);
  6971. }
  6972. };
  6973. overrideText = ''; // icons will override text
  6974. defaultText = customButtonProps.text;
  6975. }
  6976. else if ((viewSpec = calendar.getViewSpec(buttonName))) {
  6977. buttonClick = function() {
  6978. calendar.changeView(buttonName);
  6979. };
  6980. viewsWithButtons.push(buttonName);
  6981. overrideText = viewSpec.buttonTextOverride;
  6982. defaultText = viewSpec.buttonTextDefault;
  6983. }
  6984. else if (calendar[buttonName]) { // a calendar method
  6985. buttonClick = function() {
  6986. calendar[buttonName]();
  6987. };
  6988. overrideText = (calendar.overrides.buttonText || {})[buttonName];
  6989. defaultText = options.buttonText[buttonName]; // everything else is considered default
  6990. }
  6991. if (buttonClick) {
  6992. themeIcon =
  6993. customButtonProps ?
  6994. customButtonProps.themeIcon :
  6995. options.themeButtonIcons[buttonName];
  6996. normalIcon =
  6997. customButtonProps ?
  6998. customButtonProps.icon :
  6999. options.buttonIcons[buttonName];
  7000. if (overrideText) {
  7001. innerHtml = htmlEscape(overrideText);
  7002. }
  7003. else if (themeIcon && options.theme) {
  7004. innerHtml = "<span class='ui-icon ui-icon-" + themeIcon + "'></span>";
  7005. }
  7006. else if (normalIcon && !options.theme) {
  7007. innerHtml = "<span class='fc-icon fc-icon-" + normalIcon + "'></span>";
  7008. }
  7009. else {
  7010. innerHtml = htmlEscape(defaultText);
  7011. }
  7012. classes = [
  7013. 'fc-' + buttonName + '-button',
  7014. tm + '-button',
  7015. tm + '-state-default'
  7016. ];
  7017. button = $( // type="button" so that it doesn't submit a form
  7018. '<button type="button" class="' + classes.join(' ') + '">' +
  7019. innerHtml +
  7020. '</button>'
  7021. )
  7022. .click(function(ev) {
  7023. // don't process clicks for disabled buttons
  7024. if (!button.hasClass(tm + '-state-disabled')) {
  7025. buttonClick(ev);
  7026. // after the click action, if the button becomes the "active" tab, or disabled,
  7027. // it should never have a hover class, so remove it now.
  7028. if (
  7029. button.hasClass(tm + '-state-active') ||
  7030. button.hasClass(tm + '-state-disabled')
  7031. ) {
  7032. button.removeClass(tm + '-state-hover');
  7033. }
  7034. }
  7035. })
  7036. .mousedown(function() {
  7037. // the *down* effect (mouse pressed in).
  7038. // only on buttons that are not the "active" tab, or disabled
  7039. button
  7040. .not('.' + tm + '-state-active')
  7041. .not('.' + tm + '-state-disabled')
  7042. .addClass(tm + '-state-down');
  7043. })
  7044. .mouseup(function() {
  7045. // undo the *down* effect
  7046. button.removeClass(tm + '-state-down');
  7047. })
  7048. .hover(
  7049. function() {
  7050. // the *hover* effect.
  7051. // only on buttons that are not the "active" tab, or disabled
  7052. button
  7053. .not('.' + tm + '-state-active')
  7054. .not('.' + tm + '-state-disabled')
  7055. .addClass(tm + '-state-hover');
  7056. },
  7057. function() {
  7058. // undo the *hover* effect
  7059. button
  7060. .removeClass(tm + '-state-hover')
  7061. .removeClass(tm + '-state-down'); // if mouseleave happens before mouseup
  7062. }
  7063. );
  7064. groupChildren = groupChildren.add(button);
  7065. }
  7066. }
  7067. });
  7068. if (isOnlyButtons) {
  7069. groupChildren
  7070. .first().addClass(tm + '-corner-left').end()
  7071. .last().addClass(tm + '-corner-right').end();
  7072. }
  7073. if (groupChildren.length > 1) {
  7074. groupEl = $('<div/>');
  7075. if (isOnlyButtons) {
  7076. groupEl.addClass('fc-button-group');
  7077. }
  7078. groupEl.append(groupChildren);
  7079. sectionEl.append(groupEl);
  7080. }
  7081. else {
  7082. sectionEl.append(groupChildren); // 1 or 0 children
  7083. }
  7084. });
  7085. }
  7086. return sectionEl;
  7087. }
  7088. function updateTitle(text) {
  7089. el.find('h2').text(text);
  7090. }
  7091. function activateButton(buttonName) {
  7092. el.find('.fc-' + buttonName + '-button')
  7093. .addClass(tm + '-state-active');
  7094. }
  7095. function deactivateButton(buttonName) {
  7096. el.find('.fc-' + buttonName + '-button')
  7097. .removeClass(tm + '-state-active');
  7098. }
  7099. function disableButton(buttonName) {
  7100. el.find('.fc-' + buttonName + '-button')
  7101. .attr('disabled', 'disabled')
  7102. .addClass(tm + '-state-disabled');
  7103. }
  7104. function enableButton(buttonName) {
  7105. el.find('.fc-' + buttonName + '-button')
  7106. .removeAttr('disabled')
  7107. .removeClass(tm + '-state-disabled');
  7108. }
  7109. function getViewsWithButtons() {
  7110. return viewsWithButtons;
  7111. }
  7112. }
  7113. ;;
  7114. fc.sourceNormalizers = [];
  7115. fc.sourceFetchers = [];
  7116. var ajaxDefaults = {
  7117. dataType: 'json',
  7118. cache: false
  7119. };
  7120. var eventGUID = 1;
  7121. function EventManager(options) { // assumed to be a calendar
  7122. var t = this;
  7123. // exports
  7124. t.isFetchNeeded = isFetchNeeded;
  7125. t.fetchEvents = fetchEvents;
  7126. t.addEventSource = addEventSource;
  7127. t.removeEventSource = removeEventSource;
  7128. t.updateEvent = updateEvent;
  7129. t.renderEvent = renderEvent;
  7130. t.removeEvents = removeEvents;
  7131. t.clientEvents = clientEvents;
  7132. t.mutateEvent = mutateEvent;
  7133. t.normalizeEventRange = normalizeEventRange;
  7134. t.normalizeEventRangeTimes = normalizeEventRangeTimes;
  7135. t.ensureVisibleEventRange = ensureVisibleEventRange;
  7136. // imports
  7137. var reportEvents = t.reportEvents;
  7138. // locals
  7139. var stickySource = { events: [] };
  7140. var sources = [ stickySource ];
  7141. var rangeStart, rangeEnd;
  7142. var currentFetchID = 0;
  7143. var pendingSourceCnt = 0;
  7144. var cache = []; // holds events that have already been expanded
  7145. $.each(
  7146. (options.events ? [ options.events ] : []).concat(options.eventSources || []),
  7147. function(i, sourceInput) {
  7148. var source = buildEventSource(sourceInput);
  7149. if (source) {
  7150. sources.push(source);
  7151. }
  7152. }
  7153. );
  7154. /* Fetching
  7155. -----------------------------------------------------------------------------*/
  7156. function isFetchNeeded(start, end) {
  7157. return !rangeStart || // nothing has been fetched yet?
  7158. // or, a part of the new range is outside of the old range? (after normalizing)
  7159. start.clone().stripZone() < rangeStart.clone().stripZone() ||
  7160. end.clone().stripZone() > rangeEnd.clone().stripZone();
  7161. }
  7162. function fetchEvents(start, end) {
  7163. rangeStart = start;
  7164. rangeEnd = end;
  7165. cache = [];
  7166. var fetchID = ++currentFetchID;
  7167. var len = sources.length;
  7168. pendingSourceCnt = len;
  7169. for (var i=0; i<len; i++) {
  7170. fetchEventSource(sources[i], fetchID);
  7171. }
  7172. }
  7173. function fetchEventSource(source, fetchID) {
  7174. _fetchEventSource(source, function(eventInputs) {
  7175. var isArraySource = $.isArray(source.events);
  7176. var i, eventInput;
  7177. var abstractEvent;
  7178. if (fetchID == currentFetchID) {
  7179. if (eventInputs) {
  7180. for (i = 0; i < eventInputs.length; i++) {
  7181. eventInput = eventInputs[i];
  7182. if (isArraySource) { // array sources have already been convert to Event Objects
  7183. abstractEvent = eventInput;
  7184. }
  7185. else {
  7186. abstractEvent = buildEventFromInput(eventInput, source);
  7187. }
  7188. if (abstractEvent) { // not false (an invalid event)
  7189. cache.push.apply(
  7190. cache,
  7191. expandEvent(abstractEvent) // add individual expanded events to the cache
  7192. );
  7193. }
  7194. }
  7195. }
  7196. pendingSourceCnt--;
  7197. if (!pendingSourceCnt) {
  7198. reportEvents(cache);
  7199. }
  7200. }
  7201. });
  7202. }
  7203. function _fetchEventSource(source, callback) {
  7204. var i;
  7205. var fetchers = fc.sourceFetchers;
  7206. var res;
  7207. for (i=0; i<fetchers.length; i++) {
  7208. res = fetchers[i].call(
  7209. t, // this, the Calendar object
  7210. source,
  7211. rangeStart.clone(),
  7212. rangeEnd.clone(),
  7213. options.timezone,
  7214. callback
  7215. );
  7216. if (res === true) {
  7217. // the fetcher is in charge. made its own async request
  7218. return;
  7219. }
  7220. else if (typeof res == 'object') {
  7221. // the fetcher returned a new source. process it
  7222. _fetchEventSource(res, callback);
  7223. return;
  7224. }
  7225. }
  7226. var events = source.events;
  7227. if (events) {
  7228. if ($.isFunction(events)) {
  7229. t.pushLoading();
  7230. events.call(
  7231. t, // this, the Calendar object
  7232. rangeStart.clone(),
  7233. rangeEnd.clone(),
  7234. options.timezone,
  7235. function(events) {
  7236. callback(events);
  7237. t.popLoading();
  7238. }
  7239. );
  7240. }
  7241. else if ($.isArray(events)) {
  7242. callback(events);
  7243. }
  7244. else {
  7245. callback();
  7246. }
  7247. }else{
  7248. var url = source.url;
  7249. if (url) {
  7250. var success = source.success;
  7251. var error = source.error;
  7252. var complete = source.complete;
  7253. // retrieve any outbound GET/POST $.ajax data from the options
  7254. var customData;
  7255. if ($.isFunction(source.data)) {
  7256. // supplied as a function that returns a key/value object
  7257. customData = source.data();
  7258. }
  7259. else {
  7260. // supplied as a straight key/value object
  7261. customData = source.data;
  7262. }
  7263. // use a copy of the custom data so we can modify the parameters
  7264. // and not affect the passed-in object.
  7265. var data = $.extend({}, customData || {});
  7266. var startParam = firstDefined(source.startParam, options.startParam);
  7267. var endParam = firstDefined(source.endParam, options.endParam);
  7268. var timezoneParam = firstDefined(source.timezoneParam, options.timezoneParam);
  7269. if (startParam) {
  7270. data[startParam] = rangeStart.format();
  7271. }
  7272. if (endParam) {
  7273. data[endParam] = rangeEnd.format();
  7274. }
  7275. if (options.timezone && options.timezone != 'local') {
  7276. data[timezoneParam] = options.timezone;
  7277. }
  7278. t.pushLoading();
  7279. $.ajax($.extend({}, ajaxDefaults, source, {
  7280. data: data,
  7281. success: function(events) {
  7282. events = events || [];
  7283. var res = applyAll(success, this, arguments);
  7284. if ($.isArray(res)) {
  7285. events = res;
  7286. }
  7287. callback(events);
  7288. },
  7289. error: function() {
  7290. applyAll(error, this, arguments);
  7291. callback();
  7292. },
  7293. complete: function() {
  7294. applyAll(complete, this, arguments);
  7295. t.popLoading();
  7296. }
  7297. }));
  7298. }else{
  7299. callback();
  7300. }
  7301. }
  7302. }
  7303. /* Sources
  7304. -----------------------------------------------------------------------------*/
  7305. function addEventSource(sourceInput) {
  7306. var source = buildEventSource(sourceInput);
  7307. if (source) {
  7308. sources.push(source);
  7309. pendingSourceCnt++;
  7310. fetchEventSource(source, currentFetchID); // will eventually call reportEvents
  7311. }
  7312. }
  7313. function buildEventSource(sourceInput) { // will return undefined if invalid source
  7314. var normalizers = fc.sourceNormalizers;
  7315. var source;
  7316. var i;
  7317. if ($.isFunction(sourceInput) || $.isArray(sourceInput)) {
  7318. source = { events: sourceInput };
  7319. }
  7320. else if (typeof sourceInput === 'string') {
  7321. source = { url: sourceInput };
  7322. }
  7323. else if (typeof sourceInput === 'object') {
  7324. source = $.extend({}, sourceInput); // shallow copy
  7325. }
  7326. if (source) {
  7327. // TODO: repeat code, same code for event classNames
  7328. if (source.className) {
  7329. if (typeof source.className === 'string') {
  7330. source.className = source.className.split(/\s+/);
  7331. }
  7332. // otherwise, assumed to be an array
  7333. }
  7334. else {
  7335. source.className = [];
  7336. }
  7337. // for array sources, we convert to standard Event Objects up front
  7338. if ($.isArray(source.events)) {
  7339. source.origArray = source.events; // for removeEventSource
  7340. source.events = $.map(source.events, function(eventInput) {
  7341. return buildEventFromInput(eventInput, source);
  7342. });
  7343. }
  7344. for (i=0; i<normalizers.length; i++) {
  7345. normalizers[i].call(t, source);
  7346. }
  7347. return source;
  7348. }
  7349. }
  7350. function removeEventSource(source) {
  7351. sources = $.grep(sources, function(src) {
  7352. return !isSourcesEqual(src, source);
  7353. });
  7354. // remove all client events from that source
  7355. cache = $.grep(cache, function(e) {
  7356. return !isSourcesEqual(e.source, source);
  7357. });
  7358. reportEvents(cache);
  7359. }
  7360. function isSourcesEqual(source1, source2) {
  7361. return source1 && source2 && getSourcePrimitive(source1) == getSourcePrimitive(source2);
  7362. }
  7363. function getSourcePrimitive(source) {
  7364. return (
  7365. (typeof source === 'object') ? // a normalized event source?
  7366. (source.origArray || source.googleCalendarId || source.url || source.events) : // get the primitive
  7367. null
  7368. ) ||
  7369. source; // the given argument *is* the primitive
  7370. }
  7371. /* Manipulation
  7372. -----------------------------------------------------------------------------*/
  7373. // Only ever called from the externally-facing API
  7374. function updateEvent(event) {
  7375. // massage start/end values, even if date string values
  7376. event.start = t.moment(event.start);
  7377. if (event.end) {
  7378. event.end = t.moment(event.end);
  7379. }
  7380. else {
  7381. event.end = null;
  7382. }
  7383. mutateEvent(event, getMiscEventProps(event)); // will handle start/end/allDay normalization
  7384. reportEvents(cache); // reports event modifications (so we can redraw)
  7385. }
  7386. // Returns a hash of misc event properties that should be copied over to related events.
  7387. function getMiscEventProps(event) {
  7388. var props = {};
  7389. $.each(event, function(name, val) {
  7390. if (isMiscEventPropName(name)) {
  7391. if (val !== undefined && isAtomic(val)) { // a defined non-object
  7392. props[name] = val;
  7393. }
  7394. }
  7395. });
  7396. return props;
  7397. }
  7398. // non-date-related, non-id-related, non-secret
  7399. function isMiscEventPropName(name) {
  7400. return !/^_|^(id|allDay|start|end)$/.test(name);
  7401. }
  7402. // returns the expanded events that were created
  7403. function renderEvent(eventInput, stick) {
  7404. var abstractEvent = buildEventFromInput(eventInput);
  7405. var events;
  7406. var i, event;
  7407. if (abstractEvent) { // not false (a valid input)
  7408. events = expandEvent(abstractEvent);
  7409. for (i = 0; i < events.length; i++) {
  7410. event = events[i];
  7411. if (!event.source) {
  7412. if (stick) {
  7413. stickySource.events.push(event);
  7414. event.source = stickySource;
  7415. }
  7416. cache.push(event);
  7417. }
  7418. }
  7419. reportEvents(cache);
  7420. return events;
  7421. }
  7422. return [];
  7423. }
  7424. function removeEvents(filter) {
  7425. var eventID;
  7426. var i;
  7427. if (filter == null) { // null or undefined. remove all events
  7428. filter = function() { return true; }; // will always match
  7429. }
  7430. else if (!$.isFunction(filter)) { // an event ID
  7431. eventID = filter + '';
  7432. filter = function(event) {
  7433. return event._id == eventID;
  7434. };
  7435. }
  7436. // Purge event(s) from our local cache
  7437. cache = $.grep(cache, filter, true); // inverse=true
  7438. // Remove events from array sources.
  7439. // This works because they have been converted to official Event Objects up front.
  7440. // (and as a result, event._id has been calculated).
  7441. for (i=0; i<sources.length; i++) {
  7442. if ($.isArray(sources[i].events)) {
  7443. sources[i].events = $.grep(sources[i].events, filter, true);
  7444. }
  7445. }
  7446. reportEvents(cache);
  7447. }
  7448. function clientEvents(filter) {
  7449. if ($.isFunction(filter)) {
  7450. return $.grep(cache, filter);
  7451. }
  7452. else if (filter != null) { // not null, not undefined. an event ID
  7453. filter += '';
  7454. return $.grep(cache, function(e) {
  7455. return e._id == filter;
  7456. });
  7457. }
  7458. return cache; // else, return all
  7459. }
  7460. /* Event Normalization
  7461. -----------------------------------------------------------------------------*/
  7462. // Given a raw object with key/value properties, returns an "abstract" Event object.
  7463. // An "abstract" event is an event that, if recurring, will not have been expanded yet.
  7464. // Will return `false` when input is invalid.
  7465. // `source` is optional
  7466. function buildEventFromInput(input, source) {
  7467. var out = {};
  7468. var start, end;
  7469. var allDay;
  7470. if (options.eventDataTransform) {
  7471. input = options.eventDataTransform(input);
  7472. }
  7473. if (source && source.eventDataTransform) {
  7474. input = source.eventDataTransform(input);
  7475. }
  7476. // Copy all properties over to the resulting object.
  7477. // The special-case properties will be copied over afterwards.
  7478. $.extend(out, input);
  7479. if (source) {
  7480. out.source = source;
  7481. }
  7482. out._id = input._id || (input.id === undefined ? '_fc' + eventGUID++ : input.id + '');
  7483. if (input.className) {
  7484. if (typeof input.className == 'string') {
  7485. out.className = input.className.split(/\s+/);
  7486. }
  7487. else { // assumed to be an array
  7488. out.className = input.className;
  7489. }
  7490. }
  7491. else {
  7492. out.className = [];
  7493. }
  7494. start = input.start || input.date; // "date" is an alias for "start"
  7495. end = input.end;
  7496. // parse as a time (Duration) if applicable
  7497. if (isTimeString(start)) {
  7498. start = moment.duration(start);
  7499. }
  7500. if (isTimeString(end)) {
  7501. end = moment.duration(end);
  7502. }
  7503. if (input.dow || moment.isDuration(start) || moment.isDuration(end)) {
  7504. // the event is "abstract" (recurring) so don't calculate exact start/end dates just yet
  7505. out.start = start ? moment.duration(start) : null; // will be a Duration or null
  7506. out.end = end ? moment.duration(end) : null; // will be a Duration or null
  7507. out._recurring = true; // our internal marker
  7508. }
  7509. else {
  7510. if (start) {
  7511. start = t.moment(start);
  7512. if (!start.isValid()) {
  7513. return false;
  7514. }
  7515. }
  7516. if (end) {
  7517. end = t.moment(end);
  7518. if (!end.isValid()) {
  7519. end = null; // let defaults take over
  7520. }
  7521. }
  7522. allDay = input.allDay;
  7523. if (allDay === undefined) { // still undefined? fallback to default
  7524. allDay = firstDefined(
  7525. source ? source.allDayDefault : undefined,
  7526. options.allDayDefault
  7527. );
  7528. // still undefined? normalizeEventRange will calculate it
  7529. }
  7530. assignDatesToEvent(start, end, allDay, out);
  7531. }
  7532. return out;
  7533. }
  7534. // Normalizes and assigns the given dates to the given partially-formed event object.
  7535. // NOTE: mutates the given start/end moments. does not make a copy.
  7536. function assignDatesToEvent(start, end, allDay, event) {
  7537. event.start = start;
  7538. event.end = end;
  7539. event.allDay = allDay;
  7540. normalizeEventRange(event);
  7541. backupEventDates(event);
  7542. }
  7543. // Ensures proper values for allDay/start/end. Accepts an Event object, or a plain object with event-ish properties.
  7544. // NOTE: Will modify the given object.
  7545. function normalizeEventRange(props) {
  7546. normalizeEventRangeTimes(props);
  7547. if (props.end && !props.end.isAfter(props.start)) {
  7548. props.end = null;
  7549. }
  7550. if (!props.end) {
  7551. if (options.forceEventDuration) {
  7552. props.end = t.getDefaultEventEnd(props.allDay, props.start);
  7553. }
  7554. else {
  7555. props.end = null;
  7556. }
  7557. }
  7558. }
  7559. // Ensures the allDay property exists and the timeliness of the start/end dates are consistent
  7560. function normalizeEventRangeTimes(range) {
  7561. if (range.allDay == null) {
  7562. range.allDay = !(range.start.hasTime() || (range.end && range.end.hasTime()));
  7563. }
  7564. if (range.allDay) {
  7565. range.start.stripTime();
  7566. if (range.end) {
  7567. // TODO: consider nextDayThreshold here? If so, will require a lot of testing and adjustment
  7568. range.end.stripTime();
  7569. }
  7570. }
  7571. else {
  7572. if (!range.start.hasTime()) {
  7573. range.start = t.rezoneDate(range.start); // will assign a 00:00 time
  7574. }
  7575. if (range.end && !range.end.hasTime()) {
  7576. range.end = t.rezoneDate(range.end); // will assign a 00:00 time
  7577. }
  7578. }
  7579. }
  7580. // If `range` is a proper range with a start and end, returns the original object.
  7581. // If missing an end, computes a new range with an end, computing it as if it were an event.
  7582. // TODO: make this a part of the event -> eventRange system
  7583. function ensureVisibleEventRange(range) {
  7584. var allDay;
  7585. if (!range.end) {
  7586. allDay = range.allDay; // range might be more event-ish than we think
  7587. if (allDay == null) {
  7588. allDay = !range.start.hasTime();
  7589. }
  7590. range = $.extend({}, range); // make a copy, copying over other misc properties
  7591. range.end = t.getDefaultEventEnd(allDay, range.start);
  7592. }
  7593. return range;
  7594. }
  7595. // If the given event is a recurring event, break it down into an array of individual instances.
  7596. // If not a recurring event, return an array with the single original event.
  7597. // If given a falsy input (probably because of a failed buildEventFromInput call), returns an empty array.
  7598. // HACK: can override the recurring window by providing custom rangeStart/rangeEnd (for businessHours).
  7599. function expandEvent(abstractEvent, _rangeStart, _rangeEnd) {
  7600. var events = [];
  7601. var dowHash;
  7602. var dow;
  7603. var i;
  7604. var date;
  7605. var startTime, endTime;
  7606. var start, end;
  7607. var event;
  7608. _rangeStart = _rangeStart || rangeStart;
  7609. _rangeEnd = _rangeEnd || rangeEnd;
  7610. if (abstractEvent) {
  7611. if (abstractEvent._recurring) {
  7612. // make a boolean hash as to whether the event occurs on each day-of-week
  7613. if ((dow = abstractEvent.dow)) {
  7614. dowHash = {};
  7615. for (i = 0; i < dow.length; i++) {
  7616. dowHash[dow[i]] = true;
  7617. }
  7618. }
  7619. // iterate through every day in the current range
  7620. date = _rangeStart.clone().stripTime(); // holds the date of the current day
  7621. while (date.isBefore(_rangeEnd)) {
  7622. if (!dowHash || dowHash[date.day()]) { // if everyday, or this particular day-of-week
  7623. startTime = abstractEvent.start; // the stored start and end properties are times (Durations)
  7624. endTime = abstractEvent.end; // "
  7625. start = date.clone();
  7626. end = null;
  7627. if (startTime) {
  7628. start = start.time(startTime);
  7629. }
  7630. if (endTime) {
  7631. end = date.clone().time(endTime);
  7632. }
  7633. event = $.extend({}, abstractEvent); // make a copy of the original
  7634. assignDatesToEvent(
  7635. start, end,
  7636. !startTime && !endTime, // allDay?
  7637. event
  7638. );
  7639. events.push(event);
  7640. }
  7641. date.add(1, 'days');
  7642. }
  7643. }
  7644. else {
  7645. events.push(abstractEvent); // return the original event. will be a one-item array
  7646. }
  7647. }
  7648. return events;
  7649. }
  7650. /* Event Modification Math
  7651. -----------------------------------------------------------------------------------------*/
  7652. // Modifies an event and all related events by applying the given properties.
  7653. // Special date-diffing logic is used for manipulation of dates.
  7654. // If `props` does not contain start/end dates, the updated values are assumed to be the event's current start/end.
  7655. // All date comparisons are done against the event's pristine _start and _end dates.
  7656. // Returns an object with delta information and a function to undo all operations.
  7657. // For making computations in a granularity greater than day/time, specify largeUnit.
  7658. // NOTE: The given `newProps` might be mutated for normalization purposes.
  7659. function mutateEvent(event, newProps, largeUnit) {
  7660. var miscProps = {};
  7661. var oldProps;
  7662. var clearEnd;
  7663. var startDelta;
  7664. var endDelta;
  7665. var durationDelta;
  7666. var undoFunc;
  7667. // diffs the dates in the appropriate way, returning a duration
  7668. function diffDates(date1, date0) { // date1 - date0
  7669. if (largeUnit) {
  7670. return diffByUnit(date1, date0, largeUnit);
  7671. }
  7672. else if (newProps.allDay) {
  7673. return diffDay(date1, date0);
  7674. }
  7675. else {
  7676. return diffDayTime(date1, date0);
  7677. }
  7678. }
  7679. newProps = newProps || {};
  7680. // normalize new date-related properties
  7681. if (!newProps.start) {
  7682. newProps.start = event.start.clone();
  7683. }
  7684. if (newProps.end === undefined) {
  7685. newProps.end = event.end ? event.end.clone() : null;
  7686. }
  7687. if (newProps.allDay == null) { // is null or undefined?
  7688. newProps.allDay = event.allDay;
  7689. }
  7690. normalizeEventRange(newProps);
  7691. // create normalized versions of the original props to compare against
  7692. // need a real end value, for diffing
  7693. oldProps = {
  7694. start: event._start.clone(),
  7695. end: event._end ? event._end.clone() : t.getDefaultEventEnd(event._allDay, event._start),
  7696. allDay: newProps.allDay // normalize the dates in the same regard as the new properties
  7697. };
  7698. normalizeEventRange(oldProps);
  7699. // need to clear the end date if explicitly changed to null
  7700. clearEnd = event._end !== null && newProps.end === null;
  7701. // compute the delta for moving the start date
  7702. startDelta = diffDates(newProps.start, oldProps.start);
  7703. // compute the delta for moving the end date
  7704. if (newProps.end) {
  7705. endDelta = diffDates(newProps.end, oldProps.end);
  7706. durationDelta = endDelta.subtract(startDelta);
  7707. }
  7708. else {
  7709. durationDelta = null;
  7710. }
  7711. // gather all non-date-related properties
  7712. $.each(newProps, function(name, val) {
  7713. if (isMiscEventPropName(name)) {
  7714. if (val !== undefined) {
  7715. miscProps[name] = val;
  7716. }
  7717. }
  7718. });
  7719. // apply the operations to the event and all related events
  7720. undoFunc = mutateEvents(
  7721. clientEvents(event._id), // get events with this ID
  7722. clearEnd,
  7723. newProps.allDay,
  7724. startDelta,
  7725. durationDelta,
  7726. miscProps
  7727. );
  7728. return {
  7729. dateDelta: startDelta,
  7730. durationDelta: durationDelta,
  7731. undo: undoFunc
  7732. };
  7733. }
  7734. // Modifies an array of events in the following ways (operations are in order):
  7735. // - clear the event's `end`
  7736. // - convert the event to allDay
  7737. // - add `dateDelta` to the start and end
  7738. // - add `durationDelta` to the event's duration
  7739. // - assign `miscProps` to the event
  7740. //
  7741. // Returns a function that can be called to undo all the operations.
  7742. //
  7743. // TODO: don't use so many closures. possible memory issues when lots of events with same ID.
  7744. //
  7745. function mutateEvents(events, clearEnd, allDay, dateDelta, durationDelta, miscProps) {
  7746. var isAmbigTimezone = t.getIsAmbigTimezone();
  7747. var undoFunctions = [];
  7748. // normalize zero-length deltas to be null
  7749. if (dateDelta && !dateDelta.valueOf()) { dateDelta = null; }
  7750. if (durationDelta && !durationDelta.valueOf()) { durationDelta = null; }
  7751. $.each(events, function(i, event) {
  7752. var oldProps;
  7753. var newProps;
  7754. // build an object holding all the old values, both date-related and misc.
  7755. // for the undo function.
  7756. oldProps = {
  7757. start: event.start.clone(),
  7758. end: event.end ? event.end.clone() : null,
  7759. allDay: event.allDay
  7760. };
  7761. $.each(miscProps, function(name) {
  7762. oldProps[name] = event[name];
  7763. });
  7764. // new date-related properties. work off the original date snapshot.
  7765. // ok to use references because they will be thrown away when backupEventDates is called.
  7766. newProps = {
  7767. start: event._start,
  7768. end: event._end,
  7769. allDay: allDay // normalize the dates in the same regard as the new properties
  7770. };
  7771. normalizeEventRange(newProps); // massages start/end/allDay
  7772. // strip or ensure the end date
  7773. if (clearEnd) {
  7774. newProps.end = null;
  7775. }
  7776. else if (durationDelta && !newProps.end) { // the duration translation requires an end date
  7777. newProps.end = t.getDefaultEventEnd(newProps.allDay, newProps.start);
  7778. }
  7779. if (dateDelta) {
  7780. newProps.start.add(dateDelta);
  7781. if (newProps.end) {
  7782. newProps.end.add(dateDelta);
  7783. }
  7784. }
  7785. if (durationDelta) {
  7786. newProps.end.add(durationDelta); // end already ensured above
  7787. }
  7788. // if the dates have changed, and we know it is impossible to recompute the
  7789. // timezone offsets, strip the zone.
  7790. if (
  7791. isAmbigTimezone &&
  7792. !newProps.allDay &&
  7793. (dateDelta || durationDelta)
  7794. ) {
  7795. newProps.start.stripZone();
  7796. if (newProps.end) {
  7797. newProps.end.stripZone();
  7798. }
  7799. }
  7800. $.extend(event, miscProps, newProps); // copy over misc props, then date-related props
  7801. backupEventDates(event); // regenerate internal _start/_end/_allDay
  7802. undoFunctions.push(function() {
  7803. $.extend(event, oldProps);
  7804. backupEventDates(event); // regenerate internal _start/_end/_allDay
  7805. });
  7806. });
  7807. return function() {
  7808. for (var i = 0; i < undoFunctions.length; i++) {
  7809. undoFunctions[i]();
  7810. }
  7811. };
  7812. }
  7813. /* Business Hours
  7814. -----------------------------------------------------------------------------------------*/
  7815. t.getBusinessHoursEvents = getBusinessHoursEvents;
  7816. // Returns an array of events as to when the business hours occur in the given view.
  7817. // Abuse of our event system :(
  7818. function getBusinessHoursEvents(wholeDay) {
  7819. var optionVal = options.businessHours;
  7820. var defaultVal = {
  7821. className: 'fc-nonbusiness',
  7822. start: '09:00',
  7823. end: '17:00',
  7824. dow: [ 1, 2, 3, 4, 5 ], // monday - friday
  7825. rendering: 'inverse-background'
  7826. };
  7827. var view = t.getView();
  7828. var eventInput;
  7829. if (optionVal) { // `true` (which means "use the defaults") or an override object
  7830. eventInput = $.extend(
  7831. {}, // copy to a new object in either case
  7832. defaultVal,
  7833. typeof optionVal === 'object' ? optionVal : {} // override the defaults
  7834. );
  7835. }
  7836. if (eventInput) {
  7837. // if a whole-day series is requested, clear the start/end times
  7838. if (wholeDay) {
  7839. eventInput.start = null;
  7840. eventInput.end = null;
  7841. }
  7842. return expandEvent(
  7843. buildEventFromInput(eventInput),
  7844. view.start,
  7845. view.end
  7846. );
  7847. }
  7848. return [];
  7849. }
  7850. /* Overlapping / Constraining
  7851. -----------------------------------------------------------------------------------------*/
  7852. t.isEventRangeAllowed = isEventRangeAllowed;
  7853. t.isSelectionRangeAllowed = isSelectionRangeAllowed;
  7854. t.isExternalDropRangeAllowed = isExternalDropRangeAllowed;
  7855. function isEventRangeAllowed(range, event) {
  7856. var source = event.source || {};
  7857. var constraint = firstDefined(
  7858. event.constraint,
  7859. source.constraint,
  7860. options.eventConstraint
  7861. );
  7862. var overlap = firstDefined(
  7863. event.overlap,
  7864. source.overlap,
  7865. options.eventOverlap
  7866. );
  7867. range = ensureVisibleEventRange(range); // ensure a proper range with an end for isRangeAllowed
  7868. return isRangeAllowed(range, constraint, overlap, event);
  7869. }
  7870. function isSelectionRangeAllowed(range) {
  7871. return isRangeAllowed(range, options.selectConstraint, options.selectOverlap);
  7872. }
  7873. // when `eventProps` is defined, consider this an event.
  7874. // `eventProps` can contain misc non-date-related info about the event.
  7875. function isExternalDropRangeAllowed(range, eventProps) {
  7876. var eventInput;
  7877. var event;
  7878. // note: very similar logic is in View's reportExternalDrop
  7879. if (eventProps) {
  7880. eventInput = $.extend({}, eventProps, range);
  7881. event = expandEvent(buildEventFromInput(eventInput))[0];
  7882. }
  7883. if (event) {
  7884. return isEventRangeAllowed(range, event);
  7885. }
  7886. else { // treat it as a selection
  7887. range = ensureVisibleEventRange(range); // ensure a proper range with an end for isSelectionRangeAllowed
  7888. return isSelectionRangeAllowed(range);
  7889. }
  7890. }
  7891. // Returns true if the given range (caused by an event drop/resize or a selection) is allowed to exist
  7892. // according to the constraint/overlap settings.
  7893. // `event` is not required if checking a selection.
  7894. function isRangeAllowed(range, constraint, overlap, event) {
  7895. var constraintEvents;
  7896. var anyContainment;
  7897. var peerEvents;
  7898. var i, peerEvent;
  7899. var peerOverlap;
  7900. // normalize. fyi, we're normalizing in too many places :(
  7901. range = $.extend({}, range); // copy all properties in case there are misc non-date properties
  7902. range.start = range.start.clone().stripZone();
  7903. range.end = range.end.clone().stripZone();
  7904. // the range must be fully contained by at least one of produced constraint events
  7905. if (constraint != null) {
  7906. // not treated as an event! intermediate data structure
  7907. // TODO: use ranges in the future
  7908. constraintEvents = constraintToEvents(constraint);
  7909. anyContainment = false;
  7910. for (i = 0; i < constraintEvents.length; i++) {
  7911. if (eventContainsRange(constraintEvents[i], range)) {
  7912. anyContainment = true;
  7913. break;
  7914. }
  7915. }
  7916. if (!anyContainment) {
  7917. return false;
  7918. }
  7919. }
  7920. peerEvents = t.getPeerEvents(event, range);
  7921. for (i = 0; i < peerEvents.length; i++) {
  7922. peerEvent = peerEvents[i];
  7923. // there needs to be an actual intersection before disallowing anything
  7924. if (eventIntersectsRange(peerEvent, range)) {
  7925. // evaluate overlap for the given range and short-circuit if necessary
  7926. if (overlap === false) {
  7927. return false;
  7928. }
  7929. // if the event's overlap is a test function, pass the peer event in question as the first param
  7930. else if (typeof overlap === 'function' && !overlap(peerEvent, event)) {
  7931. return false;
  7932. }
  7933. // if we are computing if the given range is allowable for an event, consider the other event's
  7934. // EventObject-specific or Source-specific `overlap` property
  7935. if (event) {
  7936. peerOverlap = firstDefined(
  7937. peerEvent.overlap,
  7938. (peerEvent.source || {}).overlap
  7939. // we already considered the global `eventOverlap`
  7940. );
  7941. if (peerOverlap === false) {
  7942. return false;
  7943. }
  7944. // if the peer event's overlap is a test function, pass the subject event as the first param
  7945. if (typeof peerOverlap === 'function' && !peerOverlap(event, peerEvent)) {
  7946. return false;
  7947. }
  7948. }
  7949. }
  7950. }
  7951. return true;
  7952. }
  7953. // Given an event input from the API, produces an array of event objects. Possible event inputs:
  7954. // 'businessHours'
  7955. // An event ID (number or string)
  7956. // An object with specific start/end dates or a recurring event (like what businessHours accepts)
  7957. function constraintToEvents(constraintInput) {
  7958. if (constraintInput === 'businessHours') {
  7959. return getBusinessHoursEvents();
  7960. }
  7961. if (typeof constraintInput === 'object') {
  7962. return expandEvent(buildEventFromInput(constraintInput));
  7963. }
  7964. return clientEvents(constraintInput); // probably an ID
  7965. }
  7966. // Does the event's date range fully contain the given range?
  7967. // start/end already assumed to have stripped zones :(
  7968. function eventContainsRange(event, range) {
  7969. var eventStart = event.start.clone().stripZone();
  7970. var eventEnd = t.getEventEnd(event).stripZone();
  7971. return range.start >= eventStart && range.end <= eventEnd;
  7972. }
  7973. // Does the event's date range intersect with the given range?
  7974. // start/end already assumed to have stripped zones :(
  7975. function eventIntersectsRange(event, range) {
  7976. var eventStart = event.start.clone().stripZone();
  7977. var eventEnd = t.getEventEnd(event).stripZone();
  7978. return range.start < eventEnd && range.end > eventStart;
  7979. }
  7980. t.getEventCache = function() {
  7981. return cache;
  7982. };
  7983. }
  7984. // Returns a list of events that the given event should be compared against when being considered for a move to
  7985. // the specified range. Attached to the Calendar's prototype because EventManager is a mixin for a Calendar.
  7986. Calendar.prototype.getPeerEvents = function(event, range) {
  7987. var cache = this.getEventCache();
  7988. var peerEvents = [];
  7989. var i, otherEvent;
  7990. for (i = 0; i < cache.length; i++) {
  7991. otherEvent = cache[i];
  7992. if (
  7993. !event ||
  7994. event._id !== otherEvent._id // don't compare the event to itself or other related [repeating] events
  7995. ) {
  7996. peerEvents.push(otherEvent);
  7997. }
  7998. }
  7999. return peerEvents;
  8000. };
  8001. // updates the "backup" properties, which are preserved in order to compute diffs later on.
  8002. function backupEventDates(event) {
  8003. event._allDay = event.allDay;
  8004. event._start = event.start.clone();
  8005. event._end = event.end ? event.end.clone() : null;
  8006. }
  8007. ;;
  8008. /* An abstract class for the "basic" views, as well as month view. Renders one or more rows of day cells.
  8009. ----------------------------------------------------------------------------------------------------------------------*/
  8010. // It is a manager for a DayGrid subcomponent, which does most of the heavy lifting.
  8011. // It is responsible for managing width/height.
  8012. var BasicView = View.extend({
  8013. dayGrid: null, // the main subcomponent that does most of the heavy lifting
  8014. dayNumbersVisible: false, // display day numbers on each day cell?
  8015. weekNumbersVisible: false, // display week numbers along the side?
  8016. weekNumberWidth: null, // width of all the week-number cells running down the side
  8017. headRowEl: null, // the fake row element of the day-of-week header
  8018. initialize: function() {
  8019. this.dayGrid = new DayGrid(this);
  8020. this.coordMap = this.dayGrid.coordMap; // the view's date-to-cell mapping is identical to the subcomponent's
  8021. },
  8022. // Sets the display range and computes all necessary dates
  8023. setRange: function(range) {
  8024. View.prototype.setRange.call(this, range); // call the super-method
  8025. this.dayGrid.breakOnWeeks = /year|month|week/.test(this.intervalUnit); // do before setRange
  8026. this.dayGrid.setRange(range);
  8027. },
  8028. // Compute the value to feed into setRange. Overrides superclass.
  8029. computeRange: function(date) {
  8030. var range = View.prototype.computeRange.call(this, date); // get value from the super-method
  8031. // year and month views should be aligned with weeks. this is already done for week
  8032. if (/year|month/.test(range.intervalUnit)) {
  8033. range.start.startOf('week');
  8034. range.start = this.skipHiddenDays(range.start);
  8035. // make end-of-week if not already
  8036. if (range.end.weekday()) {
  8037. range.end.add(1, 'week').startOf('week');
  8038. range.end = this.skipHiddenDays(range.end, -1, true); // exclusively move backwards
  8039. }
  8040. }
  8041. return range;
  8042. },
  8043. // Renders the view into `this.el`, which should already be assigned
  8044. renderDates: function() {
  8045. this.dayNumbersVisible = this.dayGrid.rowCnt > 1; // TODO: make grid responsible
  8046. this.weekNumbersVisible = this.opt('weekNumbers');
  8047. this.dayGrid.numbersVisible = this.dayNumbersVisible || this.weekNumbersVisible;
  8048. this.el.addClass('fc-basic-view').html(this.renderHtml());
  8049. this.headRowEl = this.el.find('thead .fc-row');
  8050. this.scrollerEl = this.el.find('.fc-day-grid-container');
  8051. this.dayGrid.coordMap.containerEl = this.scrollerEl; // constrain clicks/etc to the dimensions of the scroller
  8052. this.dayGrid.setElement(this.el.find('.fc-day-grid'));
  8053. this.dayGrid.renderDates(this.hasRigidRows());
  8054. },
  8055. // Unrenders the content of the view. Since we haven't separated skeleton rendering from date rendering,
  8056. // always completely kill the dayGrid's rendering.
  8057. unrenderDates: function() {
  8058. this.dayGrid.unrenderDates();
  8059. this.dayGrid.removeElement();
  8060. },
  8061. renderBusinessHours: function() {
  8062. this.dayGrid.renderBusinessHours();
  8063. },
  8064. // Builds the HTML skeleton for the view.
  8065. // The day-grid component will render inside of a container defined by this HTML.
  8066. renderHtml: function() {
  8067. return '' +
  8068. '<table>' +
  8069. '<thead class="fc-head">' +
  8070. '<tr>' +
  8071. '<td class="' + this.widgetHeaderClass + '">' +
  8072. this.dayGrid.headHtml() + // render the day-of-week headers
  8073. '</td>' +
  8074. '</tr>' +
  8075. '</thead>' +
  8076. '<tbody class="fc-body">' +
  8077. '<tr>' +
  8078. '<td class="' + this.widgetContentClass + '">' +
  8079. '<div class="fc-day-grid-container">' +
  8080. '<div class="fc-day-grid"/>' +
  8081. '</div>' +
  8082. '</td>' +
  8083. '</tr>' +
  8084. '</tbody>' +
  8085. '</table>';
  8086. },
  8087. // Generates the HTML that will go before the day-of week header cells.
  8088. // Queried by the DayGrid subcomponent when generating rows. Ordering depends on isRTL.
  8089. headIntroHtml: function() {
  8090. if (this.weekNumbersVisible) {
  8091. return '' +
  8092. '<th class="fc-week-number ' + this.widgetHeaderClass + '" ' + this.weekNumberStyleAttr() + '>' +
  8093. '<span>' + // needed for matchCellWidths
  8094. htmlEscape(this.opt('weekNumberTitle')) +
  8095. '</span>' +
  8096. '</th>';
  8097. }
  8098. },
  8099. // Generates the HTML that will go before content-skeleton cells that display the day/week numbers.
  8100. // Queried by the DayGrid subcomponent. Ordering depends on isRTL.
  8101. numberIntroHtml: function(row) {
  8102. if (this.weekNumbersVisible) {
  8103. return '' +
  8104. '<td class="fc-week-number" ' + this.weekNumberStyleAttr() + '>' +
  8105. '<span>' + // needed for matchCellWidths
  8106. this.dayGrid.getCell(row, 0).start.format('w') +
  8107. '</span>' +
  8108. '</td>';
  8109. }
  8110. },
  8111. // Generates the HTML that goes before the day bg cells for each day-row.
  8112. // Queried by the DayGrid subcomponent. Ordering depends on isRTL.
  8113. dayIntroHtml: function() {
  8114. if (this.weekNumbersVisible) {
  8115. return '<td class="fc-week-number ' + this.widgetContentClass + '" ' +
  8116. this.weekNumberStyleAttr() + '></td>';
  8117. }
  8118. },
  8119. // Generates the HTML that goes before every other type of row generated by DayGrid. Ordering depends on isRTL.
  8120. // Affects helper-skeleton and highlight-skeleton rows.
  8121. introHtml: function() {
  8122. if (this.weekNumbersVisible) {
  8123. return '<td class="fc-week-number" ' + this.weekNumberStyleAttr() + '></td>';
  8124. }
  8125. },
  8126. // Generates the HTML for the <td>s of the "number" row in the DayGrid's content skeleton.
  8127. // The number row will only exist if either day numbers or week numbers are turned on.
  8128. numberCellHtml: function(cell) {
  8129. var date = cell.start;
  8130. var classes;
  8131. if (!this.dayNumbersVisible) { // if there are week numbers but not day numbers
  8132. return '<td/>'; // will create an empty space above events :(
  8133. }
  8134. classes = this.dayGrid.getDayClasses(date);
  8135. classes.unshift('fc-day-number');
  8136. return '' +
  8137. '<td class="' + classes.join(' ') + '" data-date="' + date.format() + '">' +
  8138. date.date() +
  8139. '</td>';
  8140. },
  8141. // Generates an HTML attribute string for setting the width of the week number column, if it is known
  8142. weekNumberStyleAttr: function() {
  8143. if (this.weekNumberWidth !== null) {
  8144. return 'style="width:' + this.weekNumberWidth + 'px"';
  8145. }
  8146. return '';
  8147. },
  8148. // Determines whether each row should have a constant height
  8149. hasRigidRows: function() {
  8150. var eventLimit = this.opt('eventLimit');
  8151. return eventLimit && typeof eventLimit !== 'number';
  8152. },
  8153. /* Dimensions
  8154. ------------------------------------------------------------------------------------------------------------------*/
  8155. // Refreshes the horizontal dimensions of the view
  8156. updateWidth: function() {
  8157. if (this.weekNumbersVisible) {
  8158. // Make sure all week number cells running down the side have the same width.
  8159. // Record the width for cells created later.
  8160. this.weekNumberWidth = matchCellWidths(
  8161. this.el.find('.fc-week-number')
  8162. );
  8163. }
  8164. },
  8165. // Adjusts the vertical dimensions of the view to the specified values
  8166. setHeight: function(totalHeight, isAuto) {
  8167. var eventLimit = this.opt('eventLimit');
  8168. var scrollerHeight;
  8169. // reset all heights to be natural
  8170. unsetScroller(this.scrollerEl);
  8171. uncompensateScroll(this.headRowEl);
  8172. this.dayGrid.removeSegPopover(); // kill the "more" popover if displayed
  8173. // is the event limit a constant level number?
  8174. if (eventLimit && typeof eventLimit === 'number') {
  8175. this.dayGrid.limitRows(eventLimit); // limit the levels first so the height can redistribute after
  8176. }
  8177. scrollerHeight = this.computeScrollerHeight(totalHeight);
  8178. this.setGridHeight(scrollerHeight, isAuto);
  8179. // is the event limit dynamically calculated?
  8180. if (eventLimit && typeof eventLimit !== 'number') {
  8181. this.dayGrid.limitRows(eventLimit); // limit the levels after the grid's row heights have been set
  8182. }
  8183. if (!isAuto && setPotentialScroller(this.scrollerEl, scrollerHeight)) { // using scrollbars?
  8184. compensateScroll(this.headRowEl, getScrollbarWidths(this.scrollerEl));
  8185. // doing the scrollbar compensation might have created text overflow which created more height. redo
  8186. scrollerHeight = this.computeScrollerHeight(totalHeight);
  8187. this.scrollerEl.height(scrollerHeight);
  8188. }
  8189. },
  8190. // Sets the height of just the DayGrid component in this view
  8191. setGridHeight: function(height, isAuto) {
  8192. if (isAuto) {
  8193. undistributeHeight(this.dayGrid.rowEls); // let the rows be their natural height with no expanding
  8194. }
  8195. else {
  8196. distributeHeight(this.dayGrid.rowEls, height, true); // true = compensate for height-hogging rows
  8197. }
  8198. },
  8199. /* Events
  8200. ------------------------------------------------------------------------------------------------------------------*/
  8201. // Renders the given events onto the view and populates the segments array
  8202. renderEvents: function(events) {
  8203. this.dayGrid.renderEvents(events);
  8204. this.updateHeight(); // must compensate for events that overflow the row
  8205. },
  8206. // Retrieves all segment objects that are rendered in the view
  8207. getEventSegs: function() {
  8208. return this.dayGrid.getEventSegs();
  8209. },
  8210. // Unrenders all event elements and clears internal segment data
  8211. unrenderEvents: function() {
  8212. this.dayGrid.unrenderEvents();
  8213. // we DON'T need to call updateHeight() because:
  8214. // A) a renderEvents() call always happens after this, which will eventually call updateHeight()
  8215. // B) in IE8, this causes a flash whenever events are rerendered
  8216. },
  8217. /* Dragging (for both events and external elements)
  8218. ------------------------------------------------------------------------------------------------------------------*/
  8219. // A returned value of `true` signals that a mock "helper" event has been rendered.
  8220. renderDrag: function(dropLocation, seg) {
  8221. return this.dayGrid.renderDrag(dropLocation, seg);
  8222. },
  8223. unrenderDrag: function() {
  8224. this.dayGrid.unrenderDrag();
  8225. },
  8226. /* Selection
  8227. ------------------------------------------------------------------------------------------------------------------*/
  8228. // Renders a visual indication of a selection
  8229. renderSelection: function(range) {
  8230. this.dayGrid.renderSelection(range);
  8231. },
  8232. // Unrenders a visual indications of a selection
  8233. unrenderSelection: function() {
  8234. this.dayGrid.unrenderSelection();
  8235. }
  8236. });
  8237. ;;
  8238. /* A month view with day cells running in rows (one-per-week) and columns
  8239. ----------------------------------------------------------------------------------------------------------------------*/
  8240. var MonthView = BasicView.extend({
  8241. // Produces information about what range to display
  8242. computeRange: function(date) {
  8243. var range = BasicView.prototype.computeRange.call(this, date); // get value from super-method
  8244. var rowCnt;
  8245. // ensure 6 weeks
  8246. if (this.isFixedWeeks()) {
  8247. rowCnt = Math.ceil(range.end.diff(range.start, 'weeks', true)); // could be partial weeks due to hiddenDays
  8248. range.end.add(6 - rowCnt, 'weeks');
  8249. }
  8250. return range;
  8251. },
  8252. // Overrides the default BasicView behavior to have special multi-week auto-height logic
  8253. setGridHeight: function(height, isAuto) {
  8254. isAuto = isAuto || this.opt('weekMode') === 'variable'; // LEGACY: weekMode is deprecated
  8255. // if auto, make the height of each row the height that it would be if there were 6 weeks
  8256. if (isAuto) {
  8257. height *= this.rowCnt / 6;
  8258. }
  8259. distributeHeight(this.dayGrid.rowEls, height, !isAuto); // if auto, don't compensate for height-hogging rows
  8260. },
  8261. isFixedWeeks: function() {
  8262. var weekMode = this.opt('weekMode'); // LEGACY: weekMode is deprecated
  8263. if (weekMode) {
  8264. return weekMode === 'fixed'; // if any other type of weekMode, assume NOT fixed
  8265. }
  8266. return this.opt('fixedWeekCount');
  8267. }
  8268. });
  8269. ;;
  8270. fcViews.basic = {
  8271. 'class': BasicView
  8272. };
  8273. fcViews.basicDay = {
  8274. type: 'basic',
  8275. duration: { days: 1 }
  8276. };
  8277. fcViews.basicWeek = {
  8278. type: 'basic',
  8279. duration: { weeks: 1 }
  8280. };
  8281. fcViews.month = {
  8282. 'class': MonthView,
  8283. duration: { months: 1 }, // important for prev/next
  8284. defaults: {
  8285. fixedWeekCount: true
  8286. }
  8287. };
  8288. ;;
  8289. /* An abstract class for all agenda-related views. Displays one more columns with time slots running vertically.
  8290. ----------------------------------------------------------------------------------------------------------------------*/
  8291. // Is a manager for the TimeGrid subcomponent and possibly the DayGrid subcomponent (if allDaySlot is on).
  8292. // Responsible for managing width/height.
  8293. var AgendaView = View.extend({
  8294. timeGrid: null, // the main time-grid subcomponent of this view
  8295. dayGrid: null, // the "all-day" subcomponent. if all-day is turned off, this will be null
  8296. axisWidth: null, // the width of the time axis running down the side
  8297. noScrollRowEls: null, // set of fake row elements that must compensate when scrollerEl has scrollbars
  8298. // when the time-grid isn't tall enough to occupy the given height, we render an <hr> underneath
  8299. bottomRuleEl: null,
  8300. bottomRuleHeight: null,
  8301. initialize: function() {
  8302. this.timeGrid = new TimeGrid(this);
  8303. if (this.opt('allDaySlot')) { // should we display the "all-day" area?
  8304. this.dayGrid = new DayGrid(this); // the all-day subcomponent of this view
  8305. // the coordinate grid will be a combination of both subcomponents' grids
  8306. this.coordMap = new ComboCoordMap([
  8307. this.dayGrid.coordMap,
  8308. this.timeGrid.coordMap
  8309. ]);
  8310. }
  8311. else {
  8312. this.coordMap = this.timeGrid.coordMap;
  8313. }
  8314. },
  8315. /* Rendering
  8316. ------------------------------------------------------------------------------------------------------------------*/
  8317. // Sets the display range and computes all necessary dates
  8318. setRange: function(range) {
  8319. View.prototype.setRange.call(this, range); // call the super-method
  8320. this.timeGrid.setRange(range);
  8321. if (this.dayGrid) {
  8322. this.dayGrid.setRange(range);
  8323. }
  8324. },
  8325. // Renders the view into `this.el`, which has already been assigned
  8326. renderDates: function() {
  8327. this.el.addClass('fc-agenda-view').html(this.renderHtml());
  8328. // the element that wraps the time-grid that will probably scroll
  8329. this.scrollerEl = this.el.find('.fc-time-grid-container');
  8330. this.timeGrid.coordMap.containerEl = this.scrollerEl; // don't accept clicks/etc outside of this
  8331. this.timeGrid.setElement(this.el.find('.fc-time-grid'));
  8332. this.timeGrid.renderDates();
  8333. // the <hr> that sometimes displays under the time-grid
  8334. this.bottomRuleEl = $('<hr class="fc-divider ' + this.widgetHeaderClass + '"/>')
  8335. .appendTo(this.timeGrid.el); // inject it into the time-grid
  8336. if (this.dayGrid) {
  8337. this.dayGrid.setElement(this.el.find('.fc-day-grid'));
  8338. this.dayGrid.renderDates();
  8339. // have the day-grid extend it's coordinate area over the <hr> dividing the two grids
  8340. this.dayGrid.bottomCoordPadding = this.dayGrid.el.next('hr').outerHeight();
  8341. }
  8342. this.noScrollRowEls = this.el.find('.fc-row:not(.fc-scroller *)'); // fake rows not within the scroller
  8343. },
  8344. // Unrenders the content of the view. Since we haven't separated skeleton rendering from date rendering,
  8345. // always completely kill each grid's rendering.
  8346. unrenderDates: function() {
  8347. this.timeGrid.unrenderDates();
  8348. this.timeGrid.removeElement();
  8349. if (this.dayGrid) {
  8350. this.dayGrid.unrenderDates();
  8351. this.dayGrid.removeElement();
  8352. }
  8353. },
  8354. renderBusinessHours: function() {
  8355. this.timeGrid.renderBusinessHours();
  8356. if (this.dayGrid) {
  8357. this.dayGrid.renderBusinessHours();
  8358. }
  8359. },
  8360. // Builds the HTML skeleton for the view.
  8361. // The day-grid and time-grid components will render inside containers defined by this HTML.
  8362. renderHtml: function() {
  8363. return '' +
  8364. '<table>' +
  8365. '<thead class="fc-head">' +
  8366. '<tr>' +
  8367. '<td class="' + this.widgetHeaderClass + '">' +
  8368. this.timeGrid.headHtml() + // render the day-of-week headers
  8369. '</td>' +
  8370. '</tr>' +
  8371. '</thead>' +
  8372. '<tbody class="fc-body">' +
  8373. '<tr>' +
  8374. '<td class="' + this.widgetContentClass + '">' +
  8375. (this.dayGrid ?
  8376. '<div class="fc-day-grid"/>' +
  8377. '<hr class="fc-divider ' + this.widgetHeaderClass + '"/>' :
  8378. ''
  8379. ) +
  8380. '<div class="fc-time-grid-container">' +
  8381. '<div class="fc-time-grid"/>' +
  8382. '</div>' +
  8383. '</td>' +
  8384. '</tr>' +
  8385. '</tbody>' +
  8386. '</table>';
  8387. },
  8388. // Generates the HTML that will go before the day-of week header cells.
  8389. // Queried by the TimeGrid subcomponent when generating rows. Ordering depends on isRTL.
  8390. headIntroHtml: function() {
  8391. var date;
  8392. var weekText;
  8393. if (this.opt('weekNumbers')) {
  8394. date = this.timeGrid.getCell(0).start;
  8395. weekText = date.format(this.opt('smallWeekFormat'));
  8396. return '' +
  8397. '<th class="fc-axis fc-week-number ' + this.widgetHeaderClass + '" ' + this.axisStyleAttr() + '>' +
  8398. '<span>' + // needed for matchCellWidths
  8399. htmlEscape(weekText) +
  8400. '</span>' +
  8401. '</th>';
  8402. }
  8403. else {
  8404. return '<th class="fc-axis ' + this.widgetHeaderClass + '" ' + this.axisStyleAttr() + '></th>';
  8405. }
  8406. },
  8407. // Generates the HTML that goes before the all-day cells.
  8408. // Queried by the DayGrid subcomponent when generating rows. Ordering depends on isRTL.
  8409. dayIntroHtml: function() {
  8410. return '' +
  8411. '<td class="fc-axis ' + this.widgetContentClass + '" ' + this.axisStyleAttr() + '>' +
  8412. '<span>' + // needed for matchCellWidths
  8413. (this.opt('allDayHtml') || htmlEscape(this.opt('allDayText'))) +
  8414. '</span>' +
  8415. '</td>';
  8416. },
  8417. // Generates the HTML that goes before the bg of the TimeGrid slot area. Long vertical column.
  8418. slotBgIntroHtml: function() {
  8419. return '<td class="fc-axis ' + this.widgetContentClass + '" ' + this.axisStyleAttr() + '></td>';
  8420. },
  8421. // Generates the HTML that goes before all other types of cells.
  8422. // Affects content-skeleton, helper-skeleton, highlight-skeleton for both the time-grid and day-grid.
  8423. // Queried by the TimeGrid and DayGrid subcomponents when generating rows. Ordering depends on isRTL.
  8424. introHtml: function() {
  8425. return '<td class="fc-axis" ' + this.axisStyleAttr() + '></td>';
  8426. },
  8427. // Generates an HTML attribute string for setting the width of the axis, if it is known
  8428. axisStyleAttr: function() {
  8429. if (this.axisWidth !== null) {
  8430. return 'style="width:' + this.axisWidth + 'px"';
  8431. }
  8432. return '';
  8433. },
  8434. /* Dimensions
  8435. ------------------------------------------------------------------------------------------------------------------*/
  8436. updateSize: function(isResize) {
  8437. this.timeGrid.updateSize(isResize);
  8438. View.prototype.updateSize.call(this, isResize); // call the super-method
  8439. },
  8440. // Refreshes the horizontal dimensions of the view
  8441. updateWidth: function() {
  8442. // make all axis cells line up, and record the width so newly created axis cells will have it
  8443. this.axisWidth = matchCellWidths(this.el.find('.fc-axis'));
  8444. },
  8445. // Adjusts the vertical dimensions of the view to the specified values
  8446. setHeight: function(totalHeight, isAuto) {
  8447. var eventLimit;
  8448. var scrollerHeight;
  8449. if (this.bottomRuleHeight === null) {
  8450. // calculate the height of the rule the very first time
  8451. this.bottomRuleHeight = this.bottomRuleEl.outerHeight();
  8452. }
  8453. this.bottomRuleEl.hide(); // .show() will be called later if this <hr> is necessary
  8454. // reset all dimensions back to the original state
  8455. this.scrollerEl.css('overflow', '');
  8456. unsetScroller(this.scrollerEl);
  8457. uncompensateScroll(this.noScrollRowEls);
  8458. // limit number of events in the all-day area
  8459. if (this.dayGrid) {
  8460. this.dayGrid.removeSegPopover(); // kill the "more" popover if displayed
  8461. eventLimit = this.opt('eventLimit');
  8462. if (eventLimit && typeof eventLimit !== 'number') {
  8463. eventLimit = AGENDA_ALL_DAY_EVENT_LIMIT; // make sure "auto" goes to a real number
  8464. }
  8465. if (eventLimit) {
  8466. this.dayGrid.limitRows(eventLimit);
  8467. }
  8468. }
  8469. if (!isAuto) { // should we force dimensions of the scroll container, or let the contents be natural height?
  8470. scrollerHeight = this.computeScrollerHeight(totalHeight);
  8471. if (setPotentialScroller(this.scrollerEl, scrollerHeight)) { // using scrollbars?
  8472. // make the all-day and header rows lines up
  8473. compensateScroll(this.noScrollRowEls, getScrollbarWidths(this.scrollerEl));
  8474. // the scrollbar compensation might have changed text flow, which might affect height, so recalculate
  8475. // and reapply the desired height to the scroller.
  8476. scrollerHeight = this.computeScrollerHeight(totalHeight);
  8477. this.scrollerEl.height(scrollerHeight);
  8478. }
  8479. else { // no scrollbars
  8480. // still, force a height and display the bottom rule (marks the end of day)
  8481. this.scrollerEl.height(scrollerHeight).css('overflow', 'hidden'); // in case <hr> goes outside
  8482. this.bottomRuleEl.show();
  8483. }
  8484. }
  8485. },
  8486. // Computes the initial pre-configured scroll state prior to allowing the user to change it
  8487. computeInitialScroll: function() {
  8488. var scrollTime = moment.duration(this.opt('scrollTime'));
  8489. var top = this.timeGrid.computeTimeTop(scrollTime);
  8490. // zoom can give weird floating-point values. rather scroll a little bit further
  8491. top = Math.ceil(top);
  8492. if (top) {
  8493. top++; // to overcome top border that slots beyond the first have. looks better
  8494. }
  8495. return top;
  8496. },
  8497. /* Events
  8498. ------------------------------------------------------------------------------------------------------------------*/
  8499. // Renders events onto the view and populates the View's segment array
  8500. renderEvents: function(events) {
  8501. var dayEvents = [];
  8502. var timedEvents = [];
  8503. var daySegs = [];
  8504. var timedSegs;
  8505. var i;
  8506. // separate the events into all-day and timed
  8507. for (i = 0; i < events.length; i++) {
  8508. if (events[i].allDay) {
  8509. dayEvents.push(events[i]);
  8510. }
  8511. else {
  8512. timedEvents.push(events[i]);
  8513. }
  8514. }
  8515. // render the events in the subcomponents
  8516. timedSegs = this.timeGrid.renderEvents(timedEvents);
  8517. if (this.dayGrid) {
  8518. daySegs = this.dayGrid.renderEvents(dayEvents);
  8519. }
  8520. // the all-day area is flexible and might have a lot of events, so shift the height
  8521. this.updateHeight();
  8522. },
  8523. // Retrieves all segment objects that are rendered in the view
  8524. getEventSegs: function() {
  8525. return this.timeGrid.getEventSegs().concat(
  8526. this.dayGrid ? this.dayGrid.getEventSegs() : []
  8527. );
  8528. },
  8529. // Unrenders all event elements and clears internal segment data
  8530. unrenderEvents: function() {
  8531. // unrender the events in the subcomponents
  8532. this.timeGrid.unrenderEvents();
  8533. if (this.dayGrid) {
  8534. this.dayGrid.unrenderEvents();
  8535. }
  8536. // we DON'T need to call updateHeight() because:
  8537. // A) a renderEvents() call always happens after this, which will eventually call updateHeight()
  8538. // B) in IE8, this causes a flash whenever events are rerendered
  8539. },
  8540. /* Dragging (for events and external elements)
  8541. ------------------------------------------------------------------------------------------------------------------*/
  8542. // A returned value of `true` signals that a mock "helper" event has been rendered.
  8543. renderDrag: function(dropLocation, seg) {
  8544. if (dropLocation.start.hasTime()) {
  8545. return this.timeGrid.renderDrag(dropLocation, seg);
  8546. }
  8547. else if (this.dayGrid) {
  8548. return this.dayGrid.renderDrag(dropLocation, seg);
  8549. }
  8550. },
  8551. unrenderDrag: function() {
  8552. this.timeGrid.unrenderDrag();
  8553. if (this.dayGrid) {
  8554. this.dayGrid.unrenderDrag();
  8555. }
  8556. },
  8557. /* Selection
  8558. ------------------------------------------------------------------------------------------------------------------*/
  8559. // Renders a visual indication of a selection
  8560. renderSelection: function(range) {
  8561. if (range.start.hasTime() || range.end.hasTime()) {
  8562. this.timeGrid.renderSelection(range);
  8563. }
  8564. else if (this.dayGrid) {
  8565. this.dayGrid.renderSelection(range);
  8566. }
  8567. },
  8568. // Unrenders a visual indications of a selection
  8569. unrenderSelection: function() {
  8570. this.timeGrid.unrenderSelection();
  8571. if (this.dayGrid) {
  8572. this.dayGrid.unrenderSelection();
  8573. }
  8574. }
  8575. });
  8576. ;;
  8577. var AGENDA_ALL_DAY_EVENT_LIMIT = 5;
  8578. // potential nice values for the slot-duration and interval-duration
  8579. // from largest to smallest
  8580. var AGENDA_STOCK_SUB_DURATIONS = [
  8581. { hours: 1 },
  8582. { minutes: 30 },
  8583. { minutes: 15 },
  8584. { seconds: 30 },
  8585. { seconds: 15 }
  8586. ];
  8587. fcViews.agenda = {
  8588. 'class': AgendaView,
  8589. defaults: {
  8590. allDaySlot: true,
  8591. allDayText: 'all-day',
  8592. slotDuration: '00:30:00',
  8593. minTime: '00:00:00',
  8594. maxTime: '24:00:00',
  8595. slotEventOverlap: true // a bad name. confused with overlap/constraint system
  8596. }
  8597. };
  8598. fcViews.agendaDay = {
  8599. type: 'agenda',
  8600. duration: { days: 1 }
  8601. };
  8602. fcViews.agendaWeek = {
  8603. type: 'agenda',
  8604. duration: { weeks: 1 }
  8605. };
  8606. ;;
  8607. return fc; // export for Node/CommonJS
  8608. });