'use strict'; /** * Licensed Materials - Property of IBM * IBM Cognos Products: BI Cloud (C) Copyright IBM Corp. 2014, 2019 * US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp. */ define(['jquery', '../lib/@waca/core-client/js/core-client/ui/AccessibleView', 'doT', 'underscore', './DialogBlocker'], function ($, BaseClass, dot, _, DialogBlocker) { var idCounter = 0; /** * Shows flyouts on click/tap on DOM elements identified using a selector. * Content of the flyout comes from a view implements following i/f * { * getRenderedHtml : function() { return html string } * onPopupShown : function() {}, optional * onPopupClosed : function() {}, optional * } * * There is only One flyout visible at a time, clicking/tapping/scrolling on rest of the * page or on another element associated with a flyout will close the * previous flyout. * * Input options to Flyout * { * maxHt : max height (in pixels) for content, optional * maxWd : max width (in pixels) for content, optional * content: html string to be filled in the view, optional * selector : CSS selector for the DOM node associated with the flyout e.g. '.applicationTitle' * hasCloseBtn : indicates whether or not to show close button (optional) * popoverClass: Css class to be added to the popover container, * placement: placement of the popover. Default value is 'auto' * } * * Example Usage: * require([ 'app/util/Flyout', view class module e.g.'app/util/FlyoutContentBase' ], * function(Flyout, ContentClass) { * * var popover = new Flyout( ContentClass, { * maxHt: 100, * maxWd: 100, * content: 'test content string', * hasCloseBtn: true, * selector : '.applicationTitle', * popoverClass: 'someCssClass' * }); * ... * popover.destroy(); * } * * @class Flyout */ var Flyout = BaseClass.extend({ //overriding bootstrap template to include container template, remove title element, move class="popover-content" to content div in container template bsFullTemplateString: '
{{=it.containerTemplate}}
', //flyout container template containerTemplateString: '{{=it.closeBtn}}
', //close button template closeBtnTemplateString: '
', placement: null, enableTabLooping: true, init: function init(options) { Flyout.inherited('init', this, [{ launchPoint: options.launchPoint }]); _.defaults(options, { modal: false }); this.forceRedraw = options.forceRedraw; this.logger = options.logger; this.selector = options.selector; this.notCentered = options.notCentered; this.relatedNodes = options.relatedNodes || []; this.once = options.once; this.modal = options.modal; //if this is === true, this flyout will close if you mouse into it and then out. this.hideOnMouseLeave = options.hideOnMouseLeave; if (!this.placement) { this.placement = options.placement; } this.notCentered = options.notCentered; this.alignment = options.alignment; this.hideEventsAttached = false; // Get the mid-point of the selector and use that to offset the flyout so that // it appears above the mouse pointer. if (options.pageX) { var selector = $(this.selector); var placement = options.placement || this._calculatePlacement(0, selector); if (placement === 'top' || placement === 'bottom') { var midpoint = $(this.selector).offset().left + $(this.selector).width() / 2; options.marginLeft = options.pageX - midpoint; } } idCounter++; this.id = '_pop_' + idCounter; var btnId = this.id + '_btn_'; var template = this._generateContainerTemplate(options, this.id, btnId); this.contentRootSelector = '#' + this.id + '>.popover-content'; var view = options.viewInstance; if (!view) { view = new options.viewClass({ popupContainer: this, content: options.content, viewOptions: options.viewOptions, contentRootSelector: this.contentRootSelector, maxHt: options.maxHt, maxWd: options.maxWd, logger: this.logger }); this.isDestroyContentView = true; } this.view = view; var content = view; if (view.getRenderedHtml) { content = view.getRenderedHtml.bind(view); } this.content = content; var container = options.container; if (this.modal) { container = DialogBlocker.getJqBlocker(); container.addClass('show'); container.addClass('transparent'); this._blocker = container; } var viewport = options.viewport; var popoverOptions = { placement: options.placement || this._calculatePlacement.bind(this), trigger: 'manual', container: container, content: content, viewport: viewport, html: true, template: template, sanitize: false }; this.popover = $(this.selector).popover(popoverOptions); this._bindEvents(options, btnId); }, _addBlocker: function _addBlocker() { DialogBlocker.show(document.body, this._blocker); }, /** * Find and return an arry of current opened popovers * * @return {array} Array of found opened popovwers */ _findCurrentOpenedPopoversToClose: function _findCurrentOpenedPopoversToClose() { var $flyoutContents = $('.flyout-content-container'); var flyouts = []; _.each($flyoutContents, function (flyoutContent) { var $flyoutContent = $(flyoutContent); var flyout = $flyoutContent.data('flyout'); if (flyout) { flyouts.push(flyout); } }); return flyouts; }, open: function open(nodeToOpen) { if (this.forceRedraw || !this.isOpen) { return this._open(nodeToOpen); } this.isOpenReady = Promise.resolve(); return this.isOpenReady; }, /* * Show the flyout for a given node */ _open: function _open(nodeToOpen) { var _this = this; this.isOpenReady = new Promise(function (resolve) { var openedFlyouts = _this._findCurrentOpenedPopoversToClose(); if (openedFlyouts.length > 0) { /** Work around for bootstrap $tip.show/hide using $.fn.emulateTransitionEnd defect where setTimeout not invoke callback to triggering 'shown.bs.popover' * Bootstrap uses setTimeout to both show new popover and hide the previous popover * The body and the Window objects are global instances per browser session. The code below ensure that both the body and the Windows object event listeners * are attached to the correct opened flyout. Otherwise when things get out of synch with Bootstrap hide/show, the body and the Window objects listen * to the wrong flyout resulting in not be able to close a flyout * Therefore make sure that all opened flyouts get close before open another one */ var hideContext = {}; var flyoutsToBeCloseMap = {}; _.each(openedFlyouts, function (flyout) { flyoutsToBeCloseMap[flyout.id] = flyout; hideContext[flyout.id] = { flyout: flyout, hiddenPopoverCallback: function hiddenPopoverCallback() { delete flyoutsToBeCloseMap[flyout.id]; if (_.keys(flyoutsToBeCloseMap).length === 0) { resolve(hideContext); } } }; flyout.popover.on('hidden.bs.popover', hideContext[flyout.id].hiddenPopoverCallback); }); _.each(hideContext, function (context) { context.flyout.close(); }); } else { resolve(); } }).then(function (hideContext) { _.each(hideContext, function (context) { context.flyout.popover.off('hidden.bs.popover', context.hiddenPopoverCallback); }); // Handle dialog blocker addition for modals/vischanger if (_this.modal) { if (!_this.isOpen && $(_this.popover).has(nodeToOpen)) { _this._addBlocker(); } } _this._openFlyout(nodeToOpen); }); return this.isOpenReady; }, /** * This function is added to allow unit tests to trigger an event to open a flyout * and wait for it to be in the open state and ready for use */ openIsReady: function openIsReady() { var _this2 = this; var promise = this.isOpenReady || Promise.resolve(); return promise.then(function () { return !!_this2.isOpen; }); }, /** * Call to show the popover */ _openFlyout: function _openFlyout(node) { var _this3 = this; if (!this.isOpen) { if ($(this.popover).has(node)) { var $node = $(node); $node.popover('show'); var popover = $node.length && $.data($node[0], 'bs.popover'); if (popover && popover.$tip) { if (this.alignment === 'top') { popover.$tip.css('top', popover.$element.position().top); } else if (this.alignment === 'left') { popover.$tip.css('left', popover.$element.position().left); } if (this.notCentered && (this.placement === 'right' || this.placement === 'left')) { popover.$tip.css('top', popover.$element.offset().top); } else if (this.notCentered && (this.placement === 'top' || this.placement === 'bottom')) { popover.$tip.css('left', popover.$element.offset().left); } this.adjustPopoverInViewport(popover); //IT Test hook to close flyouts popover.$tip.on('userCloseFlyout', function () { _this3.close(); }); } else { console.error('Cannot retrieve popover object'); } } this.isOpen = true; } }, /** * @param {Object} popover: a bootstrap popover object * Note: Actually we're adjusting the position of popover in the window. * The strategy is: * - if we fall off the bottom, shift up unless that would take us off the top of the screen. * - if we fall off the right, shift left unless that would take us off the left of the screen. */ adjustPopoverInViewport: function adjustPopoverInViewport(popover) { var $tip = popover.$tip; var tipWindowCoord = $tip.get(0).getBoundingClientRect(); var windowWidth = $(window).width(); var windowHeight = $(window).height(); var popoverMarginLeftWidth = parseFloat($tip.css('margin-left')); var popoverMarginRightWidth = parseFloat($tip.css('margin-right')); var popoverMarginTopWidth = parseFloat($tip.css('margin-top')); var popoverMarginBottomWidth = parseFloat($tip.css('margin-bottom')); var popoverLeftToBorder = tipWindowCoord.left + popoverMarginLeftWidth; var popoverRightToBorder = tipWindowCoord.right - popoverMarginRightWidth; var popoverTopToBorder = tipWindowCoord.top + popoverMarginTopWidth; var popoverBottomToBorder = tipWindowCoord.bottom - popoverMarginBottomWidth; if (popoverLeftToBorder < 0) { $tip.css('left', -popoverMarginLeftWidth); } else if (windowWidth < popoverRightToBorder) { var distanceToMove = Math.min(popoverRightToBorder - windowWidth, popoverMarginLeftWidth + tipWindowCoord.left); $tip.css('left', tipWindowCoord.left - distanceToMove); } if (popoverTopToBorder < 0) { $tip.css('top', -popoverMarginTopWidth); } else if (windowHeight < popoverBottomToBorder) { var _distanceToMove = Math.min(popoverBottomToBorder - windowHeight, popoverMarginBottomWidth + tipWindowCoord.top); $tip.css('top', tipWindowCoord.top - _distanceToMove); } }, /** * Hides flyout */ close: function close(e) { this.detachHideEvents(); $(this.selector).popover('hide'); this.view.trigger('flyout:hide', e); this._removeBlocker(); this.restoreFocus(); if (this.modal) { // Closing a modal dialog should destroy the dialog. this.destroy(); } this.isOpen = false; delete this.isOpenReady; if (this.view.onPopupDone) { this.view.onPopupDone(); } }, restoreFocus: function restoreFocus() { $(this.getLaunchPoint()).focus(); }, /** * Destroys bootstrap flyout * change required to support 3.4.1: * - need to call destroy in a no transition mode, to make sure the bootstrap properties are reset in a synchronous way * - need to check 'bs.popover' before calling hiding otherwise the shown.bs.popover event is never triggered * TODO: * - call popover('destroy') directly: could not do it with 3.2.0 * - remove bootstrap for the popover */ destroy: function destroy() { var _this4 = this; var options = { viewId: this.view.viewId }; this.view.trigger('flyout:destroy', options); this.once = false; // set here as well in case 'close' was not executed this.isOpen = false; delete this.isOpenReady; var promise = new Promise(function (resolve) { _this4.popover.on('hidden.bs.popover', function () { if (_this4.popover) { _this4.popover.off('shown.bs.popover'); _this4.popover.off('hidden.bs.popover'); _this4.popover.off('click.flyoutShowEvent tap.flyoutShowEvent'); } var transition = $.support.transition; $.support.transition = false; $(_this4.selector).popover('destroy'); $.support.transition = transition; _this4.detachHideEvents(); if (_this4.isDestroyContentView && _this4.view && _this4.view.destroy) { _this4.view.destroy(); _this4.view = null; } $(window).off('resize.flyoutResizeListener'); resolve(); }); _this4._removeBlocker(); if ($(_this4.selector).data && $(_this4.selector).data('bs.popover')) { $(_this4.selector).popover('hide'); } else { resolve(); } }); return promise; }, detachHideEvents: function detachHideEvents() { if (this.hideEventsAttached) { if (this.mouseHideEventHandler) { $('body').off('mousedown.flyoutHideEvent touchstart.flyoutHideEvent', this.mouseHideEventHandler); } if (this.scrollHideEventHandler) { $('body').off('wheel.flyoutHideEvent touchstart.flyoutHideEvent', this.scrollHideEventHandler); } if (this.keyboardHideEventHandler) { $('body').off('keydown.flyoutHideEvent', this.keyboardHideEventHandler); } if (this.onMouseLeaveEventHandler && this.popover) { var popover = this.popover.data('bs.popover'); var $tip = popover && popover.$tip; if ($tip) { $tip.off('mouseleave', this.onMouseLeaveEventHandler); } } $(window).off('resize.flyoutResizeListener'); this.hideEventsAttached = false; } }, _generateContainerTemplate: function _generateContainerTemplate(options, id, btnId) { if (!this.closeBtnDotTempl) { this.closeBtnDotTempl = dot.template(this.closeBtnTemplateString || ''); this.fullDotTempl = dot.template(this.bsFullTemplateString); this.containerDotTempl = dot.template(this.containerTemplateString); } var closeBtnStr; if (options.hasCloseBtn) { closeBtnStr = this.closeBtnDotTempl({ bid: btnId }); } else { closeBtnStr = ''; } var sContainer = this.containerDotTempl({ id: id, ht: options.maxHt, wd: options.maxWd, closeBtn: closeBtnStr }); var sFullTempl = this.fullDotTempl({ containerTemplate: sContainer, popoverClass: options.popoverClass ? options.popoverClass : '', marginLeft: options.marginLeft ? options.marginLeft : '' }); return sFullTempl; }, _bindEvents: function _bindEvents(options, btnId) { var _this5 = this; var view = this.view; if (options.hasCloseBtn) { this.popover.on('shown.bs.popover', function () { $('#' + btnId).onClick(_this5.close.bind(_this5)); }); } this.popover.on('shown.bs.popover', function () { var $tip = _this5.popover.data('bs.popover').$tip; if ($tip) { $tip.addClass('animationDone'); $tip.on('tap', function (e) { e.stopPropagation(); //If we tap on the popover, don't let the event bubble out. }); } _this5.setFocus(); _this5.enableLooping($(_this5.contentRootSelector)); }); this.popover.on('shown.bs.popover', this._setupFlyoutHideAction.bind(this)); if (options.onVisible) { this.popover.one('shown.bs.popover', function () { var $tip = _this5.popover.data('bs.popover').$tip; options.onVisible($tip); }); } if (view.onPopupShown) { this.popover.on('shown.bs.popover', view.onPopupShown.bind(view)); } if (view.onPopupClosed) { this.popover.on('hidden.bs.popover', function () { view.onPopupClosed(); //'once' flag indicates that popover should be shown only once if (_this5.once) { _this5.destroy(); } }); } }, setFocus: function setFocus() { if (this.view && this.view.setFocus) { this.view.setFocus(); } }, _removeBlocker: function _removeBlocker() { if (this._blocker) { this._blocker.remove(); this._blocker = null; } }, _setupFlyoutHideAction: function _setupFlyoutHideAction() { var _this6 = this; if (!this.hideEventsAttached) { this.hideEventsAttached = true; var $tip = this.popover.data('bs.popover').$tip; var $flyoutContentNode = $('#' + this.id); var $ancestor = $flyoutContentNode.closest('.flyout-content-container'); $ancestor.data('flyout', this); this.mouseHideEventHandler = function (e) { var flyout = this; $(this.selector).each(function () { var $this = $(this); var $relatedNodes = $(flyout.relatedNodes); var ignoreClick = flyout.relatedNodes && ($relatedNodes.has(e.target).length !== 0 || $relatedNodes.is(e.target)); if (!ignoreClick && !$this.is(e.target) && $this.has(e.target).length === 0 && $tip && $tip.has(e.target).length === 0) { flyout.close(e); } }); }.bind(this); $('body').on('mousedown.flyoutHideEvent touchstart.flyoutHideEvent', this.mouseHideEventHandler); this.scrollHideEventHandler = function (e) { var flyout = this; var $this = $(this); if (!$this.is(e.target) && $this.has(e.target).length === 0 && $tip && $tip.has(e.target).length === 0) { flyout.close(e); } }.bind(this); $('body').on('wheel.flyoutHideEvent touchstart.flyoutHideEvent', this.scrollHideEventHandler); //keyboard hide event handler this.keyboardHideEventHandler = function (e) { //Escape or (Ctrl + [) to close if (e.keyCode === 27 || e.keyCode === 219 && e.ctrlKey) { this.close(e); } return; }.bind(this); $('body').on('keydown.flyoutHideEvent', this.keyboardHideEventHandler); //on mouse leave if (this.hideOnMouseLeave === true) { if ($tip) { //a handler to close this Flyout when you mouse out of it this.onMouseLeaveEventHandler = function (e) { if (!_this6._oTimerOut) { _this6._oTimerOut = window.setTimeout(function () { _this6.close(e); _this6._oTimerOut = null; }, 500); } }; $tip.on('mouseleave', this.onMouseLeaveEventHandler); } } // Adding listener to close the flyout on window resize $(window).on('resize.flyoutResizeListener', function (e) { _this6.close(e); }); } }, /** Note that tip is ignored in this implementation. */ _calculatePlacement: function _calculatePlacement(tip, element) { var content = $(this.content); var $element = $(element); var placement = 'auto'; var boundary = $(window); var padding = this.viewport ? this.viewport.padding : 0; var contentOuterHeight = content.outerHeight(true) || 0; var contentOuterWidth = content.outerWidth(true) || 0; // Using Math.round() since Firefox can return decimals like 10.6px for position, which can cause unexpected behavior. if (Math.round($element.offset().top) >= Math.round(contentOuterHeight + padding)) { placement = 'top'; } else if (Math.round(boundary.height() - ($element.offset().top + $element.outerHeight(true))) >= Math.round(contentOuterHeight + padding)) { placement = 'bottom'; } else if (Math.round(boundary.width() - ($element.offset().left + $element.outerWidth(true))) >= Math.round(contentOuterWidth + padding)) { placement = 'right'; } else if (Math.round($element.offset().left) >= Math.round(contentOuterWidth + padding)) { placement = 'left'; } this.placement = placement; return placement; } }); return Flyout; }); //# sourceMappingURL=Flyout.js.map