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.

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