/* A rectangular panel that is absolutely positioned over other content ------------------------------------------------------------------------------------------------------------------------ Options: - className (string) - content (HTML string or jQuery element set) - parentEl - top - left - right (the x coord of where the right edge should be. not a "CSS" right) - autoHide (boolean) - show (callback) - hide (callback) */ import * as $ from 'jquery' import { getScrollParent } from '../util' import { default as ListenerMixin, ListenerInterface } from './ListenerMixin' export default class Popover { listenTo: ListenerInterface['listenTo'] stopListeningTo: ListenerInterface['stopListeningTo'] isHidden: boolean = true options: any el: any // the container element for the popover. generated by this object margin: number = 10 // the space required between the popover and the edges of the scroll container constructor(options) { this.options = options || {} } // Shows the popover on the specified position. Renders it if not already show() { if (this.isHidden) { if (!this.el) { this.render() } this.el.show() this.position() this.isHidden = false this.trigger('show') } } // Hides the popover, through CSS, but does not remove it from the DOM hide() { if (!this.isHidden) { this.el.hide() this.isHidden = true this.trigger('hide') } } // Creates `this.el` and renders content inside of it render() { let options = this.options this.el = $('
') .addClass(options.className || '') .css({ // position initially to the top left to avoid creating scrollbars top: 0, left: 0 }) .append(options.content) .appendTo(options.parentEl) // when a click happens on anything inside with a 'fc-close' className, hide the popover this.el.on('click', '.fc-close', () => { this.hide() }) if (options.autoHide) { this.listenTo($(document), 'mousedown', this.documentMousedown) } } // Triggered when the user clicks *anywhere* in the document, for the autoHide feature documentMousedown(ev) { // only hide the popover if the click happened outside the popover if (this.el && !$(ev.target).closest(this.el).length) { this.hide() } } // Hides and unregisters any handlers removeElement() { this.hide() if (this.el) { this.el.remove() this.el = null } this.stopListeningTo($(document), 'mousedown') } // Positions the popover optimally, using the top/left/right options position() { let options = this.options let origin = this.el.offsetParent().offset() let width = this.el.outerWidth() let height = this.el.outerHeight() let windowEl = $(window) let viewportEl = getScrollParent(this.el) let viewportTop let viewportLeft let viewportOffset let top // the "position" (not "offset") values for the popover let left // // compute top and left top = options.top || 0 if (options.left !== undefined) { left = options.left } else if (options.right !== undefined) { left = options.right - width // derive the left value from the right value } else { left = 0 } if (viewportEl.is(window) || viewportEl.is(document)) { // normalize getScrollParent's result viewportEl = windowEl viewportTop = 0 // the window is always at the top left viewportLeft = 0 // (and .offset() won't work if called here) } else { viewportOffset = viewportEl.offset() viewportTop = viewportOffset.top viewportLeft = viewportOffset.left } // if the window is scrolled, it causes the visible area to be further down viewportTop += windowEl.scrollTop() viewportLeft += windowEl.scrollLeft() // constrain to the view port. if constrained by two edges, give precedence to top/left if (options.viewportConstrain !== false) { top = Math.min(top, viewportTop + viewportEl.outerHeight() - height - this.margin) top = Math.max(top, viewportTop + this.margin) left = Math.min(left, viewportLeft + viewportEl.outerWidth() - width - this.margin) left = Math.max(left, viewportLeft + this.margin) } this.el.css({ top: top - origin.top, left: left - origin.left }) } // Triggers a callback. Calls a function in the option hash of the same name. // Arguments beyond the first `name` are forwarded on. // TODO: better code reuse for this. Repeat code trigger(name) { if (this.options[name]) { this.options[name].apply(this, Array.prototype.slice.call(arguments, 1)) } } } ListenerMixin.mixInto(Popover)