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.

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