View.js 32 KB

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