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.

688 lines
21 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. },
  84. event: {
  85. mouseenter: function(event) {
  86. var element = this;
  87. module.timer = setTimeout(function() {
  88. $.proxy(module.toggle, element)();
  89. if( $(element).hasClass(className.visible) ) {
  90. event.stopPropagation();
  91. }
  92. }, settings.delay);
  93. },
  94. mouseleave: function(event) {
  95. clearTimeout(module.timer);
  96. if( $module.is(':visible') ) {
  97. module.hide();
  98. }
  99. },
  100. click: function(event) {
  101. $.proxy(module.toggle, this)();
  102. if( $(this).hasClass(className.visible) ) {
  103. event.stopPropagation();
  104. }
  105. },
  106. resize: function() {
  107. if( $popup.is(':visible') ) {
  108. module.position();
  109. }
  110. }
  111. },
  112. // generates popup html from metadata
  113. create: function() {
  114. module.debug('Creating pop-up content');
  115. var
  116. html = $module.data(metadata.html) || settings.html,
  117. variation = $module.data(metadata.variation) || settings.variation,
  118. title = $module.data(metadata.title) || settings.title,
  119. content = $module.data(metadata.content) || $module.attr('title') || settings.content
  120. ;
  121. if(html || content || title) {
  122. if(!html) {
  123. html = settings.template({
  124. title : title,
  125. content : content
  126. });
  127. }
  128. $popup = $('<div/>')
  129. .addClass(className.popup)
  130. .addClass(variation)
  131. .html(html)
  132. ;
  133. if(settings.inline) {
  134. $popup
  135. .insertAfter($module)
  136. ;
  137. }
  138. else {
  139. $popup
  140. .appendTo( $('body') )
  141. ;
  142. }
  143. $.proxy(settings.onInit, $popup)();
  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.width(),
  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.verbose('Creating pop-up html');
  228. module.create();
  229. }
  230. if( !$module.hasClass(className.visible) ) {
  231. if( module.position() ) {
  232. module.show();
  233. }
  234. }
  235. else {
  236. module.hide();
  237. }
  238. },
  239. position: function(position, arrowOffset) {
  240. var
  241. windowWidth = $(window).width(),
  242. windowHeight = $(window).height(),
  243. width = $module.outerWidth(),
  244. height = $module.outerHeight(),
  245. popupWidth = $popup.width(),
  246. popupHeight = $popup.outerHeight(),
  247. offset = (settings.inline)
  248. ? $module.position()
  249. : $module.offset(),
  250. parentWidth = (settings.inline)
  251. ? $offsetParent.outerWidth()
  252. : $window.outerWidth(),
  253. parentHeight = (settings.inline)
  254. ? $offsetParent.outerHeight()
  255. : $window.outerHeight(),
  256. positioning,
  257. offstagePosition
  258. ;
  259. position = position || $module.data(metadata.position) || settings.position;
  260. arrowOffset = arrowOffset || $module.data(metadata.arrowOffset) || settings.arrowOffset;
  261. module.debug('Calculating offset for position', position);
  262. switch(position) {
  263. case 'top left':
  264. positioning = {
  265. top : 'auto',
  266. bottom : parentHeight - offset.top + settings.distanceAway,
  267. left : offset.left + arrowOffset
  268. };
  269. break;
  270. case 'top center':
  271. positioning = {
  272. bottom : parentHeight - offset.top + settings.distanceAway,
  273. left : offset.left + (width / 2) - (popupWidth / 2) + arrowOffset,
  274. top : 'auto',
  275. right : 'auto'
  276. };
  277. break;
  278. case 'top right':
  279. positioning = {
  280. bottom : parentHeight - offset.top + settings.distanceAway,
  281. right : parentWidth - offset.left - width - arrowOffset,
  282. top : 'auto',
  283. left : 'auto'
  284. };
  285. break;
  286. case 'left center':
  287. positioning = {
  288. top : offset.top + (height / 2) - (popupHeight / 2),
  289. right : parentWidth - offset.left + settings.distanceAway - arrowOffset,
  290. left : 'auto',
  291. bottom : 'auto'
  292. };
  293. break;
  294. case 'right center':
  295. positioning = {
  296. top : offset.top + (height / 2) - (popupHeight / 2),
  297. left : offset.left + width + settings.distanceAway + arrowOffset,
  298. bottom : 'auto',
  299. right : 'auto'
  300. };
  301. break;
  302. case 'bottom left':
  303. positioning = {
  304. top : offset.top + height + settings.distanceAway,
  305. left : offset.left + arrowOffset,
  306. bottom : 'auto',
  307. right : 'auto'
  308. };
  309. break;
  310. case 'bottom center':
  311. positioning = {
  312. top : offset.top + height + settings.distanceAway,
  313. left : offset.left + (width / 2) - (popupWidth / 2) + arrowOffset,
  314. bottom : 'auto',
  315. right : 'auto'
  316. };
  317. break;
  318. case 'bottom right':
  319. positioning = {
  320. top : offset.top + height + settings.distanceAway,
  321. right : parentWidth - offset.left - width - arrowOffset,
  322. left : 'auto',
  323. bottom : 'auto'
  324. };
  325. break;
  326. }
  327. // true width on popup, avoid rounding error
  328. $.extend(positioning, {
  329. width: $popup.width() + 1
  330. });
  331. // tentatively place on stage
  332. $popup
  333. .removeAttr('style')
  334. .removeClass('top right bottom left center')
  335. .css(positioning)
  336. .addClass(position)
  337. .addClass(className.loading)
  338. ;
  339. // check if is offstage
  340. offstagePosition = module.get.offstagePosition();
  341. // recursively find new positioning
  342. if(offstagePosition) {
  343. module.debug('Element is outside boundaries ', offstagePosition);
  344. if(searchDepth < settings.maxSearchDepth) {
  345. position = module.get.nextPosition(position);
  346. searchDepth++;
  347. module.debug('Trying new position: ', position);
  348. return module.position(position);
  349. }
  350. else {
  351. module.error(error.recursion);
  352. searchDepth = 0;
  353. return false;
  354. }
  355. }
  356. else {
  357. module.debug('Position is on stage', position);
  358. searchDepth = 0;
  359. return true;
  360. }
  361. },
  362. show: function() {
  363. module.debug('Showing pop-up');
  364. $(selector.popup)
  365. .filter(':visible')
  366. .stop()
  367. .fadeOut(200)
  368. .prev($module)
  369. .removeClass(className.visible)
  370. ;
  371. $module
  372. .addClass(className.visible)
  373. ;
  374. $popup
  375. .removeClass(className.loading)
  376. ;
  377. if(settings.animation == 'pop' && $.fn.popIn !== undefined) {
  378. $popup
  379. .stop()
  380. .popIn(settings.duration, settings.easing)
  381. ;
  382. }
  383. else {
  384. $popup
  385. .stop()
  386. .fadeIn(settings.duration, settings.easing)
  387. ;
  388. }
  389. if(settings.on == 'click' && settings.clicktoClose) {
  390. module.debug('Binding popup close event');
  391. $(document)
  392. .on('click.' + namespace, module.gracefully.hide)
  393. ;
  394. }
  395. $.proxy(settings.onShow, $popup)();
  396. },
  397. hide: function() {
  398. $module
  399. .removeClass(className.visible)
  400. ;
  401. if($popup.is(':visible') ) {
  402. module.debug('Hiding pop-up');
  403. if(settings.animation == 'pop' && $.fn.popOut !== undefined) {
  404. $popup
  405. .stop()
  406. .popOut(settings.duration, settings.easing, function() {
  407. $popup.hide();
  408. })
  409. ;
  410. }
  411. else {
  412. $popup
  413. .stop()
  414. .fadeOut(settings.duration, settings.easing)
  415. ;
  416. }
  417. }
  418. if(settings.on == 'click' && settings.clicktoClose) {
  419. $(document)
  420. .off('click.' + namespace)
  421. ;
  422. }
  423. $.proxy(settings.onHide, $popup)();
  424. if(!settings.inline) {
  425. module.remove();
  426. }
  427. },
  428. gracefully: {
  429. hide: function(event) {
  430. // don't close on clicks inside popup
  431. if( $(event.target).closest(selector.popup).size() === 0) {
  432. module.hide();
  433. }
  434. }
  435. },
  436. setting: function(name, value) {
  437. if(value !== undefined) {
  438. if( $.isPlainObject(name) ) {
  439. $.extend(true, settings, name);
  440. }
  441. else {
  442. settings[name] = value;
  443. }
  444. }
  445. else {
  446. return settings[name];
  447. }
  448. },
  449. internal: function(name, value) {
  450. if(value !== undefined) {
  451. if( $.isPlainObject(name) ) {
  452. $.extend(true, module, name);
  453. }
  454. else {
  455. module[name] = value;
  456. }
  457. }
  458. else {
  459. return module[name];
  460. }
  461. },
  462. debug: function() {
  463. if(settings.debug) {
  464. if(settings.performance) {
  465. module.performance.log(arguments);
  466. }
  467. else {
  468. module.debug = Function.prototype.bind.call(console.info, console, settings.moduleName + ':');
  469. }
  470. }
  471. },
  472. verbose: function() {
  473. if(settings.verbose && settings.debug) {
  474. if(settings.performance) {
  475. module.performance.log(arguments);
  476. }
  477. else {
  478. module.verbose = Function.prototype.bind.call(console.info, console, settings.moduleName + ':');
  479. }
  480. }
  481. },
  482. error: function() {
  483. console.log($module.next());
  484. module.error = Function.prototype.bind.call(console.warn, console, settings.moduleName + ':');
  485. },
  486. performance: {
  487. log: function(message) {
  488. var
  489. currentTime,
  490. executionTime,
  491. previousTime
  492. ;
  493. if(settings.performance) {
  494. currentTime = new Date().getTime();
  495. previousTime = time || currentTime,
  496. executionTime = currentTime - previousTime;
  497. time = currentTime;
  498. performance.push({
  499. 'Element' : element,
  500. 'Name' : message[0],
  501. 'Arguments' : message[1] || '',
  502. 'Execution Time' : executionTime
  503. });
  504. }
  505. clearTimeout(module.performance.timer);
  506. module.performance.timer = setTimeout(module.performance.display, 100);
  507. },
  508. display: function() {
  509. var
  510. title = settings.moduleName + ':',
  511. totalTime = 0
  512. ;
  513. time = false;
  514. $.each(performance, function(index, data) {
  515. totalTime += data['Execution Time'];
  516. });
  517. title += ' ' + totalTime + 'ms';
  518. if(moduleSelector) {
  519. title += ' \'' + moduleSelector + '\'';
  520. }
  521. if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
  522. console.groupCollapsed(title);
  523. if(console.table) {
  524. console.table(performance);
  525. }
  526. else {
  527. $.each(performance, function(index, data) {
  528. console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
  529. });
  530. }
  531. console.groupEnd();
  532. }
  533. performance = [];
  534. }
  535. },
  536. invoke: function(query, passedArguments, context) {
  537. var
  538. maxDepth,
  539. found
  540. ;
  541. passedArguments = passedArguments || queryArguments;
  542. context = element || context;
  543. if(typeof query == 'string' && instance !== undefined) {
  544. query = query.split('.');
  545. maxDepth = query.length - 1;
  546. $.each(query, function(depth, value) {
  547. if( $.isPlainObject( instance[value] ) && (depth != maxDepth) ) {
  548. instance = instance[value];
  549. }
  550. else if( instance[value] !== undefined ) {
  551. found = instance[value];
  552. }
  553. else {
  554. module.error(error.method);
  555. }
  556. });
  557. }
  558. if ( $.isFunction( found ) ) {
  559. instance.verbose('Executing invoked function', found);
  560. return found.apply(context, passedArguments);
  561. }
  562. return found || false;
  563. }
  564. };
  565. if(methodInvoked) {
  566. if(instance === undefined) {
  567. module.initialize();
  568. }
  569. invokedResponse = module.invoke(query);
  570. }
  571. else {
  572. if(instance === undefined) {
  573. module.destroy();
  574. }
  575. module.initialize();
  576. }
  577. })
  578. ;
  579. return (invokedResponse)
  580. ? invokedResponse
  581. : this
  582. ;
  583. };
  584. $.fn.popup.settings = {
  585. moduleName : 'Popup',
  586. debug : false,
  587. verbose : false,
  588. performance : false,
  589. namespace : 'popup',
  590. onInit : function(){},
  591. onShow : function(){},
  592. onHide : function(){},
  593. variation : '',
  594. content : false,
  595. html : false,
  596. title : false,
  597. on : 'hover',
  598. clicktoClose : true,
  599. position : 'top center',
  600. delay : 150,
  601. inline : true,
  602. duration : 250,
  603. easing : 'easeOutQuint',
  604. animation : 'pop',
  605. distanceAway : 0,
  606. arrowOffset : 0,
  607. maxSearchDepth : 10,
  608. error: {
  609. content : 'Your popup has no content specified',
  610. method : 'The method you called is not defined.',
  611. recursion : 'Popup attempted to reposition element to fit, but could not find an adequate position.'
  612. },
  613. metadata: {
  614. arrowOffset : 'arrowOffset',
  615. content : 'content',
  616. html : 'html',
  617. position : 'position',
  618. title : 'title',
  619. variation : 'variation'
  620. },
  621. className : {
  622. popup : 'ui popup',
  623. visible : 'visible',
  624. loading : 'loading'
  625. },
  626. selector : {
  627. popup : '.ui.popup'
  628. },
  629. template: function(text) {
  630. var html = '';
  631. if(typeof text !== undefined) {
  632. if(typeof text.title !== undefined && text.title) {
  633. html += '<div class="header">' + text.title + '</div class="header">';
  634. }
  635. if(typeof text.content !== undefined && text.content) {
  636. html += '<div class="content">' + text.content + '</div>';
  637. }
  638. }
  639. return html;
  640. }
  641. };
  642. })( jQuery, window , document );