UNPKG

@qutejs/popup

Version:

Qute Popup component

602 lines (528 loc) 20.4 kB
(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