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.

542 lines
16 KiB

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