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.

867 lines
26 KiB

10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
  1. /*
  2. * # Semantic - Popup
  3. * http://github.com/jlukic/semantic-ui/
  4. *
  5. *
  6. * Copyright 2014 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 : 'easeOutQuad',
  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>';
  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. // Adds easing
  799. $.extend( $.easing, {
  800. easeOutQuad: function (x, t, b, c, d) {
  801. return -c *(t/=d)*(t-2) + b;
  802. }
  803. });
  804. })( require("jquery"), window , document );