/js/pageguide.js

https://github.com/pombredanne/pageguide · JavaScript · 303 lines · 203 code · 49 blank · 51 comment · 26 complexity · 12e4776ea02e16cd787901503c9a8e3d MD5 · raw file

  1. /*
  2. * Tracelytics PageGuide
  3. *
  4. * Copyright 2012 Tracelytics
  5. * Free to use under the MIT license.
  6. * http://www.opensource.org/licenses/mit-license.php
  7. *
  8. * Contributing Author: Tracelytics Team
  9. */
  10. /*
  11. * PageGuide usage:
  12. *
  13. * Preferences:
  14. * auto_show_first - Whether or not to focus on the first visible item
  15. * immediately on PG open (default true)
  16. * loading_selector - The CSS selector for the loading element. pageguide
  17. * will wait until this element is no longer visible
  18. * starting up.
  19. * track_events_cb - Optional callback for tracking user interactions
  20. * with pageguide. Should be a method taking a single
  21. * parameter indicating the name of the interaction.
  22. * (default none)
  23. * handle_doc_switch - Optional callback to enlight or adapt interface
  24. * depending on current documented element. Should be a
  25. * function taking 2 parameters, current and previous
  26. * data-tourtarget selectors. (default null)
  27. * custom_open_button - Optional id for toggling pageguide. Default null.
  28. * If not specified then the default button is used.
  29. */
  30. tl = window.tl || {};
  31. tl.pg = tl.pg || {};
  32. tl.pg.default_prefs = {
  33. 'auto_show_first': true,
  34. 'loading_selector' : '#loading',
  35. 'track_events_cb': function() { return; },
  36. 'handle_doc_switch': null,
  37. 'custom_open_button': null
  38. };
  39. tl.pg.init = function(preferences) {
  40. /* page guide object, for pages that have one */
  41. if (jQuery("#tlyPageGuide").length === 0) {
  42. return;
  43. }
  44. var guide = jQuery("#tlyPageGuide"),
  45. wrapper = jQuery('<div>', { id: 'tlyPageGuideWrapper' }),
  46. message = jQuery('<div>', { id: 'tlyPageGuideMessages'});
  47. message.append('<a href="#" class="tlypageguide_close" title="Close Guide">close</a>')
  48. .append('<span></span>')
  49. .append('<div></div>')
  50. .append('<a href="#" class="tlypageguide_back" title="Previous">Previous</a>')
  51. .append('<a href="#" class="tlypageguide_fwd" title="Next">Next</a>');
  52. if (preferences.custom_open_button == null) {
  53. jQuery('<div/>', {
  54. 'title': 'Launch Page Guide',
  55. 'class': 'tlypageguide_toggle'
  56. }).append('page guide')
  57. .append('<div><span>' + guide.data('tourtitle') + '</span></div>')
  58. .append('<a href="javascript:void(0);" title="close guide">close guide &raquo;</a>').appendTo(wrapper);
  59. }
  60. wrapper.append(guide);
  61. wrapper.append(message);
  62. jQuery('body').append(wrapper);
  63. var pg = new tl.pg.PageGuide(jQuery('#tlyPageGuideWrapper'), preferences);
  64. pg.ready(function() {
  65. pg.setup_handlers();
  66. pg.$base.children(".tlypageguide_toggle").animate({ "right": "-120px" }, 250);
  67. });
  68. return pg;
  69. };
  70. tl.pg.PageGuide = function (pg_elem, preferences) {
  71. this.preferences = jQuery.extend({}, tl.pg.default_prefs, preferences);
  72. this.$base = pg_elem;
  73. this.$all_items = jQuery('#tlyPageGuide > li', this.$base);
  74. this.$items = jQuery([]); /* fill me with visible elements on pg expand */
  75. this.$message = jQuery('#tlyPageGuideMessages');
  76. this.$fwd = jQuery('a.tlypageguide_fwd', this.$base);
  77. this.$back = jQuery('a.tlypageguide_back', this.$base);
  78. this.cur_idx = 0;
  79. this.track_event = this.preferences.track_events_cb;
  80. this.handle_doc_switch = this.preferences.handle_doc_switch;
  81. this.custom_open_button = this.preferences.custom_open_button;
  82. };
  83. tl.pg.isScrolledIntoView = function(elem) {
  84. var dvtop = jQuery(window).scrollTop(),
  85. dvbtm = dvtop + jQuery(window).height(),
  86. eltop = jQuery(elem).offset().top,
  87. elbtm = eltop + jQuery(elem).height();
  88. return (elbtm >= dvtop) && (eltop <= dvbtm - 100);
  89. };
  90. tl.pg.PageGuide.prototype.ready = function(callback) {
  91. var that = this,
  92. interval = window.setInterval(function() {
  93. if (!jQuery(that.preferences.loading_selector).is(':visible')) {
  94. callback();
  95. clearInterval(interval);
  96. }
  97. }, 250);
  98. return this;
  99. };
  100. /* to be executed on pg expand */
  101. tl.pg.PageGuide.prototype._on_expand = function () {
  102. var that = this,
  103. $d = document,
  104. $w = window;
  105. /* set up initial state */
  106. this.position_tour();
  107. this.cur_idx = 0;
  108. // create a new stylesheet:
  109. var ns = $d.createElement('style');
  110. $d.getElementsByTagName('head')[0].appendChild(ns);
  111. // keep Safari happy
  112. if (!$w.createPopup) {
  113. ns.appendChild($d.createTextNode(''));
  114. ns.setAttribute("type", "text/css");
  115. }
  116. // get a pointer to the stylesheet you just created
  117. var sh = $d.styleSheets[$d.styleSheets.length - 1];
  118. // space for IE rule set
  119. var ie = "";
  120. /* add number tags and PG shading elements */
  121. this.$items.each(function(i) {
  122. var $p = jQuery(jQuery(this).data('tourtarget') + ":visible:first");
  123. $p.addClass("tlypageguide_shadow tlypageguide_shadow" + i);
  124. var node_text = '.tlypageguide_shadow' + i + ':after { height: ' +
  125. $p.outerHeight() + 'px; width: ' + $p.outerWidth(false) + 'px; }';
  126. if (!$w.createPopup) {
  127. // modern browsers
  128. var k = $d.createTextNode(node_text, 0);
  129. ns.appendChild(k);
  130. } else {
  131. // for IE
  132. ie += node_text;
  133. }
  134. jQuery(this).prepend('<ins>' + (i + 1) + '</ins>');
  135. jQuery(this).data('idx', i);
  136. });
  137. // is IE? slam styles in all at once:
  138. if ($w.createPopup) {
  139. sh.cssText = ie;
  140. }
  141. /* decide to show first? */
  142. if (this.preferences.auto_show_first && this.$items.length > 0) {
  143. this.show_message(0);
  144. }
  145. };
  146. tl.pg.PageGuide.prototype.open = function() {
  147. this.track_event('PG.open');
  148. this._on_expand();
  149. this.$items.toggleClass('expanded');
  150. jQuery('body').addClass('tlypageguide-open');
  151. };
  152. tl.pg.PageGuide.prototype.close = function() {
  153. this.track_event('PG.close');
  154. this.$items.toggleClass('expanded');
  155. this.$message.animate({ height: "0" }, 500, function() {
  156. jQuery(this).hide();
  157. });
  158. /* clear number tags and shading elements */
  159. jQuery('ins').remove();
  160. jQuery('body').removeClass('tlypageguide-open');
  161. };
  162. tl.pg.PageGuide.prototype.setup_handlers = function () {
  163. var that = this;
  164. /* interaction: open/close PG interface */
  165. var interactor = (that.custom_open_button == null) ? jQuery('.tlypageguide_toggle', this.$base) : jQuery(that.custom_open_button)
  166. interactor.live('click', function() {
  167. if (jQuery('body').is('.tlypageguide-open')) {
  168. that.close();
  169. } else {
  170. that.open();
  171. }
  172. return false;
  173. });
  174. jQuery('.tlypageguide_close', this.$message).live('click', function() {
  175. that.close();
  176. return false;
  177. });
  178. /* interaction: item click */
  179. this.$all_items.live('click', function() {
  180. var new_index = jQuery(this).data('idx');
  181. that.track_event('PG.specific_elt');
  182. that.show_message(new_index);
  183. });
  184. /* interaction: fwd/back click */
  185. this.$fwd.live('click', function() {
  186. var new_index = (that.cur_idx + 1) % that.$items.length;
  187. that.track_event('PG.fwd');
  188. that.show_message(new_index);
  189. return false;
  190. });
  191. this.$back.live('click', function() {
  192. /*
  193. * If -n < x < 0, then the result of x % n will be x, which is
  194. * negative. To get a positive remainder, compute (x + n) % n.
  195. */
  196. var new_index = (that.cur_idx + that.$items.length - 1) % that.$items.length;
  197. that.track_event('PG.back');
  198. that.show_message(new_index, true);
  199. return false;
  200. });
  201. /* register resize callback */
  202. jQuery(window).resize(function() { that.position_tour(); });
  203. };
  204. tl.pg.PageGuide.prototype.show_message = function (new_index, left) {
  205. var old_idx = this.cur_idx,
  206. old_item = this.$items[old_idx],
  207. new_item = this.$items[new_index];
  208. this.cur_idx = new_index;
  209. if(this.handle_doc_switch){
  210. this.handle_doc_switch(jQuery(new_item).data('tourtarget'),
  211. jQuery(old_item).data('tourtarget'));
  212. }
  213. jQuery('div', this.$message).html(jQuery(new_item).children('div').html());
  214. this.$items.removeClass("tlypageguide-active");
  215. jQuery(new_item).addClass("tlypageguide-active");
  216. if (!tl.pg.isScrolledIntoView(jQuery(new_item))) {
  217. jQuery('html,body').animate({scrollTop: jQuery(new_item).offset().top - 50}, 500);
  218. }
  219. this.$message.not(':visible').show().animate({ 'height': '100px'}, 500);
  220. this.roll_number(jQuery('span', this.$message), jQuery(new_item).children('ins').html(), left);
  221. };
  222. tl.pg.PageGuide.prototype.roll_number = function (num_wrapper, new_text, left) {
  223. num_wrapper.animate({ 'text-indent': (left ? '' : '-') + '50px' }, 'fast', function() {
  224. num_wrapper.html(new_text);
  225. num_wrapper.css({ 'text-indent': (left ? '-' : '') + '50px' }, 'fast').animate({ 'text-indent': "0" }, 'fast');
  226. });
  227. };
  228. tl.pg.PageGuide.prototype.position_tour = function () {
  229. /* set PG element positions for visible tourtargets */
  230. this.$items = this.$all_items.filter(function () {
  231. return jQuery(jQuery(this).data('tourtarget')).is(':visible');
  232. });
  233. this.$items.each(function() {
  234. var arrow = jQuery(this),
  235. target = jQuery(arrow.data('tourtarget')).filter(':visible:first'),
  236. setLeft = target.offset().left,
  237. setTop = target.offset().top;
  238. if (arrow.hasClass("tlypageguide_top")) {
  239. setTop -= 60;
  240. } else if (arrow.hasClass("tlypageguide_bottom")) {
  241. setTop += target.outerHeight() + 15;
  242. } else {
  243. setTop += 5;
  244. }
  245. if (arrow.hasClass("tlypageguide_right")) {
  246. setLeft += target.outerWidth(false) + 15;
  247. } else if (arrow.hasClass("tlypageguide_left")) {
  248. setLeft -= 65;
  249. } else {
  250. setLeft += 5;
  251. }
  252. arrow.css({ "left": setLeft + "px", "top": setTop + "px" });
  253. });
  254. };