salix/front/core/components/popover/popover.js

234 lines
7.2 KiB
JavaScript

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';
let leftMenu = this.document.querySelector('div[class="menu left"]');
if (isDescriptorMoreMenu && leftMenu) {
let descriptorDiv = this.document.querySelector('vn-side-menu div[class="vn-descriptor"]');
let leftMenuWidth = leftMenu.offsetWidth;
let descriptorWidth = descriptorDiv.offsetWidth;
this.scrollbarWidth = leftMenuWidth - descriptorWidth;
let newWidth = leftMenuWidth - this.scrollbarWidth;
leftMenu.style.overflow = 'hidden';
leftMenu.style.minWidth = `${newWidth}px`;
leftMenu.style.width = `${newWidth}px`;
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"]');
let leftMenuWidth = parseInt(leftMenu.style.width);
let newWidth = leftMenuWidth + this.scrollbarWidth;
leftMenu.style.overflow = 'auto';
leftMenu.style.minWidth = `${newWidth}px`;
leftMenu.style.width = `${newWidth}px`;
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
});