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.

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