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.

913 lines
28 KiB

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