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.

805 lines
24 KiB

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