Flyout.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623
  1. 'use strict';
  2. /**
  3. * Licensed Materials - Property of IBM
  4. * IBM Cognos Products: BI Cloud (C) Copyright IBM Corp. 2014, 2019
  5. * US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp.
  6. */
  7. define(['jquery', '../lib/@waca/core-client/js/core-client/ui/AccessibleView', 'doT', 'underscore', './DialogBlocker'], function ($, BaseClass, dot, _, DialogBlocker) {
  8. var idCounter = 0;
  9. /**
  10. * Shows flyouts on click/tap on DOM elements identified using a selector.
  11. * Content of the flyout comes from a view implements following i/f
  12. * {
  13. * getRenderedHtml : function() { return html string }
  14. * onPopupShown : function() {}, optional
  15. * onPopupClosed : function() {}, optional
  16. * }
  17. *
  18. * There is only One flyout visible at a time, clicking/tapping/scrolling on rest of the
  19. * page or on another element associated with a flyout will close the
  20. * previous flyout.
  21. *
  22. * Input options to Flyout
  23. * {
  24. * maxHt : max height (in pixels) for content, optional
  25. * maxWd : max width (in pixels) for content, optional
  26. * content: html string to be filled in the view, optional
  27. * selector : CSS selector for the DOM node associated with the flyout e.g. '.applicationTitle'
  28. * hasCloseBtn : indicates whether or not to show close button (optional)
  29. * popoverClass: Css class to be added to the popover container,
  30. * placement: placement of the popover. Default value is 'auto'
  31. * }
  32. *
  33. * Example Usage:
  34. * require([ 'app/util/Flyout', view class module e.g.'app/util/FlyoutContentBase' ],
  35. * function(Flyout, ContentClass) {
  36. *
  37. * var popover = new Flyout( ContentClass, {
  38. * maxHt: 100,
  39. * maxWd: 100,
  40. * content: 'test content string',
  41. * hasCloseBtn: true,
  42. * selector : '.applicationTitle',
  43. * popoverClass: 'someCssClass'
  44. * });
  45. * ...
  46. * popover.destroy();
  47. * }
  48. *
  49. * @class Flyout
  50. */
  51. var Flyout = BaseClass.extend({
  52. //overriding bootstrap template to include container template, remove title element, move class="popover-content" to content div in container template
  53. 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>',
  54. //flyout container template
  55. 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>',
  56. //close button template
  57. closeBtnTemplateString: '<div id="{{=it.bid}}" class="flyout-close-button"> <span class="glyphicon glyphicon-remove-circle"></span> </div>',
  58. placement: null,
  59. enableTabLooping: true,
  60. init: function init(options) {
  61. Flyout.inherited('init', this, [{ launchPoint: options.launchPoint }]);
  62. _.defaults(options, { modal: false });
  63. this.forceRedraw = options.forceRedraw;
  64. this.logger = options.logger;
  65. this.selector = options.selector;
  66. this.notCentered = options.notCentered;
  67. this.relatedNodes = options.relatedNodes || [];
  68. this.once = options.once;
  69. this.modal = options.modal;
  70. //if this is === true, this flyout will close if you mouse into it and then out.
  71. this.hideOnMouseLeave = options.hideOnMouseLeave;
  72. if (!this.placement) {
  73. this.placement = options.placement;
  74. }
  75. this.notCentered = options.notCentered;
  76. this.alignment = options.alignment;
  77. this.hideEventsAttached = false;
  78. // Get the mid-point of the selector and use that to offset the flyout so that
  79. // it appears above the mouse pointer.
  80. if (options.pageX) {
  81. var selector = $(this.selector);
  82. var placement = options.placement || this._calculatePlacement(0, selector);
  83. if (placement === 'top' || placement === 'bottom') {
  84. var midpoint = $(this.selector).offset().left + $(this.selector).width() / 2;
  85. options.marginLeft = options.pageX - midpoint;
  86. }
  87. }
  88. idCounter++;
  89. this.id = '_pop_' + idCounter;
  90. var btnId = this.id + '_btn_';
  91. var template = this._generateContainerTemplate(options, this.id, btnId);
  92. this.contentRootSelector = '#' + this.id + '>.popover-content';
  93. var view = options.viewInstance;
  94. if (!view) {
  95. view = new options.viewClass({
  96. popupContainer: this,
  97. content: options.content,
  98. viewOptions: options.viewOptions,
  99. contentRootSelector: this.contentRootSelector,
  100. maxHt: options.maxHt,
  101. maxWd: options.maxWd,
  102. logger: this.logger
  103. });
  104. this.isDestroyContentView = true;
  105. }
  106. this.view = view;
  107. var content = view;
  108. if (view.getRenderedHtml) {
  109. content = view.getRenderedHtml.bind(view);
  110. }
  111. this.content = content;
  112. var container = options.container;
  113. if (this.modal) {
  114. container = DialogBlocker.getJqBlocker();
  115. container.addClass('show');
  116. container.addClass('transparent');
  117. this._blocker = container;
  118. }
  119. var viewport = options.viewport;
  120. var popoverOptions = {
  121. placement: options.placement || this._calculatePlacement.bind(this),
  122. trigger: 'manual',
  123. container: container,
  124. content: content,
  125. viewport: viewport,
  126. html: true,
  127. template: template,
  128. sanitize: false
  129. };
  130. this.popover = $(this.selector).popover(popoverOptions);
  131. this._bindEvents(options, btnId);
  132. },
  133. _addBlocker: function _addBlocker() {
  134. DialogBlocker.show(document.body, this._blocker);
  135. },
  136. /**
  137. * Find and return an arry of current opened popovers
  138. *
  139. * @return {array} Array of found opened popovwers
  140. */
  141. _findCurrentOpenedPopoversToClose: function _findCurrentOpenedPopoversToClose() {
  142. var $flyoutContents = $('.flyout-content-container');
  143. var flyouts = [];
  144. _.each($flyoutContents, function (flyoutContent) {
  145. var $flyoutContent = $(flyoutContent);
  146. var flyout = $flyoutContent.data('flyout');
  147. if (flyout) {
  148. flyouts.push(flyout);
  149. }
  150. });
  151. return flyouts;
  152. },
  153. open: function open(nodeToOpen) {
  154. if (this.forceRedraw || !this.isOpen) {
  155. return this._open(nodeToOpen);
  156. }
  157. this.isOpenReady = Promise.resolve();
  158. return this.isOpenReady;
  159. },
  160. /*
  161. * Show the flyout for a given node
  162. */
  163. _open: function _open(nodeToOpen) {
  164. var _this = this;
  165. this.isOpenReady = new Promise(function (resolve) {
  166. var openedFlyouts = _this._findCurrentOpenedPopoversToClose();
  167. if (openedFlyouts.length > 0) {
  168. /** Work around for bootstrap $tip.show/hide using $.fn.emulateTransitionEnd defect where setTimeout not invoke callback to triggering 'shown.bs.popover'
  169. * Bootstrap uses setTimeout to both show new popover and hide the previous popover
  170. * 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
  171. * 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
  172. * to the wrong flyout resulting in not be able to close a flyout
  173. * Therefore make sure that all opened flyouts get close before open another one
  174. */
  175. var hideContext = {};
  176. var flyoutsToBeCloseMap = {};
  177. _.each(openedFlyouts, function (flyout) {
  178. flyoutsToBeCloseMap[flyout.id] = flyout;
  179. hideContext[flyout.id] = {
  180. flyout: flyout,
  181. hiddenPopoverCallback: function hiddenPopoverCallback() {
  182. delete flyoutsToBeCloseMap[flyout.id];
  183. if (_.keys(flyoutsToBeCloseMap).length === 0) {
  184. resolve(hideContext);
  185. }
  186. }
  187. };
  188. flyout.popover.on('hidden.bs.popover', hideContext[flyout.id].hiddenPopoverCallback);
  189. });
  190. _.each(hideContext, function (context) {
  191. context.flyout.close();
  192. });
  193. } else {
  194. resolve();
  195. }
  196. }).then(function (hideContext) {
  197. _.each(hideContext, function (context) {
  198. context.flyout.popover.off('hidden.bs.popover', context.hiddenPopoverCallback);
  199. });
  200. // Handle dialog blocker addition for modals/vischanger
  201. if (_this.modal) {
  202. if (!_this.isOpen && $(_this.popover).has(nodeToOpen)) {
  203. _this._addBlocker();
  204. }
  205. }
  206. _this._openFlyout(nodeToOpen);
  207. });
  208. return this.isOpenReady;
  209. },
  210. /**
  211. * This function is added to allow unit tests to trigger an event to open a flyout
  212. * and wait for it to be in the open state and ready for use
  213. */
  214. openIsReady: function openIsReady() {
  215. var _this2 = this;
  216. var promise = this.isOpenReady || Promise.resolve();
  217. return promise.then(function () {
  218. return !!_this2.isOpen;
  219. });
  220. },
  221. /**
  222. * Call to show the popover
  223. */
  224. _openFlyout: function _openFlyout(node) {
  225. var _this3 = this;
  226. if (!this.isOpen) {
  227. if ($(this.popover).has(node)) {
  228. var $node = $(node);
  229. $node.popover('show');
  230. var popover = $node.length && $.data($node[0], 'bs.popover');
  231. if (popover && popover.$tip) {
  232. if (this.alignment === 'top') {
  233. popover.$tip.css('top', popover.$element.position().top);
  234. } else if (this.alignment === 'left') {
  235. popover.$tip.css('left', popover.$element.position().left);
  236. }
  237. if (this.notCentered && (this.placement === 'right' || this.placement === 'left')) {
  238. popover.$tip.css('top', popover.$element.offset().top);
  239. } else if (this.notCentered && (this.placement === 'top' || this.placement === 'bottom')) {
  240. popover.$tip.css('left', popover.$element.offset().left);
  241. }
  242. this.adjustPopoverInViewport(popover);
  243. //IT Test hook to close flyouts
  244. popover.$tip.on('userCloseFlyout', function () {
  245. _this3.close();
  246. });
  247. } else {
  248. console.error('Cannot retrieve popover object');
  249. }
  250. }
  251. this.isOpen = true;
  252. }
  253. },
  254. /**
  255. * @param {Object} popover: a bootstrap popover object
  256. * Note: Actually we're adjusting the position of popover in the window.
  257. * The strategy is:
  258. * - if we fall off the bottom, shift up unless that would take us off the top of the screen.
  259. * - if we fall off the right, shift left unless that would take us off the left of the screen.
  260. */
  261. adjustPopoverInViewport: function adjustPopoverInViewport(popover) {
  262. var $tip = popover.$tip;
  263. var tipWindowCoord = $tip.get(0).getBoundingClientRect();
  264. var windowWidth = $(window).width();
  265. var windowHeight = $(window).height();
  266. var popoverMarginLeftWidth = parseFloat($tip.css('margin-left'));
  267. var popoverMarginRightWidth = parseFloat($tip.css('margin-right'));
  268. var popoverMarginTopWidth = parseFloat($tip.css('margin-top'));
  269. var popoverMarginBottomWidth = parseFloat($tip.css('margin-bottom'));
  270. var popoverLeftToBorder = tipWindowCoord.left + popoverMarginLeftWidth;
  271. var popoverRightToBorder = tipWindowCoord.right - popoverMarginRightWidth;
  272. var popoverTopToBorder = tipWindowCoord.top + popoverMarginTopWidth;
  273. var popoverBottomToBorder = tipWindowCoord.bottom - popoverMarginBottomWidth;
  274. if (popoverLeftToBorder < 0) {
  275. $tip.css('left', -popoverMarginLeftWidth);
  276. } else if (windowWidth < popoverRightToBorder) {
  277. var distanceToMove = Math.min(popoverRightToBorder - windowWidth, popoverMarginLeftWidth + tipWindowCoord.left);
  278. $tip.css('left', tipWindowCoord.left - distanceToMove);
  279. }
  280. if (popoverTopToBorder < 0) {
  281. $tip.css('top', -popoverMarginTopWidth);
  282. } else if (windowHeight < popoverBottomToBorder) {
  283. var _distanceToMove = Math.min(popoverBottomToBorder - windowHeight, popoverMarginBottomWidth + tipWindowCoord.top);
  284. $tip.css('top', tipWindowCoord.top - _distanceToMove);
  285. }
  286. },
  287. /**
  288. * Hides flyout
  289. */
  290. close: function close(e) {
  291. this.detachHideEvents();
  292. $(this.selector).popover('hide');
  293. this.view.trigger('flyout:hide', e);
  294. this._removeBlocker();
  295. this.restoreFocus();
  296. if (this.modal) {
  297. // Closing a modal dialog should destroy the dialog.
  298. this.destroy();
  299. }
  300. this.isOpen = false;
  301. delete this.isOpenReady;
  302. if (this.view.onPopupDone) {
  303. this.view.onPopupDone();
  304. }
  305. },
  306. restoreFocus: function restoreFocus() {
  307. $(this.getLaunchPoint()).focus();
  308. },
  309. /**
  310. * Destroys bootstrap flyout
  311. * change required to support 3.4.1:
  312. * - need to call destroy in a no transition mode, to make sure the bootstrap properties are reset in a synchronous way
  313. * - need to check 'bs.popover' before calling hiding otherwise the shown.bs.popover event is never triggered
  314. * TODO:
  315. * - call popover('destroy') directly: could not do it with 3.2.0
  316. * - remove bootstrap for the popover
  317. */
  318. destroy: function destroy() {
  319. var _this4 = this;
  320. var options = {
  321. viewId: this.view.viewId
  322. };
  323. this.view.trigger('flyout:destroy', options);
  324. this.once = false;
  325. // set here as well in case 'close' was not executed
  326. this.isOpen = false;
  327. delete this.isOpenReady;
  328. var promise = new Promise(function (resolve) {
  329. _this4.popover.on('hidden.bs.popover', function () {
  330. if (_this4.popover) {
  331. _this4.popover.off('shown.bs.popover');
  332. _this4.popover.off('hidden.bs.popover');
  333. _this4.popover.off('click.flyoutShowEvent tap.flyoutShowEvent');
  334. }
  335. var transition = $.support.transition;
  336. $.support.transition = false;
  337. $(_this4.selector).popover('destroy');
  338. $.support.transition = transition;
  339. _this4.detachHideEvents();
  340. if (_this4.isDestroyContentView && _this4.view && _this4.view.destroy) {
  341. _this4.view.destroy();
  342. _this4.view = null;
  343. }
  344. $(window).off('resize.flyoutResizeListener');
  345. resolve();
  346. });
  347. _this4._removeBlocker();
  348. if ($(_this4.selector).data && $(_this4.selector).data('bs.popover')) {
  349. $(_this4.selector).popover('hide');
  350. } else {
  351. resolve();
  352. }
  353. });
  354. return promise;
  355. },
  356. detachHideEvents: function detachHideEvents() {
  357. if (this.hideEventsAttached) {
  358. if (this.mouseHideEventHandler) {
  359. $('body').off('mousedown.flyoutHideEvent touchstart.flyoutHideEvent', this.mouseHideEventHandler);
  360. }
  361. if (this.scrollHideEventHandler) {
  362. $('body').off('wheel.flyoutHideEvent touchstart.flyoutHideEvent', this.scrollHideEventHandler);
  363. }
  364. if (this.keyboardHideEventHandler) {
  365. $('body').off('keydown.flyoutHideEvent', this.keyboardHideEventHandler);
  366. }
  367. if (this.onMouseLeaveEventHandler && this.popover) {
  368. var popover = this.popover.data('bs.popover');
  369. var $tip = popover && popover.$tip;
  370. if ($tip) {
  371. $tip.off('mouseleave', this.onMouseLeaveEventHandler);
  372. }
  373. }
  374. $(window).off('resize.flyoutResizeListener');
  375. this.hideEventsAttached = false;
  376. }
  377. },
  378. _generateContainerTemplate: function _generateContainerTemplate(options, id, btnId) {
  379. if (!this.closeBtnDotTempl) {
  380. this.closeBtnDotTempl = dot.template(this.closeBtnTemplateString || '');
  381. this.fullDotTempl = dot.template(this.bsFullTemplateString);
  382. this.containerDotTempl = dot.template(this.containerTemplateString);
  383. }
  384. var closeBtnStr;
  385. if (options.hasCloseBtn) {
  386. closeBtnStr = this.closeBtnDotTempl({
  387. bid: btnId
  388. });
  389. } else {
  390. closeBtnStr = '';
  391. }
  392. var sContainer = this.containerDotTempl({
  393. id: id,
  394. ht: options.maxHt,
  395. wd: options.maxWd,
  396. closeBtn: closeBtnStr
  397. });
  398. var sFullTempl = this.fullDotTempl({
  399. containerTemplate: sContainer,
  400. popoverClass: options.popoverClass ? options.popoverClass : '',
  401. marginLeft: options.marginLeft ? options.marginLeft : ''
  402. });
  403. return sFullTempl;
  404. },
  405. _bindEvents: function _bindEvents(options, btnId) {
  406. var _this5 = this;
  407. var view = this.view;
  408. if (options.hasCloseBtn) {
  409. this.popover.on('shown.bs.popover', function () {
  410. $('#' + btnId).onClick(_this5.close.bind(_this5));
  411. });
  412. }
  413. this.popover.on('shown.bs.popover', function () {
  414. var $tip = _this5.popover.data('bs.popover').$tip;
  415. if ($tip) {
  416. $tip.addClass('animationDone');
  417. $tip.on('tap', function (e) {
  418. e.stopPropagation(); //If we tap on the popover, don't let the event bubble out.
  419. });
  420. }
  421. _this5.setFocus();
  422. _this5.enableLooping($(_this5.contentRootSelector));
  423. });
  424. this.popover.on('shown.bs.popover', this._setupFlyoutHideAction.bind(this));
  425. if (options.onVisible) {
  426. this.popover.one('shown.bs.popover', function () {
  427. var $tip = _this5.popover.data('bs.popover').$tip;
  428. options.onVisible($tip);
  429. });
  430. }
  431. if (view.onPopupShown) {
  432. this.popover.on('shown.bs.popover', view.onPopupShown.bind(view));
  433. }
  434. if (view.onPopupClosed) {
  435. this.popover.on('hidden.bs.popover', function () {
  436. view.onPopupClosed();
  437. //'once' flag indicates that popover should be shown only once
  438. if (_this5.once) {
  439. _this5.destroy();
  440. }
  441. });
  442. }
  443. },
  444. setFocus: function setFocus() {
  445. if (this.view && this.view.setFocus) {
  446. this.view.setFocus();
  447. }
  448. },
  449. _removeBlocker: function _removeBlocker() {
  450. if (this._blocker) {
  451. this._blocker.remove();
  452. this._blocker = null;
  453. }
  454. },
  455. _setupFlyoutHideAction: function _setupFlyoutHideAction() {
  456. var _this6 = this;
  457. if (!this.hideEventsAttached) {
  458. this.hideEventsAttached = true;
  459. var $tip = this.popover.data('bs.popover').$tip;
  460. var $flyoutContentNode = $('#' + this.id);
  461. var $ancestor = $flyoutContentNode.closest('.flyout-content-container');
  462. $ancestor.data('flyout', this);
  463. this.mouseHideEventHandler = function (e) {
  464. var flyout = this;
  465. $(this.selector).each(function () {
  466. var $this = $(this);
  467. var $relatedNodes = $(flyout.relatedNodes);
  468. var ignoreClick = flyout.relatedNodes && ($relatedNodes.has(e.target).length !== 0 || $relatedNodes.is(e.target));
  469. if (!ignoreClick && !$this.is(e.target) && $this.has(e.target).length === 0 && $tip && $tip.has(e.target).length === 0) {
  470. flyout.close(e);
  471. }
  472. });
  473. }.bind(this);
  474. $('body').on('mousedown.flyoutHideEvent touchstart.flyoutHideEvent', this.mouseHideEventHandler);
  475. this.scrollHideEventHandler = function (e) {
  476. var flyout = this;
  477. var $this = $(this);
  478. if (!$this.is(e.target) && $this.has(e.target).length === 0 && $tip && $tip.has(e.target).length === 0) {
  479. flyout.close(e);
  480. }
  481. }.bind(this);
  482. $('body').on('wheel.flyoutHideEvent touchstart.flyoutHideEvent', this.scrollHideEventHandler);
  483. //keyboard hide event handler
  484. this.keyboardHideEventHandler = function (e) {
  485. //Escape or (Ctrl + [) to close
  486. if (e.keyCode === 27 || e.keyCode === 219 && e.ctrlKey) {
  487. this.close(e);
  488. }
  489. return;
  490. }.bind(this);
  491. $('body').on('keydown.flyoutHideEvent', this.keyboardHideEventHandler);
  492. //on mouse leave
  493. if (this.hideOnMouseLeave === true) {
  494. if ($tip) {
  495. //a handler to close this Flyout when you mouse out of it
  496. this.onMouseLeaveEventHandler = function (e) {
  497. if (!_this6._oTimerOut) {
  498. _this6._oTimerOut = window.setTimeout(function () {
  499. _this6.close(e);
  500. _this6._oTimerOut = null;
  501. }, 500);
  502. }
  503. };
  504. $tip.on('mouseleave', this.onMouseLeaveEventHandler);
  505. }
  506. }
  507. // Adding listener to close the flyout on window resize
  508. $(window).on('resize.flyoutResizeListener', function (e) {
  509. _this6.close(e);
  510. });
  511. }
  512. },
  513. /** Note that tip is ignored in this implementation. */
  514. _calculatePlacement: function _calculatePlacement(tip, element) {
  515. var content = $(this.content);
  516. var $element = $(element);
  517. var placement = 'auto';
  518. var boundary = $(window);
  519. var padding = this.viewport ? this.viewport.padding : 0;
  520. var contentOuterHeight = content.outerHeight(true) || 0;
  521. var contentOuterWidth = content.outerWidth(true) || 0;
  522. // Using Math.round() since Firefox can return decimals like 10.6px for position, which can cause unexpected behavior.
  523. if (Math.round($element.offset().top) >= Math.round(contentOuterHeight + padding)) {
  524. placement = 'top';
  525. } else if (Math.round(boundary.height() - ($element.offset().top + $element.outerHeight(true))) >= Math.round(contentOuterHeight + padding)) {
  526. placement = 'bottom';
  527. } else if (Math.round(boundary.width() - ($element.offset().left + $element.outerWidth(true))) >= Math.round(contentOuterWidth + padding)) {
  528. placement = 'right';
  529. } else if (Math.round($element.offset().left) >= Math.round(contentOuterWidth + padding)) {
  530. placement = 'left';
  531. }
  532. this.placement = placement;
  533. return placement;
  534. }
  535. });
  536. return Flyout;
  537. });
  538. //# sourceMappingURL=Flyout.js.map