foundation.magellan.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. 'use strict';
  2. import $ from 'jquery';
  3. import { onLoad, GetYoDigits } from './foundation.core.utils';
  4. import { Plugin } from './foundation.core.plugin';
  5. import { SmoothScroll } from './foundation.smoothScroll';
  6. /**
  7. * Magellan module.
  8. * @module foundation.magellan
  9. * @requires foundation.smoothScroll
  10. */
  11. class Magellan extends Plugin {
  12. /**
  13. * Creates a new instance of Magellan.
  14. * @class
  15. * @name Magellan
  16. * @fires Magellan#init
  17. * @param {Object} element - jQuery object to add the trigger to.
  18. * @param {Object} options - Overrides to the default plugin settings.
  19. */
  20. _setup(element, options) {
  21. this.$element = element;
  22. this.options = $.extend({}, Magellan.defaults, this.$element.data(), options);
  23. this.className = 'Magellan'; // ie9 back compat
  24. this._init();
  25. this.calcPoints();
  26. }
  27. /**
  28. * Initializes the Magellan plugin and calls functions to get equalizer functioning on load.
  29. * @private
  30. */
  31. _init() {
  32. var id = this.$element[0].id || GetYoDigits(6, 'magellan');
  33. var _this = this;
  34. this.$targets = $('[data-magellan-target]');
  35. this.$links = this.$element.find('a');
  36. this.$element.attr({
  37. 'data-resize': id,
  38. 'data-scroll': id,
  39. 'id': id
  40. });
  41. this.$active = $();
  42. this.scrollPos = parseInt(window.pageYOffset, 10);
  43. this._events();
  44. }
  45. /**
  46. * Calculates an array of pixel values that are the demarcation lines between locations on the page.
  47. * Can be invoked if new elements are added or the size of a location changes.
  48. * @function
  49. */
  50. calcPoints() {
  51. var _this = this,
  52. body = document.body,
  53. html = document.documentElement;
  54. this.points = [];
  55. this.winHeight = Math.round(Math.max(window.innerHeight, html.clientHeight));
  56. this.docHeight = Math.round(Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight));
  57. this.$targets.each(function(){
  58. var $tar = $(this),
  59. pt = Math.round($tar.offset().top - _this.options.threshold);
  60. $tar.targetPoint = pt;
  61. _this.points.push(pt);
  62. });
  63. }
  64. /**
  65. * Initializes events for Magellan.
  66. * @private
  67. */
  68. _events() {
  69. var _this = this,
  70. $body = $('html, body'),
  71. opts = {
  72. duration: _this.options.animationDuration,
  73. easing: _this.options.animationEasing
  74. };
  75. $(window).one('load', function(){
  76. if(_this.options.deepLinking){
  77. if(location.hash){
  78. _this.scrollToLoc(location.hash);
  79. }
  80. }
  81. _this.calcPoints();
  82. _this._updateActive();
  83. });
  84. _this.onLoadListener = onLoad($(window), function () {
  85. _this.$element
  86. .on({
  87. 'resizeme.zf.trigger': _this.reflow.bind(_this),
  88. 'scrollme.zf.trigger': _this._updateActive.bind(_this)
  89. })
  90. .on('click.zf.magellan', 'a[href^="#"]', function (e) {
  91. e.preventDefault();
  92. var arrival = this.getAttribute('href');
  93. _this.scrollToLoc(arrival);
  94. });
  95. });
  96. this._deepLinkScroll = function(e) {
  97. if(_this.options.deepLinking) {
  98. _this.scrollToLoc(window.location.hash);
  99. }
  100. };
  101. $(window).on('hashchange', this._deepLinkScroll);
  102. }
  103. /**
  104. * Function to scroll to a given location on the page.
  105. * @param {String} loc - a properly formatted jQuery id selector. Example: '#foo'
  106. * @function
  107. */
  108. scrollToLoc(loc) {
  109. this._inTransition = true;
  110. var _this = this;
  111. var options = {
  112. animationEasing: this.options.animationEasing,
  113. animationDuration: this.options.animationDuration,
  114. threshold: this.options.threshold,
  115. offset: this.options.offset
  116. };
  117. SmoothScroll.scrollToLoc(loc, options, function() {
  118. _this._inTransition = false;
  119. })
  120. }
  121. /**
  122. * Calls necessary functions to update Magellan upon DOM change
  123. * @function
  124. */
  125. reflow() {
  126. this.calcPoints();
  127. this._updateActive();
  128. }
  129. /**
  130. * Updates the visibility of an active location link, and updates the url hash for the page, if deepLinking enabled.
  131. * @private
  132. * @function
  133. * @fires Magellan#update
  134. */
  135. _updateActive(/*evt, elem, scrollPos*/) {
  136. if(this._inTransition) return;
  137. const newScrollPos = parseInt(window.pageYOffset, 10);
  138. const isScrollingUp = this.scrollPos > newScrollPos;
  139. this.scrollPos = newScrollPos;
  140. let activeIdx;
  141. // Before the first point: no link
  142. if(newScrollPos < this.points[0]){ /* do nothing */ }
  143. // At the bottom of the page: last link
  144. else if(newScrollPos + this.winHeight === this.docHeight){ activeIdx = this.points.length - 1; }
  145. // Otherwhise, use the last visible link
  146. else{
  147. const visibleLinks = this.points.filter((p, i) => {
  148. return (p - this.options.offset - (isScrollingUp ? this.options.threshold : 0)) <= newScrollPos;
  149. });
  150. activeIdx = visibleLinks.length ? visibleLinks.length - 1 : 0;
  151. }
  152. // Get the new active link
  153. const $oldActive = this.$active;
  154. let activeHash = '';
  155. if(typeof activeIdx !== 'undefined'){
  156. this.$active = this.$links.filter('[href="#' + this.$targets.eq(activeIdx).data('magellan-target') + '"]');
  157. if (this.$active.length) activeHash = this.$active[0].getAttribute('href');
  158. }else{
  159. this.$active = $();
  160. }
  161. const isNewActive = !(!this.$active.length && !$oldActive.length) && !this.$active.is($oldActive);
  162. const isNewHash = activeHash !== window.location.hash;
  163. // Update the active link element
  164. if(isNewActive) {
  165. $oldActive.removeClass(this.options.activeClass);
  166. this.$active.addClass(this.options.activeClass);
  167. }
  168. // Update the hash (it may have changed with the same active link)
  169. if(this.options.deepLinking && isNewHash){
  170. if(window.history.pushState){
  171. // Set or remove the hash (see: https://stackoverflow.com/a/5298684/4317384
  172. const url = activeHash ? activeHash : window.location.pathname + window.location.search;
  173. window.history.pushState(null, null, url);
  174. }else{
  175. window.location.hash = activeHash;
  176. }
  177. }
  178. if (isNewActive) {
  179. /**
  180. * Fires when magellan is finished updating to the new active element.
  181. * @event Magellan#update
  182. */
  183. this.$element.trigger('update.zf.magellan', [this.$active]);
  184. }
  185. }
  186. /**
  187. * Destroys an instance of Magellan and resets the url of the window.
  188. * @function
  189. */
  190. _destroy() {
  191. this.$element.off('.zf.trigger .zf.magellan')
  192. .find(`.${this.options.activeClass}`).removeClass(this.options.activeClass);
  193. if(this.options.deepLinking){
  194. var hash = this.$active[0].getAttribute('href');
  195. window.location.hash.replace(hash, '');
  196. }
  197. $(window).off('hashchange', this._deepLinkScroll)
  198. if (this.onLoadListener) $(window).off(this.onLoadListener);
  199. }
  200. }
  201. /**
  202. * Default settings for plugin
  203. */
  204. Magellan.defaults = {
  205. /**
  206. * Amount of time, in ms, the animated scrolling should take between locations.
  207. * @option
  208. * @type {number}
  209. * @default 500
  210. */
  211. animationDuration: 500,
  212. /**
  213. * Animation style to use when scrolling between locations. Can be `'swing'` or `'linear'`.
  214. * @option
  215. * @type {string}
  216. * @default 'linear'
  217. * @see {@link https://api.jquery.com/animate|Jquery animate}
  218. */
  219. animationEasing: 'linear',
  220. /**
  221. * Number of pixels to use as a marker for location changes.
  222. * @option
  223. * @type {number}
  224. * @default 50
  225. */
  226. threshold: 50,
  227. /**
  228. * Class applied to the active locations link on the magellan container.
  229. * @option
  230. * @type {string}
  231. * @default 'is-active'
  232. */
  233. activeClass: 'is-active',
  234. /**
  235. * Allows the script to manipulate the url of the current page, and if supported, alter the history.
  236. * @option
  237. * @type {boolean}
  238. * @default false
  239. */
  240. deepLinking: false,
  241. /**
  242. * Number of pixels to offset the scroll of the page on item click if using a sticky nav bar.
  243. * @option
  244. * @type {number}
  245. * @default 0
  246. */
  247. offset: 0
  248. }
  249. export {Magellan};