├── .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 | [](https://www.codefactor.io/repository/github/bfiessinger/vanilla-js-magic-line-navigation)
5 | [](https://github.com/prettier/prettier)
6 |
7 | ## Browser Support
8 |  |  |  |  |  | 
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 | 
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 | };
--------------------------------------------------------------------------------