Calendar.render.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. Calendar.mixin({
  2. el: null,
  3. contentEl: null,
  4. suggestedViewHeight: null,
  5. windowResizeProxy: null,
  6. ignoreWindowResize: 0,
  7. render: function() {
  8. if (!this.contentEl) {
  9. this.initialRender();
  10. }
  11. else if (this.elementVisible()) {
  12. // mainly for the public API
  13. this.calcSize();
  14. this.renderView();
  15. }
  16. },
  17. initialRender: function() {
  18. var _this = this;
  19. var el = this.el;
  20. el.addClass('fc');
  21. // event delegation for nav links
  22. el.on('click.fc', 'a[data-goto]', function(ev) {
  23. var anchorEl = $(this);
  24. var gotoOptions = anchorEl.data('goto'); // will automatically parse JSON
  25. var date = _this.moment(gotoOptions.date);
  26. var viewType = gotoOptions.type;
  27. // property like "navLinkDayClick". might be a string or a function
  28. var customAction = _this.view.opt('navLink' + capitaliseFirstLetter(viewType) + 'Click');
  29. if (typeof customAction === 'function') {
  30. customAction(date, ev);
  31. }
  32. else {
  33. if (typeof customAction === 'string') {
  34. viewType = customAction;
  35. }
  36. _this.zoomTo(date, viewType);
  37. }
  38. });
  39. // called immediately, and upon option change
  40. this.optionsModel.watch('settingTheme', [ '?theme' ], function(opts) {
  41. var themeClass = ThemeRegistry.getThemeClass(opts.theme);
  42. var theme = new themeClass(_this.optionsModel);
  43. var widgetClass = theme.getClass('widget');
  44. _this.theme = theme;
  45. if (widgetClass) {
  46. el.addClass(widgetClass);
  47. }
  48. }, function() {
  49. var widgetClass = _this.theme.getClass('widget');
  50. _this.theme = null;
  51. if (widgetClass) {
  52. el.removeClass(widgetClass);
  53. }
  54. });
  55. // called immediately, and upon option change.
  56. // HACK: locale often affects isRTL, so we explicitly listen to that too.
  57. this.optionsModel.watch('applyingDirClasses', [ '?isRTL', '?locale' ], function(opts) {
  58. el.toggleClass('fc-ltr', !opts.isRTL);
  59. el.toggleClass('fc-rtl', opts.isRTL);
  60. });
  61. this.contentEl = $("<div class='fc-view-container'/>").prependTo(el);
  62. this.initToolbars();
  63. this.renderHeader();
  64. this.renderFooter();
  65. this.renderView(this.opt('defaultView'));
  66. if (this.opt('handleWindowResize')) {
  67. $(window).resize(
  68. this.windowResizeProxy = debounce( // prevents rapid calls
  69. this.windowResize.bind(this),
  70. this.opt('windowResizeDelay')
  71. )
  72. );
  73. }
  74. },
  75. destroy: function() {
  76. if (this.view) {
  77. this.view.removeElement();
  78. // NOTE: don't null-out this.view in case API methods are called after destroy.
  79. // It is still the "current" view, just not rendered.
  80. }
  81. this.toolbarsManager.proxyCall('removeElement');
  82. this.contentEl.remove();
  83. this.el.removeClass('fc fc-ltr fc-rtl');
  84. // removes theme-related root className
  85. this.optionsModel.unwatch('settingTheme');
  86. this.el.off('.fc'); // unbind nav link handlers
  87. if (this.windowResizeProxy) {
  88. $(window).unbind('resize', this.windowResizeProxy);
  89. this.windowResizeProxy = null;
  90. }
  91. GlobalEmitter.unneeded();
  92. },
  93. elementVisible: function() {
  94. return this.el.is(':visible');
  95. },
  96. // View Rendering
  97. // -----------------------------------------------------------------------------------
  98. // Renders a view because of a date change, view-type change, or for the first time.
  99. // If not given a viewType, keep the current view but render different dates.
  100. // Accepts an optional scroll state to restore to.
  101. renderView: function(viewType, forcedScroll) {
  102. this.ignoreWindowResize++;
  103. var needsClearView = this.view && viewType && this.view.type !== viewType;
  104. // if viewType is changing, remove the old view's rendering
  105. if (needsClearView) {
  106. this.freezeContentHeight(); // prevent a scroll jump when view element is removed
  107. this.clearView();
  108. }
  109. // if viewType changed, or the view was never created, create a fresh view
  110. if (!this.view && viewType) {
  111. this.view =
  112. this.viewsByType[viewType] ||
  113. (this.viewsByType[viewType] = this.instantiateView(viewType));
  114. this.view.setElement(
  115. $("<div class='fc-view fc-" + viewType + "-view' />").appendTo(this.contentEl)
  116. );
  117. this.toolbarsManager.proxyCall('activateButton', viewType);
  118. }
  119. if (this.view) {
  120. if (forcedScroll) {
  121. this.view.addForcedScroll(forcedScroll);
  122. }
  123. if (this.elementVisible()) {
  124. this.currentDate = this.view.setDate(this.currentDate);
  125. }
  126. }
  127. if (needsClearView) {
  128. this.thawContentHeight();
  129. }
  130. this.ignoreWindowResize--;
  131. },
  132. // Unrenders the current view and reflects this change in the Header.
  133. // Unregsiters the `view`, but does not remove from viewByType hash.
  134. clearView: function() {
  135. this.toolbarsManager.proxyCall('deactivateButton', this.view.type);
  136. this.view.removeElement();
  137. this.view = null;
  138. },
  139. // Destroys the view, including the view object. Then, re-instantiates it and renders it.
  140. // Maintains the same scroll state.
  141. // TODO: maintain any other user-manipulated state.
  142. reinitView: function() {
  143. this.ignoreWindowResize++;
  144. this.freezeContentHeight();
  145. var viewType = this.view.type;
  146. var scrollState = this.view.queryScroll();
  147. this.clearView();
  148. this.calcSize();
  149. this.renderView(viewType, scrollState);
  150. this.thawContentHeight();
  151. this.ignoreWindowResize--;
  152. },
  153. // Resizing
  154. // -----------------------------------------------------------------------------------
  155. getSuggestedViewHeight: function() {
  156. if (this.suggestedViewHeight === null) {
  157. this.calcSize();
  158. }
  159. return this.suggestedViewHeight;
  160. },
  161. isHeightAuto: function() {
  162. return this.opt('contentHeight') === 'auto' || this.opt('height') === 'auto';
  163. },
  164. updateSize: function(shouldRecalc) {
  165. if (this.elementVisible()) {
  166. if (shouldRecalc) {
  167. this._calcSize();
  168. }
  169. this.ignoreWindowResize++;
  170. this.view.updateSize(true); // isResize=true. will poll getSuggestedViewHeight() and isHeightAuto()
  171. this.ignoreWindowResize--;
  172. return true; // signal success
  173. }
  174. },
  175. calcSize: function() {
  176. if (this.elementVisible()) {
  177. this._calcSize();
  178. }
  179. },
  180. _calcSize: function() { // assumes elementVisible
  181. var contentHeightInput = this.opt('contentHeight');
  182. var heightInput = this.opt('height');
  183. if (typeof contentHeightInput === 'number') { // exists and not 'auto'
  184. this.suggestedViewHeight = contentHeightInput;
  185. }
  186. else if (typeof contentHeightInput === 'function') { // exists and is a function
  187. this.suggestedViewHeight = contentHeightInput();
  188. }
  189. else if (typeof heightInput === 'number') { // exists and not 'auto'
  190. this.suggestedViewHeight = heightInput - this.queryToolbarsHeight();
  191. }
  192. else if (typeof heightInput === 'function') { // exists and is a function
  193. this.suggestedViewHeight = heightInput() - this.queryToolbarsHeight();
  194. }
  195. else if (heightInput === 'parent') { // set to height of parent element
  196. this.suggestedViewHeight = this.el.parent().height() - this.queryToolbarsHeight();
  197. }
  198. else {
  199. this.suggestedViewHeight = Math.round(
  200. this.contentEl.width() /
  201. Math.max(this.opt('aspectRatio'), .5)
  202. );
  203. }
  204. },
  205. windowResize: function(ev) {
  206. if (
  207. !this.ignoreWindowResize &&
  208. ev.target === window && // so we don't process jqui "resize" events that have bubbled up
  209. this.view.renderRange // view has already been rendered
  210. ) {
  211. if (this.updateSize(true)) {
  212. this.view.publiclyTrigger('windowResize', this.el[0]);
  213. }
  214. }
  215. },
  216. /* Height "Freezing"
  217. -----------------------------------------------------------------------------*/
  218. freezeContentHeight: function() {
  219. this.contentEl.css({
  220. width: '100%',
  221. height: this.contentEl.height(),
  222. overflow: 'hidden'
  223. });
  224. },
  225. thawContentHeight: function() {
  226. this.contentEl.css({
  227. width: '',
  228. height: '',
  229. overflow: ''
  230. });
  231. }
  232. });