├── .gitignore ├── demo └── screenshot.PNG ├── src ├── css │ └── magicline.scss └── js │ └── main.js ├── changelog.md ├── package.json ├── LICENSE ├── rollup.config.js ├── dist └── js │ ├── magicline.min.js │ └── magicline.js ├── index.html ├── README.md └── .eslintrc.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules* -------------------------------------------------------------------------------- /demo/screenshot.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bfiessinger/Vanilla-JS-Magic-Line-Navigation/HEAD/demo/screenshot.PNG -------------------------------------------------------------------------------- /src/css/magicline.scss: -------------------------------------------------------------------------------- 1 | /* Required Styling */ 2 | .init-floating-line, .floating-line-inner { position: relative; } 3 | .floating-line-inner { z-index: 1; } 4 | .floating-line { position: absolute; } 5 | .floating-line-css-transition { transition: all .2s ease-in-out; } -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### 1.0.4 4 | * Fixed a bug where the animation would not work properly if the Nav Elements Selector has child Elements 5 | 6 | ### 1.0.3 7 | * Improve all comments to make them better understandable 8 | * replaced 'rollup-plugin-uglify' with 'rollup-plugin-terser' for minification 9 | * Used Prettier to beautify the non minified version of magicline.js 10 | * Updated README.md 11 | 12 | ### 1.0.2 13 | * Automatically add an active class to the first element when no active element was specified 14 | 15 | ### 1.0.1 16 | * Fixed a naming issue 17 | 18 | ### 1.0.0 19 | * Initial Release -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vanilla-magicline", 3 | "version": "1.0.4", 4 | "description": "Vanilla JS Magic Line for nav Menus", 5 | "main": "src/js/main.js", 6 | "scripts": { 7 | "build": "rollup --config" 8 | }, 9 | "author": "Bastian Fießinger", 10 | "license": "MIT", 11 | "dependencies": {}, 12 | "devDependencies": { 13 | "@babel/core": "^7.9.6", 14 | "@babel/preset-env": "^7.9.6", 15 | "babel-plugin-add-module-exports": "^1.0.2", 16 | "rollup": "^1.32.1", 17 | "rollup-plugin-babel": "^4.4.0", 18 | "rollup-plugin-eslint": "^7.0.0", 19 | "rollup-plugin-prettier": "^0.6.0", 20 | "rollup-plugin-terser": "^5.3.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Bastian Fießinger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import { eslint } from 'rollup-plugin-eslint'; 3 | import { terser } from 'rollup-plugin-terser'; 4 | 5 | const prettier = require('rollup-plugin-prettier'); 6 | 7 | const banner = '/**\n\ 8 | * Vanilla JS Magic Line Navigation\n\ 9 | * Author: Bastian Fießinger\n\ 10 | * Version: 1.0.4\n\ 11 | */'; 12 | 13 | // Default 14 | export default [{ 15 | input: 'src/js/main.js', 16 | output: { 17 | file: 'dist/js/magicline.js', 18 | format: 'iife', 19 | name: 'magicLine', 20 | banner: banner 21 | }, 22 | plugins: [ 23 | eslint(), 24 | babel({ 25 | exclude: 'node_modules/**' 26 | }), 27 | prettier({ 28 | printWidth: 80, 29 | tabWidth: 2, 30 | tabs: true, 31 | trailingComma: 'es5', 32 | parser: 'babel' 33 | }), 34 | ] 35 | }, { 36 | input: 'src/js/main.js', 37 | output: { 38 | file: 'dist/js/magicline.min.js', 39 | format: 'iife', 40 | name: 'magicLine', 41 | banner: banner 42 | }, 43 | plugins: [ 44 | eslint(), 45 | babel({ 46 | exclude: 'node_modules/**' 47 | }), 48 | prettier({ 49 | printWidth: 80, 50 | tabWidth: 2, 51 | tabs: true, 52 | trailingComma: 'es5', 53 | parser: 'babel' 54 | }), 55 | terser({ 56 | output: { 57 | comments: function (node, comment) { 58 | if (comment.type === "comment2") { 59 | // multiline comment 60 | return /Vanilla JS Magic Line Navigation/i.test(comment.value); 61 | } 62 | return false; 63 | } 64 | } 65 | }) 66 | ] 67 | }]; -------------------------------------------------------------------------------- /dist/js/magicline.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Vanilla JS Magic Line Navigation 3 | * Author: Bastian Fießinger 4 | * Version: 1.0.4 5 | */ 6 | var magicLine=function(){"use strict";return class{constructor(e,t){var s;this.elements,(s=e)instanceof Node||s instanceof NodeList||s instanceof HTMLCollection?this.elements=e:this.elements=document.querySelectorAll(e);this.settings=((...e)=>{var t={};return Array.prototype.forEach.call(e,e=>{for(var s in e){if(!Object.prototype.hasOwnProperty.call(e,s))return;t[s]=e[s]}}),t})({navElements:"a",mode:"line",lineStrength:2,lineClass:"magic-line",wrapper:"div",animationCallback:null},t||{});const i=(e,t)=>!!(e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector).call(e,t);function n(e,t){for(;(e=e.parentElement)&&!(e.matches||e.matchesSelector).call(e,t););return e}const l=e=>e.querySelectorAll(this.settings.navElements),a=(e,t)=>{Array.prototype.forEach.call(e,e=>{e.classList.remove("active")});let s=t.target;i(s,this.settings.navElements)||(s=n(s,this.settings.navElements)),s.classList.add("active")},r=e=>{let t=Array.prototype.filter.call(e,e=>e.classList.contains("active")?e:null);return t.length?t=t[0]:(t=e[0],a(e,{target:e[0]})),{el:t,rect:t.getBoundingClientRect()}},c=(e,t)=>{let s=t.target;i(s,this.settings.navElements)||(s=n(s,this.settings.navElements));const l={el:s,rect:s.getBoundingClientRect()};h(e,l)},o=(e,t)=>{const s=r(t);h(e,s)},h=(e,t,s)=>{let i,n=t.el.offsetLeft,l=t.el.offsetTop,a=t.rect.width;"line"!==this.settings.mode?i=t.rect.height:(i=this.settings.lineStrength,l+=t.rect.height),this.settings.animationCallback&&!s?this.settings.animationCallback(e,{left:n+"px",top:l+"px",width:a+"px",height:i+"px"}):(e.style.left=n+"px",e.style.top=l+"px",e.style.width=a+"px",e.style.height=i+"px")},d=()=>{Array.prototype.forEach.call(this.elements,e=>{e.classList.add("init-magic-line","magic-line-mode-"+this.settings.mode.toLowerCase());let t=document.createElement(this.settings.wrapper);t.className="magic-line-inner";let s=document.createElement("div");for(s.className=this.settings.lineClass,null===this.settings.animationCallback&&s.classList.add("magic-line-css-transition"),e.appendChild(s);e.firstChild;)t.appendChild(e.firstChild);e.appendChild(t);let i=r(l(e));h(s,i,!0)})},m=()=>{Array.prototype.forEach.call(this.elements,e=>{let t="."+this.settings.lineClass,s=e.querySelector(t),i=e.querySelector(".magic-line-inner"),n=l(i);Array.prototype.forEach.call(n,e=>{e.addEventListener("click",a.bind(null,n)),e.addEventListener("mouseover",c.bind(null,s)),e.addEventListener("mouseleave",o.bind(null,s,n))}),window.addEventListener("resize",o.bind(null,s,n))})};this.init=function(){d.call(this),m.call(this)}}}}(); 7 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | 10 | 11 | 12 | 13 | 73 | 74 | 75 | 76 | 77 | 78 |
79 | 80 | 86 | 87 | 106 | 107 |
108 | 109 | 110 | 111 | 112 | 149 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vanilla JS - Magic Line Navigation 2 | original idea: https://css-tricks.com/jquery-magicline-navigation/ 3 | 4 | [![CodeFactor](https://www.codefactor.io/repository/github/bfiessinger/vanilla-js-magic-line-navigation/badge)](https://www.codefactor.io/repository/github/bfiessinger/vanilla-js-magic-line-navigation) 5 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) 6 | 7 | ## Browser Support 8 | ![Chrome](https://camo.githubusercontent.com/26846e979600799e9f4273d38bd9e5cb7bb8d6d0/68747470733a2f2f7261772e6769746875622e636f6d2f616c7272612f62726f777365722d6c6f676f732f6d61737465722f7372632f6368726f6d652f6368726f6d655f34387834382e706e67) | ![Firefox](https://camo.githubusercontent.com/6087557f69ec6585eb7f8d7bd7d9ecb6b7f51ba1/68747470733a2f2f7261772e6769746875622e636f6d2f616c7272612f62726f777365722d6c6f676f732f6d61737465722f7372632f66697265666f782f66697265666f785f34387834382e706e67) | ![IE](https://camo.githubusercontent.com/4b062fb12353b0ef8420a72ddc3debf6b2ee5747/68747470733a2f2f7261772e6769746875622e636f6d2f616c7272612f62726f777365722d6c6f676f732f6d61737465722f7372632f617263686976652f696e7465726e65742d6578706c6f7265725f392d31312f696e7465726e65742d6578706c6f7265725f392d31315f34387834382e706e67) | ![Edge](https://camo.githubusercontent.com/826b3030243b09465bf14cf420704344f5eee991/68747470733a2f2f7261772e6769746875622e636f6d2f616c7272612f62726f777365722d6c6f676f732f6d61737465722f7372632f656467652f656467655f34387834382e706e67) | ![Opera](https://camo.githubusercontent.com/96d2405a936da1fb8988db0c1d304d3db04b8a52/68747470733a2f2f7261772e6769746875622e636f6d2f616c7272612f62726f777365722d6c6f676f732f6d61737465722f7372632f6f706572612f6f706572615f34387834382e706e67) | ![Safari](https://camo.githubusercontent.com/6fbaeb334b99e74ddd89190a42766ea3b4600d2c/68747470733a2f2f7261772e6769746875622e636f6d2f616c7272612f62726f777365722d6c6f676f732f6d61737465722f7372632f7361666172692f7361666172695f34387834382e706e67) 9 | --- | --- | --- | --- | --- | --- | 10 | < 15 ✔ | 5+ ✔ | 10+ ✔ | < 15 ✔ | < 23 ✔ | 5.1+ ✔ | 11 | 12 | To get IE Support below Version 10 (or any other browser that does not support Element.Classlist) use a Classlist Polyfill. 13 | 14 | ## Dependencies 15 | * None 16 | 17 | However you can implement every Animation Library like anime.js for the animations. 18 | 19 | ## Features: 20 | * works with any animation Library like [anime.js](https://github.com/juliangarnier/anime), [velocity.js](https://github.com/julianshapiro/velocity), [GSAP](https://github.com/greensock/GSAP), e.g. 21 | * works with CSS Transitions (no animation library required) 22 | * fully responsive 23 | * able to animate in any direction (left to right, top to bottom, diagonal) 24 | * pillmode & linemode 25 | 26 | ## Usage 27 | ```javascript 28 | var myMagicLine = new magicLine( 29 | document.querySelectorAll('.magic-line-menu'), 30 | { 31 | navElements: 'a', // navigation element selector 32 | mode: 'line', // line or pill 33 | lineStrength: 2, // Thickness of the line 34 | lineClass: 'magic-line', // Classname to add to the line element 35 | wrapper: 'div', // the node that's being created as an element wrapper 36 | animationCallback: function (el, params) { // might be either null or a callback function 37 | animationLibrary({ 38 | targets: el, 39 | left: params.left, 40 | top: params.top, 41 | width: params.width, 42 | height: params.height 43 | }); 44 | } 45 | } 46 | ); 47 | myMagicLine.init(); 48 | ``` 49 | 50 | ## Basic Setup 51 | ### HTML 52 | The most basic html structure to use is shown below: 53 | ```html 54 | 60 | ``` 61 | ### CSS 62 | Required styling 63 | ```css 64 | .init-magic-line, 65 | .magic-line-inner { 66 | position: relative; 67 | } 68 | 69 | .magic-line { 70 | z-index: -1; 71 | position: absolute; 72 | } 73 | 74 | .magic-line-css-transition { 75 | transition: all .2s ease-in-out; 76 | } 77 | ``` 78 | 79 | ### Javascript 80 | ```javascript 81 | var myMagicLine = new magicLine('.my-magic-line'); 82 | myMagicLine.init(); 83 | ``` 84 | 85 | ## Options 86 | | Option | Value | Default | 87 | | ----------------- |---------------------------------------------------------------|-----------------| 88 | | navElements | a query Selector, you can even define multiple like 'a, span' | 'a' | 89 | | mode | might be either 'line' or 'pill' | 'line' | 90 | | lineStrength | thickness of your line in px | 2 | 91 | | lineClass | The classname of the floating-line element | 'magic-line' | 92 | | wrapper | DOMNode to be inserted as a wrapper | 'div' | 93 | | animationCallback | a callBack Function used for animation | null | 94 | 95 | ## This is how it looks like 96 | ![Alt text](https://raw.githubusercontent.com/basticodes/Vanilla-JS-Magic-Line-Navigation/master/demo/screenshot.PNG) 97 | 98 | ## Filesize 99 | * Minified Version: 100 | * 2.58 KB (1.07 KB gzipped) 101 | 102 | * Non Minified Version 103 | * 7.49 KB (2.12 KB gzipped) 104 | 105 | ## DEMO 106 | Check out the [Demo](https://codepen.io/bastian_fiessinger/full/MWYMWJN) on Codepen 107 | -------------------------------------------------------------------------------- /src/js/main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | export default class magicLine { 4 | 5 | constructor(node, settings) { 6 | 7 | this.elements; 8 | 9 | /** 10 | * Basic Helper function to check if an Element is an instance of Node 11 | * @param {any} domNode either a dom node or querySelector 12 | * @returns {boolean} either true or false 13 | */ 14 | function isDomElement(domNode) { 15 | if (domNode instanceof Node || domNode instanceof NodeList || domNode instanceof HTMLCollection) { 16 | return true; 17 | } 18 | return false; 19 | } 20 | 21 | /** 22 | * Check this.elements and declare them based on their value 23 | */ 24 | if (isDomElement(node)) { 25 | this.elements = node; 26 | } else { 27 | this.elements = document.querySelectorAll(node); 28 | } 29 | 30 | /** 31 | * Build Default Settings Object 32 | */ 33 | const defaults = { 34 | navElements: 'a', 35 | mode: 'line', 36 | lineStrength: 2, 37 | lineClass: 'magic-line', 38 | wrapper: 'div', 39 | animationCallback: null 40 | }; 41 | 42 | /** 43 | * Basic Helper Function to merge user defined settings with the defaults Object 44 | * @param {...any} args Arguments to check 45 | * @returns {object} Merged Settings Object 46 | */ 47 | const extendSettings = (...args) => { 48 | var merged = {}; 49 | Array.prototype.forEach.call(args, (obj) => { 50 | for (var key in obj) { 51 | if (!Object.prototype.hasOwnProperty.call(obj, key)) { 52 | return; 53 | } 54 | merged[key] = obj[key]; 55 | } 56 | }); 57 | return merged; 58 | }; 59 | 60 | /** 61 | * Build the final Settings Object 62 | */ 63 | this.settings = extendSettings(defaults, settings || {}); 64 | 65 | /** 66 | * Helper function to determine if an element matches a selector 67 | * @param {HTMLElement} el The HTMLElement to be checked 68 | * @param {string} selector selector 69 | * @returns {boolean} true or false 70 | */ 71 | const elementMatches = (el, selector) => { 72 | if ((el.matches || el.matchesSelector || el.msMatchesSelector || el.mozMatchesSelector || el.webkitMatchesSelector || el.oMatchesSelector).call(el, selector)) { 73 | return true; 74 | } 75 | return false; 76 | }; 77 | 78 | /** 79 | * Recursive function to get the closest ancestor by 80 | * @param {HTMLElement} el Source Element 81 | * @param {*} selector parentselector 82 | * @returns {HTMLElement} Parent Element 83 | */ 84 | function findAncestor (el, selector) { 85 | while ((el = el.parentElement) && !(el.matches || el.matchesSelector).call(el, selector)) { 86 | continue; 87 | } 88 | return el; 89 | } 90 | 91 | /** 92 | * Get all Nav Elements 93 | * @param {object} parent A parentNode of all Nav Elements 94 | * @returns {object} All Navigation Elements 95 | */ 96 | const getNavElements = (parent) => parent.querySelectorAll(this.settings.navElements); 97 | 98 | /** 99 | * Set the active Element 100 | * @param {object} links All available Nav Elements 101 | * @param {object} event the event object 102 | * @returns {null} NULL 103 | */ 104 | const setActiveElement = (links, event) => { 105 | Array.prototype.forEach.call(links, (el) => { 106 | el.classList.remove('active'); 107 | }); 108 | let curEl = event.target; 109 | if (!elementMatches(curEl, this.settings.navElements)) { 110 | curEl = findAncestor(curEl, this.settings.navElements); 111 | } 112 | curEl.classList.add('active'); 113 | }; 114 | 115 | /** 116 | * Get the currently active Element 117 | * @param {object} elements All available Nav Elements 118 | * @uses setActiveElement 119 | * @returns {object} The currently active Nav Element 120 | */ 121 | const getActiveElement = (elements) => { 122 | 123 | let active = Array.prototype.filter.call(elements, (el) => { 124 | if (el.classList.contains('active')) { 125 | return el; 126 | } 127 | return null; 128 | }); 129 | 130 | if (!active.length) { 131 | active = elements[0]; 132 | setActiveElement(elements, { 133 | target: elements[0] 134 | }); 135 | } else { 136 | active = active[0]; 137 | } 138 | 139 | return { 140 | el: active, 141 | rect: active.getBoundingClientRect() 142 | } 143 | 144 | }; 145 | 146 | /** 147 | * Move Line 148 | * @param {object} lineEl The Magic Line Element 149 | * @param {object} event The Event Object 150 | * @uses drawLine 151 | * @returns {null} NULL 152 | */ 153 | const moveLine = (lineEl, event) => { 154 | let curEl = event.target; 155 | if (!elementMatches(curEl, this.settings.navElements)) { 156 | curEl = findAncestor(curEl, this.settings.navElements); 157 | } 158 | 159 | const cur = { 160 | el: curEl, 161 | rect: curEl.getBoundingClientRect() 162 | }; 163 | drawLine(lineEl, cur); 164 | }; 165 | 166 | /** 167 | * Reset Line 168 | * @param {object} lineEl The Magic Line Element 169 | * @param {object} links All available Nav Elements 170 | * @uses drawLine 171 | * @returns {null} NULL 172 | */ 173 | const resetLine = (lineEl, links) => { 174 | const active = getActiveElement(links); 175 | drawLine(lineEl, active); 176 | }; 177 | 178 | 179 | /** 180 | * Draw Line 181 | * @param {object} line The Magic Line Element 182 | * @param {object} active The currently active Nav Element 183 | * @param {boolean} init Does the function run on Initialisation? 184 | * @returns {null} NULL 185 | */ 186 | const drawLine = (line, active, init) => { 187 | 188 | let lineLeft = active.el.offsetLeft; 189 | let lineTop = active.el.offsetTop; 190 | let lineWidth = active.rect.width; 191 | let lineHeight; 192 | 193 | if (this.settings.mode !== 'line') { 194 | lineHeight = active.rect.height; 195 | } else { 196 | lineHeight = this.settings.lineStrength; 197 | lineTop += active.rect.height; 198 | } 199 | 200 | if (this.settings.animationCallback && !init) { 201 | this.settings.animationCallback(line, { 202 | left: lineLeft + 'px', 203 | top: lineTop + 'px', 204 | width: lineWidth + 'px', 205 | height: lineHeight + 'px' 206 | }); 207 | } else { 208 | // If no animation Callback is defined use CSS Styles 209 | line.style.left = lineLeft + 'px'; 210 | line.style.top = lineTop + 'px'; 211 | line.style.width = lineWidth + 'px'; 212 | line.style.height = lineHeight + 'px'; 213 | } 214 | }; 215 | 216 | /** 217 | * Create all neccessary MagicLine Elements on Load 218 | * @returns {null} NULL 219 | */ 220 | const onLoad = () => { 221 | 222 | Array.prototype.forEach.call(this.elements, (el) => { 223 | 224 | el.classList.add('init-magic-line', 'magic-line-mode-' + this.settings.mode.toLowerCase()); 225 | 226 | // Build an Element Wrapper 227 | let linkWrapper = document.createElement(this.settings.wrapper); 228 | linkWrapper.className = 'magic-line-inner'; 229 | 230 | // Create the Line Element 231 | let magicLineEl = document.createElement('div'); 232 | magicLineEl.className = this.settings.lineClass; 233 | if (this.settings.animationCallback === null) { 234 | magicLineEl.classList.add('magic-line-css-transition'); 235 | } 236 | el.appendChild(magicLineEl); 237 | 238 | // Wrap all Child Elements 239 | while (el.firstChild) { 240 | linkWrapper.appendChild(el.firstChild); 241 | } 242 | 243 | // Insert the wrapper Element 244 | el.appendChild(linkWrapper); 245 | 246 | let initActive = getActiveElement(getNavElements(el)); 247 | 248 | // Draw 249 | drawLine(magicLineEl, initActive, true); 250 | 251 | }); 252 | 253 | }; 254 | 255 | /** 256 | * Bind Event Listeners 257 | * @returns {null} NULL 258 | */ 259 | const BindEvents = () => { 260 | 261 | Array.prototype.forEach.call(this.elements, (el) => { 262 | 263 | let lineSelector = '.' + this.settings.lineClass; 264 | let lineEl = el.querySelector(lineSelector); 265 | let linkWrapper = el.querySelector('.magic-line-inner'); 266 | let links = getNavElements(linkWrapper); 267 | 268 | Array.prototype.forEach.call(links, (link) => { 269 | link.addEventListener('click', setActiveElement.bind(null, links)); 270 | link.addEventListener('mouseover', moveLine.bind(null, lineEl)); 271 | link.addEventListener('mouseleave', resetLine.bind(null, lineEl, links)); 272 | }); 273 | 274 | window.addEventListener('resize', resetLine.bind(null, lineEl, links)); 275 | 276 | }); 277 | 278 | }; 279 | 280 | /** 281 | * Init MagicLine 282 | * @returns {null} NULL 283 | */ 284 | this.init = function () { 285 | 286 | // Set init states 287 | onLoad.call(this); 288 | 289 | // Bind all Events 290 | BindEvents.call(this); 291 | 292 | }; 293 | 294 | } 295 | 296 | } -------------------------------------------------------------------------------- /dist/js/magicline.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Vanilla JS Magic Line Navigation 3 | * Author: Bastian Fießinger 4 | * Version: 1.0.4 5 | */ 6 | var magicLine = (function() { 7 | "use strict"; 8 | 9 | class magicLine { 10 | constructor(node, settings) { 11 | this.elements; 12 | /** 13 | * Basic Helper function to check if an Element is an instance of Node 14 | * @param {any} domNode either a dom node or querySelector 15 | * @returns {boolean} either true or false 16 | */ 17 | 18 | function isDomElement(domNode) { 19 | if ( 20 | domNode instanceof Node || 21 | domNode instanceof NodeList || 22 | domNode instanceof HTMLCollection 23 | ) { 24 | return true; 25 | } 26 | 27 | return false; 28 | } 29 | /** 30 | * Check this.elements and declare them based on their value 31 | */ 32 | 33 | if (isDomElement(node)) { 34 | this.elements = node; 35 | } else { 36 | this.elements = document.querySelectorAll(node); 37 | } 38 | /** 39 | * Build Default Settings Object 40 | */ 41 | 42 | const defaults = { 43 | navElements: "a", 44 | mode: "line", 45 | lineStrength: 2, 46 | lineClass: "magic-line", 47 | wrapper: "div", 48 | animationCallback: null, 49 | }; 50 | /** 51 | * Basic Helper Function to merge user defined settings with the defaults Object 52 | * @param {...any} args Arguments to check 53 | * @returns {object} Merged Settings Object 54 | */ 55 | 56 | const extendSettings = (...args) => { 57 | var merged = {}; 58 | Array.prototype.forEach.call(args, obj => { 59 | for (var key in obj) { 60 | if (!Object.prototype.hasOwnProperty.call(obj, key)) { 61 | return; 62 | } 63 | 64 | merged[key] = obj[key]; 65 | } 66 | }); 67 | return merged; 68 | }; 69 | /** 70 | * Build the final Settings Object 71 | */ 72 | 73 | this.settings = extendSettings(defaults, settings || {}); 74 | /** 75 | * Helper function to determine if an element matches a selector 76 | * @param {HTMLElement} el The HTMLElement to be checked 77 | * @param {string} selector selector 78 | * @returns {boolean} true or false 79 | */ 80 | 81 | const elementMatches = (el, selector) => { 82 | if ( 83 | ( 84 | el.matches || 85 | el.matchesSelector || 86 | el.msMatchesSelector || 87 | el.mozMatchesSelector || 88 | el.webkitMatchesSelector || 89 | el.oMatchesSelector 90 | ).call(el, selector) 91 | ) { 92 | return true; 93 | } 94 | 95 | return false; 96 | }; 97 | /** 98 | * Recursive function to get the closest ancestor by 99 | * @param {HTMLElement} el Source Element 100 | * @param {*} selector parentselector 101 | * @returns {HTMLElement} Parent Element 102 | */ 103 | 104 | function findAncestor(el, selector) { 105 | while ( 106 | (el = el.parentElement) && 107 | !(el.matches || el.matchesSelector).call(el, selector) 108 | ) { 109 | continue; 110 | } 111 | 112 | return el; 113 | } 114 | /** 115 | * Get all Nav Elements 116 | * @param {object} parent A parentNode of all Nav Elements 117 | * @returns {object} All Navigation Elements 118 | */ 119 | 120 | const getNavElements = parent => 121 | parent.querySelectorAll(this.settings.navElements); 122 | /** 123 | * Set the active Element 124 | * @param {object} links All available Nav Elements 125 | * @param {object} event the event object 126 | * @returns {null} NULL 127 | */ 128 | 129 | const setActiveElement = (links, event) => { 130 | Array.prototype.forEach.call(links, el => { 131 | el.classList.remove("active"); 132 | }); 133 | let curEl = event.target; 134 | 135 | if (!elementMatches(curEl, this.settings.navElements)) { 136 | curEl = findAncestor(curEl, this.settings.navElements); 137 | } 138 | 139 | curEl.classList.add("active"); 140 | }; 141 | /** 142 | * Get the currently active Element 143 | * @param {object} elements All available Nav Elements 144 | * @uses setActiveElement 145 | * @returns {object} The currently active Nav Element 146 | */ 147 | 148 | const getActiveElement = elements => { 149 | let active = Array.prototype.filter.call(elements, el => { 150 | if (el.classList.contains("active")) { 151 | return el; 152 | } 153 | 154 | return null; 155 | }); 156 | 157 | if (!active.length) { 158 | active = elements[0]; 159 | setActiveElement(elements, { 160 | target: elements[0], 161 | }); 162 | } else { 163 | active = active[0]; 164 | } 165 | 166 | return { 167 | el: active, 168 | rect: active.getBoundingClientRect(), 169 | }; 170 | }; 171 | /** 172 | * Move Line 173 | * @param {object} lineEl The Magic Line Element 174 | * @param {object} event The Event Object 175 | * @uses drawLine 176 | * @returns {null} NULL 177 | */ 178 | 179 | const moveLine = (lineEl, event) => { 180 | let curEl = event.target; 181 | 182 | if (!elementMatches(curEl, this.settings.navElements)) { 183 | curEl = findAncestor(curEl, this.settings.navElements); 184 | } 185 | 186 | const cur = { 187 | el: curEl, 188 | rect: curEl.getBoundingClientRect(), 189 | }; 190 | drawLine(lineEl, cur); 191 | }; 192 | /** 193 | * Reset Line 194 | * @param {object} lineEl The Magic Line Element 195 | * @param {object} links All available Nav Elements 196 | * @uses drawLine 197 | * @returns {null} NULL 198 | */ 199 | 200 | const resetLine = (lineEl, links) => { 201 | const active = getActiveElement(links); 202 | drawLine(lineEl, active); 203 | }; 204 | /** 205 | * Draw Line 206 | * @param {object} line The Magic Line Element 207 | * @param {object} active The currently active Nav Element 208 | * @param {boolean} init Does the function run on Initialisation? 209 | * @returns {null} NULL 210 | */ 211 | 212 | const drawLine = (line, active, init) => { 213 | let lineLeft = active.el.offsetLeft; 214 | let lineTop = active.el.offsetTop; 215 | let lineWidth = active.rect.width; 216 | let lineHeight; 217 | 218 | if (this.settings.mode !== "line") { 219 | lineHeight = active.rect.height; 220 | } else { 221 | lineHeight = this.settings.lineStrength; 222 | lineTop += active.rect.height; 223 | } 224 | 225 | if (this.settings.animationCallback && !init) { 226 | this.settings.animationCallback(line, { 227 | left: lineLeft + "px", 228 | top: lineTop + "px", 229 | width: lineWidth + "px", 230 | height: lineHeight + "px", 231 | }); 232 | } else { 233 | // If no animation Callback is defined use CSS Styles 234 | line.style.left = lineLeft + "px"; 235 | line.style.top = lineTop + "px"; 236 | line.style.width = lineWidth + "px"; 237 | line.style.height = lineHeight + "px"; 238 | } 239 | }; 240 | /** 241 | * Create all neccessary MagicLine Elements on Load 242 | * @returns {null} NULL 243 | */ 244 | 245 | const onLoad = () => { 246 | Array.prototype.forEach.call(this.elements, el => { 247 | el.classList.add( 248 | "init-magic-line", 249 | "magic-line-mode-" + this.settings.mode.toLowerCase() 250 | ); // Build an Element Wrapper 251 | 252 | let linkWrapper = document.createElement(this.settings.wrapper); 253 | linkWrapper.className = "magic-line-inner"; // Create the Line Element 254 | 255 | let magicLineEl = document.createElement("div"); 256 | magicLineEl.className = this.settings.lineClass; 257 | 258 | if (this.settings.animationCallback === null) { 259 | magicLineEl.classList.add("magic-line-css-transition"); 260 | } 261 | 262 | el.appendChild(magicLineEl); // Wrap all Child Elements 263 | 264 | while (el.firstChild) { 265 | linkWrapper.appendChild(el.firstChild); 266 | } // Insert the wrapper Element 267 | 268 | el.appendChild(linkWrapper); 269 | let initActive = getActiveElement(getNavElements(el)); // Draw 270 | 271 | drawLine(magicLineEl, initActive, true); 272 | }); 273 | }; 274 | /** 275 | * Bind Event Listeners 276 | * @returns {null} NULL 277 | */ 278 | 279 | const BindEvents = () => { 280 | Array.prototype.forEach.call(this.elements, el => { 281 | let lineSelector = "." + this.settings.lineClass; 282 | let lineEl = el.querySelector(lineSelector); 283 | let linkWrapper = el.querySelector(".magic-line-inner"); 284 | let links = getNavElements(linkWrapper); 285 | Array.prototype.forEach.call(links, link => { 286 | link.addEventListener("click", setActiveElement.bind(null, links)); 287 | link.addEventListener("mouseover", moveLine.bind(null, lineEl)); 288 | link.addEventListener( 289 | "mouseleave", 290 | resetLine.bind(null, lineEl, links) 291 | ); 292 | }); 293 | window.addEventListener( 294 | "resize", 295 | resetLine.bind(null, lineEl, links) 296 | ); 297 | }); 298 | }; 299 | /** 300 | * Init MagicLine 301 | * @returns {null} NULL 302 | */ 303 | 304 | this.init = function() { 305 | // Set init states 306 | onLoad.call(this); // Bind all Events 307 | 308 | BindEvents.call(this); 309 | }; 310 | } 311 | } 312 | 313 | return magicLine; 314 | })(); 315 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "globals": { 8 | "Atomics": "readonly", 9 | "SharedArrayBuffer": "readonly" 10 | }, 11 | "parserOptions": { 12 | "ecmaVersion": 2018, 13 | "sourceType": "module" 14 | }, 15 | "rules": { 16 | "accessor-pairs": "error", 17 | "array-bracket-newline": "error", 18 | "array-bracket-spacing": "error", 19 | "array-callback-return": "error", 20 | "array-element-newline": "error", 21 | "arrow-body-style": "error", 22 | "arrow-parens": [ 23 | "error", 24 | "always" 25 | ], 26 | "arrow-spacing": [ 27 | "error", 28 | { 29 | "after": true, 30 | "before": true 31 | } 32 | ], 33 | "block-scoped-var": "error", 34 | "block-spacing": "error", 35 | "brace-style": [ 36 | "error", 37 | "1tbs" 38 | ], 39 | "callback-return": "error", 40 | "camelcase": "error", 41 | "capitalized-comments": [ 42 | "error", 43 | "always" 44 | ], 45 | "class-methods-use-this": "error", 46 | "comma-dangle": "error", 47 | "comma-spacing": [ 48 | "error", 49 | { 50 | "after": true, 51 | "before": false 52 | } 53 | ], 54 | "comma-style": [ 55 | "error", 56 | "last" 57 | ], 58 | "complexity": "error", 59 | "computed-property-spacing": [ 60 | "error", 61 | "never" 62 | ], 63 | "consistent-return": "error", 64 | "consistent-this": "error", 65 | "curly": "error", 66 | "default-case": "error", 67 | "default-param-last": "error", 68 | "dot-location": "error", 69 | "dot-notation": "error", 70 | "eol-last": [ 71 | "error", 72 | "never" 73 | ], 74 | "eqeqeq": "error", 75 | "func-call-spacing": "error", 76 | "func-name-matching": "error", 77 | "func-names": "off", 78 | "func-style": [ 79 | "error", 80 | "declaration", 81 | { 82 | "allowArrowFunctions": true 83 | } 84 | ], 85 | "function-paren-newline": "error", 86 | "generator-star-spacing": "error", 87 | "global-require": "error", 88 | "grouped-accessor-pairs": "error", 89 | "guard-for-in": "off", 90 | "handle-callback-err": "error", 91 | "id-blacklist": "error", 92 | "id-length": "error", 93 | "id-match": "error", 94 | "implicit-arrow-linebreak": [ 95 | "error", 96 | "beside" 97 | ], 98 | "indent": "off", 99 | "indent-legacy": "off", 100 | "init-declarations": "off", 101 | "jsx-quotes": "error", 102 | "key-spacing": "error", 103 | "keyword-spacing": [ 104 | "error", 105 | { 106 | "after": true, 107 | "before": true 108 | } 109 | ], 110 | "line-comment-position": "error", 111 | "linebreak-style": [ 112 | "error", 113 | "windows" 114 | ], 115 | "lines-around-comment": "error", 116 | "lines-around-directive": "error", 117 | "lines-between-class-members": "error", 118 | "max-classes-per-file": "error", 119 | "max-depth": "error", 120 | "max-len": "off", 121 | "max-lines": "error", 122 | "max-lines-per-function": "off", 123 | "max-nested-callbacks": "error", 124 | "max-params": "error", 125 | "max-statements": "off", 126 | "max-statements-per-line": "error", 127 | "multiline-comment-style": "error", 128 | "multiline-ternary": "error", 129 | "new-cap": "error", 130 | "new-parens": "error", 131 | "newline-after-var": "off", 132 | "newline-before-return": "off", 133 | "newline-per-chained-call": "error", 134 | "no-alert": "error", 135 | "no-array-constructor": "error", 136 | "no-await-in-loop": "error", 137 | "no-bitwise": "error", 138 | "no-buffer-constructor": "error", 139 | "no-caller": "error", 140 | "no-catch-shadow": "error", 141 | "no-confusing-arrow": "error", 142 | "no-console": "off", 143 | "no-constructor-return": "error", 144 | "no-div-regex": "error", 145 | "no-dupe-else-if": "error", 146 | "no-duplicate-imports": "error", 147 | "no-else-return": "error", 148 | "no-empty-function": "error", 149 | "no-eq-null": "error", 150 | "no-eval": "error", 151 | "no-extend-native": "error", 152 | "no-extra-bind": "error", 153 | "no-extra-label": "error", 154 | "no-extra-parens": "error", 155 | "no-floating-decimal": "error", 156 | "no-implicit-coercion": "error", 157 | "no-implicit-globals": "error", 158 | "no-implied-eval": "error", 159 | "no-import-assign": "error", 160 | "no-inline-comments": "error", 161 | "no-inner-declarations": [ 162 | "error", 163 | "functions" 164 | ], 165 | "no-invalid-this": "error", 166 | "no-iterator": "error", 167 | "no-label-var": "error", 168 | "no-labels": "error", 169 | "no-lone-blocks": "error", 170 | "no-lonely-if": "error", 171 | "no-loop-func": "error", 172 | "no-mixed-operators": "error", 173 | "no-mixed-requires": "error", 174 | "no-multi-assign": "error", 175 | "no-multi-spaces": "error", 176 | "no-multi-str": "error", 177 | "no-multiple-empty-lines": "error", 178 | "no-native-reassign": "error", 179 | "no-negated-condition": "off", 180 | "no-negated-in-lhs": "error", 181 | "no-nested-ternary": "error", 182 | "no-new": "error", 183 | "no-new-func": "error", 184 | "no-new-object": "error", 185 | "no-new-require": "error", 186 | "no-new-wrappers": "error", 187 | "no-octal-escape": "error", 188 | "no-path-concat": "error", 189 | "no-plusplus": "error", 190 | "no-process-env": "error", 191 | "no-process-exit": "error", 192 | "no-proto": "error", 193 | "no-restricted-globals": "error", 194 | "no-restricted-imports": "error", 195 | "no-restricted-modules": "error", 196 | "no-restricted-properties": "error", 197 | "no-restricted-syntax": "error", 198 | "no-return-assign": "error", 199 | "no-return-await": "error", 200 | "no-script-url": "error", 201 | "no-self-compare": "error", 202 | "no-sequences": "error", 203 | "no-setter-return": "error", 204 | "no-shadow": "error", 205 | "no-spaced-func": "error", 206 | "no-sync": "error", 207 | "no-tabs": [ 208 | "error", 209 | { 210 | "allowIndentationTabs": true 211 | } 212 | ], 213 | "no-template-curly-in-string": "error", 214 | "no-ternary": "error", 215 | "no-throw-literal": "error", 216 | "no-trailing-spaces": "error", 217 | "no-undef-init": "error", 218 | "no-undefined": "error", 219 | "no-underscore-dangle": "error", 220 | "no-unmodified-loop-condition": "error", 221 | "no-unneeded-ternary": "error", 222 | "no-unused-expressions": "off", 223 | "no-use-before-define": "off", 224 | "no-useless-call": "error", 225 | "no-useless-computed-key": "error", 226 | "no-useless-concat": "error", 227 | "no-useless-constructor": "error", 228 | "no-useless-rename": "error", 229 | "no-useless-return": "error", 230 | "no-var": "off", 231 | "no-void": "error", 232 | "no-warning-comments": "error", 233 | "no-whitespace-before-property": "error", 234 | "nonblock-statement-body-position": "error", 235 | "object-curly-newline": "error", 236 | "object-curly-spacing": "error", 237 | "object-property-newline": "error", 238 | "object-shorthand": "error", 239 | "one-var": "off", 240 | "one-var-declaration-per-line": "error", 241 | "operator-assignment": [ 242 | "error", 243 | "always" 244 | ], 245 | "operator-linebreak": "error", 246 | "padded-blocks": "off", 247 | "padding-line-between-statements": "error", 248 | "prefer-arrow-callback": "error", 249 | "prefer-const": "off", 250 | "prefer-destructuring": "off", 251 | "prefer-exponentiation-operator": "error", 252 | "prefer-named-capture-group": "error", 253 | "prefer-numeric-literals": "error", 254 | "prefer-object-spread": "error", 255 | "prefer-promise-reject-errors": "error", 256 | "prefer-reflect": "off", 257 | "prefer-regex-literals": "error", 258 | "prefer-rest-params": "error", 259 | "prefer-spread": "error", 260 | "prefer-template": "off", 261 | "quote-props": "off", 262 | "quotes": "off", 263 | "radix": "error", 264 | "require-atomic-updates": "error", 265 | "require-await": "error", 266 | "require-jsdoc": "error", 267 | "require-unicode-regexp": "error", 268 | "rest-spread-spacing": [ 269 | "error", 270 | "never" 271 | ], 272 | "semi": "off", 273 | "semi-spacing": "error", 274 | "semi-style": [ 275 | "error", 276 | "last" 277 | ], 278 | "sort-imports": "error", 279 | "sort-keys": "off", 280 | "sort-vars": "error", 281 | "space-before-blocks": "error", 282 | "space-before-function-paren": "off", 283 | "space-in-parens": [ 284 | "error", 285 | "never" 286 | ], 287 | "space-infix-ops": "error", 288 | "space-unary-ops": "error", 289 | "spaced-comment": [ 290 | "error", 291 | "always" 292 | ], 293 | "strict": "off", 294 | "switch-colon-spacing": "error", 295 | "symbol-description": "error", 296 | "template-curly-spacing": "error", 297 | "template-tag-spacing": "error", 298 | "unicode-bom": [ 299 | "error", 300 | "never" 301 | ], 302 | "valid-jsdoc": "error", 303 | "vars-on-top": "off", 304 | "wrap-iife": "error", 305 | "wrap-regex": "error", 306 | "yield-star-spacing": "error", 307 | "yoda": [ 308 | "error", 309 | "never" 310 | ] 311 | } 312 | }; --------------------------------------------------------------------------------