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.

729 lines
22 KiB

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