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.

683 lines
20 KiB

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