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.

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