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.

883 lines
27 KiB

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