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.

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