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.

762 lines
25 KiB

  1. // # UI Modal
  2. // A modal displays content that temporarily blocks interactions with a web site
  3. ;(function ( $, window, document, undefined ) {
  4. $.fn.modal = function(parameters) {
  5. var
  6. // ## Group
  7. // Store a cached version of all elements initialized together
  8. $allModules = $(this),
  9. // Store cached versions of elements which are the same across all instances
  10. $window = $(window),
  11. $document = $(document),
  12. // Save a reference to the selector used for logs
  13. moduleSelector = $allModules.selector || '',
  14. // Store a reference to current time for performance
  15. time = new Date().getTime(),
  16. performance = [],
  17. // Save a reference to arguments to access this 'special variable' outside of scope
  18. query = arguments[0],
  19. methodInvoked = (typeof query == 'string'),
  20. queryArguments = [].slice.call(arguments, 1),
  21. returnedValue
  22. ;
  23. // ## Singular
  24. // Iterate over all elements to initialize module
  25. $allModules
  26. .each(function() {
  27. var
  28. // Extend setting if parameters is a settings object
  29. settings = ( $.isPlainObject(parameters) )
  30. ? $.extend(true, {}, $.fn.modal.settings, parameters)
  31. : $.extend({}, $.fn.modal.settings),
  32. // Shortcut values for common settings
  33. selector = settings.selector,
  34. className = settings.className,
  35. namespace = settings.namespace,
  36. error = settings.error,
  37. // Namespace to store DOM Events
  38. eventNamespace = '.' + namespace,
  39. // Namespace to store instance in metadata
  40. moduleNamespace = 'module-' + namespace,
  41. // Cache DOM elements
  42. $module = $(this),
  43. $context = $(settings.context),
  44. $otherModals = $allModules.not($module),
  45. $close = $module.find(selector.close),
  46. // Set up blank variables for data stored across an instance
  47. $focusedElement,
  48. $dimmable,
  49. $dimmer,
  50. // Save a reference to the 'pure' DOM element node and current defined instance
  51. element = this,
  52. instance = $module.data(moduleNamespace),
  53. module
  54. ;
  55. // ## Module Behavior
  56. module = {
  57. // ### Initialize
  58. // Attaches modal events to page
  59. initialize: function() {
  60. // Debug/Verbose allows for a trace to be passed for the javascript console
  61. module.verbose('Initializing dimmer', $context);
  62. // Make sure all dependencies are available or provide an error
  63. if(typeof $.fn.dimmer === undefined) {
  64. module.error(error.dimmer);
  65. return;
  66. }
  67. // Initialize a dimmer in the current modal context that
  68. // Dimmer appears and hides slightly quicker than modal to avoid race conditions in callbacks
  69. $dimmable = $context
  70. .dimmer({
  71. closable : false,
  72. show : settings.duration * 0.95,
  73. hide : settings.duration * 1.05
  74. })
  75. .dimmer('add content', $module)
  76. ;
  77. // Use Dimmer's Behavior API to retrieve a reference to the dimmer DOM element
  78. $dimmer = $dimmable
  79. .dimmer('get dimmer')
  80. ;
  81. module.verbose('Attaching close events', $close);
  82. // Attach some dimmer event
  83. $close
  84. .on('click' + eventNamespace, module.event.close)
  85. ;
  86. $window
  87. .on('resize', function() {
  88. module.event.debounce(module.refresh, 50);
  89. })
  90. ;
  91. module.instantiate();
  92. },
  93. // Store instance in metadata so it can be retrieved and modified on future invocations
  94. instantiate: function() {
  95. module.verbose('Storing instance of modal');
  96. // Immediately define possibly undefined instance
  97. instance = module;
  98. // Store new reference in metadata
  99. $module
  100. .data(moduleNamespace, instance)
  101. ;
  102. },
  103. // ### Destroy
  104. // Remove all module data from metadata and remove all events
  105. destroy: function() {
  106. module.verbose('Destroying previous modal');
  107. $module
  108. .removeData(moduleNamespace)
  109. .off(eventNamespace)
  110. ;
  111. // Child elements must also have their events removed
  112. $close
  113. .off(eventNamespace)
  114. ;
  115. // Destroy the initialized dimmer
  116. $context
  117. .dimmer('destroy')
  118. ;
  119. },
  120. // ### Refresh
  121. // Modal must modify its behavior depending on whether or not it can fit
  122. refresh: function() {
  123. // Remove scrolling/fixed type
  124. module.remove.scrolling();
  125. // Cache new module size
  126. module.cacheSizes();
  127. // Set type to either scrolling or fixed
  128. module.set.type();
  129. module.set.position();
  130. },
  131. // ### Attach events
  132. // Attaches any module method to another element selector
  133. attachEvents: function(selector, event) {
  134. var
  135. $toggle = $(selector)
  136. ;
  137. // default to toggle event if none specified
  138. event = $.isFunction(module[event])
  139. ? module[event]
  140. : module.toggle
  141. ;
  142. // determine whether element exists
  143. if($toggle.size() > 0) {
  144. module.debug('Attaching modal events to element', selector, event);
  145. // attach events
  146. $toggle
  147. .off(eventNamespace)
  148. .on('click' + eventNamespace, event)
  149. ;
  150. }
  151. else {
  152. module.error(error.notFound);
  153. }
  154. },
  155. // ### Events
  156. // Events contain event handlers where the ``this`` context may be specific to a part of an element
  157. event: {
  158. // ### Event Close
  159. // Make sure appropriate callback occurs before hiding dimmer on close
  160. close: function() {
  161. module.verbose('Closing element pressed');
  162. // Only hide modal if onDeny or onApprove return true
  163. if( $(this).is(selector.approve) ) {
  164. if($.proxy(settings.onApprove, element)()) {
  165. modal.hide();
  166. }
  167. }
  168. if( $(this).is(selector.deny) ) {
  169. if($.proxy(settings.onDeny, element)()) {
  170. modal.hide();
  171. }
  172. }
  173. else {
  174. module.hide();
  175. }
  176. },
  177. // ### Event Click
  178. // Only allow click event on dimmer to close modal if the click was not inside the dimmer
  179. click: function(event) {
  180. module.verbose('Determining if event occured on dimmer', event);
  181. if( $dimmer.find(event.target).size() === 0 ) {
  182. module.hide();
  183. event.stopImmediatePropagation();
  184. }
  185. },
  186. // ### Debounce
  187. // Make sure resize event fires maximum of once every 50ms
  188. debounce: function(method, delay) {
  189. clearTimeout(module.timer);
  190. module.timer = setTimeout(method, delay);
  191. },
  192. // ### Event Keyboard
  193. // Determine whether keydown event was on escape key
  194. keyboard: function(event) {
  195. var
  196. keyCode = event.which,
  197. escapeKey = 27
  198. ;
  199. if(keyCode == escapeKey) {
  200. if(settings.closable) {
  201. module.debug('Escape key pressed hiding modal');
  202. module.hide();
  203. }
  204. else {
  205. module.debug('Escape key pressed, but closable is set to false');
  206. }
  207. event.preventDefault();
  208. }
  209. },
  210. // ### Event Resize
  211. // Only refresh if dimmer is visible
  212. resize: function() {
  213. if( $dimmable.dimmer('is active') ) {
  214. module.refresh();
  215. }
  216. }
  217. },
  218. // ### Toggle
  219. // Change visibility depending on current visibility state
  220. toggle: function() {
  221. if( module.is.active() ) {
  222. module.hide();
  223. }
  224. else {
  225. module.show();
  226. }
  227. },
  228. // ### Show
  229. // Find position then shows dimmer
  230. show: function() {
  231. module.showDimmer();
  232. module.cacheSizes();
  233. module.set.position();
  234. module.hideAll();
  235. // #### Loose Coupling
  236. if(settings.transition && $.fn.transition !== undefined && $module.transition('is supported')) {
  237. // Use dimmer plugin if available
  238. $module
  239. .transition(settings.transition + ' in', settings.duration, module.set.active)
  240. ;
  241. }
  242. else {
  243. // Otherwise use fallback javascript animation
  244. $module
  245. .fadeIn(settings.duration, settings.easing, module.set.active)
  246. ;
  247. }
  248. module.debug('Triggering dimmer');
  249. $.proxy(settings.onShow, element)();
  250. },
  251. // ### Show Dimmer
  252. // Keep as a separate method to allow programmatic access via
  253. // $('.modal').modal('show dimmer');
  254. showDimmer: function() {
  255. module.debug('Showing modal');
  256. $dimmable.dimmer('show');
  257. },
  258. // ### Hide
  259. // Determine whether to hide dimmer, modal, or both
  260. hide: function() {
  261. // Only attach close events if modal is allowed to close
  262. if(settings.closable) {
  263. $dimmer
  264. .off('click' + eventNamespace)
  265. ;
  266. }
  267. // Only hide dimmer once
  268. if( $dimmable.dimmer('is active') ) {
  269. $dimmable.dimmer('hide');
  270. }
  271. // Only hide modal once
  272. if( module.is.active() ) {
  273. module.hideModal();
  274. $.proxy(settings.onHide, element)();
  275. }
  276. else {
  277. module.debug('Cannot hide modal, modal is not visible');
  278. }
  279. },
  280. // ### Hide Dimmer
  281. // Hide dimmer from page
  282. hideDimmer: function() {
  283. module.debug('Hiding dimmer');
  284. $dimmable.dimmer('hide');
  285. },
  286. // ### Hide Modal
  287. // Hide modal from page
  288. hideModal: function() {
  289. module.debug('Hiding modal');
  290. module.remove.keyboardShortcuts();
  291. if(settings.transition && $.fn.transition !== undefined && $module.transition('is supported')) {
  292. $module
  293. .transition(settings.transition + ' out', settings.duration, function() {
  294. module.remove.active();
  295. module.restore.focus();
  296. })
  297. ;
  298. }
  299. else {
  300. $module
  301. .fadeOut(settings.duration, settings.easing, function() {
  302. module.remove.active();
  303. module.restore.focus();
  304. })
  305. ;
  306. }
  307. },
  308. // ### Hide All
  309. // Make sure all other modals are hidden before showing a new one
  310. hideAll: function() {
  311. $otherModals
  312. .filter(':visible')
  313. .modal('hide')
  314. ;
  315. },
  316. // ### Add Keyboard Shorcuts
  317. // Add keyboard shortcut events to page
  318. add: {
  319. keyboardShortcuts: function() {
  320. module.verbose('Adding keyboard shortcuts');
  321. $document
  322. .on('keyup' + eventNamespace, module.event.keyboard)
  323. ;
  324. }
  325. },
  326. save: {
  327. // ### Save Focus
  328. // Save current focused element on modal show
  329. focus: function() {
  330. $focusedElement = $(document.activeElement).blur();
  331. }
  332. },
  333. restore: {
  334. // ### Restore Focus
  335. // Restore focus to previous element after modal is closed
  336. focus: function() {
  337. if($focusedElement && $focusedElement.size() > 0) {
  338. $focusedElement.focus();
  339. }
  340. }
  341. },
  342. // ### Remove
  343. // Utilities for removing and adding classes related to state
  344. remove: {
  345. active: function() {
  346. $module.removeClass(className.active);
  347. },
  348. keyboardShortcuts: function() {
  349. module.verbose('Removing keyboard shortcuts');
  350. $document
  351. .off('keyup' + eventNamespace)
  352. ;
  353. },
  354. scrolling: function() {
  355. $dimmable.removeClass(className.scrolling);
  356. $module.removeClass(className.scrolling);
  357. }
  358. },
  359. // ### Cache Sizes
  360. // Cache context and modal size to avoid recalculation
  361. cacheSizes: function() {
  362. module.cache = {
  363. height : $module.outerHeight() + settings.offset,
  364. contextHeight : (settings.context == 'body')
  365. ? $(window).height()
  366. : $dimmable.height()
  367. };
  368. module.debug('Caching modal and container sizes', module.cache);
  369. },
  370. // ### Can Fit
  371. // Determine whether modal's cached size is smaller than context
  372. can: {
  373. fit: function() {
  374. return (module.cache.height < module.cache.contextHeight);
  375. }
  376. },
  377. // ### Is Active
  378. // Whether current element is active
  379. is: {
  380. active: function() {
  381. return $module.hasClass(className.active);
  382. }
  383. },
  384. set: {
  385. // ### Set Active
  386. active: function() {
  387. // Add escape key to close
  388. module.add.keyboardShortcuts();
  389. // Save reference to current focused element
  390. module.save.focus();
  391. // Set to scrolling or fixed
  392. module.set.type();
  393. // Add active class
  394. $module
  395. .addClass(className.active)
  396. ;
  397. // Add close events
  398. if(settings.closable) {
  399. $dimmer
  400. .on('click' + eventNamespace, module.event.click)
  401. ;
  402. }
  403. },
  404. // ### Set Scrolling
  405. // Add scrolling classes
  406. scrolling: function() {
  407. $dimmable.addClass(className.scrolling);
  408. $module.addClass(className.scrolling);
  409. },
  410. // ### Set Type
  411. // Set type to fixed or scrolling
  412. type: function() {
  413. if(module.can.fit()) {
  414. module.verbose('Modal fits on screen');
  415. module.remove.scrolling();
  416. }
  417. else {
  418. module.verbose('Modal cannot fit on screen setting to scrolling');
  419. module.set.scrolling();
  420. }
  421. },
  422. // ### Set Position
  423. // Position modal in center of page
  424. position: function() {
  425. module.verbose('Centering modal on page', module.cache, module.cache.height / 2);
  426. if(module.can.fit()) {
  427. // If fixed position center vertically
  428. $module
  429. .css({
  430. top: '',
  431. marginTop: -(module.cache.height / 2)
  432. })
  433. ;
  434. }
  435. else {
  436. // If modal is not fixed position place element 1em below current scrolled position in page
  437. $module
  438. .css({
  439. marginTop : '1em',
  440. top : $document.scrollTop()
  441. })
  442. ;
  443. }
  444. }
  445. },
  446. // ### Settings
  447. // Used to modify or read setting(s) after initializing
  448. setting: function(name, value) {
  449. if( $.isPlainObject(name) ) {
  450. $.extend(true, settings, name);
  451. }
  452. else if(value !== undefined) {
  453. settings[name] = value;
  454. }
  455. else {
  456. return settings[name];
  457. }
  458. },
  459. // ### Internal
  460. // Used to modify or read methods from module
  461. internal: function(name, value) {
  462. if( $.isPlainObject(name) ) {
  463. $.extend(true, module, name);
  464. }
  465. else if(value !== undefined) {
  466. module[name] = value;
  467. }
  468. else {
  469. return module[name];
  470. }
  471. },
  472. // ### Debug
  473. // Debug pushes arguments to the console formatted as a debug statement
  474. debug: function() {
  475. if(settings.debug) {
  476. if(settings.performance) {
  477. module.performance.log(arguments);
  478. }
  479. else {
  480. module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
  481. module.debug.apply(console, arguments);
  482. }
  483. }
  484. },
  485. // ### Verbose
  486. // Calling verbose internally allows for additional data to be logged which can assist in debugging
  487. verbose: function() {
  488. if(settings.verbose && settings.debug) {
  489. if(settings.performance) {
  490. module.performance.log(arguments);
  491. }
  492. else {
  493. module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
  494. module.verbose.apply(console, arguments);
  495. }
  496. }
  497. },
  498. // ### Error
  499. // Error allows for the module to report named error messages, it may be useful to modify this to push error messages to the user. Error messages are defined in the modules settings object.
  500. error: function() {
  501. module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
  502. module.error.apply(console, arguments);
  503. },
  504. // ### Performance
  505. // This is called on each debug statement and logs the time since the last debug statement.
  506. performance: {
  507. log: function(message) {
  508. var
  509. currentTime,
  510. executionTime,
  511. previousTime
  512. ;
  513. if(settings.performance) {
  514. currentTime = new Date().getTime();
  515. previousTime = time || currentTime;
  516. executionTime = currentTime - previousTime;
  517. time = currentTime;
  518. performance.push({
  519. 'Element' : element,
  520. 'Name' : message[0],
  521. 'Arguments' : [].slice.call(message, 1) || '',
  522. 'Execution Time' : executionTime
  523. });
  524. }
  525. clearTimeout(module.performance.timer);
  526. module.performance.timer = setTimeout(module.performance.display, 100);
  527. },
  528. display: function() {
  529. var
  530. title = settings.name + ':',
  531. totalTime = 0
  532. ;
  533. time = false;
  534. clearTimeout(module.performance.timer);
  535. $.each(performance, function(index, data) {
  536. totalTime += data['Execution Time'];
  537. });
  538. title += ' ' + totalTime + 'ms';
  539. if(moduleSelector) {
  540. title += ' \'' + moduleSelector + '\'';
  541. }
  542. if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
  543. console.groupCollapsed(title);
  544. if(console.table) {
  545. console.table(performance);
  546. }
  547. else {
  548. $.each(performance, function(index, data) {
  549. console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
  550. });
  551. }
  552. console.groupEnd();
  553. }
  554. performance = [];
  555. }
  556. },
  557. // ### Invoke
  558. // Invoke is used to match internal functions to string lookups.
  559. // `$('.foo').example('invoke', 'set text', 'Foo')`
  560. // Method lookups are lazy, looking for many variations of a search string
  561. // For example 'set active', will look for both `setText : function(){}`, `set: { text: function(){} }`
  562. // Invoke attempts to preserve the 'this' chaining unless a value is returned.
  563. // If multiple values are returned an array of values matching up to the length of the selector is returned
  564. invoke: function(query, passedArguments, context) {
  565. var
  566. maxDepth,
  567. found,
  568. response
  569. ;
  570. passedArguments = passedArguments || queryArguments;
  571. context = element || context;
  572. if(typeof query == 'string' && instance !== undefined) {
  573. query = query.split(/[\. ]/);
  574. maxDepth = query.length - 1;
  575. $.each(query, function(depth, value) {
  576. var camelCaseValue = (depth != maxDepth)
  577. ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
  578. : query
  579. ;
  580. if( $.isPlainObject( instance[value] ) && (depth != maxDepth) ) {
  581. instance = instance[value];
  582. }
  583. else if( $.isPlainObject( instance[camelCaseValue] ) && (depth != maxDepth) ) {
  584. instance = instance[camelCaseValue];
  585. }
  586. else if( instance[value] !== undefined ) {
  587. found = instance[value];
  588. return false;
  589. }
  590. else if( instance[camelCaseValue] !== undefined ) {
  591. found = instance[camelCaseValue];
  592. return false;
  593. }
  594. else {
  595. module.error(error.method, query);
  596. return false;
  597. }
  598. });
  599. }
  600. if ( $.isFunction( found ) ) {
  601. response = found.apply(context, passedArguments);
  602. }
  603. else if(found !== undefined) {
  604. response = found;
  605. }
  606. // ### Invocation response
  607. // If a user passes in multiple elements invoke will be called for each element and the value will be returned in an array
  608. // For example ``$('.things').example('has text')`` with two elements might return ``[true, false]`` and for one element ``true``
  609. if($.isArray(returnedValue)) {
  610. returnedValue.push(response);
  611. }
  612. else if(returnedValue !== undefined) {
  613. returnedValue = [returnedValue, response];
  614. }
  615. else if(response !== undefined) {
  616. returnedValue = response;
  617. }
  618. return found;
  619. }
  620. };
  621. // ## Method Invocation
  622. // This is where the actual action occurs.
  623. // $('.foo').module('set text', 'Ho hum');
  624. // If you call a module with a string parameter you are most likely trying to invoke a function
  625. if(methodInvoked) {
  626. if(instance === undefined) {
  627. module.initialize();
  628. }
  629. module.invoke(query);
  630. }
  631. // if no method call is required we simply initialize the plugin, destroying it if it exists already
  632. else {
  633. if(instance !== undefined) {
  634. module.destroy();
  635. }
  636. module.initialize();
  637. }
  638. })
  639. ;
  640. // ### Chaining
  641. // Return either jQuery chain or the returned value from invoke
  642. return (returnedValue !== undefined)
  643. ? returnedValue
  644. : this
  645. ;
  646. };
  647. // ## Settings
  648. // These are the default configuration settings for any element initialized without parameters
  649. $.fn.modal.settings = {
  650. // Name for debug logs
  651. name : 'Modal',
  652. // Namespace for events and metadata
  653. namespace : 'modal',
  654. // Whether to include all logging
  655. verbose : true,
  656. // Whether to include important logs
  657. debug : true,
  658. // Whether to show performance traces
  659. performance : true,
  660. // Whether modal is able to be closed by a user
  661. closable : true,
  662. // Context which modal will be centered in
  663. context : 'body',
  664. // Animation duration
  665. duration : 500,
  666. // Animation easing
  667. easing : 'easeOutExpo',
  668. // Vertical offset of modal
  669. offset : 0,
  670. // Transition to use for animation
  671. transition : 'scale',
  672. // Callback when modal shows
  673. onShow : function(){},
  674. // Callback when modal hides
  675. onHide : function(){},
  676. // Callback when modal approve action is called
  677. onApprove : function(){ return true },
  678. // Callback when modal deny action is called
  679. onDeny : function(){ return true },
  680. // List of selectors used to match behavior to DOM elements
  681. selector : {
  682. close : '.close, .actions .button',
  683. approve : '.actions .positive, .actions .approve',
  684. deny : '.actions .negative, .actions .cancel'
  685. },
  686. // List of error messages displayable to console
  687. error : {
  688. dimmer : 'UI Dimmer, a required component is not included in this page',
  689. method : 'The method you called is not defined.'
  690. },
  691. // List of class names used to represent element state
  692. className : {
  693. active : 'active',
  694. scrolling : 'scrolling'
  695. },
  696. };
  697. })( jQuery, window , document );