Calendar.render.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. Calendar.mixin({
  2. el: null,
  3. contentEl: null,
  4. suggestedViewHeight: null,
  5. ignoreUpdateViewSize: 0,
  6. windowResizeProxy: null,
  7. renderQueue: null,
  8. batchRenderDepth: 0,
  9. freezeContentHeightDepth: 0,
  10. render: function() {
  11. if (!this.contentEl) {
  12. this.initialRender();
  13. }
  14. else if (this.elementVisible()) {
  15. // mainly for the public API
  16. this.calcSize();
  17. this.renderView();
  18. }
  19. },
  20. initialRender: function() {
  21. var _this = this;
  22. var el = this.el;
  23. el.addClass('fc');
  24. // event delegation for nav links
  25. el.on('click.fc', 'a[data-goto]', function(ev) {
  26. var anchorEl = $(this);
  27. var gotoOptions = anchorEl.data('goto'); // will automatically parse JSON
  28. var date = _this.moment(gotoOptions.date);
  29. var viewType = gotoOptions.type;
  30. // property like "navLinkDayClick". might be a string or a function
  31. var customAction = _this.view.opt('navLink' + capitaliseFirstLetter(viewType) + 'Click');
  32. if (typeof customAction === 'function') {
  33. customAction(date, ev);
  34. }
  35. else {
  36. if (typeof customAction === 'string') {
  37. viewType = customAction;
  38. }
  39. _this.zoomTo(date, viewType);
  40. }
  41. });
  42. // called immediately, and upon option change
  43. this.optionsModel.watch('settingTheme', [ '?theme' ], function(opts) {
  44. var themeClass = ThemeRegistry.getThemeClass(opts.theme);
  45. var theme = new themeClass(_this.optionsModel);
  46. var widgetClass = theme.getClass('widget');
  47. _this.theme = theme;
  48. if (widgetClass) {
  49. el.addClass(widgetClass);
  50. }
  51. }, function() {
  52. var widgetClass = _this.theme.getClass('widget');
  53. _this.theme = null;
  54. if (widgetClass) {
  55. el.removeClass(widgetClass);
  56. }
  57. });
  58. this.optionsModel.watch('settingBusinessHourGenerator', [ '?businessHours' ], function(deps) {
  59. _this.businessHourGenerator = new BusinessHourGenerator(deps.businessHours, _this);
  60. if (_this.view) {
  61. _this.view.set('businessHourGenerator', _this.businessHourGenerator)
  62. }
  63. }, function() {
  64. _this.businessHourGenerator = null;
  65. });
  66. // called immediately, and upon option change.
  67. // HACK: locale often affects isRTL, so we explicitly listen to that too.
  68. this.optionsModel.watch('applyingDirClasses', [ '?isRTL', '?locale' ], function(opts) {
  69. el.toggleClass('fc-ltr', !opts.isRTL);
  70. el.toggleClass('fc-rtl', opts.isRTL);
  71. });
  72. this.contentEl = $("<div class='fc-view-container'/>").prependTo(el);
  73. this.initToolbars();
  74. this.renderHeader();
  75. this.renderFooter();
  76. this.renderView(this.opt('defaultView'));
  77. if (this.opt('handleWindowResize')) {
  78. $(window).resize(
  79. this.windowResizeProxy = debounce( // prevents rapid calls
  80. this.windowResize.bind(this),
  81. this.opt('windowResizeDelay')
  82. )
  83. );
  84. }
  85. },
  86. destroy: function() {
  87. if (this.view) {
  88. this.clearView();
  89. }
  90. this.toolbarsManager.proxyCall('removeElement');
  91. this.contentEl.remove();
  92. this.el.removeClass('fc fc-ltr fc-rtl');
  93. // removes theme-related root className
  94. this.optionsModel.unwatch('settingTheme');
  95. this.optionsModel.unwatch('settingBusinessHourGenerator');
  96. this.el.off('.fc'); // unbind nav link handlers
  97. if (this.windowResizeProxy) {
  98. $(window).unbind('resize', this.windowResizeProxy);
  99. this.windowResizeProxy = null;
  100. }
  101. GlobalEmitter.unneeded();
  102. },
  103. elementVisible: function() {
  104. return this.el.is(':visible');
  105. },
  106. // Render Queue
  107. // -----------------------------------------------------------------------------------------------------------------
  108. buildRenderQueue: function() {
  109. var renderQueue = new RenderQueue({
  110. event: this.opt('eventRenderWait')
  111. });
  112. this.listenTo(renderQueue, 'start', this.onRenderQueueStart);
  113. this.listenTo(renderQueue, 'stop', this.onRenderQueueStop);
  114. return renderQueue;
  115. },
  116. startBatchRender: function() {
  117. if (!(this.batchRenderDepth++) && this.renderQueue) {
  118. this.renderQueue.pause();
  119. }
  120. },
  121. stopBatchRender: function() {
  122. if (!(--this.batchRenderDepth) && this.renderQueue) {
  123. this.renderQueue.resume();
  124. }
  125. },
  126. onRenderQueueStart: function() {
  127. this.freezeContentHeight();
  128. this.view.addScroll(this.view.queryScroll());
  129. },
  130. onRenderQueueStop: function() {
  131. if (this.updateViewSize()) { // success?
  132. this.view.popScroll();
  133. }
  134. this.thawContentHeight();
  135. },
  136. bindViewHandlers: function(view) {
  137. var _this = this;
  138. this.listenTo(view, 'before:change', this.startBatchRender);
  139. this.listenTo(view, 'change', this.stopBatchRender);
  140. view.watch('titleForCalendar', [ 'title' ], function(deps) { // TODO: better system
  141. if (view === _this.view) { // hack
  142. _this.setToolbarsTitle(deps.title);
  143. }
  144. });
  145. view.watch('dateProfileForCalendar', [ 'dateProfile' ], function(deps) {
  146. if (view === _this.view) { // hack
  147. _this.currentDate = deps.dateProfile.date; // might have been constrained by view dates
  148. _this.updateToolbarButtons(deps.dateProfile);
  149. }
  150. });
  151. },
  152. unbindViewHandlers: function(view) {
  153. this.stopListeningTo(view);
  154. view.unwatch('titleForCalendar');
  155. view.unwatch('dateProfileForCalendar');
  156. },
  157. // View Rendering
  158. // -----------------------------------------------------------------------------------
  159. // Renders a view because of a date change, view-type change, or for the first time.
  160. // If not given a viewType, keep the current view but render different dates.
  161. // Accepts an optional scroll state to restore to.
  162. renderView: function(viewType) {
  163. var oldView = this.view;
  164. var newView;
  165. this.freezeContentHeight();
  166. if (oldView && viewType && oldView.type !== viewType) {
  167. this.clearView();
  168. }
  169. // if viewType changed, or the view was never created, create a fresh view
  170. if (!this.view && viewType) {
  171. newView = this.view =
  172. this.viewsByType[viewType] ||
  173. (this.viewsByType[viewType] = this.instantiateView(viewType));
  174. this.bindViewHandlers(newView);
  175. newView.setElement(
  176. $("<div class='fc-view fc-" + viewType + "-view' />").appendTo(this.contentEl)
  177. );
  178. this.toolbarsManager.proxyCall('activateButton', viewType);
  179. }
  180. if (this.view && this.elementVisible()) {
  181. // prevent unnecessary change firing
  182. if (this.view.get('businessHourGenerator') !== this.businessHourGenerator) {
  183. this.view.set('businessHourGenerator', this.businessHourGenerator);
  184. }
  185. this.view.setDate(this.currentDate);
  186. }
  187. this.thawContentHeight();
  188. },
  189. // Unrenders the current view and reflects this change in the Header.
  190. // Unregsiters the `view`, but does not remove from viewByType hash.
  191. clearView: function() {
  192. var currentView = this.view;
  193. this.toolbarsManager.proxyCall('deactivateButton', currentView.type);
  194. this.unbindViewHandlers(currentView);
  195. currentView.removeElement();
  196. this.view = null;
  197. },
  198. // Destroys the view, including the view object. Then, re-instantiates it and renders it.
  199. // Maintains the same scroll state.
  200. // TODO: maintain any other user-manipulated state.
  201. reinitView: function() {
  202. var oldView = this.view;
  203. var scroll = oldView.queryScroll(); // wouldn't be so complicated if Calendar owned the scroll
  204. this.startBatchRender();
  205. this.clearView();
  206. this.calcSize();
  207. this.renderView(oldView.type); // needs the type to freshly render
  208. // ensure old scroll is restored exactly
  209. scroll.isLocked = true;
  210. this.view.addScroll(scroll);
  211. this.stopBatchRender();
  212. },
  213. // Resizing
  214. // -----------------------------------------------------------------------------------
  215. getSuggestedViewHeight: function() {
  216. if (this.suggestedViewHeight === null) {
  217. this.calcSize();
  218. }
  219. return this.suggestedViewHeight;
  220. },
  221. isHeightAuto: function() {
  222. return this.opt('contentHeight') === 'auto' || this.opt('height') === 'auto';
  223. },
  224. updateViewSize: function(isResize) {
  225. var view = this.view;
  226. var scroll;
  227. if (!this.ignoreUpdateViewSize && this.elementVisible()) {
  228. if (isResize) {
  229. this._calcSize();
  230. scroll = view.queryScroll();
  231. }
  232. this.ignoreUpdateViewSize++;
  233. view.updateSize(
  234. this.getSuggestedViewHeight(),
  235. this.isHeightAuto(),
  236. isResize
  237. );
  238. this.ignoreUpdateViewSize--;
  239. if (isResize) {
  240. view.applyScroll(scroll);
  241. }
  242. return true; // signal success
  243. }
  244. },
  245. calcSize: function() {
  246. if (this.elementVisible()) {
  247. this._calcSize();
  248. }
  249. },
  250. _calcSize: function() { // assumes elementVisible
  251. var contentHeightInput = this.opt('contentHeight');
  252. var heightInput = this.opt('height');
  253. if (typeof contentHeightInput === 'number') { // exists and not 'auto'
  254. this.suggestedViewHeight = contentHeightInput;
  255. }
  256. else if (typeof contentHeightInput === 'function') { // exists and is a function
  257. this.suggestedViewHeight = contentHeightInput();
  258. }
  259. else if (typeof heightInput === 'number') { // exists and not 'auto'
  260. this.suggestedViewHeight = heightInput - this.queryToolbarsHeight();
  261. }
  262. else if (typeof heightInput === 'function') { // exists and is a function
  263. this.suggestedViewHeight = heightInput() - this.queryToolbarsHeight();
  264. }
  265. else if (heightInput === 'parent') { // set to height of parent element
  266. this.suggestedViewHeight = this.el.parent().height() - this.queryToolbarsHeight();
  267. }
  268. else {
  269. this.suggestedViewHeight = Math.round(
  270. this.contentEl.width() /
  271. Math.max(this.opt('aspectRatio'), .5)
  272. );
  273. }
  274. },
  275. windowResize: function(ev) {
  276. if (
  277. ev.target === window && // so we don't process jqui "resize" events that have bubbled up
  278. this.view &&
  279. this.view.isDatesRendered
  280. ) {
  281. if (this.updateViewSize(true)) { // isResize=true, returns true on success
  282. this.publiclyTrigger('windowResize', [ this.view ]);
  283. }
  284. }
  285. },
  286. /* Height "Freezing"
  287. -----------------------------------------------------------------------------*/
  288. freezeContentHeight: function() {
  289. if (!(this.freezeContentHeightDepth++)) {
  290. this.forceFreezeContentHeight();
  291. }
  292. },
  293. forceFreezeContentHeight: function() {
  294. this.contentEl.css({
  295. width: '100%',
  296. height: this.contentEl.height(),
  297. overflow: 'hidden'
  298. });
  299. },
  300. thawContentHeight: function() {
  301. this.freezeContentHeightDepth--;
  302. // always bring back to natural height
  303. this.contentEl.css({
  304. width: '',
  305. height: '',
  306. overflow: ''
  307. });
  308. // but if there are future thaws, re-freeze
  309. if (this.freezeContentHeightDepth) {
  310. this.forceFreezeContentHeight();
  311. }
  312. }
  313. });