123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623 |
- '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: '<div {{? it.marginLeft}}style="margin-left:{{=it.marginLeft}}px" {{?}}class="popover {{=it.popoverClass}}"><div class="arrow"></div><div class="flyout-content-container">{{=it.containerTemplate}}</div></div>',
- //flyout container template
- containerTemplateString: '{{=it.closeBtn}} <div id="{{=it.id}}" class="flyout-content"> <div style="{{? it.ht}}height:{{=it.ht}}px;{{?}}{{? it.wd}}width:{{=it.wd}}px;{{?}} {{? it.ht || it.wd}} overflow:auto;-webkit-overflow-scrolling: touch;{{?}}" class="popover-content"> </div></div>',
- //close button template
- closeBtnTemplateString: '<div id="{{=it.bid}}" class="flyout-close-button"> <span class="glyphicon glyphicon-remove-circle"></span> </div>',
- 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
|