├── .editorconfig ├── .gitattributes ├── .gitconfig ├── .gitignore ├── .jshintrc ├── .npmignore ├── Gruntfile.js ├── README.md ├── bower.json ├── demo ├── css │ ├── src │ │ └── style.scss │ └── style.css ├── img │ ├── favicon.png │ ├── logo.svg │ └── paypal.png ├── index.html └── scripts │ └── navsync.min.js ├── dist ├── navsync.js └── navsync.min.js ├── package.json └── src └── navsync.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | indent_style = tab 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | [package.json] 14 | ; The indent size used in the `package.json` file cannot be changed 15 | ; https://github.com/npm/npm/pull/3180#issuecomment-16336516 16 | indent_size = 2 17 | indent_style = space 18 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitconfig: -------------------------------------------------------------------------------- 1 | #git stpp demo origin gh-pages 2 | [alias] 3 | stpp = subtree push --prefix -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # ========================= 21 | # Operating System Files 22 | # ========================= 23 | 24 | # OSX 25 | # ========================= 26 | 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear on external disk 35 | .Spotlight-V100 36 | .Trashes 37 | 38 | # Directories potentially created on remote AFP share 39 | .AppleDB 40 | .AppleDesktop 41 | Network Trash Folder 42 | Temporary Items 43 | .apdisk 44 | 45 | # Node 46 | node_modules 47 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "boss": true, 3 | "curly": true, 4 | "eqeqeq": true, 5 | "eqnull": true, 6 | "expr": true, 7 | "immed": true, 8 | "noarg": true, 9 | "onevar": true, 10 | "quotmark": "double", 11 | "unused": true, 12 | "node": true 13 | } 14 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | grunt.initConfig({ 4 | 5 | // Import package manifest 6 | pkg: grunt.file.readJSON("package.json"), 7 | 8 | // Banner definitions 9 | meta: { 10 | banner: "/*\n" + 11 | " * <%= pkg.title || pkg.name %> - v<%= pkg.version %>\n" + 12 | " * <%= pkg.description %>\n" + 13 | " * <%= pkg.homepage %>\n" + 14 | " *\n" + 15 | " * Made by <%= pkg.author.name %>\n" + 16 | " * Under <%= pkg.license %> License\n" + 17 | " */\n" 18 | }, 19 | 20 | // Concat definitions 21 | concat: { 22 | options: { 23 | banner: "<%= meta.banner %>" 24 | }, 25 | dist: { 26 | src: ["src/navsync.js"], 27 | dest: "dist/navsync.js" 28 | } 29 | }, 30 | 31 | // Lint definitions 32 | jshint: { 33 | files: ["src/navsync.js"], 34 | options: { 35 | jshintrc: ".jshintrc" 36 | } 37 | }, 38 | 39 | // Minify definitions 40 | uglify: { 41 | my_target: { 42 | files: { 43 | 'dist/navsync.min.js': 'src/navsync.js', 44 | 'demo/scripts/navsync.min.js' : 'src/navsync.js' 45 | } 46 | }, 47 | options: { 48 | banner: "<%= meta.banner %>" 49 | } 50 | }, 51 | 52 | //sass 53 | sass: { 54 | options: { 55 | sourceMap: false 56 | }, 57 | dist: { 58 | files: { 59 | 'demo/css/style.css': 'demo/css/src/style.scss' 60 | } 61 | } 62 | }, 63 | 64 | // watch for changes to source 65 | // Better than calling grunt a million times 66 | // (call 'grunt watch') 67 | watch: { 68 | files: ['src/*', 'demo/css/src/*'], 69 | tasks: ['default'] 70 | } 71 | 72 | }); 73 | 74 | grunt.loadNpmTasks("grunt-contrib-concat"); 75 | grunt.loadNpmTasks("grunt-contrib-jshint"); 76 | grunt.loadNpmTasks("grunt-contrib-uglify"); 77 | grunt.loadNpmTasks("grunt-sass"); 78 | grunt.loadNpmTasks("grunt-contrib-watch"); 79 | 80 | 81 | grunt.registerTask("build", ["concat", "uglify"]); 82 | grunt.registerTask("default", ["sass", "jshint", "build"]); 83 | grunt.registerTask("travis", ["default"]); 84 | 85 | }; 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NavSync 2 | 3 | NavSync is an easy to use jQuery plugin that does the heavylifting for syncing your navigation to your content. All it requires is a semantically built navigation menu and a couple of lines of code, easy as pie. 4 | 5 | ###Demo 6 | Demo the buttery smoothness of NavSync at the live [demo page!](http://www.ryanpark.co.uk/navsync) 7 | 8 | ### Features 9 | So far the following is what NavSync supports; 10 | - Synchronizing menus to the content that is currently on screen. 11 | - Custom Animation Time 12 | - Accounting for fixed headers 13 | - Buttery Smooth 60 FPS animation* 14 | 15 | 16 | Below is what NavSync does **not** support, but is planned; 17 | - Custom Easing 18 | - Scroll to Anchor on Page Load 19 | 20 | *With responsible use of CSS 21 | 22 | # Getting Started 23 | Loading NavSync - Yes, that's all it takes. 24 | ```javascript 25 | $(window).load(function() { 26 | $("nav").navSync(); 27 | }); 28 | ``` 29 | Your HTML structure: 30 | ```html 31 | 37 | 38 |
39 |

I will be scrolled to!

40 |
41 | 42 |
43 |

I will be scrolled to!

44 |
45 | 46 |
47 |

I will be scrolled to!

48 |
49 | 50 |
51 |

I will not be scrolled to!

52 |
53 | ``` 54 | 55 | Note that link number 4 is a link to an external page, links without an anchor will be ignored. 56 | 57 | # Advanced Options 58 | NavSync has a number of parameters to make use of, for example the ability to tweak with scrolling behaviour, speed of scrolling animations, and the name of the class applied to your header items. These are available to let you customize the behaviour of NavSync however you see fit. 59 | ### highlightClass 60 | ```javascript 61 | $("nav").navSync({hightlightClass : "highlight-menu-item"}); 62 | ``` 63 | Changes the default class from "navsync-menu-highlight" to "highlight-menu-item". The class is removed once the item has been scrolled past, and reapplied if the user scrolls up. 64 | ### animationTime 65 | ```javascript 66 | $("nav").navSync({animationTime : 800}); 67 | ``` 68 | Changes the default animation time for scrolling from 300ms to 800ms, generally scroll speeds of above a second are unwarranted. This should serve as a smell embellishment, not a super funky feature of your website. 69 | ### ignoreNavHeightScroll 70 | ```javascript 71 | $("nav").navSync({ignoreNavHeightScroll: true}); 72 | ``` 73 | By default NavSync will also take into account the height of the header when scrolling to a given anchor, this option can be disabled independently of the former option and is advised if that is the case. 74 | 75 | ### Performance Tips 76 | Some CSS properties will severely affect draw performance, there are a few fixes to these however styling responsibly will always give the best framerate. 77 | 78 | #### Shadows 79 | Dynamically generated shadows for effects such as CSS box-shadow and text-shadow will redraw each frame, and can severely effect framerate. 80 | 81 | #### Filters 82 | Some popular effects involve blurring images or otherwise filtering depending on where the user scrolls to, these effects are also very expensive. 83 | 84 | #### Position Changes 85 | Anything that changes the position of an element will significantly effect framerate, there are two workarounds. Either prerendering your CSS animation, or using the translate transform instead. See the performance difference between using effects like [top/left/right/bottom](http://codepen.io/paulirish/full/nkwKs) and [translate](http://codepen.io/paulirish/full/LsxyF). 86 | 87 | ### Performance Fixes 88 | If you have an effect which you feel is essential and worth a hit in performance, it can sometimes be compensated for by forcing hardware acceleration. To do this, use the the following css: 89 | ```css 90 | transform: translateZ(0) 91 | ``` 92 | 93 | Note this cannot be used many times, and doing so will again result in poor performance. 94 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "navsync", 3 | "version": "0.0.3", 4 | "homepage": "https://github.com/rp4rk/navsync", 5 | "authors": [ 6 | "Ryan Park " 7 | ], 8 | "description": "Syncs navigation menus to page anchors", 9 | "main": "src/navsync.js", 10 | "keywords": [ 11 | "jquery", 12 | "plugin", 13 | "navsync", 14 | "jquery-plugin", 15 | "navigation", 16 | "anchors", 17 | "scrollto", 18 | "scroll" 19 | ], 20 | "license": "MIT" 21 | } 22 | -------------------------------------------------------------------------------- /demo/css/src/style.scss: -------------------------------------------------------------------------------- 1 | // Variables 2 | $light: darken(white, 3%); 3 | $dark: lighten(black, 20%); 4 | $medium: lighten(black, 35%); 5 | $highlight: #008B8B; 6 | $font-size: 12rem; 7 | $line-height: $font-size/4.5; 8 | $header-height: 90px; 9 | 10 | // Fonts 11 | @import url(http://fonts.googleapis.com/css?family=Open+Sans:400,600,700); 12 | @import url(http://weloveiconfonts.com/api/?family=entypo); 13 | 14 | // NavSync Highlight 15 | .navsync-menu-highlight { 16 | background-color: $highlight; 17 | color: white; 18 | &:hover { 19 | background-color: $highlight; 20 | } 21 | } 22 | 23 | // Logo splash 24 | .splash { 25 | background-color: $highlight; 26 | overflow: hidden; 27 | position: relative; 28 | 29 | img, object { 30 | max-height: 65vh; 31 | position: absolute; 32 | bottom: -5px; 33 | margin: 0 auto 0 auto; 34 | left: 50%; 35 | transform: translateX(-50%); 36 | 37 | @media (max-width: 750px) { 38 | bottom: auto; 39 | } 40 | } 41 | 42 | 43 | } 44 | 45 | // SVG animation 46 | #svg_19 { 47 | -webkit-transform-origin: 50% 50%; 48 | -ms-transform-origin: 50% 50%; 49 | transform-origin: 50% 50%; 50 | -webkit-animation: Sway 5s infinite ease-in-out alternate; 51 | animation: Sway 5s infinite ease-in-out alternate; 52 | } 53 | 54 | @-webkit-keyframes Sway { 55 | 56 | from { 57 | -webkit-transform: rotate(-5deg); 58 | transform: rotate(-5deg); 59 | } 60 | 61 | to { 62 | -webkit-transform: rotate(5deg); 63 | transform: rotate(5deg); 64 | } 65 | 66 | } 67 | 68 | @keyframes Sway { 69 | 70 | from { 71 | -webkit-transform: rotate(-5deg); 72 | transform: rotate(-5deg); 73 | } 74 | 75 | to { 76 | -webkit-transform: rotate(5deg); 77 | transform: rotate(5deg); 78 | } 79 | 80 | } 81 | 82 | h1.fancy { 83 | font-weight: 700; 84 | color: white; 85 | text-align: center; 86 | 87 | &:after { 88 | content: ' '; 89 | width: 80%; 90 | height: 2px; 91 | background-color: white; 92 | display: block; 93 | z-index: 100; 94 | margin: 20px auto 0 auto; 95 | } 96 | } 97 | 98 | 99 | /* entypo */ 100 | 101 | [class*="entypo-"]:before { 102 | font-family: 'entypo', sans-serif; 103 | font-size: 12rem; 104 | width: 100%; 105 | } 106 | 107 | [class*="entypo-"] { 108 | text-align: center; 109 | width: 100%; 110 | display: block; 111 | transition: color 0.3s ease-in-out; 112 | &:hover { 113 | color: $highlight; 114 | } 115 | } 116 | 117 | .darkdiv a { 118 | color: $light; 119 | } 120 | 121 | //General 122 | body { 123 | background-color: $light; 124 | color: $dark; 125 | font-family: 'Open Sans', sans-serif; 126 | box-sizing: border-box; 127 | } 128 | 129 | a { 130 | text-decoration: none; 131 | color: $dark; 132 | } 133 | 134 | form { 135 | display: inline-block; 136 | text-align: center; 137 | } 138 | 139 | .flexcenter { 140 | display: flex; 141 | justify-content: center; 142 | flex-wrap: wrap; 143 | } 144 | 145 | .tag { 146 | font-size: 1.25rem; 147 | text-transform: uppercase; 148 | padding: 4px; 149 | background-color: $highlight; 150 | font-weight: 700; 151 | color: white; 152 | } 153 | 154 | .nocaps { 155 | text-transform: none !important; 156 | } 157 | 158 | h5 { 159 | text-transform: uppercase; 160 | text-align: center; 161 | font-weight: 700; 162 | &:after { 163 | content: ' '; 164 | width: 200px; 165 | margin-top: 20px; 166 | margin-left: auto; 167 | margin-right: auto; 168 | background-color: lighten($medium, 40%); 169 | height: 1px; 170 | display: block; 171 | } 172 | } 173 | 174 | //Navigation 175 | nav { 176 | height: $header-height; 177 | background-color: white; 178 | line-height: $header-height; 179 | position: fixed; 180 | width: 100%; 181 | z-index: 100; 182 | box-shadow: 0 5px 3px rgba(0, 0, 0, 0.03); 183 | transform: translateZ(0); 184 | 185 | @media (max-width: 750px) { 186 | overflow-y: hidden; 187 | overflow-x: scroll; 188 | white-space: nowrap; 189 | } 190 | } 191 | 192 | nav a { 193 | text-decoration: none; 194 | font-weight: 700; 195 | padding: 12px 15px 12px 15px; 196 | transition: color 0.3s ease-in-out, background-color 0.3s ease-in-out; 197 | color: $dark; 198 | will-change: background-color; 199 | 200 | &:hover { 201 | color: white; 202 | background-color: $medium; 203 | } 204 | 205 | @media (max-width: 750px) { 206 | padding: 5px; 207 | } 208 | } 209 | 210 | nav h1 { 211 | display: inline; 212 | margin: 0; 213 | font-weight: 600; 214 | 215 | @media (max-width: 750px) { 216 | display: none; 217 | } 218 | 219 | } 220 | 221 | 222 | p { 223 | line-height: $line-height; 224 | color: $medium; 225 | } 226 | 227 | .links { 228 | float: right; 229 | display: inline; 230 | clear: left; 231 | 232 | @media (max-width: 750px) { 233 | float: none; 234 | } 235 | } 236 | 237 | //Container Stylings 238 | .fullscreen { 239 | min-height: 100vh; 240 | padding-top: $header-height + 20px; 241 | padding-bottom: $header-height + 20px; 242 | } 243 | 244 | .darkdiv { 245 | background-color: $dark; 246 | h1, 247 | h2, 248 | h3, 249 | h4, 250 | h5 { 251 | color: $light; 252 | } 253 | p { 254 | color: darken($light, 40%); 255 | } 256 | } -------------------------------------------------------------------------------- /demo/css/style.css: -------------------------------------------------------------------------------- 1 | @import url(http://fonts.googleapis.com/css?family=Open+Sans:400,600,700); 2 | @import url(http://weloveiconfonts.com/api/?family=entypo); 3 | .navsync-menu-highlight { 4 | background-color: #008B8B; 5 | color: white; } 6 | .navsync-menu-highlight:hover { 7 | background-color: #008B8B; } 8 | 9 | .splash { 10 | background-color: #008B8B; 11 | overflow: hidden; 12 | position: relative; } 13 | .splash img, .splash object { 14 | max-height: 65vh; 15 | position: absolute; 16 | bottom: -5px; 17 | margin: 0 auto 0 auto; 18 | left: 50%; 19 | transform: translateX(-50%); } 20 | @media (max-width: 750px) { 21 | .splash img, .splash object { 22 | bottom: auto; } } 23 | 24 | #svg_19 { 25 | -webkit-transform-origin: 50% 50%; 26 | -ms-transform-origin: 50% 50%; 27 | transform-origin: 50% 50%; 28 | -webkit-animation: Sway 5s infinite ease-in-out alternate; 29 | animation: Sway 5s infinite ease-in-out alternate; } 30 | 31 | @-webkit-keyframes Sway { 32 | from { 33 | -webkit-transform: rotate(-5deg); 34 | transform: rotate(-5deg); } 35 | to { 36 | -webkit-transform: rotate(5deg); 37 | transform: rotate(5deg); } } 38 | 39 | @keyframes Sway { 40 | from { 41 | -webkit-transform: rotate(-5deg); 42 | transform: rotate(-5deg); } 43 | to { 44 | -webkit-transform: rotate(5deg); 45 | transform: rotate(5deg); } } 46 | 47 | h1.fancy { 48 | font-weight: 700; 49 | color: white; 50 | text-align: center; } 51 | h1.fancy:after { 52 | content: ' '; 53 | width: 80%; 54 | height: 2px; 55 | background-color: white; 56 | display: block; 57 | z-index: 100; 58 | margin: 20px auto 0 auto; } 59 | 60 | /* entypo */ 61 | [class*="entypo-"]:before { 62 | font-family: 'entypo', sans-serif; 63 | font-size: 12rem; 64 | width: 100%; } 65 | 66 | [class*="entypo-"] { 67 | text-align: center; 68 | width: 100%; 69 | display: block; 70 | transition: color 0.3s ease-in-out; } 71 | [class*="entypo-"]:hover { 72 | color: #008B8B; } 73 | 74 | .darkdiv a { 75 | color: #f7f7f7; } 76 | 77 | body { 78 | background-color: #f7f7f7; 79 | color: #333333; 80 | font-family: 'Open Sans', sans-serif; 81 | box-sizing: border-box; } 82 | 83 | a { 84 | text-decoration: none; 85 | color: #333333; } 86 | 87 | form { 88 | display: inline-block; 89 | text-align: center; } 90 | 91 | .flexcenter { 92 | display: flex; 93 | justify-content: center; 94 | flex-wrap: wrap; } 95 | 96 | .tag { 97 | font-size: 1.25rem; 98 | text-transform: uppercase; 99 | padding: 4px; 100 | background-color: #008B8B; 101 | font-weight: 700; 102 | color: white; } 103 | 104 | .nocaps { 105 | text-transform: none !important; } 106 | 107 | h5 { 108 | text-transform: uppercase; 109 | text-align: center; 110 | font-weight: 700; } 111 | h5:after { 112 | content: ' '; 113 | width: 200px; 114 | margin-top: 20px; 115 | margin-left: auto; 116 | margin-right: auto; 117 | background-color: #bfbfbf; 118 | height: 1px; 119 | display: block; } 120 | 121 | nav { 122 | height: 90px; 123 | background-color: white; 124 | line-height: 90px; 125 | position: fixed; 126 | width: 100%; 127 | z-index: 100; 128 | box-shadow: 0 5px 3px rgba(0, 0, 0, 0.03); 129 | transform: translateZ(0); } 130 | @media (max-width: 750px) { 131 | nav { 132 | overflow-y: hidden; 133 | overflow-x: scroll; 134 | white-space: nowrap; } } 135 | 136 | nav a { 137 | text-decoration: none; 138 | font-weight: 700; 139 | padding: 12px 15px 12px 15px; 140 | transition: color 0.3s ease-in-out, background-color 0.3s ease-in-out; 141 | color: #333333; 142 | will-change: background-color; } 143 | nav a:hover { 144 | color: white; 145 | background-color: #595959; } 146 | @media (max-width: 750px) { 147 | nav a { 148 | padding: 5px; } } 149 | 150 | nav h1 { 151 | display: inline; 152 | margin: 0; 153 | font-weight: 600; } 154 | @media (max-width: 750px) { 155 | nav h1 { 156 | display: none; } } 157 | 158 | p { 159 | line-height: 2.6666666667rem; 160 | color: #595959; } 161 | 162 | .links { 163 | float: right; 164 | display: inline; 165 | clear: left; } 166 | @media (max-width: 750px) { 167 | .links { 168 | float: none; } } 169 | 170 | .fullscreen { 171 | min-height: 100vh; 172 | padding-top: 110px; 173 | padding-bottom: 110px; } 174 | 175 | .darkdiv { 176 | background-color: #333333; } 177 | .darkdiv h1, 178 | .darkdiv h2, 179 | .darkdiv h3, 180 | .darkdiv h4, 181 | .darkdiv h5 { 182 | color: #f7f7f7; } 183 | .darkdiv p { 184 | color: #919191; } 185 | -------------------------------------------------------------------------------- /demo/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rp4rk/navsync/3c00fb5f7226f9fe83810133aa810834276f9328/demo/img/favicon.png -------------------------------------------------------------------------------- /demo/img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | background 64 | 65 | 66 | 67 | Layer 1 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /demo/img/paypal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rp4rk/navsync/3c00fb5f7226f9fe83810133aa810834276f9328/demo/img/paypal.png -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NavSync 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 30 | 31 |
32 |
33 |

Buttery Smooth Navigation Management

34 | 35 | 36 | 37 |
38 |
39 | 40 |
41 |

Getting Started

42 |
43 |
44 |

NavSync is an easy to use jQuery plugin that does the heavylifting for syncing your navigation to your content. All it requires is a semantically built navigation menu and a couple of lines of code, easy as pie.

45 |

Don't believe me? The tiny code snippet below is all you need to get started with NavSync!

46 | Javascript 47 |
$(window).load(function() {
 48 |   $("nav").navSync();
 49 | });
50 |
51 |
52 |

The HTML markup required is also extremely simple, see below for an example. Note that NavSync also supports external links, and will only trigger on elements with a valid anchor on the page. Link number 4 will load without problems.

53 | HTML 54 |
<nav>
 55 |   <a href="#anchor-1">First Link</a>
 56 |   <a href="#anchor-2">Second Link</a>
 57 |   <a href="#anchor-3">Third Link</a>
 58 |   <a href="/example/">Fourth Link</a>
 59 | </nav>
60 |
61 |
62 | 63 |
64 | 65 |
66 |
67 |

Advanced

68 |
69 |
70 |

NavSync has a number of parameters to make use of, for example the ability to tweak with scrolling behaviour, speed of scrolling animations, and the name of the class applied to your header items. These are available to let you customize the behaviour of NavSync to however you see fit.

71 | 72 |
highlightClass
73 | Example 74 | String 75 |
$("nav").navSync({hightlightClass : "highlight-menu-item"});
76 |

Changes the default class from "navsync-menu-highlight" to "highlight-menu-item". The class is removed once the item has been scrolled past, and reapplied if the user scrolls up.

77 | 78 |
animationTime
79 | Example 80 | Integer 81 |
$("nav").navSync({animationTime : 800});
82 |

Changes the default animation time for scrolling from 300ms to 800ms, generally scroll speeds of above a second are unwarranted. This should serve as a smell embellishment, not a super funky feature of your website.

83 | 84 |
ignoreNavHeightScroll
85 | Example 86 | Boolean 87 |
$("nav").navSync({ignoreNavHeightScroll: true});
88 |

By default NavSync will also take into account the height of the header when scrolling to a given anchor, this option can be disabled independently of the former option and is advised if that is the case.

89 | 90 |
91 |
92 |
93 |
94 | 95 |
96 |
97 |
98 |
Project
99 |

To report bugs, problems, or other inconsistencies you can find the GitHub repo for the project below.

100 | 101 |
102 | 103 |
104 |
Contact Me
105 |

You can contact me via my GitHub page or social media, please do not report bugs through social media.

106 | 107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 | 115 |
116 |
117 |
118 |
Download
119 |
120 |

NavSync is developed using my free time, I don't require a charge but any support is always appreciated. Feel free to buy me a beer :)

121 | 122 |
123 | 124 | 125 | 126 | 127 | 128 |
129 | 130 |
131 |
132 |
133 |
134 |
GitHub
135 | 136 |
137 |
138 |
NPM
139 | 140 |
141 |
142 |
143 |
144 |
145 | 146 | 147 | 148 | 149 | 150 | 151 | 159 | 160 | 161 | -------------------------------------------------------------------------------- /demo/scripts/navsync.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * navsync - v0.1.0 3 | * Sync navigation menus to page anchors 4 | * https://github.com/rp4rk/navsync 5 | * 6 | * Made by Ryan Park 7 | * Under MIT License 8 | */ 9 | !function(a,b){"use strict";function c(b,c){this.element=b,this.settings=a.extend({},e,c),this._defaults=e,this._name=d,this.init()}var d="navSync",e={highlightClass:"navsync-menu-highlight",ignoreNavHeightScroll:!1,animationTime:500};a.extend(c.prototype,{init:function(){var c=a(this.element);this.highlightClass=this.settings.highlightClass?this.settings.highlightClass:this._defaults.highlightClass,this.initHeaderHeight(c),a(b).resize(function(){this.initHeaderHeight(c),this.watchedDivs=this.buildWatchedDivs(c),this.checkForHighlight(this.watchedDivs),this.watchedDivs.forEach(function(a){a[3].unbind("click")}),this.initClickBinding()}.bind(this)),this.watchedDivs=this.buildWatchedDivs(c),this.initClickBinding(),this.checkForHighlight(this.watchedDivs),a(b).bind("scroll",function(){this.scrollFunc()}.bind(this))},initHeaderHeight:function(a){this.headerHeight=this.settings.ignoreNavHeightScroll?0:a.height()},scrollFunc:function(){b.requestAnimationFrame(function(){this.checkForHighlight(this.watchedDivs)}.bind(this))},checkForHighlight:function(a){a.forEach(function(a){this.isInView(a[1],a[2])?a[3].addClass(this.highlightClass):a[3].removeClass(this.highlightClass)}.bind(this))},getAnchors:function(b){var c=[];return b.find("a").each(function(b,d){c.push(a(d))}),c},buildWatchedDivs:function(b){var c=this.getAnchors(b).filter(function(a){return this.isAnchor(a)}.bind(this)).map(function(b){var c=a(b.attr("href").replace("/",""));return[c,c.offset().top,c.offset().top+c.outerHeight(!0),b]}.bind(this));return c},isInView:function(c,d){var e=a(b).scrollTop()+a(b).height()/2;return e>=c&&d>=e},isAnchor:function(a){var b=a.attr("href").replace("/","");return"#"===b.charAt(0)},scrollTo:function(c,d,e){a("html, body").animate({scrollTop:c-e},d,function(){a(b).bind("scroll",function(){this.scrollFunc()}.bind(this)),this.checkForHighlight(this.watchedDivs)}.bind(this))},initClickBinding:function(){this.watchedDivs.forEach(function(c){c[3].click(function(d){a(b).unbind("scroll"),d.preventDefault(),this.scrollTo(c[1],500,this.headerHeight)}.bind(this))}.bind(this))}}),a.fn[d]=function(b){return this.each(function(){a.data(this,"plugin_"+d)||a.data(this,"plugin_"+d,new c(this,b))})}}(jQuery,window,document); -------------------------------------------------------------------------------- /dist/navsync.js: -------------------------------------------------------------------------------- 1 | /* 2 | * navsync - v0.1.0 3 | * Sync navigation menus to page anchors 4 | * https://github.com/rp4rk/navsync 5 | * 6 | * Made by Ryan Park 7 | * Under MIT License 8 | */ 9 | ;(function ($, window, document, undefined) { 10 | 11 | "use strict"; 12 | 13 | // undefined is used here as the undefined global variable in ECMAScript 3 is 14 | // mutable (ie. it can be changed by someone else). undefined isn't really being 15 | // passed in so we can ensure the value of it is truly undefined. In ES5, undefined 16 | // can no longer be modified. 17 | 18 | // window and document are passed through as local variable rather than global 19 | // as this (slightly) quickens the resolution process and can be more efficiently 20 | // minified (especially when both are regularly referenced in your plugin). 21 | 22 | // Create the defaults once 23 | var pluginName = "navSync", 24 | defaults = { 25 | highlightClass: "navsync-menu-highlight", 26 | ignoreNavHeightScroll: false, 27 | animationTime: 500 28 | }; 29 | 30 | // The actual plugin constructor 31 | function Plugin(element, options) { 32 | this.element = element; 33 | // jQuery has an extend method which merges the contents of two or 34 | // more objects, storing the result in the first object. The first object 35 | // is generally empty as we don't want to alter the default options for 36 | // future instances of the plugin 37 | this.settings = $.extend({}, defaults, options); 38 | this._defaults = defaults; 39 | this._name = pluginName; 40 | this.init(); 41 | } 42 | 43 | // Avoid Plugin.prototype conflicts 44 | $.extend(Plugin.prototype, { 45 | 46 | // Our init function 47 | init: function () { 48 | 49 | // Set our target nav element, highlight class, header height 50 | var navSyncSelection = $(this.element); 51 | this.highlightClass = this.settings.highlightClass ? this.settings.highlightClass : this._defaults.highlightClass; 52 | this.initHeaderHeight(navSyncSelection); 53 | 54 | // Re-initialize due to window resize 55 | $(window).resize(function() { 56 | 57 | this.initHeaderHeight(navSyncSelection); 58 | this.watchedDivs = this.buildWatchedDivs(navSyncSelection); 59 | this.checkForHighlight(this.watchedDivs); 60 | 61 | // Clear our anchor bindings 62 | this.watchedDivs.forEach( function (anchor) { 63 | anchor[3].unbind("click"); 64 | }); 65 | 66 | // Reset 67 | this.initClickBinding(); 68 | 69 | }.bind(this)); 70 | 71 | // Construct our watched divs 72 | this.watchedDivs = this.buildWatchedDivs(navSyncSelection); 73 | 74 | // Handle Menu Clicks 75 | this.initClickBinding(); 76 | 77 | // Initial highlight 78 | this.checkForHighlight(this.watchedDivs); 79 | 80 | // Initial scroll hook 81 | $(window).bind("scroll", function() { this.scrollFunc(); }.bind(this) ); 82 | 83 | 84 | }, 85 | // Sets our header height 86 | initHeaderHeight: function(element) { 87 | 88 | this.headerHeight = this.settings.ignoreNavHeightScroll ? 0 : element.height(); 89 | 90 | }, 91 | // Applies appropriate highlight each frame 92 | scrollFunc: function() { 93 | 94 | window.requestAnimationFrame(function() { 95 | this.checkForHighlight(this.watchedDivs); 96 | }.bind(this)); 97 | 98 | }, 99 | // Applies approrpriate highlight 100 | checkForHighlight: function(arrDivs) { 101 | 102 | arrDivs.forEach(function(anchor) { 103 | if (this.isInView(anchor[1], anchor[2])) { 104 | anchor[3].addClass(this.highlightClass); 105 | } else { 106 | anchor[3].removeClass(this.highlightClass); 107 | } 108 | }.bind(this)); 109 | 110 | }, 111 | // Returns an array of anchors in the specified element 112 | getAnchors: function(element) { 113 | 114 | var arrAnchor = []; 115 | element.find("a").each(function(n, ele) { arrAnchor.push($(ele)); }); 116 | return arrAnchor; 117 | 118 | }, 119 | // Returns an array with each div we care about 120 | // [targetDiv, targetDivTop, targetDivBottom, targetDivLink] 121 | buildWatchedDivs: function(element) { 122 | 123 | var divList = this.getAnchors(element) 124 | .filter(function(item) { 125 | return this.isAnchor(item); 126 | }.bind(this)) 127 | .map(function(item) { 128 | var targetDiv = $(item.attr("href").replace("/", "")); 129 | return [targetDiv, targetDiv.offset().top, targetDiv.offset().top + targetDiv.outerHeight(true), item]; 130 | }.bind(this)); 131 | 132 | return divList; 133 | 134 | }, 135 | // Checks if the coordinates supplied are within the field of view 136 | // In this case the field of view is in the middle of the screen 137 | isInView: function(top, bottom) { 138 | 139 | var vp_threshold = $(window).scrollTop() + $(window).height()/2; 140 | return (top <= vp_threshold && bottom >= vp_threshold); 141 | 142 | }, 143 | // Checks if the link supplied is an anchor on the current page 144 | // Returns boolean 145 | isAnchor: function(link) { 146 | 147 | var href_string = link.attr("href").replace("/",""); 148 | return href_string.charAt(0) === "#"; 149 | 150 | }, 151 | // Scrolls to the supplied Y coordinate with the given time. 152 | scrollTo: function(y, animationTime, headerHeight) { 153 | 154 | $("html, body").animate({ 155 | scrollTop: y - headerHeight 156 | }, animationTime, function() { 157 | $(window).bind("scroll", function() { this.scrollFunc(); }.bind(this) ); 158 | this.checkForHighlight(this.watchedDivs); 159 | }.bind(this)); 160 | 161 | }, 162 | // Manages click binds 163 | initClickBinding: function() { 164 | 165 | this.watchedDivs.forEach(function(anchor) { 166 | 167 | anchor[3].click(function (e) { 168 | 169 | // Unbind our scroll 170 | $(window).unbind("scroll"); 171 | 172 | // Prevent Default Action and scrollTo 173 | e.preventDefault(); 174 | this.scrollTo(anchor[1], 500, this.headerHeight); 175 | 176 | }.bind(this)); 177 | }.bind(this)); 178 | 179 | } 180 | }); 181 | 182 | // A really lightweight plugin wrapper around the constructor, 183 | // preventing against multiple instantiations 184 | $.fn[pluginName] = function (options) { 185 | return this.each(function () { 186 | if (!$.data(this, "plugin_" + pluginName)) { 187 | $.data(this, "plugin_" + pluginName, new Plugin(this, options)); 188 | } 189 | }); 190 | }; 191 | 192 | 193 | })(jQuery, window, document); -------------------------------------------------------------------------------- /dist/navsync.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * navsync - v0.1.0 3 | * Sync navigation menus to page anchors 4 | * https://github.com/rp4rk/navsync 5 | * 6 | * Made by Ryan Park 7 | * Under MIT License 8 | */ 9 | !function(a,b){"use strict";function c(b,c){this.element=b,this.settings=a.extend({},e,c),this._defaults=e,this._name=d,this.init()}var d="navSync",e={highlightClass:"navsync-menu-highlight",ignoreNavHeightScroll:!1,animationTime:500};a.extend(c.prototype,{init:function(){var c=a(this.element);this.highlightClass=this.settings.highlightClass?this.settings.highlightClass:this._defaults.highlightClass,this.initHeaderHeight(c),a(b).resize(function(){this.initHeaderHeight(c),this.watchedDivs=this.buildWatchedDivs(c),this.checkForHighlight(this.watchedDivs),this.watchedDivs.forEach(function(a){a[3].unbind("click")}),this.initClickBinding()}.bind(this)),this.watchedDivs=this.buildWatchedDivs(c),this.initClickBinding(),this.checkForHighlight(this.watchedDivs),a(b).bind("scroll",function(){this.scrollFunc()}.bind(this))},initHeaderHeight:function(a){this.headerHeight=this.settings.ignoreNavHeightScroll?0:a.height()},scrollFunc:function(){b.requestAnimationFrame(function(){this.checkForHighlight(this.watchedDivs)}.bind(this))},checkForHighlight:function(a){a.forEach(function(a){this.isInView(a[1],a[2])?a[3].addClass(this.highlightClass):a[3].removeClass(this.highlightClass)}.bind(this))},getAnchors:function(b){var c=[];return b.find("a").each(function(b,d){c.push(a(d))}),c},buildWatchedDivs:function(b){var c=this.getAnchors(b).filter(function(a){return this.isAnchor(a)}.bind(this)).map(function(b){var c=a(b.attr("href").replace("/",""));return[c,c.offset().top,c.offset().top+c.outerHeight(!0),b]}.bind(this));return c},isInView:function(c,d){var e=a(b).scrollTop()+a(b).height()/2;return e>=c&&d>=e},isAnchor:function(a){var b=a.attr("href").replace("/","");return"#"===b.charAt(0)},scrollTo:function(c,d,e){a("html, body").animate({scrollTop:c-e},d,function(){a(b).bind("scroll",function(){this.scrollFunc()}.bind(this)),this.checkForHighlight(this.watchedDivs)}.bind(this))},initClickBinding:function(){this.watchedDivs.forEach(function(c){c[3].click(function(d){a(b).unbind("scroll"),d.preventDefault(),this.scrollTo(c[1],500,this.headerHeight)}.bind(this))}.bind(this))}}),a.fn[d]=function(b){return this.each(function(){a.data(this,"plugin_"+d)||a.data(this,"plugin_"+d,new c(this,b))})}}(jQuery,window,document); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "navsync", 3 | "version": "0.1.0", 4 | "main": "dist/navsync.min.js", 5 | "description": "Sync navigation menus to page anchors", 6 | "keywords": [ 7 | "jquery", 8 | "plugin", 9 | "navsync", 10 | "jquery-plugin", 11 | "navigation", 12 | "anchors", 13 | "scrollto", 14 | "scroll" 15 | ], 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/rp4rk/navsync.git" 19 | }, 20 | "bugs": { 21 | "url": "https://github.com/rp4rk/navsync" 22 | }, 23 | "author": { 24 | "name": "Ryan Park", 25 | "email": "admin@ryanpark.co.uk", 26 | "url": "https://github.com/rp4rk" 27 | }, 28 | "homepage": "https://github.com/rp4rk/navsync", 29 | "license": "MIT", 30 | "devDependencies": { 31 | "grunt": "~0.4.5", 32 | "grunt-cli": "~0.1.13", 33 | "grunt-contrib-concat": "^0.5.1", 34 | "grunt-contrib-jshint": "^0.11.0", 35 | "grunt-contrib-uglify": "^0.8.0", 36 | "grunt-contrib-watch": "^0.6.1", 37 | "grunt-sass": "^1.0.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/navsync.js: -------------------------------------------------------------------------------- 1 | ;(function ($, window, document, undefined) { 2 | 3 | "use strict"; 4 | 5 | // undefined is used here as the undefined global variable in ECMAScript 3 is 6 | // mutable (ie. it can be changed by someone else). undefined isn't really being 7 | // passed in so we can ensure the value of it is truly undefined. In ES5, undefined 8 | // can no longer be modified. 9 | 10 | // window and document are passed through as local variable rather than global 11 | // as this (slightly) quickens the resolution process and can be more efficiently 12 | // minified (especially when both are regularly referenced in your plugin). 13 | 14 | // Create the defaults once 15 | var pluginName = "navSync", 16 | defaults = { 17 | highlightClass: "navsync-menu-highlight", 18 | ignoreNavHeightScroll: false, 19 | animationTime: 500 20 | }; 21 | 22 | // The actual plugin constructor 23 | function Plugin(element, options) { 24 | this.element = element; 25 | // jQuery has an extend method which merges the contents of two or 26 | // more objects, storing the result in the first object. The first object 27 | // is generally empty as we don't want to alter the default options for 28 | // future instances of the plugin 29 | this.settings = $.extend({}, defaults, options); 30 | this._defaults = defaults; 31 | this._name = pluginName; 32 | this.init(); 33 | } 34 | 35 | // Avoid Plugin.prototype conflicts 36 | $.extend(Plugin.prototype, { 37 | 38 | // Our init function 39 | init: function () { 40 | 41 | // Set our target nav element, highlight class, header height 42 | var navSyncSelection = $(this.element); 43 | this.highlightClass = this.settings.highlightClass ? this.settings.highlightClass : this._defaults.highlightClass; 44 | this.initHeaderHeight(navSyncSelection); 45 | 46 | // Re-initialize due to window resize 47 | $(window).resize(function() { 48 | 49 | this.initHeaderHeight(navSyncSelection); 50 | this.watchedDivs = this.buildWatchedDivs(navSyncSelection); 51 | this.checkForHighlight(this.watchedDivs); 52 | 53 | // Clear our anchor bindings 54 | this.watchedDivs.forEach( function (anchor) { 55 | anchor[3].unbind("click"); 56 | }); 57 | 58 | // Reset 59 | this.initClickBinding(); 60 | 61 | }.bind(this)); 62 | 63 | // Construct our watched divs 64 | this.watchedDivs = this.buildWatchedDivs(navSyncSelection); 65 | 66 | // Handle Menu Clicks 67 | this.initClickBinding(); 68 | 69 | // Initial highlight 70 | this.checkForHighlight(this.watchedDivs); 71 | 72 | // Initial scroll hook 73 | $(window).bind("scroll", function() { this.scrollFunc(); }.bind(this) ); 74 | 75 | 76 | }, 77 | // Sets our header height 78 | initHeaderHeight: function(element) { 79 | 80 | this.headerHeight = this.settings.ignoreNavHeightScroll ? 0 : element.height(); 81 | 82 | }, 83 | // Applies appropriate highlight each frame 84 | scrollFunc: function() { 85 | 86 | window.requestAnimationFrame(function() { 87 | this.checkForHighlight(this.watchedDivs); 88 | }.bind(this)); 89 | 90 | }, 91 | // Applies approrpriate highlight 92 | checkForHighlight: function(arrDivs) { 93 | 94 | arrDivs.forEach(function(anchor) { 95 | if (this.isInView(anchor[1], anchor[2])) { 96 | anchor[3].addClass(this.highlightClass); 97 | } else { 98 | anchor[3].removeClass(this.highlightClass); 99 | } 100 | }.bind(this)); 101 | 102 | }, 103 | // Returns an array of anchors in the specified element 104 | getAnchors: function(element) { 105 | 106 | var arrAnchor = []; 107 | element.find("a").each(function(n, ele) { arrAnchor.push($(ele)); }); 108 | return arrAnchor; 109 | 110 | }, 111 | // Returns an array with each div we care about 112 | // [targetDiv, targetDivTop, targetDivBottom, targetDivLink] 113 | buildWatchedDivs: function(element) { 114 | 115 | var divList = this.getAnchors(element) 116 | .filter(function(item) { 117 | return this.isAnchor(item); 118 | }.bind(this)) 119 | .map(function(item) { 120 | var targetDiv = $(item.attr("href").replace("/", "")); 121 | return [targetDiv, targetDiv.offset().top, targetDiv.offset().top + targetDiv.outerHeight(true), item]; 122 | }.bind(this)); 123 | 124 | return divList; 125 | 126 | }, 127 | // Checks if the coordinates supplied are within the field of view 128 | // In this case the field of view is in the middle of the screen 129 | isInView: function(top, bottom) { 130 | 131 | var vp_threshold = $(window).scrollTop() + $(window).height()/2; 132 | return (top <= vp_threshold && bottom >= vp_threshold); 133 | 134 | }, 135 | // Checks if the link supplied is an anchor on the current page 136 | // Returns boolean 137 | isAnchor: function(link) { 138 | 139 | var href_string = link.attr("href").replace("/",""); 140 | return href_string.charAt(0) === "#"; 141 | 142 | }, 143 | // Scrolls to the supplied Y coordinate with the given time. 144 | scrollTo: function(y, animationTime, headerHeight) { 145 | 146 | $("html, body").animate({ 147 | scrollTop: y - headerHeight - (this.settings.offset || 0) 148 | }, animationTime, function() { 149 | $(window).bind("scroll", function() { this.scrollFunc(); }.bind(this) ); 150 | this.checkForHighlight(this.watchedDivs); 151 | }.bind(this)); 152 | 153 | }, 154 | // Manages click binds 155 | initClickBinding: function() { 156 | 157 | this.watchedDivs.forEach(function(anchor) { 158 | 159 | anchor[3].click(function (e) { 160 | 161 | // Unbind our scroll 162 | $(window).unbind("scroll"); 163 | 164 | // Prevent Default Action and scrollTo 165 | e.preventDefault(); 166 | this.scrollTo(anchor[1], 500, this.headerHeight); 167 | 168 | }.bind(this)); 169 | }.bind(this)); 170 | 171 | } 172 | }); 173 | 174 | // A really lightweight plugin wrapper around the constructor, 175 | // preventing against multiple instantiations 176 | $.fn[pluginName] = function (options) { 177 | return this.each(function () { 178 | if (!$.data(this, "plugin_" + pluginName)) { 179 | $.data(this, "plugin_" + pluginName, new Plugin(this, options)); 180 | } 181 | }); 182 | }; 183 | 184 | 185 | })(jQuery, window, document); 186 | --------------------------------------------------------------------------------