import ngModule from '../../module'; import Component from '../../lib/component'; import './style.scss'; /** * A simple popover. * * @property {HTMLElement} parent The parent element to show drop down relative to * * @event open Thrown when popover is displayed * @event close Thrown when popover is hidden */ export default class Popover extends Component { constructor($element, $scope, $timeout, $transitions) { super($element, $scope); this.$timeout = $timeout; this.$transitions = $transitions; this._shown = false; } $postLink() { super.$postLink(); this.$element.addClass('vn-popover'); this.docKeyDownHandler = e => this.onDocKeyDown(e); this.docFocusInHandler = e => this.onDocFocusIn(e); this.bgMouseDownHandler = e => this.onBgMouseDown(e); this.element.addEventListener('focusin', e => this.onFocusIn(e)); this.popover = this.element.querySelector('.popover'); this.popover.addEventListener('mousedown', e => this.onMouseDown(e)); this.arrow = this.element.querySelector('.arrow'); this.content = this.element.querySelector('.content'); } /** * @type {HTMLElement} The popover child. */ get child() { return this.content.firstChild; } set child(value) { this.content.innerHTML = ''; this.content.appendChild(value); } /** * @type {Boolean} Wether to show or hide the popover. */ get shown() { return this._shown; } set shown(value) { if (value) this.show(); else this.hide(); } /** * Shows the popover emitting the open signal. If a parent is specified * it is shown in a visible relative position to it. * * @param {HTMLElement} parent Overrides the parent property */ show(parent) { if (this._shown) return; if (parent) this.parent = parent; this._shown = true; this.element.style.display = 'block'; this.$timeout.cancel(this.showTimeout); this.showTimeout = this.$timeout(() => { this.$element.addClass('shown'); this.showTimeout = null; }, 30); this.document.addEventListener('keydown', this.docKeyDownHandler); this.document.addEventListener('focusin', this.docFocusInHandler); this.element.addEventListener('mousedown', this.bgMouseDownHandler); this.deregisterCallback = this.$transitions.onStart({}, () => this.hide()); this.relocate(); this.emit('open'); } /** * Hides the popover emitting the close signal. */ hide() { if (!this._shown) return; this._shown = false; this.$element.removeClass('shown'); this.$timeout.cancel(this.showTimeout); this.showTimeout = this.$timeout(() => { this.element.style.display = 'none'; this.showTimeout = null; this.emit('close'); }, 250); this.document.removeEventListener('keydown', this.docKeyDownHandler); this.document.removeEventListener('focusin', this.docFocusInHandler); this.element.removeEventListener('mousedown', this.bgMouseDownHandler); if (this.deregisterCallback) this.deregisterCallback(); } /** * Repositions the popover to a correct location relative to the parent. */ relocate() { if (!(this.parent && this._shown)) return; let style = this.popover.style; style.width = ''; style.height = ''; let arrowStyle = this.arrow.style; arrowStyle.top = ''; arrowStyle.bottom = ''; let parentRect = this.parent.getBoundingClientRect(); let popoverRect = this.popover.getBoundingClientRect(); let arrowRect = this.arrow.getBoundingClientRect(); let arrowHeight = Math.sqrt(Math.pow(arrowRect.height, 2) * 2) / 2; let height = popoverRect.height; let width = Math.max(popoverRect.width, parentRect.width); let top = parentRect.top + parentRect.height + arrowHeight; let left = parentRect.left + parentRect.width / 2 - width / 2; let margin = 10; let showTop = top + height + margin > window.innerHeight; if (showTop) top = Math.max(parentRect.top - height - arrowHeight, margin); if (left + width + margin > window.innerWidth) left = window.innerWidth - width - margin; if (showTop) arrowStyle.bottom = `0`; else arrowStyle.top = `0`; arrowStyle.left = `${(parentRect.left - left) + parentRect.width / 2}px`; style.top = `${top}px`; style.left = `${left}px`; style.width = `${width}px`; if (height + margin * 2 + arrowHeight > window.innerHeight) style.height = `${window.innerHeight - margin * 2 - arrowHeight}px`; } onDocKeyDown(event) { if (event.defaultPrevented) return; if (event.keyCode == 27) { // Esc event.preventDefault(); this.hide(); } } onMouseDown(event) { this.lastMouseEvent = event; } onBgMouseDown(event) { if (event != this.lastMouseEvent) this.hide(); } onFocusIn(event) { this.lastFocusEvent = event; } onDocFocusIn(event) { if (event !== this.lastFocusEvent) this.hide(); } } Popover.$inject = ['$element', '$scope', '$timeout', '$transitions']; ngModule.component('vnPopover', { template: require('./popover.html'), controller: Popover, transclude: true });