203 lines
5.9 KiB
JavaScript
203 lines
5.9 KiB
JavaScript
import ngModule from '../../module';
|
|
import Component from '../../lib/component';
|
|
import template from './popover.html';
|
|
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, $transclude, $compile) {
|
|
super($element, $scope);
|
|
this.$timeout = $timeout;
|
|
this.$transitions = $transitions;
|
|
this._shown = false;
|
|
|
|
this.element = $compile(template)($scope)[0];
|
|
|
|
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');
|
|
|
|
$transclude($scope.$parent,
|
|
clone => angular.element(this.content).append(clone));
|
|
}
|
|
|
|
$onDestroy() {
|
|
this.hide();
|
|
}
|
|
|
|
/**
|
|
* @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;
|
|
|
|
if (!this.showTimeout) {
|
|
this.document.body.appendChild(this.element);
|
|
this.element.style.display = 'block';
|
|
}
|
|
|
|
this.$timeout.cancel(this.showTimeout);
|
|
this.showTimeout = this.$timeout(() => {
|
|
this.showTimeout = null;
|
|
this.element.classList.add('shown');
|
|
}, 30);
|
|
|
|
this.docKeyDownHandler = e => this.onDocKeyDown(e);
|
|
this.document.addEventListener('keydown', this.docKeyDownHandler);
|
|
|
|
this.bgMouseDownHandler = e => this.onBgMouseDown(e);
|
|
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.classList.remove('shown');
|
|
|
|
this.$timeout.cancel(this.showTimeout);
|
|
this.showTimeout = this.$timeout(() => {
|
|
this.showTimeout = null;
|
|
this.element.style.display = 'none';
|
|
this.document.body.removeChild(this.element);
|
|
this.emit('close');
|
|
}, 250);
|
|
|
|
this.document.removeEventListener('keydown', this.docKeyDownHandler);
|
|
this.docKeyDownHandler = null;
|
|
|
|
this.element.removeEventListener('mousedown', this.bgMouseDownHandler);
|
|
this.bgMouseDownHandler = null;
|
|
|
|
if (this.deregisterCallback) this.deregisterCallback();
|
|
this.emit('closeStart');
|
|
}
|
|
|
|
/**
|
|
* 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 = this.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.code == 'Escape')
|
|
this.hide();
|
|
}
|
|
|
|
onMouseDown(event) {
|
|
this.lastMouseEvent = event;
|
|
}
|
|
|
|
onBgMouseDown(event) {
|
|
if (event == this.lastMouseEvent || event.defaultPrevented) return;
|
|
event.preventDefault();
|
|
this.hide();
|
|
}
|
|
}
|
|
Popover.$inject = ['$element', '$scope', '$timeout', '$transitions', '$transclude', '$compile'];
|
|
|
|
ngModule.component('vnPopover', {
|
|
controller: Popover,
|
|
transclude: true
|
|
});
|