modal.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. /**
  2. * Copyright © Magento, Inc. All rights reserved.
  3. * See COPYING.txt for license details.
  4. */
  5. /**
  6. * @api
  7. */
  8. define([
  9. 'jquery',
  10. 'underscore',
  11. 'mage/template',
  12. 'text!ui/template/modal/modal-popup.html',
  13. 'text!ui/template/modal/modal-slide.html',
  14. 'text!ui/template/modal/modal-custom.html',
  15. 'Magento_Ui/js/lib/key-codes',
  16. 'jquery/ui',
  17. 'mage/translate'
  18. ], function ($, _, template, popupTpl, slideTpl, customTpl, keyCodes) {
  19. 'use strict';
  20. /**
  21. * Detect browser transition end event.
  22. * @return {String|undefined} - transition event.
  23. */
  24. var transitionEvent = (function () {
  25. var transition,
  26. elementStyle = document.createElement('div').style,
  27. transitions = {
  28. 'transition': 'transitionend',
  29. 'OTransition': 'oTransitionEnd',
  30. 'MozTransition': 'transitionend',
  31. 'WebkitTransition': 'webkitTransitionEnd'
  32. };
  33. for (transition in transitions) {
  34. if (elementStyle[transition] !== undefined && transitions.hasOwnProperty(transition)) {
  35. return transitions[transition];
  36. }
  37. }
  38. })();
  39. /**
  40. * Modal Window Widget
  41. */
  42. $.widget('mage.modal', {
  43. options: {
  44. id: null,
  45. type: 'popup',
  46. title: '',
  47. subTitle: '',
  48. modalClass: '',
  49. focus: '[data-role="closeBtn"]',
  50. autoOpen: false,
  51. clickableOverlay: true,
  52. popupTpl: popupTpl,
  53. slideTpl: slideTpl,
  54. customTpl: customTpl,
  55. modalVisibleClass: '_show',
  56. parentModalClass: '_has-modal',
  57. innerScrollClass: '_inner-scroll',
  58. responsive: false,
  59. innerScroll: false,
  60. modalTitle: '[data-role="title"]',
  61. modalSubTitle: '[data-role="subTitle"]',
  62. modalBlock: '[data-role="modal"]',
  63. modalCloseBtn: '[data-role="closeBtn"]',
  64. modalContent: '[data-role="content"]',
  65. modalAction: '[data-role="action"]',
  66. focusableScope: '[data-role="focusable-scope"]',
  67. focusableStart: '[data-role="focusable-start"]',
  68. focusableEnd: '[data-role="focusable-end"]',
  69. appendTo: 'body',
  70. wrapperClass: 'modals-wrapper',
  71. overlayClass: 'modals-overlay',
  72. responsiveClass: 'modal-slide',
  73. trigger: '',
  74. modalLeftMargin: 45,
  75. closeText: $.mage.__('Close'),
  76. buttons: [{
  77. text: $.mage.__('Ok'),
  78. class: '',
  79. attr: {},
  80. /**
  81. * Default action on button click
  82. */
  83. click: function (event) {
  84. this.closeModal(event);
  85. }
  86. }],
  87. keyEventHandlers: {
  88. /**
  89. * Tab key press handler,
  90. * set focus to elements
  91. */
  92. tabKey: function () {
  93. if (document.activeElement === this.modal[0]) {
  94. this._setFocus('start');
  95. }
  96. },
  97. /**
  98. * Escape key press handler,
  99. * close modal window
  100. * @param {Object} event - event
  101. */
  102. escapeKey: function (event) {
  103. if (this.options.isOpen && this.modal.find(document.activeElement).length ||
  104. this.options.isOpen && this.modal[0] === document.activeElement) {
  105. this.closeModal(event);
  106. }
  107. }
  108. }
  109. },
  110. /**
  111. * Creates modal widget.
  112. */
  113. _create: function () {
  114. _.bindAll(
  115. this,
  116. 'keyEventSwitcher',
  117. '_tabSwitcher',
  118. 'closeModal'
  119. );
  120. this.options.id = this.uuid;
  121. this.options.transitionEvent = transitionEvent;
  122. this._createWrapper();
  123. this._renderModal();
  124. this._createButtons();
  125. $(this.options.trigger).on('click', _.bind(this.toggleModal, this));
  126. this._on(this.modal.find(this.options.modalCloseBtn), {
  127. 'click': this.options.modalCloseBtnHandler ? this.options.modalCloseBtnHandler : this.closeModal
  128. });
  129. this._on(this.element, {
  130. 'openModal': this.openModal,
  131. 'closeModal': this.closeModal
  132. });
  133. this.options.autoOpen ? this.openModal() : false;
  134. },
  135. /**
  136. * Returns element from modal node.
  137. * @return {Object} - element.
  138. */
  139. _getElem: function (elem) {
  140. return this.modal.find(elem);
  141. },
  142. /**
  143. * Gets visible modal count.
  144. * * @return {Number} - visible modal count.
  145. */
  146. _getVisibleCount: function () {
  147. var modals = this.modalWrapper.find(this.options.modalBlock);
  148. return modals.filter('.' + this.options.modalVisibleClass).length;
  149. },
  150. /**
  151. * Gets count of visible modal by slide type.
  152. * * @return {Number} - visible modal count.
  153. */
  154. _getVisibleSlideCount: function () {
  155. var elems = this.modalWrapper.find('[data-type="slide"]');
  156. return elems.filter('.' + this.options.modalVisibleClass).length;
  157. },
  158. /**
  159. * Listener key events.
  160. * Call handler function if it exists
  161. */
  162. keyEventSwitcher: function (event) {
  163. var key = keyCodes[event.keyCode];
  164. if (this.options.keyEventHandlers.hasOwnProperty(key)) {
  165. this.options.keyEventHandlers[key].apply(this, arguments);
  166. }
  167. },
  168. /**
  169. * Set title for modal.
  170. *
  171. * @param {String} title
  172. */
  173. setTitle: function (title) {
  174. var $title = $(this.options.modalTitle),
  175. $subTitle = this.modal.find(this.options.modalSubTitle);
  176. $title.text(title);
  177. $title.append($subTitle);
  178. },
  179. /**
  180. * Set sub title for modal.
  181. *
  182. * @param {String} subTitle
  183. */
  184. setSubTitle: function (subTitle) {
  185. this.options.subTitle = subTitle;
  186. this.modal.find(this.options.modalSubTitle).html(subTitle);
  187. },
  188. /**
  189. * Toggle modal.
  190. * * @return {Element} - current element.
  191. */
  192. toggleModal: function () {
  193. if (this.options.isOpen === true) {
  194. this.closeModal();
  195. } else {
  196. this.openModal();
  197. }
  198. },
  199. /**
  200. * Open modal.
  201. * * @return {Element} - current element.
  202. */
  203. openModal: function () {
  204. this.options.isOpen = true;
  205. this.focussedElement = document.activeElement;
  206. this._createOverlay();
  207. this._setActive();
  208. this._setKeyListener();
  209. this.modal.one(this.options.transitionEvent, _.bind(this._setFocus, this, 'end', 'opened'));
  210. this.modal.one(this.options.transitionEvent, _.bind(this._trigger, this, 'opened'));
  211. this.modal.addClass(this.options.modalVisibleClass);
  212. if (!this.options.transitionEvent) {
  213. this._trigger('opened');
  214. }
  215. return this.element;
  216. },
  217. /**
  218. * Set focus to element.
  219. * @param {String} position - can be "start" and "end"
  220. * positions.
  221. * If position is "end" - sets focus to first
  222. * focusable element in modal window scope.
  223. * If position is "start" - sets focus to last
  224. * focusable element in modal window scope
  225. *
  226. * @param {String} type - can be "opened" or false
  227. * If type is "opened" - looks to "this.options.focus"
  228. * property and sets focus
  229. */
  230. _setFocus: function (position, type) {
  231. var focusableElements,
  232. infelicity;
  233. if (type === 'opened' && this.options.focus) {
  234. this.modal.find($(this.options.focus)).focus();
  235. } else if (type === 'opened' && !this.options.focus) {
  236. this.modal.find(this.options.focusableScope).focus();
  237. } else if (position === 'end') {
  238. this.modal.find(this.options.modalCloseBtn).focus();
  239. } else if (position === 'start') {
  240. infelicity = 2; //Constant for find last focusable element
  241. focusableElements = this.modal.find(':focusable');
  242. focusableElements.eq(focusableElements.length - infelicity).focus();
  243. }
  244. },
  245. /**
  246. * Set events listener when modal is opened.
  247. */
  248. _setKeyListener: function () {
  249. this.modal.find(this.options.focusableStart).bind('focusin', this._tabSwitcher);
  250. this.modal.find(this.options.focusableEnd).bind('focusin', this._tabSwitcher);
  251. this.modal.bind('keydown', this.keyEventSwitcher);
  252. },
  253. /**
  254. * Remove events listener when modal is closed.
  255. */
  256. _removeKeyListener: function () {
  257. this.modal.find(this.options.focusableStart).unbind('focusin', this._tabSwitcher);
  258. this.modal.find(this.options.focusableEnd).unbind('focusin', this._tabSwitcher);
  259. this.modal.unbind('keydown', this.keyEventSwitcher);
  260. },
  261. /**
  262. * Switcher for focus event.
  263. * @param {Object} e - event
  264. */
  265. _tabSwitcher: function (e) {
  266. var target = $(e.target);
  267. if (target.is(this.options.focusableStart)) {
  268. this._setFocus('start');
  269. } else if (target.is(this.options.focusableEnd)) {
  270. this._setFocus('end');
  271. }
  272. },
  273. /**
  274. * Close modal.
  275. * * @return {Element} - current element.
  276. */
  277. closeModal: function () {
  278. var that = this;
  279. this._removeKeyListener();
  280. this.options.isOpen = false;
  281. this.modal.one(this.options.transitionEvent, function () {
  282. that._close();
  283. });
  284. this.modal.removeClass(this.options.modalVisibleClass);
  285. if (!this.options.transitionEvent) {
  286. that._close();
  287. }
  288. return this.element;
  289. },
  290. /**
  291. * Helper for closeModal function.
  292. */
  293. _close: function () {
  294. var trigger = _.bind(this._trigger, this, 'closed', this.modal);
  295. $(this.focussedElement).focus();
  296. this._destroyOverlay();
  297. this._unsetActive();
  298. _.defer(trigger, this);
  299. },
  300. /**
  301. * Set z-index and margin for modal and overlay.
  302. */
  303. _setActive: function () {
  304. var zIndex = this.modal.zIndex(),
  305. baseIndex = zIndex + this._getVisibleCount();
  306. if (this.modal.data('active')) {
  307. return;
  308. }
  309. this.modal.data('active', true);
  310. this.overlay.zIndex(++baseIndex);
  311. this.prevOverlayIndex = this.overlay.zIndex();
  312. this.modal.zIndex(this.overlay.zIndex() + 1);
  313. if (this._getVisibleSlideCount()) {
  314. this.modal.css('marginLeft', this.options.modalLeftMargin * this._getVisibleSlideCount());
  315. }
  316. },
  317. /**
  318. * Unset styles for modal and set z-index for previous modal.
  319. */
  320. _unsetActive: function () {
  321. this.modal.removeAttr('style');
  322. this.modal.data('active', false);
  323. if (this.overlay) {
  324. this.overlay.zIndex(this.prevOverlayIndex - 1);
  325. }
  326. },
  327. /**
  328. * Creates wrapper to hold all modals.
  329. */
  330. _createWrapper: function () {
  331. this.modalWrapper = $(this.options.appendTo).find('.' + this.options.wrapperClass);
  332. if (!this.modalWrapper.length) {
  333. this.modalWrapper = $('<div></div>')
  334. .addClass(this.options.wrapperClass)
  335. .appendTo(this.options.appendTo);
  336. }
  337. },
  338. /**
  339. * Compile template and append to wrapper.
  340. */
  341. _renderModal: function () {
  342. $(template(
  343. this.options[this.options.type + 'Tpl'],
  344. {
  345. data: this.options
  346. })).appendTo(this.modalWrapper);
  347. this.modal = this.modalWrapper.find(this.options.modalBlock).last();
  348. this.element.appendTo(this._getElem(this.options.modalContent));
  349. if (this.element.is(':hidden')) {
  350. this.element.show();
  351. }
  352. },
  353. /**
  354. * Creates buttons pane.
  355. */
  356. _createButtons: function () {
  357. this.buttons = this._getElem(this.options.modalAction);
  358. _.each(this.options.buttons, function (btn, key) {
  359. var button = this.buttons[key];
  360. if (btn.attr) {
  361. $(button).attr(btn.attr);
  362. }
  363. if (btn.class) {
  364. $(button).addClass(btn.class);
  365. }
  366. if (!btn.click) {
  367. btn.click = this.closeModal;
  368. }
  369. $(button).on('click', _.bind(btn.click, this));
  370. }, this);
  371. },
  372. /**
  373. * Creates overlay, append it to wrapper, set previous click event on overlay.
  374. */
  375. _createOverlay: function () {
  376. var events,
  377. outerClickHandler = this.options.outerClickHandler || this.closeModal;
  378. this.overlay = $('.' + this.options.overlayClass);
  379. if (!this.overlay.length) {
  380. $(this.options.appendTo).addClass(this.options.parentModalClass);
  381. this.overlay = $('<div></div>')
  382. .addClass(this.options.overlayClass)
  383. .appendTo(this.modalWrapper);
  384. }
  385. events = $._data(this.overlay.get(0), 'events');
  386. events ? this.prevOverlayHandler = events.click[0].handler : false;
  387. this.options.clickableOverlay ? this.overlay.unbind().on('click', outerClickHandler) : false;
  388. },
  389. /**
  390. * Destroy overlay.
  391. */
  392. _destroyOverlay: function () {
  393. if (this._getVisibleCount()) {
  394. this.overlay.unbind().on('click', this.prevOverlayHandler);
  395. } else {
  396. $(this.options.appendTo).removeClass(this.options.parentModalClass);
  397. this.overlay.remove();
  398. this.overlay = null;
  399. }
  400. }
  401. });
  402. return $.mage.modal;
  403. });