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.

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