@qutejs/popup
Version:
Qute Popup component
377 lines (339 loc) • 10.4 kB
JavaScript
import window from '@qutejs/window';
import { onTransitionEnd } from '@qutejs/ui';
// cover option: coverup / coverdown
function toBottomCover(erect, rect, crect, out) {
out.top = rect.top;
if (out.top + erect.height > crect.bottom) { // flip to top
out.top = rect.bottom - erect.height;
}
}
function toTopCover(erect, rect, crect, out) {
out.top = rect.bottom - erect.height;
if (out.top < crect.top) { // flip to bottom
out.top = rect.top;
}
}
function toBottom(erect, rect, crect, out) {
out.top = rect.bottom;
if (out.top + erect.height > crect.bottom) { // flip to top
out.top = rect.top - erect.height;
}
}
function toTop(erect, rect, crect, out) {
out.top = rect.top - erect.height;
if (out.top < crect.top) { // flip to bottom
out.top = rect.bottom;
}
}
function toVStart(erect, rect, crect, out) {
out.top = rect.top;
if (out.top + erect.height > crect.bottom) { // flip
out.top = rect.bottom - erect.height;
}
}
function toVEnd(erect, rect, crect, out) {
out.top = rect.bottom - erect.height;
if (out.top < crect.top) { // flip
out.top = rect.top;
}
}
function toVCenter(erect, rect, crect, out) {
out.top = rect.top + (rect.height - erect.height) / 2;
}
function toRight(erect, rect, crect, out) {
out.left = rect.right;
if (out.left + erect.width > crect.right) { // flip to left
out.left = rect.left - erect.width;
}
}
function toLeft(erect, rect, crect, out) {
out.left = rect.left - erect.width;
if (out.left < crect.left) { // flip to right
out.left = rect.right;
}
}
function toHStart(erect, rect, crect, out) {
out.left = rect.left;
if (out.left + erect.width > crect.right) { // flip
out.left = rect.right - erect.width;
}
}
function toHEnd(erect, rect, crect, out) {
out.left = rect.right - erect.width;
if (out.left < crect.left) { // flip
out.left = rect.left;
}
}
function toHCenter(erect, rect, crect, out) {
out.left = rect.left + (rect.width - erect.width) / 2;
if (out.left < crect.left) { // move to the right so the popup is entirely visible on the left side
out.left = crect.left;
} else if (out.left + erect.width > crect.right) { // move to the left so the popup is entirely visible on the right side
out.left = crect.right - erect.width;
}
}
var POS_FNS = {
top: toTop,
bottom: toBottom,
left: toLeft,
right: toRight,
coverup: toTopCover,
coverdown: toBottomCover
};
var VALIGN_FNS = {
start: toVStart,
end: toVEnd,
center: toVCenter,
top: toTop,
bottom: toBottom
};
var HALIGN_FNS = {
start: toHStart,
end: toHEnd,
center: toHCenter,
left: toLeft,
right: toRight
};
/*
* Get the visible container rect defined by the given overflow parents. If no overflow parents are given the viewport will be used.
*/
function getVisibleClientRect(container, overflowingParents) {
var left, top, right, bottom;
if (container) {
var rect = container.getBoundingClientRect();
left = rect.left;
top = rect.top;
right = rect.right;
bottom = rect.bottom;
} else {
left = 0;
top = 0;
right = window.innerWidth;
bottom = window.innerHeight;
}
if (overflowingParents.length) {
for (var i=0,l=overflowingParents.length; i<l; i++) {
var parent = overflowingParents[i];
// TODO bounding client rect includes the border -> use clientRect to remove border?
var prect = parent.getBoundingClientRect();
if (prect.left > left) left = prect.left;
if (prect.right < right) right = prect.right;
if (prect.top > top) top = prect.top;
if (prect.bottom < bottom) bottom = prect.bottom;
}
}
return {
left: left, top: top,
right: right, bottom: bottom,
width: right-left, height: bottom-top
};
}
function createPopup(content, modifierClass) {
let document = window.document;
var el = document.createElement('DIV');
var style = el.style;
style.visibility = "hidden";
style.position = "absolute";
style.overflow = "hidden";
el.className = modifierClass ? 'qute-Popup '+modifierClass : 'qute-Popup';
var contentEl = document.createElement('DIV');
contentEl.style.position = "relative";
contentEl.className = 'qute-Popup-content';
el.appendChild(contentEl);
if (content.jquery) {
contentEl.appendChild(content[0]);
} else if (typeof content === 'string') {
contentEl.innerHTML = content;
} else if (Array.isArray(content)) {
for (var i=0, l=content.length; i<l; i++) {
contentEl.appendChild(content[i]);
}
} else { // assume an element
contentEl.appendChild(content);
}
return el;
}
/*
* options: closeOnClick, position, align, effect, modifierClass, open, ready, close
* the open callback is called before the popup is added to the DOM (it is not yet visible)
* the ready callback is called when the popup was opened (after it was added to the DOM and it is visible on the screen)
*
* To control the viewport where the popup is diplayed you can use a .qute-Popup--container class on a parent element to restrict to that element
*/
function Popup(content, options) {
if (!options) options = {};
this.el = createPopup(content, options.modifierClass);
this.opts = {
position: 'bottom',
align: 'start',
closeOnClick: true,
animation: null,
open: null,
ready: null,
close: null
};
if (options) {
Object.assign(this.opts, options);
}
}
Popup.prototype = {
update: function(anchor) {
const opts = this.opts;
if (anchor.jquery) anchor = anchor[0];
var crect = getVisibleClientRect(this.container, this.ofs);
var rect = anchor.getBoundingClientRect();
// if anchor is not hidden by the overflow then hide the popup
if (rect.top >= crect.bottom || rect.bottom <= crect.top
|| rect.left >= crect.right || rect.right <= crect.left) {
this.el.style.visibility = 'hidden';
return;
}
var style = this.el.style;
// first check for special modifier 'fill' (before getting the boundingclient rect since setting the height/width will modify the rect)
var pos = opts.position;
var align = opts.align;
if (align === 'fill') {
if (pos === 'left' || pos === 'right') {
style.height = anchor.offsetHeight+'px';
} else {
style.width = anchor.offsetWidth+'px';
}
align = 'start';
} else {
style.width && (style.width = '');
style.height && (style.height = '');
}
var erect = this.el.getBoundingClientRect(); // we only need width and height
var out = {};
var posFn = POS_FNS[pos];
if (!posFn) throw new Error('Invalid position argument: '+pos+'. Expecting: top|bottom|left|right');
posFn(erect, rect, crect, out);
var ALIGN_FNS = out.top == null ? VALIGN_FNS : HALIGN_FNS;
var alignFn = ALIGN_FNS[align];
if (!alignFn) throw new Error('Invalid vert align argument: '+align+'. Expecting: '+Object.keys(ALIGN_FNS).join('|'));
alignFn(erect, rect, crect, out);
style.left = (out.left + window.pageXOffset)+'px';
style.top = (out.top + window.pageYOffset)+'px';
style.visibility = 'visible';
return this;
},
open: function(anchor) {
if (!anchor) throw new Error('Attempting to open a popup without specifying a target element!');
let document = window.document;
// compute overflowing parents and register scroll listeners
if (this.el.parentNode) { // already opened
return;
}
var updating = false, self = this;
var updateFn = function() {
if (!updating) {
window.requestAnimationFrame(function() {
self.update(anchor);
updating = false;
});
updating = true;
}
};
var container = anchor.closest('.qute-Popup--container');
var root = container ? container : document.body;
var ofs = [], parent = anchor.parentNode;
while (parent && parent !== root) {
if (parent.scrollHeight > parent.clientHeight || parent.scrollWidth > parent.clientWidth) {
ofs.push(parent);
parent.addEventListener('scroll', updateFn);
}
parent = parent.parentNode;
}
window.addEventListener('scroll', updateFn);
window.addEventListener('resize', updateFn);
// TODO add resize listener
this.container = container;
this.ofs = ofs;
// add close on click listener
var closeOnClick;
if (this.opts.closeOnClick) {
closeOnClick = function(e) {
if (!self.el.contains(e.target)) {
self.close();
}
};
window.setTimeout(function() {
document.addEventListener('click', closeOnClick);
}, 0);
}
this.cleanup = function() {
if (closeOnClick) {
document.removeEventListener('click', closeOnClick);
}
window.removeEventListener('resize', updateFn);
window.removeEventListener('scroll', updateFn);
for (var i=0,l=ofs.length; i<l; i++) {
ofs[i].removeEventListener('scroll', updateFn);
}
};
this.opts.open && this.opts.open(this);
// mount the popup
document.body.appendChild(this.el);
// show it
this.update(anchor);
this.el.classList.add('is-visible');
this.opts.ready && this.opts.ready(this);
return this;
},
close: function() {
this.opts.close && this.opts.close(this);
this.cleanup();
var el = this.el;
el.classList.remove('is-visible');
if (this.opts.animation) {
onTransitionEnd(el, function() {
el.style.visibility = 'hidden';
el.parentNode && el.parentNode.removeChild(el);
});
} else {
el.style.visibility = 'hidden';
el.parentNode.removeChild(el);
}
return this;
},
toggle: function(anchor) {
if (this.el.parentNode) { // already opened
this.close();
} else {
this.open(anchor);
}
return this;
},
isOpen: function() {
return this.el.parentNode;
},
position: function(position, align) {
if (align === undefined) {
position = position.trim();
var i = position.indexOf(' ');
if (i > -1) {
align = position.substring(i+1).trim();
position = position.substring(0, i);
}
}
this.opts.position = position;
if (align) this.opts.align = align;
return this;
},
closeOnClick: function(closeOnClick) {
this.opts.closeOnClick = closeOnClick;
return this;
},
animation: function(animation) {
var previousAnimation = this.opts.animation;
this.opts.animation = animation;
var cl = this.el.classList;
if (previousAnimation) {
cl.remove('qute-Popup--'+previousAnimation);
}
cl.add('qute-Popup--'+animation);
return this;
}
};
export default Popup;
//# sourceMappingURL=popup.js.map