@qutejs/popup
Version:
Qute Popup component
602 lines (528 loc) • 20.4 kB
JavaScript
(function (css, beforeTarget) {
var style = document.createElement('style');
style.type = 'text/css';
style.appendChild(document.createTextNode(css));
if (beforeTarget) {
var target = document.getElementById(beforeTarget);
if (target) {
target.parentNode.insertBefore(style, target);
return;
}
}
(document.head || document.getElementsByTagName('head')[0]).appendChild(style);
})(".qute-Popup-content{position:relative;will-change:opacity,transform}.qute-Popup--fade .qute-Popup-content{opacity:0;transition:opacity .3s}.qute-Popup--fade.is-visible .qute-Popup-content{opacity:1}.qute-Popup--slide .qute-Popup-content{opacity:0;transform:translateY(-20%);transition:opacity .3s,transform .3s}.qute-Popup--slide.is-visible .qute-Popup-content{opacity:1;transform:translateY(0)}", null);
(function (Qute, window) {
'use strict';
// we don't use vars to store computed values like the transitionend event or
// to ensure the rollup tree-shaking is correctly working (otherwise it may include code that is not needed)
// because of the additional side effects
// see https://github.com/rollup/rollup/wiki/Troubleshooting#tree-shaking-doesnt-seem-to-be-working
//
// this is why we store the computed trabsitionend event on the whichTransitionend function itself
// and we don't store the runOnNextRepaint in a global var.
function whichTransitionend() {
// check cached result
if ('__qute_cache' in whichTransitionend) {
return whichTransitionend.__qute_cache;
} else {
var transitions = {
"transition" : "transitionend",
"OTransition" : "oTransitionEnd",
"MozTransition" : "transitionend",
"WebkitTransition": "webkitTransitionEnd"
};
var transitionEndEvent, style = window.document.createElement('DIV').style;
for(var transition in transitions) {
if (style[transition] != undefined) {
transitionEndEvent = transitions[transition];
break;
}
}
// cache event name
return (whichTransitionend.__qute_cache = transitionEndEvent);
}
}
// handle the first transition end event on the given element.
// After the event is handled the handler is unregistered.
function onTransitionEnd(elt, handler, timeout) {
var transitionEndEvent = whichTransitionend();
if (transitionEndEvent) {
var _handler = function(e) {
try {
return handler(e);
} finally {
elt.removeEventListener(transitionEndEvent, _handler);
}
};
elt.addEventListener(transitionEndEvent, _handler);
} else {
window.setTimeout(handler, timeout || 20);
}
}
// 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) {
var 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$1(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$1.prototype = {
update: function(anchor) {
var 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!'); }
var 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;
}
};
var ViewModel = Qute.ViewModel;
Qute.Property;
var Watch = Qute.Watch;
/*
Attributes:
- animation: optional: is set can be one of fade or slidde
- position is a string in the form of "position algin" where
position is one of: top, bottom, left, right
and align is one of: start, end, center, fill, top, bottom, left, right
left and right align are only valid for vertical positions.
top and bottom align are ony valid for horizontal positions
- auto-close: boolean - toggle close on click. Defaults to true
The defaults are: animation: null, position: "bottom start", auto-close: true
API:
open(anchor)
toggle(anchor)
close()
*/
function qPopupTrigger(attrs, val, el, comp) {
return function (el) {
var id = this.eval(val);
var app = this.model.$app;
if (typeof id !== 'string') {
throw new Error('Invalid value for q:popup-trigger. A popup id is expected.');
}
el.addEventListener('click', function (ev) {
var popup = app.lookup(id);
if (!popup) {
throw Error('Popup not found: ' + id);
}
popup.toggle(el);
});
}
}
var qPopup = /*@__PURE__*/(function (ViewModel) {
function qPopup() {
var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];
ViewModel.apply(this, args);
this.defineProp(String, "position", 'bottom start');
this.defineProp(String, "animation", null);
this.defineProp(Boolean, "autoClose", true);
this.defineProp(String, "id", void(0));
}
if ( ViewModel ) qPopup.__proto__ = ViewModel;
qPopup.prototype = Object.create( ViewModel && ViewModel.prototype );
qPopup.prototype.constructor = qPopup;
var prototypeAccessors = { isOpen: { configurable: true } };
qPopup.prototype.render = function render () {
return window.document.createComment('[popup]');
};
qPopup.prototype.created = function created () {
var self = this;
var slots = this.$slots;
if (!slots || !slots.default) { throw new Error('<popup> requires a content!'); }
this.popup = new Popup$1(slots.default, {
modifierClass: this.$attrs.class || '',
open: function () {
self.emit("open", self.popup.el);
},
ready: function () {
self.emit("ready", self.popup.el);
},
close: function () {
self.emit("close", self.popup.el);
}
}).animation(this.animation).position(this.position).closeOnClick(this.autoClose);
this.id && this.publish(this.id);
};
qPopup.prototype.element = function element () {
return this.popup.el;
};
qPopup.prototype.find = function find (selector) {
return this.popup.el && this.popup.el.querySelector(selector);
};
qPopup.prototype.open = function open (target, now) {
this.popup.open(target);
};
qPopup.prototype.openAsync = function openAsync (target) {
var popup = this.popup;
window.setTimeout(function () {
popup.open(target);
}, 0);
};
qPopup.prototype.toggle = function toggle (target, now) {
this.popup.toggle(target);
};
qPopup.prototype.toggleAsync = function toggleAsync (target) {
var popup = this.popup;
window.setTimeout(function () {
popup.toggle(target);
}, 0);
};
qPopup.prototype.close = function close () {
this.popup.close();
};
prototypeAccessors.isOpen.get = function () {
return this.popup.isOpen();
};
qPopup.prototype.onPositionChanged = function onPositionChanged (value) {
this.popup.position(value);
return false;
};
qPopup.prototype.onAnimationChanged = function onAnimationChanged (value) {
this.popup.animation(value);
return false;
};
qPopup.prototype.onAutoCloseChanged = function onAutoCloseChanged (value) {
this.popup.closeOnClick(!!value);
return false;
};
Object.defineProperties( qPopup.prototype, prototypeAccessors );
return qPopup;
}(ViewModel));
(function(proto) {
var d;
d = Object.getOwnPropertyDescriptor(proto, "onPositionChanged");
Watch('position')(proto, "onPositionChanged", d);
d = Object.getOwnPropertyDescriptor(proto, "onAnimationChanged");
Watch('animation')(proto, "onAnimationChanged", d);
d = Object.getOwnPropertyDescriptor(proto, "onAutoCloseChanged");
Watch('autoClose')(proto, "onAutoCloseChanged", d);
})(qPopup.prototype);
var Popup = /*#__PURE__*/Object.freeze({
__proto__: null,
Popup: Popup$1,
qPopup: qPopup,
qPopupTrigger: qPopupTrigger
});
Qute.import(Popup);
}(Qute, window));
//# sourceMappingURL=qute-popup.js.map