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.

859 lines
26 KiB

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