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; let isDescriptorMoreMenu = parent && parent.attributes[0].nodeValue == 'more-button'; const leftMenu = this.document.querySelector('div[class="menu left"]'); if (isDescriptorMoreMenu && leftMenu) { leftMenu.style.overflow = 'hidden'; this.restoreOverflow = true; } 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; if (this.restoreOverflow) { const leftMenu = this.document.querySelector('div[class="menu left"]'); leftMenu.style.overflow = 'auto'; this.restoreOverflow = false; } 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 margin = 10; 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 clamp = (value, min, max) => Math.min(Math.max(value, min), max); let arrowHeight = Math.floor(arrowRect.height / 2); let arrowOffset = arrowHeight + margin / 2; let docEl = document.documentElement; let maxRight = Math.min(window.innerWidth, docEl.clientWidth) - margin; let maxBottom = Math.min(window.innerHeight, docEl.clientHeight) - margin; let maxWith = maxRight - margin; let maxHeight = maxBottom - margin - arrowHeight; let width = clamp(popoverRect.width, parentRect.width, maxWith); let height = popoverRect.height; let left = parentRect.left + parentRect.width / 2 - width / 2; left = clamp(left, margin, maxRight - width); let top = parentRect.top + parentRect.height + arrowOffset; let showTop = top + height > maxBottom; if (showTop) top = parentRect.top - height - arrowOffset; top = Math.max(top, margin); if (showTop) arrowStyle.bottom = `0`; else arrowStyle.top = `0`; let arrowLeft = (parentRect.left - left) + parentRect.width / 2; arrowLeft = clamp(arrowLeft, arrowHeight, width - arrowHeight); arrowStyle.left = `${arrowLeft}px`; style.top = `${top}px`; style.left = `${left}px`; style.width = `${width}px`; if (height > maxHeight) style.height = `${maxHeight}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 });