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.

862 lines
26 KiB

  1. /*
  2. * # Semantic - Popup
  3. * http://github.com/jlukic/semantic-ui/
  4. *
  5. *
  6. * Copyright 2013 Contributors
  7. * Released under the MIT license
  8. * http://opensource.org/licenses/MIT
  9. *
  10. */
  11. ;(function ($, window, document, undefined) {
  12. $.fn.popup = function(parameters) {
  13. var
  14. $allModules = $(this),
  15. $document = $(document),
  16. moduleSelector = $allModules.selector || '',
  17. time = new Date().getTime(),
  18. performance = [],
  19. query = arguments[0],
  20. methodInvoked = (typeof query == 'string'),
  21. queryArguments = [].slice.call(arguments, 1),
  22. returnedValue
  23. ;
  24. $allModules
  25. .each(function() {
  26. var
  27. settings = ( $.isPlainObject(parameters) )
  28. ? $.extend(true, {}, $.fn.popup.settings, parameters)
  29. : $.extend({}, $.fn.popup.settings),
  30. selector = settings.selector,
  31. className = settings.className,
  32. error = settings.error,
  33. metadata = settings.metadata,
  34. namespace = settings.namespace,
  35. eventNamespace = '.' + settings.namespace,
  36. moduleNamespace = 'module-' + namespace,
  37. $module = $(this),
  38. $context = $(settings.context),
  39. $target = (settings.target)
  40. ? $(settings.target)
  41. : $module,
  42. $window = $(window),
  43. $offsetParent = (settings.inline)
  44. ? $target.offsetParent()
  45. : $window,
  46. $popup = (settings.inline)
  47. ? $target.next(settings.selector.popup)
  48. : $window.children(settings.selector.popup).last(),
  49. searchDepth = 0,
  50. element = this,
  51. instance = $module.data(moduleNamespace),
  52. module
  53. ;
  54. module = {
  55. // binds events
  56. initialize: function() {
  57. module.debug('Initializing module', $module);
  58. if(settings.on == 'click') {
  59. $module
  60. .on('click', module.toggle)
  61. ;
  62. }
  63. else {
  64. $module
  65. .on(module.get.startEvent() + eventNamespace, module.event.start)
  66. .on(module.get.endEvent() + eventNamespace, module.event.end)
  67. ;
  68. }
  69. if(settings.target) {
  70. module.debug('Target set to element', $target);
  71. }
  72. $window
  73. .on('resize' + eventNamespace, module.event.resize)
  74. ;
  75. module.instantiate();
  76. },
  77. instantiate: function() {
  78. module.verbose('Storing instance of module', module);
  79. instance = module;
  80. $module
  81. .data(moduleNamespace, instance)
  82. ;
  83. },
  84. refresh: function() {
  85. if(settings.inline) {
  86. $popup = $target.next(selector.popup);
  87. $offsetParent = $target.offsetParent();
  88. }
  89. else {
  90. $popup = $window.children(selector.popup).last();
  91. }
  92. },
  93. destroy: function() {
  94. module.debug('Destroying previous module');
  95. $window
  96. .off(eventNamespace)
  97. ;
  98. $popup
  99. .remove()
  100. ;
  101. $module
  102. .off(eventNamespace)
  103. .removeData(moduleNamespace)
  104. ;
  105. },
  106. event: {
  107. start: function(event) {
  108. module.timer = setTimeout(function() {
  109. if( module.is.hidden() ) {
  110. module.show();
  111. }
  112. }, settings.delay);
  113. },
  114. end: function() {
  115. clearTimeout(module.timer);
  116. if( module.is.visible() ) {
  117. module.hide();
  118. }
  119. },
  120. resize: function() {
  121. if( module.is.visible() ) {
  122. module.set.position();
  123. }
  124. }
  125. },
  126. // generates popup html from metadata
  127. create: function() {
  128. module.debug('Creating pop-up html');
  129. var
  130. html = $module.data(metadata.html) || settings.html,
  131. variation = $module.data(metadata.variation) || settings.variation,
  132. title = $module.data(metadata.title) || settings.title,
  133. content = $module.data(metadata.content) || $module.attr('title') || settings.content
  134. ;
  135. if(html || content || title) {
  136. if(!html) {
  137. html = settings.template({
  138. title : title,
  139. content : content
  140. });
  141. }
  142. $popup = $('<div/>')
  143. .addClass(className.popup)
  144. .addClass(variation)
  145. .html(html)
  146. ;
  147. if(settings.inline) {
  148. module.verbose('Inserting popup element inline', $popup);
  149. $popup
  150. .insertAfter($module)
  151. ;
  152. }
  153. else {
  154. module.verbose('Appending popup element to body', $popup);
  155. $popup
  156. .appendTo( $context )
  157. ;
  158. }
  159. $.proxy(settings.onCreate, $popup)();
  160. }
  161. else {
  162. module.error(error.content);
  163. }
  164. },
  165. // determines popup state
  166. toggle: function() {
  167. module.debug('Toggling pop-up');
  168. if( module.is.hidden() ) {
  169. module.debug('Popup is hidden, showing pop-up');
  170. module.unbind.close();
  171. module.hideAll();
  172. module.show();
  173. }
  174. else {
  175. module.debug('Popup is visible, hiding pop-up');
  176. module.hide();
  177. }
  178. },
  179. show: function(callback) {
  180. callback = callback || function(){};
  181. module.debug('Showing pop-up', settings.transition);
  182. if(!settings.preserve) {
  183. module.refresh();
  184. }
  185. if( !module.exists() ) {
  186. module.create();
  187. }
  188. if( module.set.position() ) {
  189. module.save.conditions();
  190. module.animate.show(callback);
  191. }
  192. },
  193. hide: function(callback) {
  194. callback = callback || function(){};
  195. $module
  196. .removeClass(className.visible)
  197. ;
  198. module.unbind.close();
  199. if( module.is.visible() ) {
  200. module.restore.conditions();
  201. module.animate.hide(callback);
  202. }
  203. },
  204. hideAll: function() {
  205. $(selector.popup)
  206. .filter(':visible')
  207. .popup('hide')
  208. ;
  209. },
  210. hideGracefully: function(event) {
  211. // don't close on clicks inside popup
  212. if(event && $(event.target).closest(selector.popup).size() === 0) {
  213. module.debug('Click occurred outside popup hiding popup');
  214. module.hide();
  215. }
  216. else {
  217. module.debug('Click was inside popup, keeping popup open');
  218. }
  219. },
  220. exists: function() {
  221. if(settings.inline) {
  222. return ( $popup.size() !== 0 );
  223. }
  224. else {
  225. return ( $popup.parent($context).size() );
  226. }
  227. },
  228. remove: function() {
  229. module.debug('Removing popup');
  230. $popup
  231. .remove()
  232. ;
  233. },
  234. save: {
  235. conditions: function() {
  236. module.cache = {
  237. title: $module.attr('title')
  238. };
  239. if (module.cache.title) {
  240. $module.removeAttr('title');
  241. }
  242. module.verbose('Saving original attributes', module.cache.title);
  243. }
  244. },
  245. restore: {
  246. conditions: function() {
  247. if(module.cache && module.cache.title) {
  248. $module.attr('title', module.cache.title);
  249. }
  250. module.verbose('Restoring original attributes', module.cache.title);
  251. return true;
  252. }
  253. },
  254. animate: {
  255. show: function(callback) {
  256. callback = callback || function(){};
  257. $module
  258. .addClass(className.visible)
  259. ;
  260. if(settings.transition && $.fn.transition !== undefined && $module.transition('is supported')) {
  261. $popup
  262. .transition(settings.transition + ' in', settings.duration, function() {
  263. module.bind.close();
  264. $.proxy(callback, element)();
  265. })
  266. ;
  267. }
  268. else {
  269. $popup
  270. .stop()
  271. .fadeIn(settings.duration, settings.easing, function() {
  272. module.bind.close();
  273. $.proxy(callback, element)();
  274. })
  275. ;
  276. }
  277. $.proxy(settings.onShow, element)();
  278. },
  279. hide: function(callback) {
  280. callback = callback || function(){};
  281. module.debug('Hiding pop-up');
  282. if(settings.transition && $.fn.transition !== undefined && $module.transition('is supported')) {
  283. $popup
  284. .transition(settings.transition + ' out', settings.duration, function() {
  285. module.reset();
  286. callback();
  287. })
  288. ;
  289. }
  290. else {
  291. $popup
  292. .stop()
  293. .fadeOut(settings.duration, settings.easing, function() {
  294. module.reset();
  295. callback();
  296. })
  297. ;
  298. }
  299. $.proxy(settings.onHide, element)();
  300. }
  301. },
  302. get: {
  303. startEvent: function() {
  304. if(settings.on == 'hover') {
  305. return 'mouseenter';
  306. }
  307. else if(settings.on == 'focus') {
  308. return 'focus';
  309. }
  310. },
  311. endEvent: function() {
  312. if(settings.on == 'hover') {
  313. return 'mouseleave';
  314. }
  315. else if(settings.on == 'focus') {
  316. return 'blur';
  317. }
  318. },
  319. offstagePosition: function() {
  320. var
  321. boundary = {
  322. top : $(window).scrollTop(),
  323. bottom : $(window).scrollTop() + $(window).height(),
  324. left : 0,
  325. right : $(window).width()
  326. },
  327. popup = {
  328. width : $popup.width(),
  329. height : $popup.outerHeight(),
  330. position : $popup.offset()
  331. },
  332. offstage = {},
  333. offstagePositions = []
  334. ;
  335. if(popup.position) {
  336. offstage = {
  337. top : (popup.position.top < boundary.top),
  338. bottom : (popup.position.top + popup.height > boundary.bottom),
  339. right : (popup.position.left + popup.width > boundary.right),
  340. left : (popup.position.left < boundary.left)
  341. };
  342. }
  343. module.verbose('Checking if outside viewable area', popup.position);
  344. // return only boundaries that have been surpassed
  345. $.each(offstage, function(direction, isOffstage) {
  346. if(isOffstage) {
  347. offstagePositions.push(direction);
  348. }
  349. });
  350. return (offstagePositions.length > 0)
  351. ? offstagePositions.join(' ')
  352. : false
  353. ;
  354. },
  355. nextPosition: function(position) {
  356. switch(position) {
  357. case 'top left':
  358. position = 'bottom left';
  359. break;
  360. case 'bottom left':
  361. position = 'top right';
  362. break;
  363. case 'top right':
  364. position = 'bottom right';
  365. break;
  366. case 'bottom right':
  367. position = 'top center';
  368. break;
  369. case 'top center':
  370. position = 'bottom center';
  371. break;
  372. case 'bottom center':
  373. position = 'right center';
  374. break;
  375. case 'right center':
  376. position = 'left center';
  377. break;
  378. case 'left center':
  379. position = 'top center';
  380. break;
  381. }
  382. return position;
  383. }
  384. },
  385. set: {
  386. position: function(position, arrowOffset) {
  387. var
  388. windowWidth = $(window).width(),
  389. windowHeight = $(window).height(),
  390. width = $target.outerWidth(),
  391. height = $target.outerHeight(),
  392. popupWidth = $popup.width(),
  393. popupHeight = $popup.outerHeight(),
  394. parentWidth = $offsetParent.outerWidth(),
  395. parentHeight = $offsetParent.outerHeight(),
  396. distanceAway = settings.distanceAway,
  397. offset = (settings.inline)
  398. ? $target.position()
  399. : $target.offset(),
  400. positioning,
  401. offstagePosition
  402. ;
  403. position = position || $module.data(metadata.position) || settings.position;
  404. arrowOffset = arrowOffset || $module.data(metadata.offset) || settings.offset;
  405. // adjust for margin when inline
  406. if(settings.inline) {
  407. if(position == 'left center' || position == 'right center') {
  408. arrowOffset += parseInt( window.getComputedStyle(element).getPropertyValue('margin-top'), 10);
  409. distanceAway += -parseInt( window.getComputedStyle(element).getPropertyValue('margin-left'), 10);
  410. }
  411. else {
  412. arrowOffset += parseInt( window.getComputedStyle(element).getPropertyValue('margin-left'), 10);
  413. distanceAway += parseInt( window.getComputedStyle(element).getPropertyValue('margin-top'), 10);
  414. }
  415. }
  416. module.debug('Calculating offset for position', position);
  417. switch(position) {
  418. case 'top left':
  419. positioning = {
  420. bottom : parentHeight - offset.top + distanceAway,
  421. right : parentWidth - offset.left - arrowOffset,
  422. top : 'auto',
  423. left : 'auto'
  424. };
  425. break;
  426. case 'top center':
  427. positioning = {
  428. bottom : parentHeight - offset.top + distanceAway,
  429. left : offset.left + (width / 2) - (popupWidth / 2) + arrowOffset,
  430. top : 'auto',
  431. right : 'auto'
  432. };
  433. break;
  434. case 'top right':
  435. positioning = {
  436. top : 'auto',
  437. bottom : parentHeight - offset.top + distanceAway,
  438. left : offset.left + width + arrowOffset,
  439. right : 'auto'
  440. };
  441. break;
  442. case 'left center':
  443. positioning = {
  444. top : offset.top + (height / 2) - (popupHeight / 2) + arrowOffset,
  445. right : parentWidth - offset.left + distanceAway,
  446. left : 'auto',
  447. bottom : 'auto'
  448. };
  449. break;
  450. case 'right center':
  451. positioning = {
  452. top : offset.top + (height / 2) - (popupHeight / 2) + arrowOffset,
  453. left : offset.left + width + distanceAway,
  454. bottom : 'auto',
  455. right : 'auto'
  456. };
  457. break;
  458. case 'bottom left':
  459. positioning = {
  460. top : offset.top + height + distanceAway,
  461. right : parentWidth - offset.left - arrowOffset,
  462. left : 'auto',
  463. bottom : 'auto'
  464. };
  465. break;
  466. case 'bottom center':
  467. positioning = {
  468. top : offset.top + height + distanceAway,
  469. left : offset.left + (width / 2) - (popupWidth / 2) + arrowOffset,
  470. bottom : 'auto',
  471. right : 'auto'
  472. };
  473. break;
  474. case 'bottom right':
  475. positioning = {
  476. top : offset.top + height + distanceAway,
  477. left : offset.left + width + arrowOffset,
  478. bottom : 'auto',
  479. right : 'auto'
  480. };
  481. break;
  482. }
  483. // tentatively place on stage
  484. $popup
  485. .css(positioning)
  486. .removeClass(className.position)
  487. .addClass(position)
  488. .addClass(className.loading)
  489. ;
  490. // check if is offstage
  491. offstagePosition = module.get.offstagePosition();
  492. // recursively find new positioning
  493. if(offstagePosition) {
  494. module.debug('Element is outside boundaries', offstagePosition);
  495. if(searchDepth < settings.maxSearchDepth) {
  496. position = module.get.nextPosition(position);
  497. searchDepth++;
  498. module.debug('Trying new position', position);
  499. return module.set.position(position);
  500. }
  501. else {
  502. module.error(error.recursion);
  503. searchDepth = 0;
  504. module.reset();
  505. $popup.removeClass(className.loading);
  506. return false;
  507. }
  508. }
  509. else {
  510. module.debug('Position is on stage', position);
  511. searchDepth = 0;
  512. $popup.removeClass(className.loading);
  513. return true;
  514. }
  515. }
  516. },
  517. bind: {
  518. close:function() {
  519. if(settings.on == 'click' && settings.closable) {
  520. module.verbose('Binding popup close event to document');
  521. $document
  522. .on('click' + eventNamespace, function(event) {
  523. module.verbose('Pop-up clickaway intent detected');
  524. $.proxy(module.hideGracefully, this)(event);
  525. })
  526. ;
  527. }
  528. }
  529. },
  530. unbind: {
  531. close: function() {
  532. if(settings.on == 'click' && settings.closable) {
  533. module.verbose('Removing close event from document');
  534. $document
  535. .off('click' + eventNamespace)
  536. ;
  537. }
  538. }
  539. },
  540. is: {
  541. animating: function() {
  542. return ( $popup.is(':animated') || $popup.hasClass(className.animating) );
  543. },
  544. visible: function() {
  545. return $popup.is(':visible');
  546. },
  547. hidden: function() {
  548. return !module.is.visible();
  549. }
  550. },
  551. reset: function() {
  552. $popup
  553. .removeClass(className.visible)
  554. ;
  555. if(settings.preserve) {
  556. if($.fn.transition !== undefined) {
  557. $popup
  558. .transition('remove transition')
  559. ;
  560. }
  561. }
  562. else {
  563. module.remove();
  564. }
  565. },
  566. setting: function(name, value) {
  567. if( $.isPlainObject(name) ) {
  568. $.extend(true, settings, name);
  569. }
  570. else if(value !== undefined) {
  571. settings[name] = value;
  572. }
  573. else {
  574. return settings[name];
  575. }
  576. },
  577. internal: function(name, value) {
  578. if( $.isPlainObject(name) ) {
  579. $.extend(true, module, name);
  580. }
  581. else if(value !== undefined) {
  582. module[name] = value;
  583. }
  584. else {
  585. return module[name];
  586. }
  587. },
  588. debug: function() {
  589. if(settings.debug) {
  590. if(settings.performance) {
  591. module.performance.log(arguments);
  592. }
  593. else {
  594. module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
  595. module.debug.apply(console, arguments);
  596. }
  597. }
  598. },
  599. verbose: function() {
  600. if(settings.verbose && settings.debug) {
  601. if(settings.performance) {
  602. module.performance.log(arguments);
  603. }
  604. else {
  605. module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
  606. module.verbose.apply(console, arguments);
  607. }
  608. }
  609. },
  610. error: function() {
  611. module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
  612. module.error.apply(console, arguments);
  613. },
  614. performance: {
  615. log: function(message) {
  616. var
  617. currentTime,
  618. executionTime,
  619. previousTime
  620. ;
  621. if(settings.performance) {
  622. currentTime = new Date().getTime();
  623. previousTime = time || currentTime;
  624. executionTime = currentTime - previousTime;
  625. time = currentTime;
  626. performance.push({
  627. 'Element' : element,
  628. 'Name' : message[0],
  629. 'Arguments' : [].slice.call(message, 1) || '',
  630. 'Execution Time' : executionTime
  631. });
  632. }
  633. clearTimeout(module.performance.timer);
  634. module.performance.timer = setTimeout(module.performance.display, 100);
  635. },
  636. display: function() {
  637. var
  638. title = settings.name + ':',
  639. totalTime = 0
  640. ;
  641. time = false;
  642. clearTimeout(module.performance.timer);
  643. $.each(performance, function(index, data) {
  644. totalTime += data['Execution Time'];
  645. });
  646. title += ' ' + totalTime + 'ms';
  647. if(moduleSelector) {
  648. title += ' \'' + moduleSelector + '\'';
  649. }
  650. if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
  651. console.groupCollapsed(title);
  652. if(console.table) {
  653. console.table(performance);
  654. }
  655. else {
  656. $.each(performance, function(index, data) {
  657. console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
  658. });
  659. }
  660. console.groupEnd();
  661. }
  662. performance = [];
  663. }
  664. },
  665. invoke: function(query, passedArguments, context) {
  666. var
  667. object = instance,
  668. maxDepth,
  669. found,
  670. response
  671. ;
  672. passedArguments = passedArguments || queryArguments;
  673. context = element || context;
  674. if(typeof query == 'string' && object !== undefined) {
  675. query = query.split(/[\. ]/);
  676. maxDepth = query.length - 1;
  677. $.each(query, function(depth, value) {
  678. var camelCaseValue = (depth != maxDepth)
  679. ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
  680. : query
  681. ;
  682. if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
  683. object = object[camelCaseValue];
  684. }
  685. else if( object[camelCaseValue] !== undefined ) {
  686. found = object[camelCaseValue];
  687. return false;
  688. }
  689. else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
  690. object = object[value];
  691. }
  692. else if( object[value] !== undefined ) {
  693. found = object[value];
  694. return false;
  695. }
  696. else {
  697. return false;
  698. }
  699. });
  700. }
  701. if ( $.isFunction( found ) ) {
  702. response = found.apply(context, passedArguments);
  703. }
  704. else if(found !== undefined) {
  705. response = found;
  706. }
  707. if($.isArray(returnedValue)) {
  708. returnedValue.push(response);
  709. }
  710. else if(returnedValue !== undefined) {
  711. returnedValue = [returnedValue, response];
  712. }
  713. else if(response !== undefined) {
  714. returnedValue = response;
  715. }
  716. return found;
  717. }
  718. };
  719. if(methodInvoked) {
  720. if(instance === undefined) {
  721. module.initialize();
  722. }
  723. module.invoke(query);
  724. }
  725. else {
  726. if(instance !== undefined) {
  727. module.destroy();
  728. }
  729. module.initialize();
  730. }
  731. })
  732. ;
  733. return (returnedValue !== undefined)
  734. ? returnedValue
  735. : this
  736. ;
  737. };
  738. $.fn.popup.settings = {
  739. name : 'Popup',
  740. debug : true,
  741. verbose : true,
  742. performance : true,
  743. namespace : 'popup',
  744. onCreate : function(){},
  745. onShow : function(){},
  746. onHide : function(){},
  747. variation : '',
  748. content : false,
  749. html : false,
  750. title : false,
  751. on : 'hover',
  752. target : false,
  753. closable : true,
  754. context : 'body',
  755. position : 'top center',
  756. delay : 150,
  757. inline : false,
  758. preserve : false,
  759. duration : 250,
  760. easing : 'easeOutQuint',
  761. transition : 'scale',
  762. distanceAway : 0,
  763. offset : 0,
  764. maxSearchDepth : 10,
  765. error: {
  766. content : 'Your popup has no content specified',
  767. method : 'The method you called is not defined.',
  768. recursion : 'Popup attempted to reposition element to fit, but could not find an adequate position.'
  769. },
  770. metadata: {
  771. content : 'content',
  772. html : 'html',
  773. offset : 'offset',
  774. position : 'position',
  775. title : 'title',
  776. variation : 'variation'
  777. },
  778. className : {
  779. animating : 'animating',
  780. loading : 'loading',
  781. popup : 'ui popup',
  782. position : 'top left center bottom right',
  783. visible : 'visible'
  784. },
  785. selector : {
  786. popup : '.ui.popup'
  787. },
  788. template: function(text) {
  789. var html = '';
  790. if(typeof text !== undefined) {
  791. if(typeof text.title !== undefined && text.title) {
  792. html += '<div class="header">' + text.title + '</div class="header">';
  793. }
  794. if(typeof text.content !== undefined && text.content) {
  795. html += '<div class="content">' + text.content + '</div>';
  796. }
  797. }
  798. return html;
  799. }
  800. };
  801. })( jQuery, window , document );