'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