View.js 44 KB

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