commit f3781775ff177843857ad57105c0c34cfeb32972 Author: Rick Dullaart Date: Fri Nov 18 21:42:43 2022 +0100 Initial commit diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..dfeec2c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,47 @@ +# Contributing to this project + +Please take a moment to review this document in order to make the contribution +process easy and effective for everyone involved. + + +## Using the issue tracker + +The issue tracker is the preferred channel for [bug reports](#bugs) and +[features requests](#features), but please respect the following restrictions: + +* Please **do not** use the issue tracker for personal support requests. + +* Please keep the discussion **on topic** and respect the opinions of others. + + + +## Bug reports + +A bug is a _demonstrable problem_ that is caused by the code in the repository. +Good bug reports are extremely helpful - thank you! + +Guidelines for bug reports: + +1. **Use the GitHub issue search** — check if the issue has already been + reported. + +2. **Check if the issue has been fixed** — try to reproduce it using the + latest branch in the repository. + +3. **Isolate the problem** — create a [reduced test + case](http://css-tricks.com/reduced-test-cases/) and a live example. + +A good bug report shouldn't leave others needing to chase you up for more +information. Please try to be as detailed as possible in your report. What is +your environment? What steps will reproduce the issue? What browser(s) and OS +experience the problem? What would you expect to be the outcome? All these +details will help people to fix any potential bugs. + + + +## Feature requests + +Feature requests are welcome. But take a moment to find out whether your idea +fits with the scope and aims of the project. It's up to *you* to make a strong +case to convince the project's developers of the merits of this feature. Please +provide as much detail and context as possible. \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..d14dbf9 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,2 @@ +This work is licensed under the Creative Commons Attribution 4.0 International License. +To view a copy of this license, visit http://creativecommons.org/licenses/by/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5f48b63 --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +mhead.js +================ + +A small framework for creating a beautiful mobile navigation header to accompany the mmenu.js navigation menu on your website and webapp. + +Need help? Have a look at [the documentation](https://mmenujs.com/mhead-plugin/) for demos and documentation. + +### Licence +The mhead javascript plugin is licensed under the [CC-BY-4.0 license](http://creativecommons.org/licenses/by/4.0/). + +### Development +This project uses [Gulp](http://gulpjs.com/) to transpile, minify and concatenate the TS/JS and SCSS/CSS files. +If you are unfamiliar with Gulp, check [this tutorial](https://travismaynard.com/writing/getting-started-with-gulp) on how to get started.
+Run `gulp watch` in the command-line to put a watch on the files and run all scripts immediately after saving your changes. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..b63d7bc --- /dev/null +++ b/composer.json @@ -0,0 +1,15 @@ +{ + "name" : "mhead-js", + "version" : "2.0.0", + "authors" : "Fred Heusschen ", + "description" : "A small framework for creating a beautiful mobile navigation header to accompany the mmenu.js menu on your website and webapp.", + "keywords" : [ + "app", + "mobile", + "navigation", + "head", + "header", + "navbar" + ], + "license": "CC-BY-4.0" +} diff --git a/demo/css/demo.css b/demo/css/demo.css new file mode 100644 index 0000000..471d0b0 --- /dev/null +++ b/demo/css/demo.css @@ -0,0 +1,96 @@ +html, body { + padding: 0; + margin: 0; +} +body { + background-color: #fff; + font-family: Arial, Helvetica, Verdana; + font-size: 14px; + line-height: 22px; + color: #666; + position: relative; + -webkit-text-size-adjust: none; +} +body * { + text-shadow: none; +} +h1, h2, h3, h4, h5, h6 { + line-height: 1.25; + font-weight: bold; + margin: 20px 0 10px 0; +} +h1, h2, h3 { + font-size: 18px; +} +h4, h5, h6 { + font-size: 16px; +} +p { + margin: 0 0 10px 0; +} +a, a:link, a:active, a:visited, a:hover { + color: inherit; + text-decoration: underline; +} + +nav:not(.mm-menu) { + display: none; +} + +.header { + position: relative; + display: block; + padding: 10px; + line-height: 24px; + font-size: 16px; + font-weight: bold; + color: #fff; + background: #4bb5ef; +} + +.header a { + position: absolute; + top: 50%; + left: 10px; + transform: translate( 0, -50% ); + text-decoration: none; +} +.header span { + display: block; + text-align: center; +} +.header input { + display: block; + box-sizing: border-box; + height: 30px; + width: calc(100% - 30px); + padding: 0 10px; + margin: 0 0 0 30px; + border: none; + border-radius: 3px; + font-size: inherit; +} +.header button { + position: absolute; + top: 50%; + right: 20px; + display: block; + box-sizing: border-box; + padding: 0; + margin: 0; + transform: translate( 0, -50% ); + border: none; + background: none; + color: #999; + cursor: pointer; +} + +#page { + padding-bottom: 100px; +} + +.content { + box-sizing: border-box; + padding: 30vh 50px; + text-align: center; +} diff --git a/demo/css/site.css b/demo/css/site.css new file mode 100644 index 0000000..3f34b11 --- /dev/null +++ b/demo/css/site.css @@ -0,0 +1,75 @@ +html, +body { + padding: 0; + margin: 0; + height: 100%; +} +body { + background-color: #3ea7e1; + -webkit-text-size-adjust: none; + font-family: Arial, Helvetica, Verdana; + font-size: 18px; + line-height: 26px; + color: #fff; + position: relative; +} +h1 { + text-shadow: 8px 10px 1px rgba(0,0,0,.1); + text-transform: lowercase; + font-family: 'Pacifico', Arial, sans-serif; + font-weight: normal; + font-size: 150px; + line-height: 150px; + letter-spacing: -10px; + margin: 0 0 20px 0; +} +a, +a:hover +{ + color: #fff; + text-decoration: underline; +} + +.phone { + position: fixed; + top: 50%; + right: 50%; + height: 760px; + width: 430px; + margin-top: -380px; + background: url( ../img/iphonex-example-blue.png ) center top / 395px auto no-repeat transparent; +} + +.phone:before { + content: ''; + border-radius: 30px 30px 0 0; + background: url( ../img/iphonex-example-camera.png ) center top no-repeat #4bb5ef; + display: block; + width: 290px; + height: 25px; + position: absolute; + top: 62px; + left: calc( 50% - 290px / 2); + z-index: 1; +} + +.phone iframe { + position: absolute; + top: 87px; + left: 70px; + z-index: 0; + width: 290px; + height: 600px; + border: none; + border-radius: 0 0 30px 30px; +} + +#page { + width: 350px; + position: fixed; + margin-top: -20px; + margin-left: 0; + top: 50%; + left: 50%; + transform: translateY(-50%); +} diff --git a/demo/default.html b/demo/default.html new file mode 100644 index 0000000..e90a453 --- /dev/null +++ b/demo/default.html @@ -0,0 +1,75 @@ + + + + + + + + + mhead.js demo + + + + + + + + +
+ +
+ demo +
+ +
+ + + +
+ +
+

This is a demo.
+ Scroll down to see the header in action.

+
+ +
+

Scroll a bit faster.
+ If the header did not yet disappear.

+
+ +
+

Now scroll back up.
+ To make the header appear again.

+
+
+ + + + + + diff --git a/demo/img/iphonex-example-blue.png b/demo/img/iphonex-example-blue.png new file mode 100644 index 0000000..0260b7e Binary files /dev/null and b/demo/img/iphonex-example-blue.png differ diff --git a/demo/img/iphonex-example-camera.png b/demo/img/iphonex-example-camera.png new file mode 100644 index 0000000..fa6e40f Binary files /dev/null and b/demo/img/iphonex-example-camera.png differ diff --git a/dist/_helpers.js b/dist/_helpers.js new file mode 100644 index 0000000..f0f2410 --- /dev/null +++ b/dist/_helpers.js @@ -0,0 +1,40 @@ +/** + * Deep extend an object with the given defaults. + * Note that the extended object is not a clone, meaning the original object will also be updated. + * + * @param {object} orignl The object to extend to. + * @param {object} dfault The object to extend from. + * @return {object} The extended "orignl" object. + */ +export function extend(orignl, dfault) { + if (type(orignl) != 'object') { + orignl = {}; + } + if (type(dfault) != 'object') { + dfault = {}; + } + for (let k in dfault) { + if (!dfault.hasOwnProperty(k)) { + continue; + } + if (typeof orignl[k] == 'undefined') { + orignl[k] = dfault[k]; + } + else if (type(orignl[k]) == 'object') { + extend(orignl[k], dfault[k]); + } + } + return orignl; +} +/** + * Get the type of any given variable. Improvement of "typeof". + * + * @param {any} variable The variable. + * @return {string} The type of the variable in lowercase. + */ +export function type(variable) { + return {}.toString + .call(variable) + .match(/\s([a-zA-Z]+)/)[1] + .toLowerCase(); +} diff --git a/dist/_version.js b/dist/_version.js new file mode 100644 index 0000000..ff603d7 --- /dev/null +++ b/dist/_version.js @@ -0,0 +1 @@ +export default '2.0.0'; diff --git a/dist/core/_options.js b/dist/core/_options.js new file mode 100644 index 0000000..0ca36b3 --- /dev/null +++ b/dist/core/_options.js @@ -0,0 +1,9 @@ +const options = { + hooks: {}, + scroll: { + pin: 0, + unpin: 0, + tolerance: 5 + } +}; +export default options; diff --git a/dist/core/mhead.core.js b/dist/core/mhead.core.js new file mode 100644 index 0000000..8073f0c --- /dev/null +++ b/dist/core/mhead.core.js @@ -0,0 +1,138 @@ +import options from './_options'; +import version from '../_version'; +/** + * Class for a sticky navigational header. + */ +export default class Mhead { + /** + * Create a sticky header. + * @param {HTMLElement|string} header The header node. + * @param {object} [options=Mhead.options] Options for the header. + */ + constructor(header, options = Mhead.options) { + // Get header node from string or element. + this.header = + typeof header == 'string' ? document.querySelector(header) : header; + // Stop if there is no header element found. + if (!header) { + return; + } + // Extend options from defaults. + this.opts = Object.assign(options, Mhead.options); + this.initHooks(); + this.initScroll(); + } + /** + * Initiate the scroll functionality. + */ + initScroll() { + if (!this.opts.scroll || this.opts.scroll.unpin === false) { + return; + } + this.header.classList.add('mh-sticky'); + /** Minimum scroll position to unpin / hide the header. */ + var _min = this.header.offsetHeight * 2; + this.opts.scroll.unpin = Math.max(_min, this.opts.scroll.unpin || 0); + this.opts.scroll.pin = Math.max(_min, this.opts.scroll.pin || 0); + this.state = null; + /** Previous scroll position. */ + var lastYpos = 0; + const onscroll = (evnt = {}) => { + /** Current scroll position. */ + var pos = document.documentElement.scrollTop || document.body.scrollTop; + /** Difference between current scroll position and previous scroll position. */ + var dif = lastYpos - pos; + /** Direction of the scroll. */ + var dir = dif < 0 ? 'down' : 'up'; + dif = Math.abs(dif); + lastYpos = pos; + // If not pinned / scrolled out the viewport. + if (this.state == Mhead.UNPINNED) { + // If scrolling up + if (dir == 'up') { + // If scrolling fast enough or past minimum + if (pos < this.opts.scroll.pin || + dif > this.opts.scroll.tolerance) { + this.pin(); + } + } + } + // If pinned / not scrolled out the viewport. + else if (this.state == Mhead.PINNED) { + // If scrolling down. + if (dir == 'down') { + // If scrolling fast enough and past minimum. + if (pos > this.opts.scroll.unpin && + dif > this.opts.scroll.tolerance) { + this.unpin(); + } + } + } + else { + this.pin(); + } + }; + window.addEventListener('scroll', onscroll, { + passive: true + }); + onscroll(); + } + /** + * Pin the header to the top of the viewport. + */ + pin() { + this.header.classList.add('mh-pinned'); + this.header.classList.remove('mh-unpinned'); + this.state = Mhead.PINNED; + this.trigger('pinned'); + } + /** + * Release the header from the top of the viewport. + */ + unpin() { + this.header.classList.remove('mh-pinned'); + this.header.classList.add('mh-unpinned'); + this.state = Mhead.UNPINNED; + this.trigger('unpinned'); + } + /** + * Bind the hooks specified in the options (publisher). + */ + initHooks() { + this.hooks = {}; + for (let hook in this.opts.hooks) { + this.bind(hook, this.opts.hooks[hook]); + } + } + /** + * Bind functions to a hook (subscriber). + * @param {string} hook The hook. + * @param {function} func The function. + */ + bind(hook, func) { + // Create an array for the hook if it does not yet excist. + this.hooks[hook] = this.hooks[hook] || []; + // Push the function to the array. + this.hooks[hook].push(func); + } + /** + * Invoke the functions bound to a hook (publisher). + * @param {string} hook The hook. + * @param {array} [args] Arguments for the function. + */ + trigger(hook, args) { + if (this.hooks[hook]) { + for (var h = 0, l = this.hooks[hook].length; h < l; h++) { + this.hooks[hook][h].apply(this, args); + } + } + } +} +/** Plugin version. */ +Mhead.version = version; +/** Default options for headers. */ +Mhead.options = options; +/** State for a "pinned" header. */ +Mhead.PINNED = 'pinned'; +/** State for a "unpinned" header. */ +Mhead.UNPINNED = 'unpinned'; diff --git a/dist/core/options.js b/dist/core/options.js new file mode 100644 index 0000000..92e730f --- /dev/null +++ b/dist/core/options.js @@ -0,0 +1,8 @@ +export default { + hooks: {}, + scroll: { + hide: 0, + show: 0, + tolerance: 5 + } +}; diff --git a/dist/mhead.css b/dist/mhead.css new file mode 100644 index 0000000..9830a16 --- /dev/null +++ b/dist/mhead.css @@ -0,0 +1,10 @@ +/*! + * mhead.js + * mmenu.frebsite.nl/mhead-plugin + * + * Copyright (c) Fred Heusschen + * www.frebsite.nl + * + * License: CC-BY-4.0 + * http://creativecommons.org/licenses/by/4.0/ + */.mh-sticky{position:-webkit-sticky;position:sticky;top:0;z-index:10;-webkit-transition:-webkit-transform .2s ease;transition:-webkit-transform .2s ease;-o-transition:transform .2s ease;transition:transform .2s ease;transition:transform .2s ease,-webkit-transform .2s ease;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.mh-sticky.mh-unpinned{-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)} \ No newline at end of file diff --git a/dist/mhead.js b/dist/mhead.js new file mode 100644 index 0000000..73e9de5 --- /dev/null +++ b/dist/mhead.js @@ -0,0 +1,12 @@ +!function(t){var s={};function e(n){if(s[n])return s[n].exports;var i=s[n]={i:n,l:!1,exports:{}};return t[n].call(i.exports,i,i.exports,e),i.l=!0,i.exports}e.m=t,e.c=s,e.d=function(t,s,n){e.o(t,s)||Object.defineProperty(t,s,{enumerable:!0,get:n})},e.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},e.t=function(t,s){if(1&s&&(t=e(t)),8&s)return t;if(4&s&&"object"==typeof t&&t&&t.__esModule)return t;var n=Object.create(null);if(e.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:t}),2&s&&"string"!=typeof t)for(var i in t)e.d(n,i,function(s){return t[s]}.bind(null,i));return n},e.n=function(t){var s=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(s,"a",s),s},e.o=function(t,s){return Object.prototype.hasOwnProperty.call(t,s)},e.p="",e(e.s=0)}([function(t,s,e){"use strict";e.r(s);var n,i={hooks:{},scroll:{pin:0,unpin:0,tolerance:5}};class o{constructor(t,s=o.options){this.header="string"==typeof t?document.querySelector(t):t,t&&(this.opts=Object.assign(s,o.options),this.initHooks(),this.initScroll())}initScroll(){if(!this.opts.scroll||!1===this.opts.scroll.unpin)return;this.header.classList.add("mh-sticky");var t=2*this.header.offsetHeight;this.opts.scroll.unpin=Math.max(t,this.opts.scroll.unpin||0),this.opts.scroll.pin=Math.max(t,this.opts.scroll.pin||0),this.state=null;var s=0;const e=(t={})=>{var e=document.documentElement.scrollTop||document.body.scrollTop,n=s-e,i=n<0?"down":"up";n=Math.abs(n),s=e,this.state==o.UNPINNED?"up"==i&&(ethis.opts.scroll.tolerance)&&this.pin():this.state==o.PINNED?"down"==i&&e>this.opts.scroll.unpin&&n>this.opts.scroll.tolerance&&this.unpin():this.pin()};window.addEventListener("scroll",e,{passive:!0}),e()}pin(){this.header.classList.add("mh-pinned"),this.header.classList.remove("mh-unpinned"),this.state=o.PINNED,this.trigger("pinned")}unpin(){this.header.classList.remove("mh-pinned"),this.header.classList.add("mh-unpinned"),this.state=o.UNPINNED,this.trigger("unpinned")}initHooks(){this.hooks={};for(let t in this.opts.hooks)this.bind(t,this.opts.hooks[t])}bind(t,s){this.hooks[t]=this.hooks[t]||[],this.hooks[t].push(s)}trigger(t,s){if(this.hooks[t])for(var e=0,n=this.hooks[t].length;e{new o(e,t)})})}]); \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..fd7de1d --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,66 @@ +/* + Tasks: + + $ gulp : Runs "css" and "js" tasks + $ gulp watch : Starts a watch on "css" and "js" tasks +*/ + +const gulp = require('gulp'); +const sass = require('gulp-sass'); +const autoprefixer = require('gulp-autoprefixer'); +const cleancss = require('gulp-clean-css'); +const typescript = require('gulp-typescript'); +const webpack = require('webpack-stream'); + +const inputDir = 'src'; +const outputDir = 'dist'; + +exports.css = css = () => { + return gulp + .src(inputDir + '/*.scss') + .pipe(sass().on('error', sass.logError)) + .pipe(autoprefixer(['> 5%', 'last 5 versions'])) + .pipe(cleancss()) + .pipe(gulp.dest(outputDir)); +}; + +// Transpile all TS files to JS. +const JStranspile = cb => { + return gulp + .src(inputDir + '/**/*.ts') + .pipe( + typescript({ + target: 'es6', + module: 'es6' + }) + ) + .pipe(gulp.dest(outputDir)); +}; + +// Pack the files. +const JSpack = () => { + return gulp + .src(inputDir + '/mhead.js') + .pipe( + webpack({ + // mode: 'development', + mode: 'production', + output: { + filename: 'mhead.js' + } + // optimization: { + // minimize: false + // } + }) + ) + .pipe(gulp.dest(outputDir)); +}; + +exports.js = js = gulp.series(JStranspile, JSpack); +exports.default = gulp.parallel(js, css); + +exports.watch = cb => { + gulp.watch('src/**/*.ts', js); + gulp.watch('src/**/*.scss', css); + cb(); +}; diff --git a/index.html b/index.html new file mode 100644 index 0000000..1dd6744 --- /dev/null +++ b/index.html @@ -0,0 +1,26 @@ + + + + + + + + + mhead.js - sticky mobile navigation headers. + + + + + +
+
+ +
+
+

mhead

+

Keep your header in view when appropriate, while saving space the rest of the time.

+

Check out the documentation for more information.

+
+
+ + diff --git a/package.json b/package.json new file mode 100644 index 0000000..ff64bc6 --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "mhead-js", + "version": "2.0.0", + "main": "dist/mhead.js", + "module": "src/mhead.js", + "author": "Fred Heusschen ", + "repository": { + "type": "git", + "url": "https://github.com/FrDH/jQuery.mhead.git" + }, + "description": "A small framework for creating a beautiful mobile navigation header to accompany the mmenu.js menu on your website and webapp.", + "keywords": [ + "app", + "mobile", + "navigation", + "head", + "header", + "navbar" + ], + "license": "CC-BY-NC-4.0", + "devDependencies": { + "gulp": "^4.0.0", + "gulp-autoprefixer": "^6.1.0", + "gulp-clean-css": "^4.0.0", + "gulp-sass": "^4.0.2", + "gulp-typescript": "^5.0.1", + "typescript": "^3.4.4", + "webpack-stream": "^5.2.1" + } +} diff --git a/src/_version.ts b/src/_version.ts new file mode 100644 index 0000000..ff603d7 --- /dev/null +++ b/src/_version.ts @@ -0,0 +1 @@ +export default '2.0.0'; diff --git a/src/core/_options.ts b/src/core/_options.ts new file mode 100644 index 0000000..c57373a --- /dev/null +++ b/src/core/_options.ts @@ -0,0 +1,9 @@ +const options: mhOptions = { + hooks: {}, + scroll: { + pin: 0, + unpin: 0, + tolerance: 5 + } +}; +export default options; diff --git a/src/core/_typings.d.ts b/src/core/_typings.d.ts new file mode 100644 index 0000000..71cdefb --- /dev/null +++ b/src/core/_typings.d.ts @@ -0,0 +1,30 @@ +/** An object with array of functions values. */ +interface mhFunctionArrayObject { + [key: string]: Function[]; +} + +/** An object with function values. */ +interface mhFunctionObject { + [key: string]: Function; +} + +/** Options for the header. */ +interface mhOptions { + /** Set of hooks for the header. */ + hooks?: mhFunctionObject; + + /** Scroll options for the header. */ + scroll?: mhOptionsScroll; +} + +/** Scroll options for the header. */ +interface mhOptionsScroll { + /** Minimum scroll position to pin the header when scrolling up. */ + pin?: number | false; + + /** Minimum scroll position to unpin the header when scrolling down. */ + unpin?: number | false; + + /** Tolerance for scrolling speed. */ + tolerance?: number; +} diff --git a/src/core/mhead.core.scss b/src/core/mhead.core.scss new file mode 100644 index 0000000..f81f5ae --- /dev/null +++ b/src/core/mhead.core.scss @@ -0,0 +1,11 @@ +.mh-sticky { + position: sticky; + top: 0; + z-index: 10; + transition: transform 0.2s ease; + transform: translate3d(0, 0, 0); + + &.mh-unpinned { + transform: translate3d(0, -100%, 0); + } +} diff --git a/src/core/mhead.core.ts b/src/core/mhead.core.ts new file mode 100644 index 0000000..0c22d53 --- /dev/null +++ b/src/core/mhead.core.ts @@ -0,0 +1,183 @@ +import options from './_options'; +import version from '../_version'; + +/** + * Class for a sticky navigational header. + */ +export default class Mhead { + /** Plugin version. */ + static version: string = version; + + /** Default options for headers. */ + static options: mhOptions = options; + + /** State for a "pinned" header. */ + static PINNED: string = 'pinned'; + + /** State for a "unpinned" header. */ + static UNPINNED: string = 'unpinned'; + + /** The HTML element for the header. */ + header: HTMLElement; + + /** Options for the header. */ + opts: mhOptions; + + /** Callback hooks used for the header. */ + hooks: mhFunctionArrayObject; + + /** State of the header (pinned or unpinned). */ + state: string; + + /** + * Create a sticky header. + * @param {HTMLElement|string} header (Selector for) the header node. + * @param {object} [options=Mhead.options] Options for the header. + */ + constructor(header: HTMLElement | string, options: mhOptions = {}) { + // Get header node from string or element. + this.header = + typeof header == 'string' ? document.querySelector(header) : header; + + // Stop if there is no header element found. + if (!header) { + return; + } + + // Extend options from defaults. + this.opts = Object.assign(options, Mhead.options); + + this.initHooks(); + this.initScroll(); + } + + /** + * Initiate the scroll functionality. + */ + initScroll() { + if (!this.opts.scroll || this.opts.scroll.unpin === false) { + return; + } + + this.header.classList.add('mh-sticky'); + + /** Minimum scroll position to unpin / hide the header. */ + var _min = this.header.offsetHeight * 2; + this.opts.scroll.unpin = Math.max(_min, this.opts.scroll.unpin || 0); + this.opts.scroll.pin = Math.max(_min, this.opts.scroll.pin || 0); + + this.state = null; + + /** Previous scroll position. */ + var lastYpos = 0; + + const onscroll = (evnt = {}) => { + /** Current scroll position. */ + var pos = + document.documentElement.scrollTop || document.body.scrollTop; + + /** Difference between current scroll position and previous scroll position. */ + var dif = lastYpos - pos; + + /** Direction of the scroll. */ + var dir = dif < 0 ? 'down' : 'up'; + + dif = Math.abs(dif); + lastYpos = pos; + + // If not pinned / scrolled out the viewport. + if (this.state == Mhead.UNPINNED) { + // If scrolling up + if (dir == 'up') { + // If scrolling fast enough or past minimum + if ( + pos < this.opts.scroll.pin || + dif > this.opts.scroll.tolerance + ) { + this.pin(); + } + } + } + + // If pinned / not scrolled out the viewport. + else if (this.state == Mhead.PINNED) { + // If scrolling down. + if (dir == 'down') { + // If scrolling fast enough and past minimum. + if ( + pos > this.opts.scroll.unpin && + dif > this.opts.scroll.tolerance + ) { + this.unpin(); + } + } + } else { + this.pin(); + } + }; + + window.addEventListener('scroll', onscroll, { + passive: true + }); + + onscroll(); + } + + /** + * Pin the header to the top of the viewport. + */ + pin() { + this.header.classList.add('mh-pinned'); + this.header.classList.remove('mh-unpinned'); + this.state = Mhead.PINNED; + + this.trigger('pinned'); + } + + /** + * Release the header from the top of the viewport. + */ + unpin() { + this.header.classList.remove('mh-pinned'); + this.header.classList.add('mh-unpinned'); + this.state = Mhead.UNPINNED; + + this.trigger('unpinned'); + } + + /** + * Bind the hooks specified in the options (publisher). + */ + initHooks() { + this.hooks = {}; + for (let hook in this.opts.hooks) { + this.bind(hook, this.opts.hooks[hook]); + } + } + + /** + * Bind functions to a hook (subscriber). + * @param {string} hook The hook. + * @param {function} func The function. + */ + bind(hook: string, func: Function) { + // Create an array for the hook if it does not yet excist. + this.hooks[hook] = this.hooks[hook] || []; + + // Push the function to the array. + this.hooks[hook].push(func); + } + + /** + * Invoke the functions bound to a hook (publisher). + * @param {string} hook The hook. + * @param {array} [args] Arguments for the function. + */ + trigger(hook: string, args?: any[]) { + if (this.hooks[hook]) { + for (var h = 0, l = this.hooks[hook].length; h < l; h++) { + this.hooks[hook][h].apply(this, args); + } + } + } +} diff --git a/src/mhead.js b/src/mhead.js new file mode 100644 index 0000000..70016bd --- /dev/null +++ b/src/mhead.js @@ -0,0 +1,26 @@ +/*! + * mhead.js + * mmenu.frebsite.nl/mhead + * + * Copyright (c) Fred Heusschen + * www.frebsite.nl + * + * License: CC-BY-4.0 + * http://creativecommons.org/licenses/by/4.0/ + */ + +import Mhead from '../dist/core/mhead.core'; + +// Global namespace +window.Mhead = Mhead; + +// jQuery plugin +(function($) { + if ($) { + $.fn.mhead = function(options) { + return this.each((e, element) => { + new Mhead(element, options); + }); + }; + } +})(window.jQuery || window.Zepto || null); diff --git a/src/mhead.scss b/src/mhead.scss new file mode 100644 index 0000000..421f44c --- /dev/null +++ b/src/mhead.scss @@ -0,0 +1,12 @@ +/*! + * mhead.js + * mmenu.frebsite.nl/mhead + * + * Copyright (c) Fred Heusschen + * www.frebsite.nl + * + * License: CC-BY-4.0 + * http://creativecommons.org/licenses/by/4.0/ + */ + +@import 'core/mhead.core'; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..9e884cd --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,8 @@ +{ + "include": [ + "src/**/*" + ], + "compilerOptions": { + "target": "es6" + } +}