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.

546 lines
16 KiB

  1. /*
  2. * # Semantic - Nag
  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.nag = function(parameters) {
  13. var
  14. $allModules = $(this),
  15. moduleSelector = $allModules.selector || '',
  16. time = new Date().getTime(),
  17. performance = [],
  18. query = arguments[0],
  19. methodInvoked = (typeof query == 'string'),
  20. queryArguments = [].slice.call(arguments, 1),
  21. invokedResponse
  22. ;
  23. $(this)
  24. .each(function() {
  25. var
  26. settings = $.extend(true, {}, $.fn.nag.settings, parameters),
  27. className = settings.className,
  28. selector = settings.selector,
  29. error = settings.error,
  30. namespace = settings.namespace,
  31. eventNamespace = '.' + namespace,
  32. moduleNamespace = namespace + '-module',
  33. $module = $(this),
  34. $close = $module.find(selector.close),
  35. $context = $(settings.context),
  36. element = this,
  37. instance = $module.data(moduleNamespace),
  38. moduleOffset,
  39. moduleHeight,
  40. contextWidth,
  41. contextHeight,
  42. contextOffset,
  43. yOffset,
  44. yPosition,
  45. timer,
  46. module,
  47. requestAnimationFrame = window.requestAnimationFrame
  48. || window.mozRequestAnimationFrame
  49. || window.webkitRequestAnimationFrame
  50. || window.msRequestAnimationFrame
  51. || function(callback) { setTimeout(callback, 0); }
  52. ;
  53. module = {
  54. initialize: function() {
  55. module.verbose('Initializing element');
  56. // calculate module offset once
  57. moduleOffset = $module.offset();
  58. moduleHeight = $module.outerHeight();
  59. contextWidth = $context.outerWidth();
  60. contextHeight = $context.outerHeight();
  61. contextOffset = $context.offset();
  62. $module
  63. .data(moduleNamespace, module)
  64. ;
  65. $close
  66. .on('click' + eventNamespace, module.dismiss)
  67. ;
  68. // lets avoid javascript if we dont need to reposition
  69. if(settings.context == window && settings.position == 'fixed') {
  70. $module
  71. .addClass(className.fixed)
  72. ;
  73. }
  74. if(settings.sticky) {
  75. module.verbose('Adding scroll events');
  76. // retrigger on scroll for absolute
  77. if(settings.position == 'absolute') {
  78. $context
  79. .on('scroll' + eventNamespace, module.event.scroll)
  80. .on('resize' + eventNamespace, module.event.scroll)
  81. ;
  82. }
  83. // fixed is always relative to window
  84. else {
  85. $(window)
  86. .on('scroll' + eventNamespace, module.event.scroll)
  87. .on('resize' + eventNamespace, module.event.scroll)
  88. ;
  89. }
  90. // fire once to position on init
  91. $.proxy(module.event.scroll, this)();
  92. }
  93. if(settings.displayTime > 0) {
  94. setTimeout(module.hide, settings.displayTime);
  95. }
  96. if(module.should.show()) {
  97. if( !$module.is(':visible') ) {
  98. module.show();
  99. }
  100. }
  101. else {
  102. module.hide();
  103. }
  104. },
  105. destroy: function() {
  106. module.verbose('Destroying instance');
  107. $module
  108. .removeData(moduleNamespace)
  109. .off(eventNamespace)
  110. ;
  111. if(settings.sticky) {
  112. $context
  113. .off(eventNamespace)
  114. ;
  115. }
  116. },
  117. refresh: function() {
  118. module.debug('Refreshing cached calculations');
  119. moduleOffset = $module.offset();
  120. moduleHeight = $module.outerHeight();
  121. contextWidth = $context.outerWidth();
  122. contextHeight = $context.outerHeight();
  123. contextOffset = $context.offset();
  124. },
  125. show: function() {
  126. module.debug('Showing nag', settings.animation.show);
  127. if(settings.animation.show == 'fade') {
  128. $module
  129. .fadeIn(settings.duration, settings.easing)
  130. ;
  131. }
  132. else {
  133. $module
  134. .slideDown(settings.duration, settings.easing)
  135. ;
  136. }
  137. },
  138. hide: function() {
  139. module.debug('Showing nag', settings.animation.hide);
  140. if(settings.animation.show == 'fade') {
  141. $module
  142. .fadeIn(settings.duration, settings.easing)
  143. ;
  144. }
  145. else {
  146. $module
  147. .slideUp(settings.duration, settings.easing)
  148. ;
  149. }
  150. },
  151. onHide: function() {
  152. module.debug('Removing nag', settings.animation.hide);
  153. $module.remove();
  154. if (settings.onHide) {
  155. settings.onHide();
  156. }
  157. },
  158. stick: function() {
  159. module.refresh();
  160. if(settings.position == 'fixed') {
  161. var
  162. windowScroll = $(window).prop('pageYOffset') || $(window).scrollTop(),
  163. fixedOffset = ( $module.hasClass(className.bottom) )
  164. ? contextOffset.top + (contextHeight - moduleHeight) - windowScroll
  165. : contextOffset.top - windowScroll
  166. ;
  167. $module
  168. .css({
  169. position : 'fixed',
  170. top : fixedOffset,
  171. left : contextOffset.left,
  172. width : contextWidth - settings.scrollBarWidth
  173. })
  174. ;
  175. }
  176. else {
  177. $module
  178. .css({
  179. top : yPosition
  180. })
  181. ;
  182. }
  183. },
  184. unStick: function() {
  185. $module
  186. .css({
  187. top : ''
  188. })
  189. ;
  190. },
  191. dismiss: function(event) {
  192. if(settings.storageMethod) {
  193. module.storage.set(settings.storedKey, settings.storedValue);
  194. }
  195. module.hide();
  196. event.stopImmediatePropagation();
  197. event.preventDefault();
  198. },
  199. should: {
  200. show: function() {
  201. if(settings.persist) {
  202. module.debug('Persistent nag is set, can show nag');
  203. return true;
  204. }
  205. if(module.storage.get(settings.storedKey) != settings.storedValue) {
  206. module.debug('Stored value is not set, can show nag', module.storage.get(settings.storedKey));
  207. return true;
  208. }
  209. module.debug('Stored value is set, cannot show nag', module.storage.get(settings.storedKey));
  210. return false;
  211. },
  212. stick: function() {
  213. yOffset = $context.prop('pageYOffset') || $context.scrollTop();
  214. yPosition = ( $module.hasClass(className.bottom) )
  215. ? (contextHeight - $module.outerHeight() ) + yOffset
  216. : yOffset
  217. ;
  218. // absolute position calculated when y offset met
  219. if(yPosition > moduleOffset.top) {
  220. return true;
  221. }
  222. else if(settings.position == 'fixed') {
  223. return true;
  224. }
  225. return false;
  226. }
  227. },
  228. storage: {
  229. set: function(key, value) {
  230. module.debug('Setting stored value', key, value, settings.storageMethod);
  231. if(settings.storageMethod == 'local' && window.store !== undefined) {
  232. window.store.set(key, value);
  233. }
  234. // store by cookie
  235. else if($.cookie !== undefined) {
  236. $.cookie(key, value);
  237. }
  238. else {
  239. module.error(error.noStorage);
  240. }
  241. },
  242. get: function(key) {
  243. module.debug('Getting stored value', key, settings.storageMethod);
  244. if(settings.storageMethod == 'local' && window.store !== undefined) {
  245. return window.store.get(key);
  246. }
  247. // get by cookie
  248. else if($.cookie !== undefined) {
  249. return $.cookie(key);
  250. }
  251. else {
  252. module.error(error.noStorage);
  253. }
  254. }
  255. },
  256. event: {
  257. scroll: function() {
  258. if(timer !== undefined) {
  259. clearTimeout(timer);
  260. }
  261. timer = setTimeout(function() {
  262. if(module.should.stick() ) {
  263. requestAnimationFrame(module.stick);
  264. }
  265. else {
  266. module.unStick();
  267. }
  268. }, settings.lag);
  269. }
  270. },
  271. setting: function(name, value) {
  272. module.debug('Changing setting', name, value);
  273. if(value !== undefined) {
  274. if( $.isPlainObject(name) ) {
  275. $.extend(true, settings, name);
  276. }
  277. else {
  278. settings[name] = value;
  279. }
  280. }
  281. else {
  282. return settings[name];
  283. }
  284. },
  285. internal: function(name, value) {
  286. module.debug('Changing internal', name, value);
  287. if(value !== undefined) {
  288. if( $.isPlainObject(name) ) {
  289. $.extend(true, module, name);
  290. }
  291. else {
  292. module[name] = value;
  293. }
  294. }
  295. else {
  296. return module[name];
  297. }
  298. },
  299. debug: function() {
  300. if(settings.debug) {
  301. if(settings.performance) {
  302. module.performance.log(arguments);
  303. }
  304. else {
  305. module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
  306. module.debug.apply(console, arguments);
  307. }
  308. }
  309. },
  310. verbose: function() {
  311. if(settings.verbose && settings.debug) {
  312. if(settings.performance) {
  313. module.performance.log(arguments);
  314. }
  315. else {
  316. module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
  317. module.verbose.apply(console, arguments);
  318. }
  319. }
  320. },
  321. error: function() {
  322. module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
  323. module.error.apply(console, arguments);
  324. },
  325. performance: {
  326. log: function(message) {
  327. var
  328. currentTime,
  329. executionTime,
  330. previousTime
  331. ;
  332. if(settings.performance) {
  333. currentTime = new Date().getTime();
  334. previousTime = time || currentTime;
  335. executionTime = currentTime - previousTime;
  336. time = currentTime;
  337. performance.push({
  338. 'Element' : element,
  339. 'Name' : message[0],
  340. 'Arguments' : [].slice.call(message, 1) || '',
  341. 'Execution Time' : executionTime
  342. });
  343. }
  344. clearTimeout(module.performance.timer);
  345. module.performance.timer = setTimeout(module.performance.display, 100);
  346. },
  347. display: function() {
  348. var
  349. title = settings.name + ':',
  350. totalTime = 0
  351. ;
  352. time = false;
  353. clearTimeout(module.performance.timer);
  354. $.each(performance, function(index, data) {
  355. totalTime += data['Execution Time'];
  356. });
  357. title += ' ' + totalTime + 'ms';
  358. if(moduleSelector) {
  359. title += ' \'' + moduleSelector + '\'';
  360. }
  361. if($allModules.size() > 1) {
  362. title += ' ' + '(' + $allModules.size() + ')';
  363. }
  364. if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
  365. console.groupCollapsed(title);
  366. if(console.table) {
  367. console.table(performance);
  368. }
  369. else {
  370. $.each(performance, function(index, data) {
  371. console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
  372. });
  373. }
  374. console.groupEnd();
  375. }
  376. performance = [];
  377. }
  378. },
  379. invoke: function(query, passedArguments, context) {
  380. var
  381. maxDepth,
  382. found,
  383. response
  384. ;
  385. passedArguments = passedArguments || queryArguments;
  386. context = element || context;
  387. if(typeof query == 'string' && instance !== undefined) {
  388. query = query.split(/[\. ]/);
  389. maxDepth = query.length - 1;
  390. $.each(query, function(depth, value) {
  391. var camelCaseValue = (depth != maxDepth)
  392. ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
  393. : query
  394. ;
  395. if( $.isPlainObject( instance[value] ) && (depth != maxDepth) ) {
  396. instance = instance[value];
  397. }
  398. else if( $.isPlainObject( instance[camelCaseValue] ) && (depth != maxDepth) ) {
  399. instance = instance[camelCaseValue];
  400. }
  401. else if( instance[value] !== undefined ) {
  402. found = instance[value];
  403. return false;
  404. }
  405. else if( instance[camelCaseValue] !== undefined ) {
  406. found = instance[camelCaseValue];
  407. return false;
  408. }
  409. else {
  410. module.error(error.method);
  411. return false;
  412. }
  413. });
  414. }
  415. if ( $.isFunction( found ) ) {
  416. response = found.apply(context, passedArguments);
  417. }
  418. else if(found !== undefined) {
  419. response = found;
  420. }
  421. if($.isArray(invokedResponse)) {
  422. invokedResponse.push(response);
  423. }
  424. else if(typeof invokedResponse == 'string') {
  425. invokedResponse = [invokedResponse, response];
  426. }
  427. else if(response !== undefined) {
  428. invokedResponse = response;
  429. }
  430. return found;
  431. }
  432. };
  433. if(methodInvoked) {
  434. if(instance === undefined) {
  435. module.initialize();
  436. }
  437. module.invoke(query);
  438. }
  439. else {
  440. if(instance !== undefined) {
  441. module.destroy();
  442. }
  443. module.initialize();
  444. }
  445. })
  446. ;
  447. return (invokedResponse !== undefined)
  448. ? invokedResponse
  449. : this
  450. ;
  451. };
  452. $.fn.nag.settings = {
  453. name : 'Nag',
  454. verbose : true,
  455. debug : true,
  456. performance : true,
  457. namespace : 'Nag',
  458. // allows cookie to be overriden
  459. persist : false,
  460. // set to zero to manually dismiss, otherwise hides on its own
  461. displayTime : 0,
  462. animation : {
  463. show: 'slide',
  464. hide: 'slide'
  465. },
  466. // method of stickyness
  467. position : 'fixed',
  468. scrollBarWidth : 18,
  469. // type of storage to use
  470. storageMethod : 'cookie',
  471. // value to store in dismissed localstorage/cookie
  472. storedKey : 'nag',
  473. storedValue : 'dismiss',
  474. // need to calculate stickyness on scroll
  475. sticky : false,
  476. // how often to check scroll event
  477. lag : 0,
  478. // context for scroll event
  479. context : window,
  480. error: {
  481. noStorage : 'Neither $.cookie or store is defined. A storage solution is required for storing state',
  482. method : 'The method you called is not defined.'
  483. },
  484. className : {
  485. bottom : 'bottom',
  486. fixed : 'fixed'
  487. },
  488. selector : {
  489. close: '.icon.close'
  490. },
  491. speed : 500,
  492. easing : 'easeOutQuad',
  493. onHide: function() {}
  494. };
  495. })( jQuery, window , document );