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.

718 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. module.hideAll();
  235. module.show();
  236. }
  237. else {
  238. module.hide();
  239. }
  240. },
  241. position: function(position, arrowOffset) {
  242. var
  243. windowWidth = $(window).width(),
  244. windowHeight = $(window).height(),
  245. width = $module.outerWidth(),
  246. height = $module.outerHeight(),
  247. popupWidth = $popup.width(),
  248. popupHeight = $popup.outerHeight(),
  249. offset = (settings.inline)
  250. ? $module.position()
  251. : $module.offset(),
  252. parentWidth = (settings.inline)
  253. ? $offsetParent.outerWidth()
  254. : $window.outerWidth(),
  255. parentHeight = (settings.inline)
  256. ? $offsetParent.outerHeight()
  257. : $window.outerHeight(),
  258. positioning,
  259. offstagePosition
  260. ;
  261. position = position || $module.data(metadata.position) || settings.position;
  262. arrowOffset = arrowOffset || $module.data(metadata.arrowOffset) || settings.arrowOffset;
  263. module.debug('Calculating offset for position', position);
  264. switch(position) {
  265. case 'top left':
  266. positioning = {
  267. top : 'auto',
  268. bottom : parentHeight - offset.top + settings.distanceAway,
  269. left : offset.left + arrowOffset
  270. };
  271. break;
  272. case 'top center':
  273. positioning = {
  274. bottom : parentHeight - offset.top + settings.distanceAway,
  275. left : offset.left + (width / 2) - (popupWidth / 2) + arrowOffset,
  276. top : 'auto',
  277. right : 'auto'
  278. };
  279. break;
  280. case 'top right':
  281. positioning = {
  282. bottom : parentHeight - offset.top + settings.distanceAway,
  283. right : parentWidth - offset.left - width - arrowOffset,
  284. top : 'auto',
  285. left : 'auto'
  286. };
  287. break;
  288. case 'left center':
  289. positioning = {
  290. top : offset.top + (height / 2) - (popupHeight / 2),
  291. right : parentWidth - offset.left + settings.distanceAway - arrowOffset,
  292. left : 'auto',
  293. bottom : 'auto'
  294. };
  295. break;
  296. case 'right center':
  297. positioning = {
  298. top : offset.top + (height / 2) - (popupHeight / 2),
  299. left : offset.left + width + settings.distanceAway + arrowOffset,
  300. bottom : 'auto',
  301. right : 'auto'
  302. };
  303. break;
  304. case 'bottom left':
  305. positioning = {
  306. top : offset.top + height + settings.distanceAway,
  307. left : offset.left + arrowOffset,
  308. bottom : 'auto',
  309. right : 'auto'
  310. };
  311. break;
  312. case 'bottom center':
  313. positioning = {
  314. top : offset.top + height + settings.distanceAway,
  315. left : offset.left + (width / 2) - (popupWidth / 2) + arrowOffset,
  316. bottom : 'auto',
  317. right : 'auto'
  318. };
  319. break;
  320. case 'bottom right':
  321. positioning = {
  322. top : offset.top + height + settings.distanceAway,
  323. right : parentWidth - offset.left - width - arrowOffset,
  324. left : 'auto',
  325. bottom : 'auto'
  326. };
  327. break;
  328. }
  329. // true width on popup, avoid rounding error
  330. $.extend(positioning, {
  331. width: $popup.width() + 1
  332. });
  333. // tentatively place on stage
  334. $popup
  335. .removeAttr('style')
  336. .removeClass('top right bottom left center')
  337. .css(positioning)
  338. .addClass(position)
  339. .addClass(className.loading)
  340. ;
  341. // check if is offstage
  342. offstagePosition = module.get.offstagePosition();
  343. // recursively find new positioning
  344. if(offstagePosition) {
  345. module.debug('Element is outside boundaries ', offstagePosition);
  346. if(searchDepth < settings.maxSearchDepth) {
  347. position = module.get.nextPosition(position);
  348. searchDepth++;
  349. module.debug('Trying new position: ', position);
  350. return module.position(position);
  351. }
  352. else {
  353. module.error(error.recursion);
  354. searchDepth = 0;
  355. return false;
  356. }
  357. }
  358. else {
  359. module.debug('Position is on stage', position);
  360. searchDepth = 0;
  361. return true;
  362. }
  363. },
  364. show: function() {
  365. module.debug('Showing pop-up', settings.transition);
  366. if($popup.size() === 0) {
  367. module.create();
  368. }
  369. module.position();
  370. $module
  371. .addClass(className.visible)
  372. ;
  373. $popup
  374. .removeClass(className.loading)
  375. ;
  376. if(settings.transition && $.fn.transition !== undefined) {
  377. $popup
  378. .transition(settings.transition + ' in', settings.duration)
  379. ;
  380. }
  381. else {
  382. $popup
  383. .stop()
  384. .fadeIn(settings.duration, settings.easing)
  385. ;
  386. }
  387. if(settings.on == 'click' && settings.clicktoClose) {
  388. module.debug('Binding popup close event');
  389. $(document)
  390. .on('click.' + namespace, module.gracefully.hide)
  391. ;
  392. }
  393. $.proxy(settings.onShow, $popup)();
  394. },
  395. hideAll: function() {
  396. $(selector.popup)
  397. .filter(':visible')
  398. .popup('hide')
  399. ;
  400. },
  401. hide: function() {
  402. $module
  403. .removeClass(className.visible)
  404. ;
  405. if($popup.is(':visible') ) {
  406. module.debug('Hiding pop-up');
  407. if(settings.transition && $.fn.transition !== undefined) {
  408. $popup
  409. .transition(settings.transition + ' out', settings.duration)
  410. ;
  411. }
  412. else {
  413. $popup
  414. .stop()
  415. .fadeOut(settings.duration, settings.easing)
  416. ;
  417. }
  418. }
  419. if(settings.on == 'click' && settings.clicktoClose) {
  420. $(document)
  421. .off('click.' + namespace)
  422. ;
  423. }
  424. $.proxy(settings.onHide, $popup)();
  425. if(!settings.inline) {
  426. module.remove();
  427. }
  428. },
  429. gracefully: {
  430. hide: function(event) {
  431. // don't close on clicks inside popup
  432. if( $(event.target).closest(selector.popup).size() === 0) {
  433. module.hide();
  434. }
  435. }
  436. },
  437. setting: function(name, value) {
  438. if(value !== undefined) {
  439. if( $.isPlainObject(name) ) {
  440. $.extend(true, settings, name);
  441. }
  442. else {
  443. settings[name] = value;
  444. }
  445. }
  446. else {
  447. return settings[name];
  448. }
  449. },
  450. internal: function(name, value) {
  451. if(value !== undefined) {
  452. if( $.isPlainObject(name) ) {
  453. $.extend(true, module, name);
  454. }
  455. else {
  456. module[name] = value;
  457. }
  458. }
  459. else {
  460. return module[name];
  461. }
  462. },
  463. debug: function() {
  464. if(settings.debug) {
  465. if(settings.performance) {
  466. module.performance.log(arguments);
  467. }
  468. else {
  469. module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
  470. module.debug.apply(console, arguments);
  471. }
  472. }
  473. },
  474. verbose: function() {
  475. if(settings.verbose && settings.debug) {
  476. if(settings.performance) {
  477. module.performance.log(arguments);
  478. }
  479. else {
  480. module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
  481. module.verbose.apply(console, arguments);
  482. }
  483. }
  484. },
  485. error: function() {
  486. module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
  487. module.error.apply(console, arguments);
  488. },
  489. performance: {
  490. log: function(message) {
  491. var
  492. currentTime,
  493. executionTime,
  494. previousTime
  495. ;
  496. if(settings.performance) {
  497. currentTime = new Date().getTime();
  498. previousTime = time || currentTime;
  499. executionTime = currentTime - previousTime;
  500. time = currentTime;
  501. performance.push({
  502. 'Element' : element,
  503. 'Name' : message[0],
  504. 'Arguments' : [].slice.call(message, 1) || '',
  505. 'Execution Time' : executionTime
  506. });
  507. }
  508. clearTimeout(module.performance.timer);
  509. module.performance.timer = setTimeout(module.performance.display, 100);
  510. },
  511. display: function() {
  512. var
  513. title = settings.name + ':',
  514. totalTime = 0
  515. ;
  516. time = false;
  517. clearTimeout(module.performance.timer);
  518. $.each(performance, function(index, data) {
  519. totalTime += data['Execution Time'];
  520. });
  521. title += ' ' + totalTime + 'ms';
  522. if(moduleSelector) {
  523. title += ' \'' + moduleSelector + '\'';
  524. }
  525. if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
  526. console.groupCollapsed(title);
  527. if(console.table) {
  528. console.table(performance);
  529. }
  530. else {
  531. $.each(performance, function(index, data) {
  532. console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
  533. });
  534. }
  535. console.groupEnd();
  536. }
  537. performance = [];
  538. }
  539. },
  540. invoke: function(query, passedArguments, context) {
  541. var
  542. maxDepth,
  543. found,
  544. response
  545. ;
  546. passedArguments = passedArguments || queryArguments;
  547. context = element || context;
  548. if(typeof query == 'string' && instance !== undefined) {
  549. query = query.split(/[\. ]/);
  550. maxDepth = query.length - 1;
  551. $.each(query, function(depth, value) {
  552. var camelCaseValue = (depth != maxDepth)
  553. ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
  554. : query
  555. ;
  556. if( $.isPlainObject( instance[value] ) && (depth != maxDepth) ) {
  557. instance = instance[value];
  558. }
  559. else if( $.isPlainObject( instance[camelCaseValue] ) && (depth != maxDepth) ) {
  560. instance = instance[camelCaseValue];
  561. }
  562. else if( instance[value] !== undefined ) {
  563. found = instance[value];
  564. return false;
  565. }
  566. else if( instance[camelCaseValue] !== undefined ) {
  567. found = instance[camelCaseValue];
  568. return false;
  569. }
  570. else {
  571. module.error(error.method);
  572. return false;
  573. }
  574. });
  575. }
  576. if ( $.isFunction( found ) ) {
  577. response = found.apply(context, passedArguments);
  578. }
  579. else if(found !== undefined) {
  580. response = found;
  581. }
  582. if($.isArray(invokedResponse)) {
  583. invokedResponse.push(response);
  584. }
  585. else if(typeof invokedResponse == 'string') {
  586. invokedResponse = [invokedResponse, response];
  587. }
  588. else if(response !== undefined) {
  589. invokedResponse = response;
  590. }
  591. return found;
  592. }
  593. };
  594. if(methodInvoked) {
  595. if(instance === undefined) {
  596. module.initialize();
  597. }
  598. module.invoke(query);
  599. }
  600. else {
  601. if(instance !== undefined) {
  602. module.destroy();
  603. }
  604. module.initialize();
  605. }
  606. })
  607. ;
  608. return (invokedResponse !== undefined)
  609. ? invokedResponse
  610. : this
  611. ;
  612. };
  613. $.fn.popup.settings = {
  614. name : 'Popup',
  615. debug : true,
  616. verbose : true,
  617. performance : true,
  618. namespace : 'popup',
  619. onInit : function(){},
  620. onShow : function(){},
  621. onHide : function(){},
  622. variation : '',
  623. content : false,
  624. html : false,
  625. title : false,
  626. on : 'hover',
  627. clicktoClose : true,
  628. position : 'top center',
  629. delay : 150,
  630. inline : true,
  631. duration : 150,
  632. easing : 'easeOutQuint',
  633. transition : 'scale',
  634. distanceAway : 0,
  635. arrowOffset : 0,
  636. maxSearchDepth : 10,
  637. error: {
  638. content : 'Your popup has no content specified',
  639. method : 'The method you called is not defined.',
  640. recursion : 'Popup attempted to reposition element to fit, but could not find an adequate position.'
  641. },
  642. metadata: {
  643. arrowOffset : 'arrowOffset',
  644. content : 'content',
  645. html : 'html',
  646. position : 'position',
  647. title : 'title',
  648. variation : 'variation'
  649. },
  650. className : {
  651. popup : 'ui popup',
  652. visible : 'visible',
  653. loading : 'loading'
  654. },
  655. selector : {
  656. popup : '.ui.popup'
  657. },
  658. template: function(text) {
  659. var html = '';
  660. if(typeof text !== undefined) {
  661. if(typeof text.title !== undefined && text.title) {
  662. html += '<div class="header">' + text.title + '</div class="header">';
  663. }
  664. if(typeof text.content !== undefined && text.content) {
  665. html += '<div class="content">' + text.content + '</div>';
  666. }
  667. }
  668. return html;
  669. }
  670. };
  671. })( jQuery, window , document );