import ngModule from '../../module'; import Component from '../../lib/component'; import './style.scss'; /** * A simple popover. */ export default class Popover extends Component { constructor($element, $scope, $timeout, $transitions) { super($element, $scope); this.$timeout = $timeout; this.$transitions = $transitions; this._shown = false; } $postLink() { this.$element.addClass('vn-popover'); this.docKeyDownHandler = e => this.onDocKeyDown(e); this.docFocusInHandler = e => this.onDocFocusIn(e); this.element.addEventListener('mousedown', e => this.onBackgroundMouseDown(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'); } set child(value) { this.content.appendChild(value); } get child() { return this.content.firstChild; } get shown() { return this._shown; } set shown(value) { if (value) this.show(); else this.hide(); } /** * Shows the popover. If a parent is specified it is shown in a visible * relative position to it. */ show() { if (this._shown) return; 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.deregisterCallback = this.$transitions.onStart({}, () => this.hide()); this.relocate(); if (this.onOpen) this.onOpen(); } /** * Hides the popover. */ 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; }, 250); this.document.removeEventListener('keydown', this.docKeyDownHandler); this.document.removeEventListener('focusin', this.docFocusInHandler); if (this.deregisterCallback) this.deregisterCallback(); if (this.onClose) this.onClose(); } /** * 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 top = parentRect.top + parentRect.height + arrowHeight; let left = parentRect.left; let height = popoverRect.height; let width = Math.max(popoverRect.width, parentRect.width); 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; } onBackgroundMouseDown(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, bindings: { onOpen: '&?', onClose: '&?' } }); class PopoverService { constructor($document, $compile, $transitions, $rootScope) { this.$compile = $compile; this.$rootScope = $rootScope; this.$document = $document; this.stack = []; } show(child, parent, $scope) { let element = this.$compile('')($scope || this.$rootScope)[0]; let popover = element.$ctrl; popover.parent = parent; popover.child = child; popover.show(); popover.onClose = () => { this.$document[0].body.removeChild(element); if ($scope) $scope.$destroy(); }; this.$document[0].body.appendChild(element); return popover; } showComponent(componentTag, $scope, parent) { let $newScope = $scope.$new(); let childElement = this.$compile(`<${componentTag}/>`)($newScope)[0]; this.show(childElement, parent, $newScope); return childElement; } } PopoverService.$inject = ['$document', '$compile', '$transitions', '$rootScope']; ngModule.service('vnPopover', PopoverService);