diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 0000000..ce4b4cd
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,245 @@
+{
+ "env": {
+ "browser": true,
+ "commonjs": true,
+ "node": true
+ },
+ "parserOptions": {
+ "ecmaVersion": 6,
+ "sourceType": "module"
+ },
+ "extends": "eslint:recommended",
+ "rules": {
+ "no-irregular-whitespace": [
+ "error",
+ {
+ "skipComments": true
+ }
+ ],
+ "accessor-pairs": "error",
+ "array-bracket-spacing": [
+ "error",
+ "never"
+ ],
+ "array-callback-return": "error",
+ "arrow-body-style": "error",
+ "arrow-parens": "error",
+ "arrow-spacing": "error",
+ "block-scoped-var": "error",
+ "block-spacing": [
+ "error",
+ "always"
+ ],
+ "brace-style": [
+ "error",
+ "1tbs",
+ {
+ "allowSingleLine": true
+ }
+ ],
+ "callback-return": "error",
+ "camelcase": "error",
+ "comma-spacing": [
+ "error",
+ {
+ "after": true,
+ "before": false
+ }
+ ],
+ "comma-style": [
+ "error",
+ "last"
+ ],
+ "complexity": "error",
+ "computed-property-spacing": "off",
+ "consistent-return": "off",
+ "consistent-this": "off",
+ "curly": "error",
+ "default-case": "error",
+ "dot-location": [
+ "error",
+ "property"
+ ],
+ "dot-notation": "error",
+ "eol-last": "off",
+ "eqeqeq": "off",
+ "func-names": "off",
+ "func-style": [
+ "error",
+ "expression"
+ ],
+ "generator-star-spacing": "error",
+ "global-require": "off",
+ "guard-for-in": "error",
+ "handle-callback-err": "error",
+ "id-blacklist": "error",
+ "id-length": "off",
+ "id-match": "error",
+ "indent": [
+ "error",
+ 2
+ ],
+ "init-declarations": "error",
+ "jsx-quotes": "error",
+ "key-spacing": "off",
+ "keyword-spacing": [
+ "error",
+ {
+ "after": true,
+ "before": true
+ }
+ ],
+ "linebreak-style": [
+ "error",
+ "unix"
+ ],
+ "lines-around-comment": "off",
+ "max-depth": "error",
+ "max-len": "off",
+ "max-nested-callbacks": "error",
+ "max-params": "error",
+ "max-statements": "off",
+ "new-cap": "error",
+ "new-parens": "error",
+ "newline-after-var": "off",
+ "newline-before-return": "off",
+ "newline-per-chained-call": "off",
+ "no-alert": "error",
+ "no-array-constructor": "error",
+ "no-bitwise": "off",
+ "no-caller": "error",
+ "no-catch-shadow": "error",
+ "no-cond-assign": [
+ "error",
+ "except-parens"
+ ],
+ "no-confusing-arrow": "error",
+ "no-continue": "error",
+ "no-div-regex": "error",
+ "no-else-return": "error",
+ "no-empty-function": "error",
+ "no-eq-null": "error",
+ "no-eval": "error",
+ "no-extend-native": "error",
+ "no-extra-bind": "error",
+ "no-extra-label": "error",
+ "no-extra-parens": "off",
+ "no-floating-decimal": "error",
+ "no-implicit-globals": "error",
+ "no-implied-eval": "error",
+ "no-inline-comments": "off",
+ "no-inner-declarations": [
+ "error",
+ "functions"
+ ],
+ "no-invalid-this": "off",
+ "no-iterator": "error",
+ "no-label-var": "error",
+ "no-labels": "error",
+ "no-lone-blocks": "error",
+ "no-lonely-if": "error",
+ "no-loop-func": "error",
+ "no-magic-numbers": "off",
+ "no-mixed-requires": "error",
+ "no-multi-spaces": "off",
+ "no-multi-str": "error",
+ "no-multiple-empty-lines": "error",
+ "no-native-reassign": "error",
+ "no-negated-condition": "off",
+ "no-nested-ternary": "off",
+ "no-new": "error",
+ "no-new-func": "error",
+ "no-new-object": "error",
+ "no-new-require": "error",
+ "no-new-wrappers": "error",
+ "no-octal-escape": "error",
+ "no-param-reassign": "off",
+ "no-path-concat": "error",
+ "no-plusplus": "off",
+ "no-process-env": "error",
+ "no-process-exit": "error",
+ "no-proto": "error",
+ "no-restricted-globals": "error",
+ "no-restricted-imports": "error",
+ "no-restricted-modules": "error",
+ "no-restricted-syntax": "error",
+ "no-return-assign": "error",
+ "no-script-url": "error",
+ "no-self-compare": "error",
+ "no-sequences": "error",
+ "no-shadow": "off",
+ "no-shadow-restricted-names": "error",
+ "no-spaced-func": "error",
+ "no-sync": "error",
+ "no-ternary": "off",
+ "no-throw-literal": "error",
+ "no-trailing-spaces": "error",
+ "no-undef-init": "error",
+ "no-undefined": "off",
+ "no-underscore-dangle": "off",
+ "no-unmodified-loop-condition": "error",
+ "no-unneeded-ternary": "error",
+ "no-unused-expressions": "off",
+ "no-use-before-define": "error",
+ "no-useless-call": "error",
+ "no-useless-concat": "error",
+ "no-useless-constructor": "error",
+ "no-var": "off",
+ "no-void": "error",
+ "no-warning-comments": "error",
+ "no-whitespace-before-property": "error",
+ "no-with": "error",
+ "object-curly-spacing": [
+ "error",
+ "always"
+ ],
+ "object-shorthand": "off",
+ "one-var": "off",
+ "one-var-declaration-per-line": "error",
+ "operator-assignment": [
+ "error",
+ "always"
+ ],
+ "operator-linebreak": "off",
+ "padded-blocks": "off",
+ "prefer-arrow-callback": "off",
+ "prefer-const": "error",
+ "prefer-reflect": "off",
+ "prefer-rest-params": "off",
+ "prefer-spread": "error",
+ "prefer-template": "off",
+ "quote-props": "off",
+ "quotes": [
+ "error",
+ "single"
+ ],
+ "radix": "error",
+ "require-jsdoc": "error",
+ "require-yield": "error",
+ "semi": "error",
+ "semi-spacing": "error",
+ "sort-imports": "error",
+ "sort-vars": "error",
+ "space-before-blocks": "error",
+ "space-before-function-paren": "off",
+ "space-in-parens": "off",
+ "space-infix-ops": "error",
+ "space-unary-ops": "error",
+ "spaced-comment": [
+ "error",
+ "always"
+ ],
+ "strict": "off",
+ "template-curly-spacing": "error",
+ "valid-jsdoc": "off",
+ "valid-typeof": "error",
+ "vars-on-top": "off",
+ "wrap-iife": "error",
+ "wrap-regex": "off",
+ "yield-star-spacing": "error",
+ "yoda": [
+ "error",
+ "never"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..bdb0cab
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,17 @@
+# Auto detect text files and perform LF normalization
+* text=auto
+
+# Custom for Visual Studio
+*.cs diff=csharp
+
+# Standard to msysgit
+*.doc diff=astextplain
+*.DOC diff=astextplain
+*.docx diff=astextplain
+*.DOCX diff=astextplain
+*.dot diff=astextplain
+*.DOT diff=astextplain
+*.pdf diff=astextplain
+*.PDF diff=astextplain
+*.rtf diff=astextplain
+*.RTF diff=astextplain
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..f076526
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+node_modules
+.npm-debug.log
+tmp
+.sass-cache
+.publish
+coverage
+*.sublime-*
\ No newline at end of file
diff --git a/README.md b/README.md
index 3e058fd..c64d836 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,61 @@
-# menuspy
+# MenuSpy
+
A JavaScript library to make navigation menus highlight active item based on the scroll position.
+
+* Features
+* Usage
+* Options
+* Examples
+
+## Usage
+
+Include MenuSpy
+
+```html
+
+```
+
+`MenuSpy` will be available in the global scope.
+
+Or install with NPM and require as a module
+
+```
+npm install menuspy
+```
+
+```js
+var MenuSpy = require('menuspy');
+```
+
+Initialize the plugin on your menu element
+
+```html
+
+```
+
+```js
+var elm = document.querySelector('#main-header');
+var ms = new MenuSpy(elm);
+```
+
+The `MenuSpy()` constructor accepts two arguments: the container element and an options object.
+
+
+## Options
+
+| Option | Type | Default | Description |
+| ------------------ | -------- | ----------------------------------- | ------------------------------------------------------------------------ |
+| `menuItemSelector` | String | `a[href^="#"]` | Menu items selector. |
+| `menuItemSelector` | String | `active` | Class applied on menu item relative to the currently visible section. |
+| `threshold` | Integer | `15` | Ammount of space between your menu and the next section to be activated. |
+| `hashTimeout` | Integer | `600` | Timeout to apply browser's hash location. |
+| `callback` | Function | `function(anchorElm, targetElm) {}` | A function to be called every time a new menu item activates. |
\ No newline at end of file
diff --git a/bower.json b/bower.json
new file mode 100644
index 0000000..d81a780
--- /dev/null
+++ b/bower.json
@@ -0,0 +1,26 @@
+{
+ "name": "MenuSpy",
+ "version": "1.0.0",
+ "homepage": "https://github.com/lcdsantos/menuspy",
+ "authors": [
+ "Leonardo Santos "
+ ],
+ "description": "A JavaScript library to make navigation menus highlight active item based on the scroll position.",
+ "main": "dist/menuspy.js",
+ "keywords": [
+ "nav",
+ "navigation",
+ "menu",
+ "menuspy",
+ "scrollspy"
+ ],
+ "license": "MIT",
+ "ignore": [
+ "**/.*",
+ "node_modules",
+ "bower_components",
+ "test",
+ "tests",
+ "examples"
+ ]
+}
diff --git a/dist/menuspy.js b/dist/menuspy.js
new file mode 100644
index 0000000..e36cd8f
--- /dev/null
+++ b/dist/menuspy.js
@@ -0,0 +1,169 @@
+/*! MenuSpy v1.0.0 (Aug 29 2016) - http://lcdsantos.github.io/menuspy/ - Copyright (c) 2016 Leonardo Santos; MIT License */
+(function (global, factory) {
+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
+ typeof define === 'function' && define.amd ? define(factory) :
+ (global.MenuSpy = factory());
+}(this, (function () { 'use strict';
+
+var utils = {
+ extend: function extend(a, b) {
+ for (var key in b) {
+ if (b.hasOwnProperty(key)) {
+ a[key] = b[key];
+ }
+ }
+
+ return a;
+ },
+
+ offset: function offset(el) {
+ var rect = el.getBoundingClientRect();
+
+ return {
+ top: rect.top + document.body.scrollTop,
+ left: rect.left + document.body.scrollLeft
+ };
+ },
+
+ scrollTop: function scrollTop() {
+ return window.pageYOffset || document.documentElement.scrollTop;
+ },
+
+ addClass: function addClass(el, className) {
+ if (el.classList) {
+ el.classList.add(className);
+ } else {
+ var classes = el.className.split(' ');
+ var existingIndex = classes.indexOf(className);
+
+ if (existingIndex === -1) {
+ classes.push(className);
+ }
+
+ el.className = classes.join(' ');
+ }
+ },
+
+ removeClass: function removeClass(el, className) {
+ if (el.classList) {
+ el.classList.remove(className);
+ } else {
+ el.className = el.className.replace(new RegExp(("(^|\\b)" + (className.split(' ').join('|')) + "(\\b|$)"), 'gi'), ' ');
+ }
+ },
+
+ debounce: function debounce(fn, delay) {
+ var timeout = null;
+ return function() {
+ var args = arguments;
+ var context = this;
+ if (!timeout) {
+ timeout = setTimeout(function () {
+ timeout = 0;
+ return fn.apply(context, args);
+ }, delay);
+ }
+ };
+ }
+};
+
+var MenuSpy = function MenuSpy(element, options) {
+ var this$1 = this;
+
+ if (!element) {
+ return;
+ }
+
+ var defaults = {
+ menuItemSelector: 'a[href^="#"]',
+ activeClass : 'active',
+ threshold : 15,
+ hashTimeout : 600,
+ callback : null
+ };
+
+ this.element = element;
+ this.options = utils.extend(defaults, options);
+
+ this.assignValues();
+ window.addEventListener('resize', utils.debounce(function () { return this$1.assignValues(); }));
+
+ this.debouncedHashFn = utils.debounce(function () {
+ if (history.replaceState) {
+ history.replaceState(null, null, ("#" + (this$1.lastId)));
+ } else {
+ var st = utils.scrollTop();
+ window.location.hash = this$1.lastId;
+ window.scrollTo(0, st);
+ }
+ }, this.options.hashTimeout);
+
+ this.cacheItems();
+ this.scrollFn();
+};
+
+MenuSpy.prototype.assignValues = function assignValues () {
+ this.currScrollTop = 0;
+ this.lastId = '';
+ this.menuHeight = this.element.offsetHeight + this.options.threshold;
+ this.menuItems = [].slice.call(this.element.querySelectorAll(this.options.menuItemSelector));
+};
+
+MenuSpy.prototype.cacheItems = function cacheItems () {
+ this.scrollItems = this.menuItems.map(function (a) {
+ var elm = document.querySelector(a.getAttribute('href'));
+ var offset = utils.offset(elm).top;
+
+ return { elm: elm, offset: offset };
+ });
+};
+
+MenuSpy.prototype.tick = function tick () {
+ var fromTop = this.currScrollTop + this.menuHeight;
+ var inViewElms = this.scrollItems
+ .filter(function (item) { return item.offset < fromTop; })
+ .map(function (item) { return item.elm; });
+
+ this.activateItem(inViewElms.pop());
+};
+
+MenuSpy.prototype.activateItem = function activateItem (inViewElm) {
+ var this$1 = this;
+
+ var id = inViewElm ? inViewElm.id : '';
+ var activeClass = this.options.activeClass;
+ var callback = this.options.callback;
+
+ if (this.lastId !== id) {
+ this.lastId = id;
+
+ this.menuItems.forEach(function (item) {
+ utils.removeClass(item.parentNode, activeClass);
+
+ if (item.getAttribute('href') === ("#" + id)) {
+ utils.addClass(item.parentNode, activeClass);
+
+ if (typeof callback === 'function') {
+ callback.call(this$1, item, inViewElm);
+ }
+
+ this$1.debouncedHashFn();
+ }
+ });
+ }
+};
+
+MenuSpy.prototype.scrollFn = function scrollFn () {
+ var st = utils.scrollTop();
+
+ if (this.currScrollTop !== st) {
+ this.currScrollTop = st;
+ this.tick();
+ }
+
+ window.requestAnimationFrame(this.scrollFn.bind(this));
+};
+
+return MenuSpy;
+
+})));
\ No newline at end of file
diff --git a/dist/menuspy.min.js b/dist/menuspy.min.js
new file mode 100644
index 0000000..1d5b05f
--- /dev/null
+++ b/dist/menuspy.min.js
@@ -0,0 +1,2 @@
+/*! MenuSpy v1.0.0 (Aug 29 2016) - http://lcdsantos.github.io/menuspy/ - Copyright (c) 2016 Leonardo Santos; MIT License */
+!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.MenuSpy=e()}(this,function(){"use strict";var t={extend:function(t,e){for(var s in e)e.hasOwnProperty(s)&&(t[s]=e[s]);return t},offset:function(t){var e=t.getBoundingClientRect();return{top:e.top+document.body.scrollTop,left:e.left+document.body.scrollLeft}},scrollTop:function(){return window.pageYOffset||document.documentElement.scrollTop},addClass:function(t,e){if(t.classList)t.classList.add(e);else{var s=t.className.split(" "),o=s.indexOf(e);o===-1&&s.push(e),t.className=s.join(" ")}},removeClass:function(t,e){t.classList?t.classList.remove(e):t.className=t.className.replace(new RegExp("(^|\\b)"+e.split(" ").join("|")+"(\\b|$)","gi")," ")},debounce:function(t,e){var s=null;return function(){var o=arguments,n=this;s||(s=setTimeout(function(){return s=0,t.apply(n,o)},e))}}},e=function(e,s){var o=this;if(e){var n={menuItemSelector:'a[href^="#"]',activeClass:"active",threshold:15,hashTimeout:600,callback:null};this.element=e,this.options=t.extend(n,s),this.assignValues(),window.addEventListener("resize",t.debounce(function(){return o.assignValues()})),this.debouncedHashFn=t.debounce(function(){if(history.replaceState)history.replaceState(null,null,"#"+o.lastId);else{var e=t.scrollTop();window.location.hash=o.lastId,window.scrollTo(0,e)}},this.options.hashTimeout),this.cacheItems(),this.scrollFn()}};return e.prototype.assignValues=function(){this.currScrollTop=0,this.lastId="",this.menuHeight=this.element.offsetHeight+this.options.threshold,this.menuItems=[].concat(this.element.querySelectorAll(this.options.menuItemSelector))},e.prototype.cacheItems=function(){this.scrollItems=this.menuItems.map(function(e){var s=document.querySelector(e.getAttribute("href")),o=t.offset(s).top;return{elm:s,offset:o}})},e.prototype.tick=function(){var t=this.currScrollTop+this.menuHeight,e=this.scrollItems.filter(function(e){return e.offset
+
+
+
+ MenuSpy
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/lavalamp.html b/examples/lavalamp.html
new file mode 100644
index 0000000..a01ae66
--- /dev/null
+++ b/examples/lavalamp.html
@@ -0,0 +1,69 @@
+
+
+
+
+ MenuSpy
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/sidemenu.html b/examples/sidemenu.html
new file mode 100644
index 0000000..0593c18
--- /dev/null
+++ b/examples/sidemenu.html
@@ -0,0 +1,59 @@
+
+
+
+
+ MenuSpy
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/karma.conf.js b/karma.conf.js
new file mode 100644
index 0000000..8051170
--- /dev/null
+++ b/karma.conf.js
@@ -0,0 +1,69 @@
+// Karma configuration
+
+module.exports = function(config) {
+ config.set({
+
+ // base path that will be used to resolve all patterns (eg. files, exclude)
+ basePath: '',
+
+
+ // frameworks to use
+ // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
+ frameworks: ['jasmine'],
+
+
+ // list of files / patterns to load in the browser
+ files: [
+ 'dist/menuspy.js',
+ 'test/**/*.spec.js'
+ ],
+
+
+ // list of files to exclude
+ exclude: [
+ ],
+
+
+ // preprocess matching files before serving them to the browser
+ // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
+ preprocessors: {
+ },
+
+
+ // test results reporter to use
+ // possible values: 'dots', 'progress'
+ // available reporters: https://npmjs.org/browse/keyword/karma-reporter
+ reporters: ['progress'],
+
+
+ // web server port
+ port: 9876,
+
+
+ // enable / disable colors in the output (reporters and logs)
+ colors: true,
+
+
+ // level of logging
+ // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
+ logLevel: config.LOG_INFO,
+
+
+ // enable / disable watching file and executing tests whenever any file changes
+ autoWatch: true,
+
+
+ // start these browsers
+ // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
+ browsers: ['PhantomJS'],
+
+
+ // Continuous Integration mode
+ // if true, Karma captures browsers, runs the tests and exits
+ singleRun: true,
+
+ // Concurrency level
+ // how many browser should be started simultaneous
+ concurrency: Infinity
+ })
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..1b13c88
--- /dev/null
+++ b/package.json
@@ -0,0 +1,48 @@
+{
+ "name": "menuspy",
+ "version": "1.0.0",
+ "title": "MenuSpy",
+ "author": {
+ "email": "leocs.1991@gmail.com",
+ "name": "Leonardo Santos"
+ },
+ "license": "MIT",
+ "description": "A JavaScript library to make navigation menus highlight active item based on the scroll position.",
+ "main": "dist/menuspy.js",
+ "keywords": [
+ "nav",
+ "navigation",
+ "menu",
+ "menuspy",
+ "scrollspy"
+ ],
+ "docs": "http://leocs.me/menuspy/",
+ "demo": "http://leocs.me/menuspy/demo",
+ "bugs": "https://github.com/lcdsantos/menuspy/issues",
+ "homepage": "http://leocs.me/menuspy/",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/lcdsantos/menuspy.git"
+ },
+ "devDependencies": {
+ "eslint": "^3.4.0",
+ "jasmine-core": "^2.5.0",
+ "karma": "^1.2.0",
+ "karma-chrome-launcher": "^2.0.0",
+ "karma-jasmine": "^1.0.2",
+ "karma-phantomjs-launcher": "^1.0.2",
+ "rollup-plugin-buble": "^0.13.0",
+ "rollup-plugin-eslint": "^2.0.2",
+ "rollup-watch": "^2.5.0",
+ "uglify-js": "^2.7.3"
+ },
+ "scripts": {
+ "start": "npm run dev",
+ "dev": "npm run build:rollup -- --watch",
+ "build": "npm run build:rollup && npm run build:minify",
+ "build:rollup": "rollup -c",
+ "build:minify": "uglifyjs dist/menuspy.js --comments /^/*!/ --compress --mangle --output dist/menuspy.min.js",
+ "lint": "eslint \"src/**\"",
+ "test": "karma start"
+ }
+}
diff --git a/rollup.config.js b/rollup.config.js
new file mode 100644
index 0000000..e5d6150
--- /dev/null
+++ b/rollup.config.js
@@ -0,0 +1,17 @@
+import buble from 'rollup-plugin-buble';
+import eslint from 'rollup-plugin-eslint';
+
+const pkg = require('./package.json');
+const banner = `/*! ${pkg.title} v${pkg.version} (${new Date().toString().substr(4, 11)}) - ${pkg.homepage} - Copyright (c) ${new Date().getFullYear()} Leonardo Santos; MIT License */`;
+
+export default {
+ entry: 'src/menuspy.js',
+ dest: 'dist/menuspy.js',
+ format: 'umd',
+ moduleName: 'MenuSpy',
+ plugins: [
+ eslint(),
+ buble()
+ ],
+ banner: banner
+};
\ No newline at end of file
diff --git a/src/menuspy.js b/src/menuspy.js
new file mode 100644
index 0000000..5d8a724
--- /dev/null
+++ b/src/menuspy.js
@@ -0,0 +1,98 @@
+import utils from './utils.js';
+
+class MenuSpy {
+ constructor(element, options) {
+ if (!element) {
+ return;
+ }
+
+ const defaults = {
+ menuItemSelector: 'a[href^="#"]',
+ activeClass : 'active',
+ threshold : 15,
+ hashTimeout : 600,
+ callback : null
+ };
+
+ this.element = element;
+ this.options = utils.extend(defaults, options);
+
+ this.assignValues();
+ window.addEventListener('resize', utils.debounce(() => this.assignValues()));
+
+ this.debouncedHashFn = utils.debounce(() => {
+ if (history.replaceState) {
+ history.replaceState(null, null, `#${this.lastId}`);
+ } else {
+ const st = utils.scrollTop();
+ window.location.hash = this.lastId;
+ window.scrollTo(0, st);
+ }
+ }, this.options.hashTimeout);
+
+ this.cacheItems();
+ this.scrollFn();
+ }
+
+ assignValues() {
+ this.currScrollTop = 0;
+ this.lastId = '';
+ this.menuHeight = this.element.offsetHeight + this.options.threshold;
+ this.menuItems = [].slice.call(this.element.querySelectorAll(this.options.menuItemSelector));
+ }
+
+ cacheItems() {
+ this.scrollItems = this.menuItems.map((a) => {
+ const elm = document.querySelector(a.getAttribute('href'));
+ const offset = utils.offset(elm).top;
+
+ return { elm, offset };
+ });
+ }
+
+ tick() {
+ const fromTop = this.currScrollTop + this.menuHeight;
+ const inViewElms = this.scrollItems
+ .filter((item) => item.offset < fromTop)
+ .map((item) => item.elm);
+
+ this.activateItem(inViewElms.pop());
+ }
+
+ activateItem(inViewElm) {
+ const id = inViewElm ? inViewElm.id : '';
+ const activeClass = this.options.activeClass;
+ const callback = this.options.callback;
+
+ if (this.lastId !== id) {
+ this.lastId = id;
+
+ this.menuItems.forEach((item) => {
+ utils.removeClass(item.parentNode, activeClass);
+
+ if (item.getAttribute('href') === `#${id}`) {
+ utils.addClass(item.parentNode, activeClass);
+
+ if (typeof callback === 'function') {
+ callback.call(this, item, inViewElm);
+ }
+
+ this.debouncedHashFn();
+ }
+ });
+ }
+ }
+
+ scrollFn() {
+ const st = utils.scrollTop();
+
+ if (this.currScrollTop !== st) {
+ this.currScrollTop = st;
+ this.tick();
+ }
+
+ window.requestAnimationFrame(this.scrollFn.bind(this));
+ }
+}
+
+export default MenuSpy;
\ No newline at end of file
diff --git a/src/utils.js b/src/utils.js
new file mode 100644
index 0000000..692ac8a
--- /dev/null
+++ b/src/utils.js
@@ -0,0 +1,63 @@
+const utils = {
+ extend(a, b) {
+ for (const key in b) {
+ if (b.hasOwnProperty(key)) {
+ a[key] = b[key];
+ }
+ }
+
+ return a;
+ },
+
+ offset(el) {
+ const rect = el.getBoundingClientRect();
+
+ return {
+ top: rect.top + document.body.scrollTop,
+ left: rect.left + document.body.scrollLeft
+ };
+ },
+
+ scrollTop() {
+ return window.pageYOffset || document.documentElement.scrollTop;
+ },
+
+ addClass(el, className) {
+ if (el.classList) {
+ el.classList.add(className);
+ } else {
+ const classes = el.className.split(' ');
+ const existingIndex = classes.indexOf(className);
+
+ if (existingIndex === -1) {
+ classes.push(className);
+ }
+
+ el.className = classes.join(' ');
+ }
+ },
+
+ removeClass(el, className) {
+ if (el.classList) {
+ el.classList.remove(className);
+ } else {
+ el.className = el.className.replace(new RegExp(`(^|\\b)${className.split(' ').join('|')}(\\b|$)`, 'gi'), ' ');
+ }
+ },
+
+ debounce(fn, delay) {
+ let timeout = null;
+ return function() {
+ const args = arguments;
+ const context = this;
+ if (!timeout) {
+ timeout = setTimeout(() => {
+ timeout = 0;
+ return fn.apply(context, args);
+ }, delay);
+ }
+ };
+ }
+};
+
+export default utils;
\ No newline at end of file
diff --git a/test/basic.spec.js b/test/basic.spec.js
new file mode 100644
index 0000000..3951497
--- /dev/null
+++ b/test/basic.spec.js
@@ -0,0 +1,5 @@
+describe('Basic suite', function() {
+ it('should be defined', function() {
+ expect(MenuSpy).toBeDefined();
+ });
+});
\ No newline at end of file