Popover.js 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. /* A rectangular panel that is absolutely positioned over other content
  2. ------------------------------------------------------------------------------------------------------------------------
  3. Options:
  4. - className (string)
  5. - content (HTML string or jQuery element set)
  6. - parentEl
  7. - top
  8. - left
  9. - right (the x coord of where the right edge should be. not a "CSS" right)
  10. - autoHide (boolean)
  11. - show (callback)
  12. - hide (callback)
  13. */
  14. var Popover = Class.extend({
  15. isHidden: true,
  16. options: null,
  17. el: null, // the container element for the popover. generated by this object
  18. documentMousedownProxy: null, // document mousedown handler bound to `this`
  19. margin: 10, // the space required between the popover and the edges of the scroll container
  20. constructor: function(options) {
  21. this.options = options || {};
  22. },
  23. // Shows the popover on the specified position. Renders it if not already
  24. show: function() {
  25. if (this.isHidden) {
  26. if (!this.el) {
  27. this.render();
  28. }
  29. this.el.show();
  30. this.position();
  31. this.isHidden = false;
  32. this.trigger('show');
  33. }
  34. },
  35. // Hides the popover, through CSS, but does not remove it from the DOM
  36. hide: function() {
  37. if (!this.isHidden) {
  38. this.el.hide();
  39. this.isHidden = true;
  40. this.trigger('hide');
  41. }
  42. },
  43. // Creates `this.el` and renders content inside of it
  44. render: function() {
  45. var _this = this;
  46. var options = this.options;
  47. this.el = $('<div class="fc-popover"/>')
  48. .addClass(options.className || '')
  49. .css({
  50. // position initially to the top left to avoid creating scrollbars
  51. top: 0,
  52. left: 0
  53. })
  54. .append(options.content)
  55. .appendTo(options.parentEl);
  56. // when a click happens on anything inside with a 'fc-close' className, hide the popover
  57. this.el.on('click', '.fc-close', function() {
  58. _this.hide();
  59. });
  60. if (options.autoHide) {
  61. $(document).on('mousedown', this.documentMousedownProxy = proxy(this, 'documentMousedown'));
  62. }
  63. },
  64. // Triggered when the user clicks *anywhere* in the document, for the autoHide feature
  65. documentMousedown: function(ev) {
  66. // only hide the popover if the click happened outside the popover
  67. if (this.el && !$(ev.target).closest(this.el).length) {
  68. this.hide();
  69. }
  70. },
  71. // Hides and unregisters any handlers
  72. removeElement: function() {
  73. this.hide();
  74. if (this.el) {
  75. this.el.remove();
  76. this.el = null;
  77. }
  78. $(document).off('mousedown', this.documentMousedownProxy);
  79. },
  80. // Positions the popover optimally, using the top/left/right options
  81. position: function() {
  82. var options = this.options;
  83. var origin = this.el.offsetParent().offset();
  84. var width = this.el.outerWidth();
  85. var height = this.el.outerHeight();
  86. var windowEl = $(window);
  87. var viewportEl = getScrollParent(this.el);
  88. var viewportTop;
  89. var viewportLeft;
  90. var viewportOffset;
  91. var top; // the "position" (not "offset") values for the popover
  92. var left; //
  93. // compute top and left
  94. top = options.top || 0;
  95. if (options.left !== undefined) {
  96. left = options.left;
  97. }
  98. else if (options.right !== undefined) {
  99. left = options.right - width; // derive the left value from the right value
  100. }
  101. else {
  102. left = 0;
  103. }
  104. if (viewportEl.is(window) || viewportEl.is(document)) { // normalize getScrollParent's result
  105. viewportEl = windowEl;
  106. viewportTop = 0; // the window is always at the top left
  107. viewportLeft = 0; // (and .offset() won't work if called here)
  108. }
  109. else {
  110. viewportOffset = viewportEl.offset();
  111. viewportTop = viewportOffset.top;
  112. viewportLeft = viewportOffset.left;
  113. }
  114. // if the window is scrolled, it causes the visible area to be further down
  115. viewportTop += windowEl.scrollTop();
  116. viewportLeft += windowEl.scrollLeft();
  117. // constrain to the view port. if constrained by two edges, give precedence to top/left
  118. if (options.viewportConstrain !== false) {
  119. top = Math.min(top, viewportTop + viewportEl.outerHeight() - height - this.margin);
  120. top = Math.max(top, viewportTop + this.margin);
  121. left = Math.min(left, viewportLeft + viewportEl.outerWidth() - width - this.margin);
  122. left = Math.max(left, viewportLeft + this.margin);
  123. }
  124. this.el.css({
  125. top: top - origin.top,
  126. left: left - origin.left
  127. });
  128. },
  129. // Triggers a callback. Calls a function in the option hash of the same name.
  130. // Arguments beyond the first `name` are forwarded on.
  131. // TODO: better code reuse for this. Repeat code
  132. trigger: function(name) {
  133. if (this.options[name]) {
  134. this.options[name].apply(this, Array.prototype.slice.call(arguments, 1));
  135. }
  136. }
  137. });