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.

511 lines
15 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.errors,
  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. }
  303. }
  304. },
  305. verbose: function() {
  306. if(settings.verbose && settings.debug) {
  307. if(settings.performance) {
  308. module.performance.log(arguments);
  309. }
  310. else {
  311. module.verbose = Function.prototype.bind.call(console.info, console, settings.moduleName + ':');
  312. }
  313. }
  314. },
  315. error: function() {
  316. module.error = Function.prototype.bind.call(console.log, console, settings.moduleName + ':');
  317. },
  318. performance: {
  319. log: function(message) {
  320. var
  321. currentTime,
  322. executionTime,
  323. previousTime
  324. ;
  325. if(settings.performance) {
  326. currentTime = new Date().getTime();
  327. previousTime = time || currentTime;
  328. executionTime = currentTime - previousTime;
  329. time = currentTime;
  330. performance.push({
  331. 'Element' : element,
  332. 'Name' : message[0],
  333. 'Arguments' : [].slice.call(message, 1) || '',
  334. 'Execution Time' : executionTime
  335. });
  336. }
  337. clearTimeout(module.performance.timer);
  338. module.performance.timer = setTimeout(module.performance.display, 100);
  339. },
  340. display: function() {
  341. var
  342. title = settings.moduleName + ':',
  343. totalTime = 0
  344. ;
  345. time = false;
  346. clearTimeout(module.performance.timer);
  347. $.each(performance, function(index, data) {
  348. totalTime += data['Execution Time'];
  349. });
  350. title += ' ' + totalTime + 'ms';
  351. if(moduleSelector) {
  352. title += ' \'' + moduleSelector + '\'';
  353. }
  354. if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
  355. console.groupCollapsed(title);
  356. if(console.table) {
  357. console.table(performance);
  358. }
  359. else {
  360. $.each(performance, function(index, data) {
  361. console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
  362. });
  363. }
  364. console.groupEnd();
  365. }
  366. performance = [];
  367. }
  368. },
  369. invoke: function(query, passedArguments, context) {
  370. var
  371. maxDepth,
  372. found
  373. ;
  374. passedArguments = passedArguments || queryArguments;
  375. context = element || context;
  376. if(typeof query == 'string' && instance !== undefined) {
  377. query = query.split(/[\. ]/);
  378. maxDepth = query.length - 1;
  379. $.each(query, function(depth, value) {
  380. if( $.isPlainObject( instance[value] ) && (depth != maxDepth) ) {
  381. instance = instance[value];
  382. return true;
  383. }
  384. else if( instance[value] !== undefined ) {
  385. found = instance[value];
  386. return true;
  387. }
  388. module.error(error.method);
  389. return false;
  390. });
  391. }
  392. if ( $.isFunction( found ) ) {
  393. return found.apply(context, passedArguments);
  394. }
  395. return found || false;
  396. }
  397. };
  398. if(methodInvoked) {
  399. if(instance === undefined) {
  400. module.initialize();
  401. }
  402. invokedResponse = module.invoke(query);
  403. }
  404. else {
  405. if(instance !== undefined) {
  406. module.destroy();
  407. }
  408. module.initialize();
  409. }
  410. })
  411. ;
  412. return (invokedResponse)
  413. ? invokedResponse
  414. : this
  415. ;
  416. };
  417. $.fn.nag.settings = {
  418. moduleName : 'Nag',
  419. verbose : true,
  420. debug : true,
  421. performance : true,
  422. namespace : 'Nag',
  423. // allows cookie to be overriden
  424. persist : false,
  425. // set to zero to manually dismiss, otherwise hides on its own
  426. displayTime : 0,
  427. animation : {
  428. show: 'slide',
  429. hide: 'slide'
  430. },
  431. // method of stickyness
  432. position : 'fixed',
  433. scrollBarWidth : 18,
  434. // type of storage to use
  435. storageMethod : 'cookie',
  436. // value to store in dismissed localstorage/cookie
  437. storedKey : 'nag',
  438. storedValue : 'dismiss',
  439. // need to calculate stickyness on scroll
  440. sticky : false,
  441. // how often to check scroll event
  442. lag : 0,
  443. // context for scroll event
  444. context : window,
  445. error: {
  446. noStorage : 'Neither $.cookie or store is defined. A storage solution is required for storing state',
  447. method : 'The method you called is not defined.'
  448. },
  449. className : {
  450. bottom : 'bottom',
  451. fixed : 'fixed'
  452. },
  453. selector : {
  454. close: '.icon.close'
  455. },
  456. speed : 500,
  457. easing : 'easeOutQuad',
  458. onHide: function() {}
  459. };
  460. })( jQuery, window , document );