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.

589 lines
19 KiB

  1. /* ******************************
  2. Tooltip / Popup
  3. Author: Jack Lukic
  4. Notes: First Commit Sep 07, 2012
  5. ****************************** */
  6. ;(function ($, window, document, undefined) {
  7. $.fn.popup = function(parameters) {
  8. var
  9. settings = $.extend(true, {}, $.fn.popup.settings, parameters),
  10. // make arguments available
  11. moduleArguments = arguments || false,
  12. invokedResponse
  13. ;
  14. $(this)
  15. .each(function() {
  16. var
  17. $module = $(this),
  18. $window = $(window),
  19. $offsetParent = $module.offsetParent(),
  20. $popup = (settings.inline)
  21. ? $module.next(settings.selector.popup)
  22. : $window.children(settings.selector.popup).last(),
  23. timer,
  24. recursionDepth = 0,
  25. instance = $module.data('module-' + settings.namespace),
  26. methodInvoked = (instance !== undefined && typeof parameters == 'string'),
  27. selector = settings.selector,
  28. className = settings.className,
  29. errors = settings.errors,
  30. metadata = settings.metadata,
  31. namespace = settings.namespace,
  32. module
  33. ;
  34. module = {
  35. // binds events
  36. initialize: function() {
  37. if(settings.event == 'hover') {
  38. $module
  39. .on('mouseenter.' + namespace, module.event.mouseenter)
  40. .on('mouseleave.' + namespace, module.event.mouseleave)
  41. ;
  42. }
  43. else {
  44. $module
  45. .on(settings.event + '.' + namespace, module.event[settings.event])
  46. ;
  47. }
  48. $window
  49. .on('resize.' + namespace, module.event.resize)
  50. ;
  51. $module
  52. .data('module-' + namespace, module)
  53. ;
  54. },
  55. refresh: function() {
  56. $popup = (settings.inline)
  57. ? $module.next(selector.popup)
  58. : $window.children(selector.popup).last()
  59. ;
  60. $offsetParent = $module.offsetParent();
  61. },
  62. destroy: function() {
  63. module.debug('Destroying existing popups');
  64. $module
  65. .off('.' + namespace)
  66. ;
  67. $popup
  68. .remove()
  69. ;
  70. },
  71. event: {
  72. mouseenter: function(event) {
  73. var element = this;
  74. timer = setTimeout(function() {
  75. $.proxy(module.toggle, element)();
  76. if( $(element).hasClass(className.visible) ) {
  77. event.stopPropagation();
  78. }
  79. }, settings.delay);
  80. },
  81. mouseleave: function(event) {
  82. clearTimeout(timer);
  83. if( $module.is(':visible') ) {
  84. module.hide();
  85. }
  86. },
  87. click: function(event) {
  88. $.proxy(module.toggle, this)();
  89. if( $(this).hasClass(className.visible) ) {
  90. event.stopPropagation();
  91. }
  92. },
  93. resize: function() {
  94. if( $popup.is(':visible') ) {
  95. module.position();
  96. }
  97. }
  98. },
  99. // generates popup html from metadata
  100. create: function() {
  101. module.debug('Creating pop-up content');
  102. var
  103. html = $module.data(metadata.html) || settings.html,
  104. title = $module.data(metadata.title) || settings.title,
  105. content = $module.data(metadata.content) || $module.attr('title') || settings.content
  106. ;
  107. if(html || content || title) {
  108. if(!html) {
  109. html = settings.template({
  110. title : title,
  111. content : content
  112. });
  113. }
  114. $popup = $('<div/>')
  115. .addClass(className.popup)
  116. .html(html)
  117. ;
  118. if(settings.inline) {
  119. $popup
  120. .insertAfter($module)
  121. ;
  122. }
  123. else {
  124. $popup
  125. .appendTo( $('body') )
  126. ;
  127. }
  128. }
  129. else {
  130. module.error(errors.content);
  131. }
  132. },
  133. remove: function() {
  134. $popup
  135. .remove()
  136. ;
  137. },
  138. get: {
  139. offstagePosition: function() {
  140. var
  141. boundary = {
  142. top : $(window).scrollTop(),
  143. bottom : $(window).scrollTop() + $(window).height(),
  144. left : 0,
  145. right : $(window).width()
  146. },
  147. popup = {
  148. width : $popup.outerWidth(),
  149. height : $popup.outerHeight(),
  150. position : $popup.offset()
  151. },
  152. offstage = {},
  153. offstagePositions = []
  154. ;
  155. if(popup.position) {
  156. offstage = {
  157. top : (popup.position.top < boundary.top),
  158. bottom : (popup.position.top + popup.height > boundary.bottom),
  159. right : (popup.position.left + popup.width > boundary.right),
  160. left : (popup.position.left < boundary.left)
  161. };
  162. }
  163. // return only boundaries that have been surpassed
  164. $.each(offstage, function(direction, isOffstage) {
  165. if(isOffstage) {
  166. offstagePositions.push(direction);
  167. }
  168. });
  169. return (offstagePositions.length > 0)
  170. ? offstagePositions.join(' ')
  171. : false
  172. ;
  173. },
  174. nextPosition: function(position) {
  175. switch(position) {
  176. case 'top left':
  177. position = 'bottom left';
  178. break;
  179. case 'bottom left':
  180. position = 'top right';
  181. break;
  182. case 'top right':
  183. position = 'bottom right';
  184. break;
  185. case 'bottom right':
  186. position = 'top center';
  187. break;
  188. case 'top center':
  189. position = 'bottom center';
  190. break;
  191. case 'bottom center':
  192. position = 'right center';
  193. break;
  194. case 'right center':
  195. position = 'left center';
  196. break;
  197. case 'left center':
  198. position = 'top center';
  199. break;
  200. }
  201. return position;
  202. }
  203. },
  204. // determines popup state
  205. toggle: function() {
  206. $module = $(this);
  207. module.debug('Toggling pop-up');
  208. // refresh state of module
  209. module.refresh();
  210. if($popup.size() === 0) {
  211. module.create();
  212. }
  213. if( !$module.hasClass(className.visible) ) {
  214. if( module.position() ) {
  215. module.show();
  216. }
  217. }
  218. else {
  219. module.hide();
  220. }
  221. },
  222. position: function(position, arrowOffset) {
  223. var
  224. windowWidth = $(window).width(),
  225. windowHeight = $(window).height(),
  226. width = $module.outerWidth(),
  227. height = $module.outerHeight(),
  228. popupWidth = $popup.outerWidth(),
  229. popupHeight = $popup.outerHeight(),
  230. offset = (settings.inline)
  231. ? $module.position()
  232. : $module.offset(),
  233. parentWidth = (settings.inline)
  234. ? $offsetParent.outerWidth()
  235. : $window.outerWidth(),
  236. parentHeight = (settings.inline)
  237. ? $offsetParent.outerHeight()
  238. : $window.outerHeight(),
  239. positioning,
  240. offstagePosition
  241. ;
  242. position = position || $module.data(metadata.position) || settings.position;
  243. arrowOffset = arrowOffset || $module.data(metadata.arrowOffset) || settings.arrowOffset;
  244. module.debug('Calculating offset for position', position);
  245. switch(position) {
  246. case 'top left':
  247. positioning = {
  248. top : 'auto',
  249. bottom : parentHeight - offset.top + settings.distanceAway,
  250. left : offset.left + arrowOffset
  251. };
  252. break;
  253. case 'top center':
  254. positioning = {
  255. bottom : parentHeight - offset.top + settings.distanceAway,
  256. left : offset.left + (width / 2) - (popupWidth / 2) + arrowOffset,
  257. top : 'auto',
  258. right : 'auto'
  259. };
  260. break;
  261. case 'top right':
  262. positioning = {
  263. bottom : parentHeight - offset.top + settings.distanceAway,
  264. right : parentWidth - offset.left - width - arrowOffset,
  265. top : 'auto',
  266. left : 'auto'
  267. };
  268. break;
  269. case 'left center':
  270. positioning = {
  271. top : offset.top + (height / 2) - (popupHeight / 2),
  272. right : parentWidth - offset.left + settings.distanceAway - arrowOffset,
  273. left : 'auto',
  274. bottom : 'auto'
  275. };
  276. break;
  277. case 'right center':
  278. positioning = {
  279. top : offset.top + (height / 2) - (popupHeight / 2),
  280. left : offset.left + width + settings.distanceAway + arrowOffset,
  281. bottom : 'auto',
  282. right : 'auto'
  283. };
  284. break;
  285. case 'bottom left':
  286. positioning = {
  287. top : offset.top + height + settings.distanceAway,
  288. left : offset.left + arrowOffset,
  289. bottom : 'auto',
  290. right : 'auto'
  291. };
  292. break;
  293. case 'bottom center':
  294. positioning = {
  295. top : offset.top + height + settings.distanceAway,
  296. left : offset.left + (width / 2) - (popupWidth / 2) + arrowOffset,
  297. bottom : 'auto',
  298. right : 'auto'
  299. };
  300. break;
  301. case 'bottom right':
  302. positioning = {
  303. top : offset.top + height + settings.distanceAway,
  304. right : parentWidth - offset.left - width - arrowOffset,
  305. left : 'auto',
  306. bottom : 'auto'
  307. };
  308. break;
  309. }
  310. // true width on popup, avoid rounding error
  311. $.extend(positioning, {
  312. width: $popup.width() + 1
  313. });
  314. // tentatively place on stage
  315. $popup
  316. .removeAttr('style')
  317. .removeClass('top right bottom left center')
  318. .css(positioning)
  319. .addClass(position)
  320. .addClass(className.loading)
  321. ;
  322. // check if is offstage
  323. offstagePosition = module.get.offstagePosition();
  324. // recursively find new positioning
  325. if(offstagePosition) {
  326. module.debug('Element is outside boundaries ', offstagePosition);
  327. if(recursionDepth < settings.maxRecursion) {
  328. position = module.get.nextPosition(position);
  329. recursionDepth++;
  330. module.debug('Trying new position: ', position);
  331. return module.position(position);
  332. }
  333. else {
  334. module.error(errors.recursion);
  335. recursionDepth = 0;
  336. return false;
  337. }
  338. }
  339. else {
  340. module.debug('Position is on stage', position);
  341. recursionDepth = 0;
  342. return true;
  343. }
  344. },
  345. show: function() {
  346. module.debug('Showing pop-up');
  347. $(selector.popup)
  348. .filter(':visible')
  349. .stop()
  350. .fadeOut(200)
  351. .prev($module)
  352. .removeClass(className.visible)
  353. ;
  354. $module
  355. .addClass(className.visible)
  356. ;
  357. $popup
  358. .removeClass(className.loading)
  359. ;
  360. if(settings.animation == 'pop' && $.fn.popIn !== undefined) {
  361. $popup
  362. .stop()
  363. .popIn(settings.duration, settings.easing)
  364. ;
  365. }
  366. else {
  367. $popup
  368. .stop()
  369. .fadeIn(settings.duration, settings.easing)
  370. ;
  371. }
  372. if(settings.event == 'click' && settings.clicktoClose) {
  373. module.debug('Binding popup close event');
  374. $(document)
  375. .on('click.' + namespace, module.gracefully.hide)
  376. ;
  377. }
  378. $.proxy(settings.onShow, $popup)();
  379. },
  380. hide: function() {
  381. $module
  382. .removeClass(className.visible)
  383. ;
  384. if($popup.is(':visible') ) {
  385. module.debug('Hiding pop-up');
  386. if(settings.animation == 'pop' && $.fn.popOut !== undefined) {
  387. $popup
  388. .stop()
  389. .popOut(settings.duration, settings.easing, function() {
  390. $popup.hide();
  391. })
  392. ;
  393. }
  394. else {
  395. $popup
  396. .stop()
  397. .fadeOut(settings.duration, settings.easing)
  398. ;
  399. }
  400. }
  401. if(settings.event == 'click' && settings.clicktoClose) {
  402. $(document)
  403. .off('click.' + namespace)
  404. ;
  405. }
  406. $.proxy(settings.onHide, $popup)();
  407. if(!settings.inline) {
  408. module.remove();
  409. }
  410. },
  411. gracefully: {
  412. hide: function(event) {
  413. // don't close on clicks inside popup
  414. if( $(event.target).closest(selector.popup).size() === 0) {
  415. module.hide();
  416. }
  417. }
  418. },
  419. /* standard module */
  420. setting: function(name, value) {
  421. if(value === undefined) {
  422. return settings[name];
  423. }
  424. settings[name] = value;
  425. },
  426. debug: function() {
  427. var
  428. output = [],
  429. message = settings.moduleName + ': ' + arguments[0],
  430. variables = [].slice.call( arguments, 1 ),
  431. log = console.info || console.log || function(){}
  432. ;
  433. if(settings.debug) {
  434. output.push(message);
  435. log.apply(console, output.concat(variables) );
  436. }
  437. },
  438. error: function() {
  439. var
  440. output = [],
  441. errorMessage = settings.moduleName + ': ' + arguments[0],
  442. variables = [].slice.call( arguments, 1 ),
  443. log = console.warn || console.log || function(){}
  444. ;
  445. if(settings.debug) {
  446. output.push(errorMessage);
  447. output.concat(variables);
  448. log.apply(console, output.concat(variables) );
  449. }
  450. },
  451. invoke: function(query, context, passedArguments) {
  452. var
  453. maxDepth,
  454. found
  455. ;
  456. passedArguments = passedArguments || [].slice.call( arguments, 2 );
  457. if(typeof query == 'string' && instance !== undefined) {
  458. query = query.split('.');
  459. maxDepth = query.length - 1;
  460. $.each(query, function(depth, value) {
  461. if( $.isPlainObject( instance[value] ) && (depth != maxDepth) ) {
  462. instance = instance[value];
  463. return true;
  464. }
  465. else if( instance[value] !== undefined ) {
  466. found = instance[value];
  467. return true;
  468. }
  469. module.error(settings.errors.method);
  470. return false;
  471. });
  472. }
  473. if ( $.isFunction( found ) ) {
  474. return found.apply(context, passedArguments);
  475. }
  476. // return retrieved variable or chain
  477. return found;
  478. }
  479. };
  480. // check for invoking internal method
  481. if(methodInvoked) {
  482. invokedResponse = module.invoke(moduleArguments[0], this, Array.prototype.slice.call( moduleArguments, 1 ) );
  483. }
  484. // otherwise initialize
  485. else {
  486. if(instance) {
  487. module.destroy();
  488. }
  489. module.initialize();
  490. }
  491. })
  492. ;
  493. // chain or return queried method
  494. return (invokedResponse !== undefined)
  495. ? invokedResponse
  496. : this
  497. ;
  498. };
  499. $.fn.popup.settings = {
  500. moduleName : 'Pop-up Module',
  501. debug : true,
  502. namespace : 'popup',
  503. onShow : function(){},
  504. onHide : function(){},
  505. content : false,
  506. html : false,
  507. title : false,
  508. position : 'top center',
  509. delay : 0,
  510. inline : true,
  511. duration : 250,
  512. easing : 'easeOutQuint',
  513. animation : 'pop',
  514. errors: {
  515. content : 'Warning: Your popup has no content specified',
  516. method : 'The method you called is not defined.',
  517. recursion : 'Popup attempted to reposition element to fit, but could not find an adequate position.'
  518. },
  519. distanceAway : 2,
  520. arrowOffset : 0,
  521. maxRecursion : 10,
  522. event : 'hover',
  523. clicktoClose : true,
  524. metadata: {
  525. content : 'content',
  526. html : 'html',
  527. title : 'title',
  528. position : 'position',
  529. arrowOffset : 'arrowOffset'
  530. },
  531. className : {
  532. popup : 'ui popup',
  533. visible : 'visible',
  534. loading : 'loading'
  535. },
  536. selector : {
  537. popup : '.ui.popup'
  538. },
  539. template: function(text) {
  540. var html = '';
  541. if(typeof text !== undefined) {
  542. if(typeof text.title !== undefined && text.title) {
  543. html += '<h2>' + text.title + '</h2>';
  544. }
  545. if(typeof text.content !== undefined && text.content) {
  546. html += '<div class="content">' + text.content + '</div>';
  547. }
  548. }
  549. return html;
  550. }
  551. };
  552. })( jQuery, window , document );