View.js 34 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273
  1. /* An abstract class from which other views inherit from
  2. ----------------------------------------------------------------------------------------------------------------------*/
  3. var View = FC.View = Class.extend(EmitterMixin, ListenerMixin, {
  4. type: null, // subclass' view name (string)
  5. name: null, // deprecated. use `type` instead
  6. title: null, // the text that will be displayed in the header's title
  7. calendar: null, // owner Calendar object
  8. options: null, // hash containing all options. already merged with view-specific-options
  9. el: null, // the view's containing element. set by Calendar
  10. isDateSet: false,
  11. dateSetQueue: null,
  12. displayingEvents: null, // a promise
  13. isEventsSet: false,
  14. isEventsBounds: false,
  15. eventRenderQueue: null,
  16. // range the view is actually displaying (moments)
  17. start: null,
  18. end: null, // exclusive
  19. // range the view is formally responsible for (moments)
  20. // may be different from start/end. for example, a month view might have 1st-31st, excluding padded dates
  21. intervalStart: null,
  22. intervalEnd: null, // exclusive
  23. intervalDuration: null,
  24. intervalUnit: null, // name of largest unit being displayed, like "month" or "week"
  25. isRTL: false,
  26. isSelected: false, // boolean whether a range of time is user-selected or not
  27. selectedEvent: null,
  28. eventOrderSpecs: null, // criteria for ordering events when they have same date/time
  29. // classNames styled by jqui themes
  30. widgetHeaderClass: null,
  31. widgetContentClass: null,
  32. highlightStateClass: null,
  33. // for date utils, computed from options
  34. nextDayThreshold: null,
  35. isHiddenDayHash: null,
  36. // now indicator
  37. isNowIndicatorRendered: null,
  38. initialNowDate: null, // result first getNow call
  39. initialNowQueriedMs: null, // ms time the getNow was called
  40. nowIndicatorTimeoutID: null, // for refresh timing of now indicator
  41. nowIndicatorIntervalID: null, // "
  42. constructor: function(calendar, type, options, intervalDuration) {
  43. this.calendar = calendar;
  44. this.type = this.name = type; // .name is deprecated
  45. this.options = options;
  46. this.intervalDuration = intervalDuration || moment.duration(1, 'day');
  47. this.nextDayThreshold = moment.duration(this.opt('nextDayThreshold'));
  48. this.initThemingProps();
  49. this.initHiddenDays();
  50. this.isRTL = this.opt('isRTL');
  51. this.eventOrderSpecs = parseFieldSpecs(this.opt('eventOrder'));
  52. this.dateSetQueue = new RunQueue();
  53. this.eventRenderQueue = new RunQueue();
  54. this.initialize();
  55. },
  56. // A good place for subclasses to initialize member variables
  57. initialize: function() {
  58. // subclasses can implement
  59. },
  60. // Retrieves an option with the given name
  61. opt: function(name) {
  62. return this.options[name];
  63. },
  64. // Triggers handlers that are view-related. Modifies args before passing to calendar.
  65. trigger: function(name, thisObj) { // arguments beyond thisObj are passed along
  66. var calendar = this.calendar;
  67. return calendar.trigger.apply(
  68. calendar,
  69. [name, thisObj || this].concat(
  70. Array.prototype.slice.call(arguments, 2), // arguments beyond thisObj
  71. [ this ] // always make the last argument a reference to the view. TODO: deprecate
  72. )
  73. );
  74. },
  75. /* Date Computation
  76. ------------------------------------------------------------------------------------------------------------------*/
  77. // Updates all internal dates for displaying the given unzoned range.
  78. setRange: function(range) {
  79. $.extend(this, range); // assigns every property to this object's member variables
  80. this.updateTitle();
  81. },
  82. // Given a single current unzoned date, produce information about what range to display.
  83. // Subclasses can override. Must return all properties.
  84. computeRange: function(date) {
  85. var intervalUnit = computeIntervalUnit(this.intervalDuration);
  86. var intervalStart = date.clone().startOf(intervalUnit);
  87. var intervalEnd = intervalStart.clone().add(this.intervalDuration);
  88. var start, end;
  89. // normalize the range's time-ambiguity
  90. if (/year|month|week|day/.test(intervalUnit)) { // whole-days?
  91. intervalStart.stripTime();
  92. intervalEnd.stripTime();
  93. }
  94. else { // needs to have a time?
  95. if (!intervalStart.hasTime()) {
  96. intervalStart = this.calendar.time(0); // give 00:00 time
  97. }
  98. if (!intervalEnd.hasTime()) {
  99. intervalEnd = this.calendar.time(0); // give 00:00 time
  100. }
  101. }
  102. start = intervalStart.clone();
  103. start = this.skipHiddenDays(start);
  104. end = intervalEnd.clone();
  105. end = this.skipHiddenDays(end, -1, true); // exclusively move backwards
  106. return {
  107. intervalUnit: intervalUnit,
  108. intervalStart: intervalStart,
  109. intervalEnd: intervalEnd,
  110. start: start,
  111. end: end
  112. };
  113. },
  114. // Computes the new date when the user hits the prev button, given the current date
  115. computePrevDate: function(date) {
  116. return this.massageCurrentDate(
  117. date.clone().startOf(this.intervalUnit).subtract(this.intervalDuration), -1
  118. );
  119. },
  120. // Computes the new date when the user hits the next button, given the current date
  121. computeNextDate: function(date) {
  122. return this.massageCurrentDate(
  123. date.clone().startOf(this.intervalUnit).add(this.intervalDuration)
  124. );
  125. },
  126. // Given an arbitrarily calculated current date of the calendar, returns a date that is ensured to be completely
  127. // visible. `direction` is optional and indicates which direction the current date was being
  128. // incremented or decremented (1 or -1).
  129. massageCurrentDate: function(date, direction) {
  130. if (this.intervalDuration.as('days') <= 1) { // if the view displays a single day or smaller
  131. if (this.isHiddenDay(date)) {
  132. date = this.skipHiddenDays(date, direction);
  133. date.startOf('day');
  134. }
  135. }
  136. return date;
  137. },
  138. /* Title and Date Formatting
  139. ------------------------------------------------------------------------------------------------------------------*/
  140. // Sets the view's title property to the most updated computed value
  141. updateTitle: function() {
  142. this.title = this.computeTitle();
  143. this.calendar.setToolbarsTitle(this.title);
  144. },
  145. // Computes what the title at the top of the calendar should be for this view
  146. computeTitle: function() {
  147. return this.formatRange(
  148. {
  149. // in case intervalStart/End has a time, make sure timezone is correct
  150. start: this.calendar.applyTimezone(this.intervalStart),
  151. end: this.calendar.applyTimezone(this.intervalEnd)
  152. },
  153. this.opt('titleFormat') || this.computeTitleFormat(),
  154. this.opt('titleRangeSeparator')
  155. );
  156. },
  157. // Generates the format string that should be used to generate the title for the current date range.
  158. // Attempts to compute the most appropriate format if not explicitly specified with `titleFormat`.
  159. computeTitleFormat: function() {
  160. if (this.intervalUnit == 'year') {
  161. return 'YYYY';
  162. }
  163. else if (this.intervalUnit == 'month') {
  164. return this.opt('monthYearFormat'); // like "September 2014"
  165. }
  166. else if (this.intervalDuration.as('days') > 1) {
  167. return 'll'; // multi-day range. shorter, like "Sep 9 - 10 2014"
  168. }
  169. else {
  170. return 'LL'; // one day. longer, like "September 9 2014"
  171. }
  172. },
  173. // Utility for formatting a range. Accepts a range object, formatting string, and optional separator.
  174. // Displays all-day ranges naturally, with an inclusive end. Takes the current isRTL into account.
  175. // The timezones of the dates within `range` will be respected.
  176. formatRange: function(range, formatStr, separator) {
  177. var end = range.end;
  178. if (!end.hasTime()) { // all-day?
  179. end = end.clone().subtract(1); // convert to inclusive. last ms of previous day
  180. }
  181. return formatRange(range.start, end, formatStr, separator, this.opt('isRTL'));
  182. },
  183. getAllDayHtml: function() {
  184. return this.opt('allDayHtml') || htmlEscape(this.opt('allDayText'));
  185. },
  186. /* Navigation
  187. ------------------------------------------------------------------------------------------------------------------*/
  188. // Generates HTML for an anchor to another view into the calendar.
  189. // Will either generate an <a> tag or a non-clickable <span> tag, depending on enabled settings.
  190. // `gotoOptions` can either be a moment input, or an object with the form:
  191. // { date, type, forceOff }
  192. // `type` is a view-type like "day" or "week". default value is "day".
  193. // `attrs` and `innerHtml` are use to generate the rest of the HTML tag.
  194. buildGotoAnchorHtml: function(gotoOptions, attrs, innerHtml) {
  195. var date, type, forceOff;
  196. var finalOptions;
  197. if ($.isPlainObject(gotoOptions)) {
  198. date = gotoOptions.date;
  199. type = gotoOptions.type;
  200. forceOff = gotoOptions.forceOff;
  201. }
  202. else {
  203. date = gotoOptions; // a single moment input
  204. }
  205. date = FC.moment(date); // if a string, parse it
  206. finalOptions = { // for serialization into the link
  207. date: date.format('YYYY-MM-DD'),
  208. type: type || 'day'
  209. };
  210. if (typeof attrs === 'string') {
  211. innerHtml = attrs;
  212. attrs = null;
  213. }
  214. attrs = attrs ? ' ' + attrsToStr(attrs) : ''; // will have a leading space
  215. innerHtml = innerHtml || '';
  216. if (!forceOff && this.opt('navLinks')) {
  217. return '<a' + attrs +
  218. ' data-goto="' + htmlEscape(JSON.stringify(finalOptions)) + '">' +
  219. innerHtml +
  220. '</a>';
  221. }
  222. else {
  223. return '<span' + attrs + '>' +
  224. innerHtml +
  225. '</span>';
  226. }
  227. },
  228. /* Rendering
  229. ------------------------------------------------------------------------------------------------------------------*/
  230. // Non-date-related content, like the view's skeleton
  231. // --------------------------------------------------
  232. // Sets the container element that the view should render inside of, does global DOM-related initializations,
  233. // and renders all the non-date-related content inside.
  234. setElement: function(el) {
  235. this.el = el;
  236. this.bindGlobalHandlers();
  237. this.renderSkeleton();
  238. },
  239. // Removes the view's container element from the DOM, clearing any content beforehand.
  240. // Undoes any other DOM-related attachments.
  241. removeElement: function() {
  242. this.unbindGlobalHandlers();
  243. this.unsetDate();
  244. this.unrenderSkeleton();
  245. this.el.remove();
  246. // NOTE: don't null-out this.el in case the View was destroyed within an API callback.
  247. // We don't null-out the View's other jQuery element references upon destroy,
  248. // so we shouldn't kill this.el either.
  249. },
  250. // Renders the basic structure of the view before any content is rendered
  251. renderSkeleton: function() {
  252. // subclasses should implement
  253. },
  254. // Unrenders the basic structure of the view
  255. unrenderSkeleton: function() {
  256. // subclasses should implement
  257. },
  258. // Date-related-content, like date cells, date indicators, and events
  259. // ------------------------------------------------------------------
  260. // Renders ALL date related content, including events. Guaranteed to redraw content.
  261. // async
  262. setDate: function(date, forcedScroll) {
  263. var _this = this;
  264. // do this before unsetDate, which is destructive
  265. this.captureScroll();
  266. this.calendar.freezeContentHeight();
  267. this.unsetDate();
  268. this.isDateSet = true;
  269. return this.dateSetQueue.push(function() {
  270. _this.setRange(_this.computeRange(date));
  271. _this.displayDateVisuals();
  272. _this.calendar.unfreezeContentHeight();
  273. _this.releaseScroll(true, forcedScroll); // isInitial=true
  274. _this.triggerDateVisualsRendered();
  275. }).then(function() {
  276. return _this.displayEvents();
  277. }, function() {
  278. // failure. TODO: implement in RunQueue
  279. _this.calendar.unfreezeContentHeight();
  280. _this.discardScroll();
  281. });
  282. },
  283. // sync
  284. unsetDate: function() {
  285. if (this.isDateSet) {
  286. this.isDateSet = false; // important to do first
  287. this.dateSetQueue.clear();
  288. this.stopDisplayingEvents();
  289. this.stopDisplayingDateVisuals();
  290. }
  291. },
  292. // sync
  293. displayDateVisuals: function() {
  294. this.stopDisplayingDateVisuals();
  295. this.isDisplayingDateVisuals = true;
  296. if (this.render) {
  297. this.render(); // TODO: deprecate
  298. }
  299. this.renderDates();
  300. this.updateSize();
  301. this.renderBusinessHours(); // might need coordinates, so should go after updateSize()
  302. this.startNowIndicator();
  303. },
  304. // sync
  305. stopDisplayingDateVisuals: function() {
  306. if (this.isDisplayingDateVisuals) {
  307. this.isDisplayingDateVisuals = false;
  308. this.unselect();
  309. this.stopNowIndicator();
  310. this.triggerUnrender();
  311. this.unrenderBusinessHours();
  312. this.unrenderDates();
  313. if (this.destroy) {
  314. this.destroy(); // TODO: deprecate
  315. }
  316. }
  317. },
  318. // Renders the view's date-related content.
  319. // Assumes setRange has already been called and the skeleton has already been rendered.
  320. renderDates: function() {
  321. // subclasses should implement
  322. },
  323. // Unrenders the view's date-related content
  324. unrenderDates: function() {
  325. // subclasses should override
  326. },
  327. // Misc rendering utils
  328. // --------------------
  329. // Can be extended to rely on other things
  330. triggerDateVisualsRendered: function() {
  331. this.triggerRender();
  332. },
  333. // Signals that the view's content has been rendered
  334. triggerRender: function() {
  335. this.trigger('viewRender', this, this, this.el);
  336. },
  337. // Signals that the view's content is about to be unrendered
  338. triggerUnrender: function() {
  339. this.trigger('viewDestroy', this, this, this.el);
  340. },
  341. // Binds DOM handlers to elements that reside outside the view container, such as the document
  342. bindGlobalHandlers: function() {
  343. this.listenTo($(document), 'mousedown', this.handleDocumentMousedown);
  344. this.listenTo($(document), 'touchstart', this.processUnselect);
  345. },
  346. // Unbinds DOM handlers from elements that reside outside the view container
  347. unbindGlobalHandlers: function() {
  348. this.stopListeningTo($(document));
  349. },
  350. // Initializes internal variables related to theming
  351. initThemingProps: function() {
  352. var tm = this.opt('theme') ? 'ui' : 'fc';
  353. this.widgetHeaderClass = tm + '-widget-header';
  354. this.widgetContentClass = tm + '-widget-content';
  355. this.highlightStateClass = tm + '-state-highlight';
  356. },
  357. /* Business Hours
  358. ------------------------------------------------------------------------------------------------------------------*/
  359. // Renders business-hours onto the view. Assumes updateSize has already been called.
  360. renderBusinessHours: function() {
  361. // subclasses should implement
  362. },
  363. // Unrenders previously-rendered business-hours
  364. unrenderBusinessHours: function() {
  365. // subclasses should implement
  366. },
  367. /* Now Indicator
  368. ------------------------------------------------------------------------------------------------------------------*/
  369. // Immediately render the current time indicator and begins re-rendering it at an interval,
  370. // which is defined by this.getNowIndicatorUnit().
  371. // TODO: somehow do this for the current whole day's background too
  372. startNowIndicator: function() {
  373. var _this = this;
  374. var unit;
  375. var update;
  376. var delay; // ms wait value
  377. if (this.opt('nowIndicator')) {
  378. unit = this.getNowIndicatorUnit();
  379. if (unit) {
  380. update = proxy(this, 'updateNowIndicator'); // bind to `this`
  381. this.initialNowDate = this.calendar.getNow();
  382. this.initialNowQueriedMs = +new Date();
  383. this.renderNowIndicator(this.initialNowDate);
  384. this.isNowIndicatorRendered = true;
  385. // wait until the beginning of the next interval
  386. delay = this.initialNowDate.clone().startOf(unit).add(1, unit) - this.initialNowDate;
  387. this.nowIndicatorTimeoutID = setTimeout(function() {
  388. _this.nowIndicatorTimeoutID = null;
  389. update();
  390. delay = +moment.duration(1, unit);
  391. delay = Math.max(100, delay); // prevent too frequent
  392. _this.nowIndicatorIntervalID = setInterval(update, delay); // update every interval
  393. }, delay);
  394. }
  395. }
  396. },
  397. // rerenders the now indicator, computing the new current time from the amount of time that has passed
  398. // since the initial getNow call.
  399. updateNowIndicator: function() {
  400. if (this.isNowIndicatorRendered) {
  401. this.unrenderNowIndicator();
  402. this.renderNowIndicator(
  403. this.initialNowDate.clone().add(new Date() - this.initialNowQueriedMs) // add ms
  404. );
  405. }
  406. },
  407. // Immediately unrenders the view's current time indicator and stops any re-rendering timers.
  408. // Won't cause side effects if indicator isn't rendered.
  409. stopNowIndicator: function() {
  410. if (this.isNowIndicatorRendered) {
  411. if (this.nowIndicatorTimeoutID) {
  412. clearTimeout(this.nowIndicatorTimeoutID);
  413. this.nowIndicatorTimeoutID = null;
  414. }
  415. if (this.nowIndicatorIntervalID) {
  416. clearTimeout(this.nowIndicatorIntervalID);
  417. this.nowIndicatorIntervalID = null;
  418. }
  419. this.unrenderNowIndicator();
  420. this.isNowIndicatorRendered = false;
  421. }
  422. },
  423. // Returns a string unit, like 'second' or 'minute' that defined how often the current time indicator
  424. // should be refreshed. If something falsy is returned, no time indicator is rendered at all.
  425. getNowIndicatorUnit: function() {
  426. // subclasses should implement
  427. },
  428. // Renders a current time indicator at the given datetime
  429. renderNowIndicator: function(date) {
  430. // subclasses should implement
  431. },
  432. // Undoes the rendering actions from renderNowIndicator
  433. unrenderNowIndicator: function() {
  434. // subclasses should implement
  435. },
  436. /* Dimensions
  437. ------------------------------------------------------------------------------------------------------------------*/
  438. // Refreshes anything dependant upon sizing of the container element of the grid
  439. updateSize: function(isResize) {
  440. var scrollState;
  441. if (isResize) {
  442. this.capturedScroll();
  443. }
  444. this.updateHeight(isResize);
  445. this.updateWidth(isResize);
  446. this.updateNowIndicator();
  447. if (isResize) {
  448. this.releaseScroll();
  449. }
  450. },
  451. // Refreshes the horizontal dimensions of the calendar
  452. updateWidth: function(isResize) {
  453. // subclasses should implement
  454. },
  455. // Refreshes the vertical dimensions of the calendar
  456. updateHeight: function(isResize) {
  457. var calendar = this.calendar; // we poll the calendar for height information
  458. this.setHeight(
  459. calendar.getSuggestedViewHeight(),
  460. calendar.isHeightAuto()
  461. );
  462. },
  463. // Updates the vertical dimensions of the calendar to the specified height.
  464. // if `isAuto` is set to true, height becomes merely a suggestion and the view should use its "natural" height.
  465. setHeight: function(height, isAuto) {
  466. // subclasses should implement
  467. },
  468. /* Scroller
  469. ------------------------------------------------------------------------------------------------------------------*/
  470. capturedScrollDepth: 0,
  471. capturedScroll: null,
  472. captureScroll: function() {
  473. if (!(this.capturedScrollDepth++)) {
  474. this.capturedScroll = this.isDisplayingDateVisuals ? this.queryScroll() : {};
  475. }
  476. },
  477. releaseScroll: function(isInitial, forcedScroll) {
  478. var _this = this;
  479. var scroll;
  480. var exec;
  481. if (!(--this.capturedScrollDepth)) {
  482. scroll = forcedScroll || this.capturedScroll;
  483. this.capturedScroll = null;
  484. if (isInitial) {
  485. $.extend(scroll, this.computeInitialScroll());
  486. }
  487. exec = function() { _this.setScroll(scroll); };
  488. exec();
  489. if (isInitial) {
  490. setTimeout(exec, 0);
  491. }
  492. }
  493. },
  494. discardScroll: function() {
  495. if (!(--this.capturedScrollDepth)) {
  496. this.capturedScroll = null;
  497. }
  498. },
  499. computeInitialScroll: function() {
  500. return {};
  501. },
  502. queryScroll: function() {
  503. return {};
  504. },
  505. setScroll: function(scroll) {
  506. },
  507. /* Event Elements / Segments
  508. ------------------------------------------------------------------------------------------------------------------*/
  509. // Does everything necessary to display the given events onto the current view. Guaranteed to redraw content.
  510. // async. promise might not resolve if rendering cancelled.
  511. // Assumes date visuals already displayed.
  512. displayEvents: function() {
  513. var _this = this;
  514. return this.displayingEvents = this.requestEvents().then(function(events) {
  515. _this.bindEvents(); // listen to changes. do this before the setEvents, because might trigger a reset itself
  516. return _this.setEvents(events);
  517. });
  518. },
  519. // Does everything necessary to clear the view's currently-rendered events.
  520. // sync
  521. stopDisplayingEvents: function() {
  522. this.displayingEvents = null;
  523. this.unbindEvents();
  524. this.unsetEvents();
  525. },
  526. requestEvents: function() {
  527. return this.calendar.requestEvents(this.start, this.end);
  528. },
  529. bindEvents: function() {
  530. if (!this.isEventsBounds) {
  531. this.listenTo(this.calendar, 'resetEvents', this.resetEvents);
  532. this.isEventsBounds = true;
  533. }
  534. },
  535. unbindEvents: function() {
  536. if (this.isEventsBounds) {
  537. this.stopListeningTo(this.calendar, 'resetEvents');
  538. this.isEventsBounds = false;
  539. }
  540. },
  541. // async
  542. resetEvents: function(events) {
  543. var _this = this;
  544. // do this before unsetEvents, a destructive action
  545. this.captureScroll();
  546. this.calendar.freezeContentHeight();
  547. this.unsetEvents();
  548. return this.setEvents(events).then(function() {
  549. _this.releaseScroll();
  550. _this.calendar.freezeContentHeight();
  551. });
  552. },
  553. // async
  554. setEvents: function(events) {
  555. var _this = this;
  556. if (this.isEventsSet) {
  557. return this.resetEvents(events);
  558. }
  559. else {
  560. this.isEventsSet = true;
  561. return this.eventRenderQueue.push(function() {
  562. _this.captureScroll();
  563. _this.calendar.freezeContentHeight();
  564. _this.renderEvents(events);
  565. _this.calendar.unfreezeContentHeight();
  566. _this.releaseScroll();
  567. _this.triggerEventRender();
  568. });
  569. }
  570. },
  571. // sync
  572. unsetEvents: function() {
  573. var _this = this;
  574. if (this.isEventsSet) {
  575. this.isEventsSet = false; // must go first. so triggers don't reinvoke unsetEvents
  576. this.eventRenderQueue.clear(); // kill in-progress renders
  577. this.triggerEventUnrender();
  578. if (this.destroyEvents) {
  579. this.destroyEvents(); // TODO: deprecate
  580. }
  581. this.unrenderEvents();
  582. }
  583. },
  584. // Renders the events onto the view.
  585. renderEvents: function(events) {
  586. // subclasses should implement
  587. },
  588. // Removes event elements from the view.
  589. unrenderEvents: function() {
  590. // subclasses should implement
  591. },
  592. // Signals that all events have been rendered
  593. triggerEventRender: function() {
  594. this.renderedEventSegEach(function(seg) {
  595. this.trigger('eventAfterRender', seg.event, seg.event, seg.el);
  596. });
  597. this.trigger('eventAfterAllRender');
  598. },
  599. // Signals that all event elements are about to be removed
  600. triggerEventUnrender: function() {
  601. this.renderedEventSegEach(function(seg) {
  602. this.trigger('eventDestroy', seg.event, seg.event, seg.el);
  603. });
  604. },
  605. // Given an event and the default element used for rendering, returns the element that should actually be used.
  606. // Basically runs events and elements through the eventRender hook.
  607. resolveEventEl: function(event, el) {
  608. var custom = this.trigger('eventRender', event, event, el);
  609. if (custom === false) { // means don't render at all
  610. el = null;
  611. }
  612. else if (custom && custom !== true) {
  613. el = $(custom);
  614. }
  615. return el;
  616. },
  617. // Hides all rendered event segments linked to the given event
  618. showEvent: function(event) {
  619. this.renderedEventSegEach(function(seg) {
  620. seg.el.css('visibility', '');
  621. }, event);
  622. },
  623. // Shows all rendered event segments linked to the given event
  624. hideEvent: function(event) {
  625. this.renderedEventSegEach(function(seg) {
  626. seg.el.css('visibility', 'hidden');
  627. }, event);
  628. },
  629. // Iterates through event segments that have been rendered (have an el). Goes through all by default.
  630. // If the optional `event` argument is specified, only iterates through segments linked to that event.
  631. // The `this` value of the callback function will be the view.
  632. renderedEventSegEach: function(func, event) {
  633. var segs = this.getEventSegs();
  634. var i;
  635. for (i = 0; i < segs.length; i++) {
  636. if (!event || segs[i].event._id === event._id) {
  637. if (segs[i].el) {
  638. func.call(this, segs[i]);
  639. }
  640. }
  641. }
  642. },
  643. // Retrieves all the rendered segment objects for the view
  644. getEventSegs: function() {
  645. // subclasses must implement
  646. return [];
  647. },
  648. /* Event Drag-n-Drop
  649. ------------------------------------------------------------------------------------------------------------------*/
  650. // Computes if the given event is allowed to be dragged by the user
  651. isEventDraggable: function(event) {
  652. return this.isEventStartEditable(event);
  653. },
  654. isEventStartEditable: function(event) {
  655. return firstDefined(
  656. event.startEditable,
  657. (event.source || {}).startEditable,
  658. this.opt('eventStartEditable'),
  659. this.isEventGenerallyEditable(event)
  660. );
  661. },
  662. isEventGenerallyEditable: function(event) {
  663. return firstDefined(
  664. event.editable,
  665. (event.source || {}).editable,
  666. this.opt('editable')
  667. );
  668. },
  669. // Must be called when an event in the view is dropped onto new location.
  670. // `dropLocation` is an object that contains the new zoned start/end/allDay values for the event.
  671. reportEventDrop: function(event, dropLocation, largeUnit, el, ev) {
  672. var calendar = this.calendar;
  673. var mutateResult = calendar.mutateEvent(event, dropLocation, largeUnit);
  674. var undoFunc = function() {
  675. mutateResult.undo();
  676. calendar.reportEventChange();
  677. };
  678. this.triggerEventDrop(event, mutateResult.dateDelta, undoFunc, el, ev);
  679. calendar.reportEventChange(); // will rerender events
  680. },
  681. // Triggers event-drop handlers that have subscribed via the API
  682. triggerEventDrop: function(event, dateDelta, undoFunc, el, ev) {
  683. this.trigger('eventDrop', el[0], event, dateDelta, undoFunc, ev, {}); // {} = jqui dummy
  684. },
  685. /* External Element Drag-n-Drop
  686. ------------------------------------------------------------------------------------------------------------------*/
  687. // Must be called when an external element, via jQuery UI, has been dropped onto the calendar.
  688. // `meta` is the parsed data that has been embedded into the dragging event.
  689. // `dropLocation` is an object that contains the new zoned start/end/allDay values for the event.
  690. reportExternalDrop: function(meta, dropLocation, el, ev, ui) {
  691. var eventProps = meta.eventProps;
  692. var eventInput;
  693. var event;
  694. // Try to build an event object and render it. TODO: decouple the two
  695. if (eventProps) {
  696. eventInput = $.extend({}, eventProps, dropLocation);
  697. event = this.calendar.renderEvent(eventInput, meta.stick)[0]; // renderEvent returns an array
  698. }
  699. this.triggerExternalDrop(event, dropLocation, el, ev, ui);
  700. },
  701. // Triggers external-drop handlers that have subscribed via the API
  702. triggerExternalDrop: function(event, dropLocation, el, ev, ui) {
  703. // trigger 'drop' regardless of whether element represents an event
  704. this.trigger('drop', el[0], dropLocation.start, ev, ui);
  705. if (event) {
  706. this.trigger('eventReceive', null, event); // signal an external event landed
  707. }
  708. },
  709. /* Drag-n-Drop Rendering (for both events and external elements)
  710. ------------------------------------------------------------------------------------------------------------------*/
  711. // Renders a visual indication of a event or external-element drag over the given drop zone.
  712. // If an external-element, seg will be `null`.
  713. // Must return elements used for any mock events.
  714. renderDrag: function(dropLocation, seg) {
  715. // subclasses must implement
  716. },
  717. // Unrenders a visual indication of an event or external-element being dragged.
  718. unrenderDrag: function() {
  719. // subclasses must implement
  720. },
  721. /* Event Resizing
  722. ------------------------------------------------------------------------------------------------------------------*/
  723. // Computes if the given event is allowed to be resized from its starting edge
  724. isEventResizableFromStart: function(event) {
  725. return this.opt('eventResizableFromStart') && this.isEventResizable(event);
  726. },
  727. // Computes if the given event is allowed to be resized from its ending edge
  728. isEventResizableFromEnd: function(event) {
  729. return this.isEventResizable(event);
  730. },
  731. // Computes if the given event is allowed to be resized by the user at all
  732. isEventResizable: function(event) {
  733. var source = event.source || {};
  734. return firstDefined(
  735. event.durationEditable,
  736. source.durationEditable,
  737. this.opt('eventDurationEditable'),
  738. event.editable,
  739. source.editable,
  740. this.opt('editable')
  741. );
  742. },
  743. // Must be called when an event in the view has been resized to a new length
  744. reportEventResize: function(event, resizeLocation, largeUnit, el, ev) {
  745. var calendar = this.calendar;
  746. var mutateResult = calendar.mutateEvent(event, resizeLocation, largeUnit);
  747. var undoFunc = function() {
  748. mutateResult.undo();
  749. calendar.reportEventChange();
  750. };
  751. this.triggerEventResize(event, mutateResult.durationDelta, undoFunc, el, ev);
  752. calendar.reportEventChange(); // will rerender events
  753. },
  754. // Triggers event-resize handlers that have subscribed via the API
  755. triggerEventResize: function(event, durationDelta, undoFunc, el, ev) {
  756. this.trigger('eventResize', el[0], event, durationDelta, undoFunc, ev, {}); // {} = jqui dummy
  757. },
  758. /* Selection (time range)
  759. ------------------------------------------------------------------------------------------------------------------*/
  760. // Selects a date span on the view. `start` and `end` are both Moments.
  761. // `ev` is the native mouse event that begin the interaction.
  762. select: function(span, ev) {
  763. this.unselect(ev);
  764. this.renderSelection(span);
  765. this.reportSelection(span, ev);
  766. },
  767. // Renders a visual indication of the selection
  768. renderSelection: function(span) {
  769. // subclasses should implement
  770. },
  771. // Called when a new selection is made. Updates internal state and triggers handlers.
  772. reportSelection: function(span, ev) {
  773. this.isSelected = true;
  774. this.triggerSelect(span, ev);
  775. },
  776. // Triggers handlers to 'select'
  777. triggerSelect: function(span, ev) {
  778. this.trigger(
  779. 'select',
  780. null,
  781. this.calendar.applyTimezone(span.start), // convert to calendar's tz for external API
  782. this.calendar.applyTimezone(span.end), // "
  783. ev
  784. );
  785. },
  786. // Undoes a selection. updates in the internal state and triggers handlers.
  787. // `ev` is the native mouse event that began the interaction.
  788. unselect: function(ev) {
  789. if (this.isSelected) {
  790. this.isSelected = false;
  791. if (this.destroySelection) {
  792. this.destroySelection(); // TODO: deprecate
  793. }
  794. this.unrenderSelection();
  795. this.trigger('unselect', null, ev);
  796. }
  797. },
  798. // Unrenders a visual indication of selection
  799. unrenderSelection: function() {
  800. // subclasses should implement
  801. },
  802. /* Event Selection
  803. ------------------------------------------------------------------------------------------------------------------*/
  804. selectEvent: function(event) {
  805. if (!this.selectedEvent || this.selectedEvent !== event) {
  806. this.unselectEvent();
  807. this.renderedEventSegEach(function(seg) {
  808. seg.el.addClass('fc-selected');
  809. }, event);
  810. this.selectedEvent = event;
  811. }
  812. },
  813. unselectEvent: function() {
  814. if (this.selectedEvent) {
  815. this.renderedEventSegEach(function(seg) {
  816. seg.el.removeClass('fc-selected');
  817. }, this.selectedEvent);
  818. this.selectedEvent = null;
  819. }
  820. },
  821. isEventSelected: function(event) {
  822. // event references might change on refetchEvents(), while selectedEvent doesn't,
  823. // so compare IDs
  824. return this.selectedEvent && this.selectedEvent._id === event._id;
  825. },
  826. /* Mouse / Touch Unselecting (time range & event unselection)
  827. ------------------------------------------------------------------------------------------------------------------*/
  828. // TODO: move consistently to down/start or up/end?
  829. // TODO: don't kill previous selection if touch scrolling
  830. handleDocumentMousedown: function(ev) {
  831. if (isPrimaryMouseButton(ev)) {
  832. this.processUnselect(ev);
  833. }
  834. },
  835. processUnselect: function(ev) {
  836. this.processRangeUnselect(ev);
  837. this.processEventUnselect(ev);
  838. },
  839. processRangeUnselect: function(ev) {
  840. var ignore;
  841. // is there a time-range selection?
  842. if (this.isSelected && this.opt('unselectAuto')) {
  843. // only unselect if the clicked element is not identical to or inside of an 'unselectCancel' element
  844. ignore = this.opt('unselectCancel');
  845. if (!ignore || !$(ev.target).closest(ignore).length) {
  846. this.unselect(ev);
  847. }
  848. }
  849. },
  850. processEventUnselect: function(ev) {
  851. if (this.selectedEvent) {
  852. if (!$(ev.target).closest('.fc-selected').length) {
  853. this.unselectEvent();
  854. }
  855. }
  856. },
  857. /* Day Click
  858. ------------------------------------------------------------------------------------------------------------------*/
  859. // Triggers handlers to 'dayClick'
  860. // Span has start/end of the clicked area. Only the start is useful.
  861. triggerDayClick: function(span, dayEl, ev) {
  862. this.trigger(
  863. 'dayClick',
  864. dayEl,
  865. this.calendar.applyTimezone(span.start), // convert to calendar's timezone for external API
  866. ev
  867. );
  868. },
  869. /* Date Utils
  870. ------------------------------------------------------------------------------------------------------------------*/
  871. // Initializes internal variables related to calculating hidden days-of-week
  872. initHiddenDays: function() {
  873. var hiddenDays = this.opt('hiddenDays') || []; // array of day-of-week indices that are hidden
  874. var isHiddenDayHash = []; // is the day-of-week hidden? (hash with day-of-week-index -> bool)
  875. var dayCnt = 0;
  876. var i;
  877. if (this.opt('weekends') === false) {
  878. hiddenDays.push(0, 6); // 0=sunday, 6=saturday
  879. }
  880. for (i = 0; i < 7; i++) {
  881. if (
  882. !(isHiddenDayHash[i] = $.inArray(i, hiddenDays) !== -1)
  883. ) {
  884. dayCnt++;
  885. }
  886. }
  887. if (!dayCnt) {
  888. throw 'invalid hiddenDays'; // all days were hidden? bad.
  889. }
  890. this.isHiddenDayHash = isHiddenDayHash;
  891. },
  892. // Is the current day hidden?
  893. // `day` is a day-of-week index (0-6), or a Moment
  894. isHiddenDay: function(day) {
  895. if (moment.isMoment(day)) {
  896. day = day.day();
  897. }
  898. return this.isHiddenDayHash[day];
  899. },
  900. // Incrementing the current day until it is no longer a hidden day, returning a copy.
  901. // If the initial value of `date` is not a hidden day, don't do anything.
  902. // Pass `isExclusive` as `true` if you are dealing with an end date.
  903. // `inc` defaults to `1` (increment one day forward each time)
  904. skipHiddenDays: function(date, inc, isExclusive) {
  905. var out = date.clone();
  906. inc = inc || 1;
  907. while (
  908. this.isHiddenDayHash[(out.day() + (isExclusive ? inc : 0) + 7) % 7]
  909. ) {
  910. out.add(inc, 'days');
  911. }
  912. return out;
  913. },
  914. // Returns the date range of the full days the given range visually appears to occupy.
  915. // Returns a new range object.
  916. computeDayRange: function(range) {
  917. var startDay = range.start.clone().stripTime(); // the beginning of the day the range starts
  918. var end = range.end;
  919. var endDay = null;
  920. var endTimeMS;
  921. if (end) {
  922. endDay = end.clone().stripTime(); // the beginning of the day the range exclusively ends
  923. endTimeMS = +end.time(); // # of milliseconds into `endDay`
  924. // If the end time is actually inclusively part of the next day and is equal to or
  925. // beyond the next day threshold, adjust the end to be the exclusive end of `endDay`.
  926. // Otherwise, leaving it as inclusive will cause it to exclude `endDay`.
  927. if (endTimeMS && endTimeMS >= this.nextDayThreshold) {
  928. endDay.add(1, 'days');
  929. }
  930. }
  931. // If no end was specified, or if it is within `startDay` but not past nextDayThreshold,
  932. // assign the default duration of one day.
  933. if (!end || endDay <= startDay) {
  934. endDay = startDay.clone().add(1, 'days');
  935. }
  936. return { start: startDay, end: endDay };
  937. },
  938. // Does the given event visually appear to occupy more than one day?
  939. isMultiDayEvent: function(event) {
  940. var range = this.computeDayRange(event); // event is range-ish
  941. return range.end.diff(range.start, 'days') > 1;
  942. }
  943. });