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.

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