You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

887 lines
26 KiB

  1. /*!
  2. * # Semantic UI 2.0.0 - Modal
  3. * http://github.com/semantic-org/semantic-ui/
  4. *
  5. *
  6. * Copyright 2015 Contributors
  7. * Released under the MIT license
  8. * http://opensource.org/licenses/MIT
  9. *
  10. */
  11. ;(function ( $, window, document, undefined ) {
  12. "use strict";
  13. $.fn.modal = function(parameters) {
  14. var
  15. $allModules = $(this),
  16. $window = $(window),
  17. $document = $(document),
  18. $body = $('body'),
  19. moduleSelector = $allModules.selector || '',
  20. time = new Date().getTime(),
  21. performance = [],
  22. query = arguments[0],
  23. methodInvoked = (typeof query == 'string'),
  24. queryArguments = [].slice.call(arguments, 1),
  25. requestAnimationFrame = window.requestAnimationFrame
  26. || window.mozRequestAnimationFrame
  27. || window.webkitRequestAnimationFrame
  28. || window.msRequestAnimationFrame
  29. || function(callback) { setTimeout(callback, 0); },
  30. returnedValue
  31. ;
  32. $allModules
  33. .each(function() {
  34. var
  35. settings = ( $.isPlainObject(parameters) )
  36. ? $.extend(true, {}, $.fn.modal.settings, parameters)
  37. : $.extend({}, $.fn.modal.settings),
  38. selector = settings.selector,
  39. className = settings.className,
  40. namespace = settings.namespace,
  41. error = settings.error,
  42. eventNamespace = '.' + namespace,
  43. moduleNamespace = 'module-' + namespace,
  44. $module = $(this),
  45. $context = $(settings.context),
  46. $close = $module.find(selector.close),
  47. $allModals,
  48. $otherModals,
  49. $focusedElement,
  50. $dimmable,
  51. $dimmer,
  52. element = this,
  53. instance = $module.data(moduleNamespace),
  54. elementNamespace,
  55. id,
  56. observer,
  57. module
  58. ;
  59. module = {
  60. initialize: function() {
  61. module.verbose('Initializing dimmer', $context);
  62. module.create.id();
  63. module.create.dimmer();
  64. module.refreshModals();
  65. module.bind.events();
  66. if(settings.observeChanges) {
  67. module.observeChanges();
  68. }
  69. module.instantiate();
  70. },
  71. instantiate: function() {
  72. module.verbose('Storing instance of modal');
  73. instance = module;
  74. $module
  75. .data(moduleNamespace, instance)
  76. ;
  77. },
  78. create: {
  79. dimmer: function() {
  80. var
  81. defaultSettings = {
  82. debug : settings.debug,
  83. dimmerName : 'modals',
  84. duration : {
  85. show : settings.duration,
  86. hide : settings.duration
  87. }
  88. },
  89. dimmerSettings = $.extend(true, defaultSettings, settings.dimmerSettings)
  90. ;
  91. if(settings.inverted) {
  92. dimmerSettings.variation = (dimmerSettings.variation !== undefined)
  93. ? dimmerSettings.variation + ' inverted'
  94. : 'inverted'
  95. ;
  96. }
  97. if($.fn.dimmer === undefined) {
  98. module.error(error.dimmer);
  99. return;
  100. }
  101. module.debug('Creating dimmer with settings', dimmerSettings);
  102. $dimmable = $context.dimmer(dimmerSettings);
  103. if(settings.detachable) {
  104. module.verbose('Modal is detachable, moving content into dimmer');
  105. $dimmable.dimmer('add content', $module);
  106. }
  107. else {
  108. module.set.undetached();
  109. }
  110. if(settings.blurring) {
  111. $dimmable.addClass(className.blurring);
  112. }
  113. $dimmer = $dimmable.dimmer('get dimmer');
  114. },
  115. id: function() {
  116. id = (Math.random().toString(16) + '000000000').substr(2,8);
  117. elementNamespace = '.' + id;
  118. module.verbose('Creating unique id for element', id);
  119. }
  120. },
  121. destroy: function() {
  122. module.verbose('Destroying previous modal');
  123. $module
  124. .removeData(moduleNamespace)
  125. .off(eventNamespace)
  126. ;
  127. $window.off(elementNamespace);
  128. $close.off(eventNamespace);
  129. $context.dimmer('destroy');
  130. },
  131. observeChanges: function() {
  132. if('MutationObserver' in window) {
  133. observer = new MutationObserver(function(mutations) {
  134. module.debug('DOM tree modified, refreshing');
  135. module.refresh();
  136. });
  137. observer.observe(element, {
  138. childList : true,
  139. subtree : true
  140. });
  141. module.debug('Setting up mutation observer', observer);
  142. }
  143. },
  144. refresh: function() {
  145. module.remove.scrolling();
  146. module.cacheSizes();
  147. module.set.screenHeight();
  148. module.set.type();
  149. module.set.position();
  150. },
  151. refreshModals: function() {
  152. $otherModals = $module.siblings(selector.modal);
  153. $allModals = $otherModals.add($module);
  154. },
  155. attachEvents: function(selector, event) {
  156. var
  157. $toggle = $(selector)
  158. ;
  159. event = $.isFunction(module[event])
  160. ? module[event]
  161. : module.toggle
  162. ;
  163. if($toggle.length > 0) {
  164. module.debug('Attaching modal events to element', selector, event);
  165. $toggle
  166. .off(eventNamespace)
  167. .on('click' + eventNamespace, event)
  168. ;
  169. }
  170. else {
  171. module.error(error.notFound, selector);
  172. }
  173. },
  174. bind: {
  175. events: function() {
  176. module.verbose('Attaching events');
  177. $module
  178. .on('click' + eventNamespace, selector.close, module.event.close)
  179. .on('click' + eventNamespace, selector.approve, module.event.approve)
  180. .on('click' + eventNamespace, selector.deny, module.event.deny)
  181. ;
  182. $window
  183. .on('resize' + elementNamespace, module.event.resize)
  184. ;
  185. }
  186. },
  187. get: {
  188. id: function() {
  189. return (Math.random().toString(16) + '000000000').substr(2,8);
  190. }
  191. },
  192. event: {
  193. approve: function() {
  194. if(settings.onApprove.call(element, $(this)) === false) {
  195. module.verbose('Approve callback returned false cancelling hide');
  196. return;
  197. }
  198. module.hide();
  199. },
  200. deny: function() {
  201. if(settings.onDeny.call(element, $(this)) === false) {
  202. module.verbose('Deny callback returned false cancelling hide');
  203. return;
  204. }
  205. module.hide();
  206. },
  207. close: function() {
  208. module.hide();
  209. },
  210. click: function(event) {
  211. var
  212. $target = $(event.target),
  213. isInModal = ($target.closest(selector.modal).length > 0),
  214. isInDOM = $.contains(document.documentElement, event.target)
  215. ;
  216. if(!isInModal && isInDOM) {
  217. module.debug('Dimmer clicked, hiding all modals');
  218. if( module.is.active() ) {
  219. module.remove.clickaway();
  220. if(settings.allowMultiple) {
  221. module.hide();
  222. }
  223. else {
  224. module.hideAll();
  225. }
  226. }
  227. }
  228. },
  229. debounce: function(method, delay) {
  230. clearTimeout(module.timer);
  231. module.timer = setTimeout(method, delay);
  232. },
  233. keyboard: function(event) {
  234. var
  235. keyCode = event.which,
  236. escapeKey = 27
  237. ;
  238. if(keyCode == escapeKey) {
  239. if(settings.closable) {
  240. module.debug('Escape key pressed hiding modal');
  241. module.hide();
  242. }
  243. else {
  244. module.debug('Escape key pressed, but closable is set to false');
  245. }
  246. event.preventDefault();
  247. }
  248. },
  249. resize: function() {
  250. if( $dimmable.dimmer('is active') ) {
  251. requestAnimationFrame(module.refresh);
  252. }
  253. }
  254. },
  255. toggle: function() {
  256. if( module.is.active() || module.is.animating() ) {
  257. module.hide();
  258. }
  259. else {
  260. module.show();
  261. }
  262. },
  263. show: function(callback) {
  264. callback = $.isFunction(callback)
  265. ? callback
  266. : function(){}
  267. ;
  268. module.refreshModals();
  269. module.showModal(callback);
  270. },
  271. hide: function(callback) {
  272. callback = $.isFunction(callback)
  273. ? callback
  274. : function(){}
  275. ;
  276. module.refreshModals();
  277. module.hideModal(callback);
  278. },
  279. showModal: function(callback) {
  280. callback = $.isFunction(callback)
  281. ? callback
  282. : function(){}
  283. ;
  284. if( module.is.animating() || !module.is.active() ) {
  285. module.showDimmer();
  286. module.cacheSizes();
  287. module.set.position();
  288. module.set.screenHeight();
  289. module.set.type();
  290. module.set.clickaway();
  291. if( !settings.allowMultiple && module.others.active() ) {
  292. module.hideOthers(module.showModal);
  293. }
  294. else {
  295. settings.onShow.call(element);
  296. if(settings.transition && $.fn.transition !== undefined && $module.transition('is supported')) {
  297. module.debug('Showing modal with css animations');
  298. $module
  299. .transition({
  300. debug : settings.debug,
  301. animation : settings.transition + ' in',
  302. queue : settings.queue,
  303. duration : settings.duration,
  304. useFailSafe : true,
  305. onComplete : function() {
  306. settings.onVisible.apply(element);
  307. module.add.keyboardShortcuts();
  308. module.save.focus();
  309. module.set.active();
  310. module.set.autofocus();
  311. callback();
  312. }
  313. })
  314. ;
  315. }
  316. else {
  317. module.error(error.noTransition);
  318. }
  319. }
  320. }
  321. else {
  322. module.debug('Modal is already visible');
  323. }
  324. },
  325. hideModal: function(callback, keepDimmed) {
  326. callback = $.isFunction(callback)
  327. ? callback
  328. : function(){}
  329. ;
  330. module.debug('Hiding modal');
  331. settings.onHide.call(element);
  332. if( module.is.animating() || module.is.active() ) {
  333. if(settings.transition && $.fn.transition !== undefined && $module.transition('is supported')) {
  334. module.remove.active();
  335. $module
  336. .transition({
  337. debug : settings.debug,
  338. animation : settings.transition + ' out',
  339. queue : settings.queue,
  340. duration : settings.duration,
  341. useFailSafe : true,
  342. onStart : function() {
  343. if(!module.others.active() && !keepDimmed) {
  344. module.hideDimmer();
  345. }
  346. module.remove.keyboardShortcuts();
  347. },
  348. onComplete : function() {
  349. settings.onHidden.call(element);
  350. module.restore.focus();
  351. callback();
  352. }
  353. })
  354. ;
  355. }
  356. else {
  357. module.error(error.noTransition);
  358. }
  359. }
  360. },
  361. showDimmer: function() {
  362. if($dimmable.dimmer('is animating') || !$dimmable.dimmer('is active') ) {
  363. module.debug('Showing dimmer');
  364. $dimmable.dimmer('show');
  365. }
  366. else {
  367. module.debug('Dimmer already visible');
  368. }
  369. },
  370. hideDimmer: function() {
  371. if( $dimmable.dimmer('is animating') || ($dimmable.dimmer('is active')) ) {
  372. $dimmable.dimmer('hide', function() {
  373. if(settings.transition && $.fn.transition !== undefined && $module.transition('is supported')) {
  374. module.remove.clickaway();
  375. module.remove.screenHeight();
  376. }
  377. });
  378. }
  379. else {
  380. module.debug('Dimmer is not visible cannot hide');
  381. return;
  382. }
  383. },
  384. hideAll: function(callback) {
  385. var
  386. $visibleModals = $allModals.filter('.' + className.active + ', .' + className.animating)
  387. ;
  388. callback = $.isFunction(callback)
  389. ? callback
  390. : function(){}
  391. ;
  392. if( $visibleModals.length > 0 ) {
  393. module.debug('Hiding all visible modals');
  394. module.hideDimmer();
  395. $visibleModals
  396. .modal('hide modal', callback)
  397. ;
  398. }
  399. },
  400. hideOthers: function(callback) {
  401. var
  402. $visibleModals = $otherModals.filter('.' + className.active + ', .' + className.animating)
  403. ;
  404. callback = $.isFunction(callback)
  405. ? callback
  406. : function(){}
  407. ;
  408. if( $visibleModals.length > 0 ) {
  409. module.debug('Hiding other modals', $otherModals);
  410. $visibleModals
  411. .modal('hide modal', callback, true)
  412. ;
  413. }
  414. },
  415. others: {
  416. active: function() {
  417. return ($otherModals.filter('.' + className.active).length > 0);
  418. },
  419. animating: function() {
  420. return ($otherModals.filter('.' + className.animating).length > 0);
  421. }
  422. },
  423. add: {
  424. keyboardShortcuts: function() {
  425. module.verbose('Adding keyboard shortcuts');
  426. $document
  427. .on('keyup' + eventNamespace, module.event.keyboard)
  428. ;
  429. }
  430. },
  431. save: {
  432. focus: function() {
  433. $focusedElement = $(document.activeElement).blur();
  434. }
  435. },
  436. restore: {
  437. focus: function() {
  438. if($focusedElement && $focusedElement.length > 0) {
  439. $focusedElement.focus();
  440. }
  441. }
  442. },
  443. remove: {
  444. active: function() {
  445. $module.removeClass(className.active);
  446. },
  447. clickaway: function() {
  448. if(settings.closable) {
  449. $dimmer
  450. .off('click' + elementNamespace)
  451. ;
  452. }
  453. },
  454. screenHeight: function() {
  455. if(module.cache.height > module.cache.pageHeight) {
  456. module.debug('Removing page height');
  457. $body
  458. .css('height', '')
  459. ;
  460. }
  461. },
  462. keyboardShortcuts: function() {
  463. module.verbose('Removing keyboard shortcuts');
  464. $document
  465. .off('keyup' + eventNamespace)
  466. ;
  467. },
  468. scrolling: function() {
  469. $dimmable.removeClass(className.scrolling);
  470. $module.removeClass(className.scrolling);
  471. }
  472. },
  473. cacheSizes: function() {
  474. var
  475. modalHeight = $module.outerHeight()
  476. ;
  477. if(module.cache === undefined || modalHeight !== 0) {
  478. module.cache = {
  479. pageHeight : $(document).outerHeight(),
  480. height : modalHeight + settings.offset,
  481. contextHeight : (settings.context == 'body')
  482. ? $(window).height()
  483. : $dimmable.height()
  484. };
  485. }
  486. module.debug('Caching modal and container sizes', module.cache);
  487. },
  488. can: {
  489. fit: function() {
  490. return ( ( module.cache.height + (settings.padding * 2) ) < module.cache.contextHeight);
  491. }
  492. },
  493. is: {
  494. active: function() {
  495. return $module.hasClass(className.active);
  496. },
  497. animating: function() {
  498. return $module.transition('is supported')
  499. ? $module.transition('is animating')
  500. : $module.is(':visible')
  501. ;
  502. },
  503. scrolling: function() {
  504. return $dimmable.hasClass(className.scrolling);
  505. },
  506. modernBrowser: function() {
  507. // appName for IE11 reports 'Netscape' can no longer use
  508. return !(window.ActiveXObject || "ActiveXObject" in window);
  509. }
  510. },
  511. set: {
  512. autofocus: function() {
  513. if(settings.autofocus) {
  514. var
  515. $inputs = $module.filter(':input').filter(':visible'),
  516. $autofocus = $inputs.filter('[autofocus]'),
  517. $input = ($autofocus.length > 0)
  518. ? $autofocus.first()
  519. : $inputs.first()
  520. ;
  521. if($input.length > 0) {
  522. $input.focus();
  523. }
  524. }
  525. },
  526. clickaway: function() {
  527. if(settings.closable) {
  528. $dimmer
  529. .on('click' + elementNamespace, module.event.click)
  530. ;
  531. }
  532. },
  533. screenHeight: function() {
  534. if( module.can.fit() ) {
  535. $body.css('height', '');
  536. }
  537. else {
  538. module.debug('Modal is taller than page content, resizing page height');
  539. $body
  540. .css('height', module.cache.height + (settings.padding * 2) )
  541. ;
  542. }
  543. },
  544. active: function() {
  545. $module.addClass(className.active);
  546. },
  547. scrolling: function() {
  548. $dimmable.addClass(className.scrolling);
  549. $module.addClass(className.scrolling);
  550. },
  551. type: function() {
  552. if(module.can.fit()) {
  553. module.verbose('Modal fits on screen');
  554. if(!module.others.active() && !module.others.animating()) {
  555. module.remove.scrolling();
  556. }
  557. }
  558. else {
  559. module.verbose('Modal cannot fit on screen setting to scrolling');
  560. module.set.scrolling();
  561. }
  562. },
  563. position: function() {
  564. module.verbose('Centering modal on page', module.cache);
  565. if(module.can.fit()) {
  566. $module
  567. .css({
  568. top: '',
  569. marginTop: -(module.cache.height / 2)
  570. })
  571. ;
  572. }
  573. else {
  574. $module
  575. .css({
  576. marginTop : '',
  577. top : $document.scrollTop()
  578. })
  579. ;
  580. }
  581. },
  582. undetached: function() {
  583. $dimmable.addClass(className.undetached);
  584. }
  585. },
  586. setting: function(name, value) {
  587. module.debug('Changing setting', name, value);
  588. if( $.isPlainObject(name) ) {
  589. $.extend(true, settings, name);
  590. }
  591. else if(value !== undefined) {
  592. settings[name] = value;
  593. }
  594. else {
  595. return settings[name];
  596. }
  597. },
  598. internal: function(name, value) {
  599. if( $.isPlainObject(name) ) {
  600. $.extend(true, module, name);
  601. }
  602. else if(value !== undefined) {
  603. module[name] = value;
  604. }
  605. else {
  606. return module[name];
  607. }
  608. },
  609. debug: function() {
  610. if(settings.debug) {
  611. if(settings.performance) {
  612. module.performance.log(arguments);
  613. }
  614. else {
  615. module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
  616. module.debug.apply(console, arguments);
  617. }
  618. }
  619. },
  620. verbose: function() {
  621. if(settings.verbose && settings.debug) {
  622. if(settings.performance) {
  623. module.performance.log(arguments);
  624. }
  625. else {
  626. module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
  627. module.verbose.apply(console, arguments);
  628. }
  629. }
  630. },
  631. error: function() {
  632. module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
  633. module.error.apply(console, arguments);
  634. },
  635. performance: {
  636. log: function(message) {
  637. var
  638. currentTime,
  639. executionTime,
  640. previousTime
  641. ;
  642. if(settings.performance) {
  643. currentTime = new Date().getTime();
  644. previousTime = time || currentTime;
  645. executionTime = currentTime - previousTime;
  646. time = currentTime;
  647. performance.push({
  648. 'Name' : message[0],
  649. 'Arguments' : [].slice.call(message, 1) || '',
  650. 'Element' : element,
  651. 'Execution Time' : executionTime
  652. });
  653. }
  654. clearTimeout(module.performance.timer);
  655. module.performance.timer = setTimeout(module.performance.display, 500);
  656. },
  657. display: function() {
  658. var
  659. title = settings.name + ':',
  660. totalTime = 0
  661. ;
  662. time = false;
  663. clearTimeout(module.performance.timer);
  664. $.each(performance, function(index, data) {
  665. totalTime += data['Execution Time'];
  666. });
  667. title += ' ' + totalTime + 'ms';
  668. if(moduleSelector) {
  669. title += ' \'' + moduleSelector + '\'';
  670. }
  671. if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
  672. console.groupCollapsed(title);
  673. if(console.table) {
  674. console.table(performance);
  675. }
  676. else {
  677. $.each(performance, function(index, data) {
  678. console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
  679. });
  680. }
  681. console.groupEnd();
  682. }
  683. performance = [];
  684. }
  685. },
  686. invoke: function(query, passedArguments, context) {
  687. var
  688. object = instance,
  689. maxDepth,
  690. found,
  691. response
  692. ;
  693. passedArguments = passedArguments || queryArguments;
  694. context = element || context;
  695. if(typeof query == 'string' && object !== undefined) {
  696. query = query.split(/[\. ]/);
  697. maxDepth = query.length - 1;
  698. $.each(query, function(depth, value) {
  699. var camelCaseValue = (depth != maxDepth)
  700. ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
  701. : query
  702. ;
  703. if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
  704. object = object[camelCaseValue];
  705. }
  706. else if( object[camelCaseValue] !== undefined ) {
  707. found = object[camelCaseValue];
  708. return false;
  709. }
  710. else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
  711. object = object[value];
  712. }
  713. else if( object[value] !== undefined ) {
  714. found = object[value];
  715. return false;
  716. }
  717. else {
  718. return false;
  719. }
  720. });
  721. }
  722. if ( $.isFunction( found ) ) {
  723. response = found.apply(context, passedArguments);
  724. }
  725. else if(found !== undefined) {
  726. response = found;
  727. }
  728. if($.isArray(returnedValue)) {
  729. returnedValue.push(response);
  730. }
  731. else if(returnedValue !== undefined) {
  732. returnedValue = [returnedValue, response];
  733. }
  734. else if(response !== undefined) {
  735. returnedValue = response;
  736. }
  737. return found;
  738. }
  739. };
  740. if(methodInvoked) {
  741. if(instance === undefined) {
  742. module.initialize();
  743. }
  744. module.invoke(query);
  745. }
  746. else {
  747. if(instance !== undefined) {
  748. instance.invoke('destroy');
  749. }
  750. module.initialize();
  751. }
  752. })
  753. ;
  754. return (returnedValue !== undefined)
  755. ? returnedValue
  756. : this
  757. ;
  758. };
  759. $.fn.modal.settings = {
  760. name : 'Modal',
  761. namespace : 'modal',
  762. debug : false,
  763. verbose : false,
  764. performance : true,
  765. observeChanges : false,
  766. allowMultiple : false,
  767. detachable : true,
  768. closable : true,
  769. autofocus : true,
  770. inverted : false,
  771. blurring : false,
  772. dimmerSettings : {
  773. closable : false,
  774. useCSS : true
  775. },
  776. context : 'body',
  777. queue : false,
  778. duration : 500,
  779. offset : 0,
  780. transition : 'scale',
  781. // padding with edge of page
  782. padding : 50,
  783. // called before show animation
  784. onShow : function(){},
  785. // called after show animation
  786. onVisible : function(){},
  787. // called before hide animation
  788. onHide : function(){},
  789. // called after hide animation
  790. onHidden : function(){},
  791. // called after approve selector match
  792. onApprove : function(){ return true; },
  793. // called after deny selector match
  794. onDeny : function(){ return true; },
  795. selector : {
  796. close : '.close',
  797. approve : '.actions .positive, .actions .approve, .actions .ok',
  798. deny : '.actions .negative, .actions .deny, .actions .cancel',
  799. modal : '.ui.modal'
  800. },
  801. error : {
  802. dimmer : 'UI Dimmer, a required component is not included in this page',
  803. method : 'The method you called is not defined.',
  804. notFound : 'The element you specified could not be found'
  805. },
  806. className : {
  807. active : 'active',
  808. animating : 'animating',
  809. blurring : 'blurring',
  810. scrolling : 'scrolling',
  811. undetached : 'undetached'
  812. }
  813. };
  814. })( jQuery, window , document );