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.

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