mmenu-js
Version:
The best javascript plugin for app look-alike on- and off-canvas menus with sliding submenus for your website and webapp.
423 lines (349 loc) • 11.8 kB
text/typescript
import Mmenu from './../oncanvas/mmenu.oncanvas';
import options from './_options';
import configs from './_configs';
import { extendShorthandOptions } from './_options';
import * as DOM from '../../_modules/dom';
import * as events from '../../_modules/eventlisteners';
import {
extend,
transitionend,
uniqueId,
originalId
} from '../../_modules/helpers';
// Add the options and configs.
Mmenu.options.offCanvas = options;
Mmenu.configs.offCanvas = configs;
export default function(this: Mmenu) {
if (!this.opts.offCanvas) {
return;
}
var options = extendShorthandOptions(this.opts.offCanvas);
this.opts.offCanvas = extend(options, Mmenu.options.offCanvas);
var configs = this.conf.offCanvas;
// Add methods to the API.
this._api.push('open', 'close', 'setPage');
// Setup the menu.
this.vars.opened = false;
// Add off-canvas behavior.
this.bind('initMenu:before', () => {
// Clone if needed.
if (configs.clone) {
// Clone the original menu and store it.
this.node.menu = this.node.menu.cloneNode(true) as HTMLElement;
// Prefix all ID's in the cloned menu.
if (this.node.menu.id) {
this.node.menu.id = 'mm-' + this.node.menu.id;
}
DOM.find(this.node.menu, '[id]').forEach(elem => {
elem.id = 'mm-' + elem.id;
});
}
this.node.wrpr = document.body;
// Prepend to the <body>
document
.querySelector(configs.menu.insertSelector)
[configs.menu.insertMethod](this.node.menu);
});
this.bind('initMenu:after', () => {
// Setup the UI blocker.
initBlocker.call(this);
// Setup the page.
this.setPage(Mmenu.node.page);
// Setup window events.
initWindow.call(this);
// Setup the menu.
this.node.menu.classList.add('mm-menu_offcanvas');
// Open if url hash equals menu id (usefull when user clicks the hamburger icon before the menu is created)
let hash = window.location.hash;
if (hash) {
let id = originalId(this.node.menu.id);
if (id && id == hash.slice(1)) {
setTimeout(() => {
this.open();
}, 1000);
}
}
});
// Sync the blocker to target the page.
this.bind('setPage:after', (page: HTMLElement) => {
if (Mmenu.node.blck) {
DOM.children(Mmenu.node.blck, 'a').forEach(anchor => {
anchor.setAttribute('href', '#' + page.id);
});
}
});
// Add screenreader / aria support
this.bind('open:start:sr-aria', () => {
Mmenu.sr_aria(this.node.menu, 'hidden', false);
});
this.bind('close:finish:sr-aria', () => {
Mmenu.sr_aria(this.node.menu, 'hidden', true);
});
this.bind('initMenu:after:sr-aria', () => {
Mmenu.sr_aria(this.node.menu, 'hidden', true);
});
// Add screenreader / text support
this.bind('initBlocker:after:sr-text', () => {
DOM.children(Mmenu.node.blck, 'a').forEach(anchor => {
anchor.innerHTML = Mmenu.sr_text(
this.i18n(this.conf.screenReader.text.closeMenu)
);
});
});
// Add click behavior.
// Prevents default behavior when clicking an anchor
this.clck.push((anchor: HTMLElement, args: mmClickArguments) => {
// Open menu if the clicked anchor links to the menu
let id = originalId(this.node.menu.id);
if (id) {
if (anchor.matches('[href="#' + id + '"]')) {
// Opening this menu from within this menu
// -> Open menu
if (args.inMenu) {
this.open();
return true;
}
// Opening this menu from within a second menu
// -> Close the second menu before opening this menu
var menu = anchor.closest('.mm-menu') as HTMLElement;
if (menu) {
var api: mmApi = menu['mmApi'];
if (api && api.close) {
api.close();
transitionend(
menu,
() => {
this.open();
},
this.conf.transitionDuration
);
return true;
}
}
// Opening this menu
this.open();
return true;
}
}
// Close menu
id = Mmenu.node.page.id;
if (id) {
if (anchor.matches('[href="#' + id + '"]')) {
this.close();
return true;
}
}
return;
});
}
/**
* Open the menu.
*/
Mmenu.prototype.open = function(this: Mmenu) {
// Invoke "before" hook.
this.trigger('open:before');
if (this.vars.opened) {
return;
}
this._openSetup();
// Without the timeout, the animation won't work because the menu had display: none;
setTimeout(() => {
this._openStart();
}, this.conf.openingInterval);
// Invoke "after" hook.
this.trigger('open:after');
};
Mmenu.prototype._openSetup = function(this: Mmenu) {
var options = this.opts.offCanvas;
// Close other menus
this.closeAllOthers();
// Store style and position
Mmenu.node.page['mmStyle'] = Mmenu.node.page.getAttribute('style') || '';
// Trigger window-resize to measure height
events.trigger(window, 'resize.page', { force: true });
var clsn = ['mm-wrapper_opened'];
// Add options
if (options.blockUI) {
clsn.push('mm-wrapper_blocking');
}
if (options.blockUI == 'modal') {
clsn.push('mm-wrapper_modal');
}
if (options.moveBackground) {
clsn.push('mm-wrapper_background');
}
// IE11:
clsn.forEach(classname => {
this.node.wrpr.classList.add(classname);
});
// Better browsers:
// this.node.wrpr.classList.add(...clsn);
// Open
// Without the timeout, the animation won't work because the menu had display: none;
setTimeout(() => {
this.vars.opened = true;
}, this.conf.openingInterval);
this.node.menu.classList.add('mm-menu_opened');
};
/**
* Finish opening the menu.
*/
Mmenu.prototype._openStart = function(this: Mmenu) {
// Callback when the page finishes opening.
transitionend(
Mmenu.node.page,
() => {
this.trigger('open:finish');
},
this.conf.transitionDuration
);
// Opening
this.trigger('open:start');
this.node.wrpr.classList.add('mm-wrapper_opening');
};
Mmenu.prototype.close = function(this: Mmenu) {
// Invoke "before" hook.
this.trigger('close:before');
if (!this.vars.opened) {
return;
}
// Callback when the page finishes closing.
transitionend(
Mmenu.node.page,
() => {
this.node.menu.classList.remove('mm-menu_opened');
var classnames = [
'mm-wrapper_opened',
'mm-wrapper_blocking',
'mm-wrapper_modal',
'mm-wrapper_background'
];
// IE11:
classnames.forEach(classname => {
this.node.wrpr.classList.remove(classname);
});
// Better browsers:
// this.node.wrpr.classList.remove(...classnames);
// Restore style and position
Mmenu.node.page.setAttribute('style', Mmenu.node.page['mmStyle']);
this.vars.opened = false;
this.trigger('close:finish');
},
this.conf.transitionDuration
);
// Closing
this.trigger('close:start');
this.node.wrpr.classList.remove('mm-wrapper_opening');
// Invoke "after" hook.
this.trigger('close:after');
};
/**
* Close all other menus.
*/
Mmenu.prototype.closeAllOthers = function(this: Mmenu) {
DOM.find(document.body, '.mm-menu_offcanvas').forEach(menu => {
if (menu !== this.node.menu) {
let api: mmApi = menu['mmApi'];
if (api && api.close) {
api.close();
}
}
});
};
/**
* Set the "page" node.
*
* @param {HTMLElement} page Element to set as the page.
*/
Mmenu.prototype.setPage = function(this: Mmenu, page: HTMLElement) {
// Invoke "before" hook.
this.trigger('setPage:before', [page]);
var configs = this.conf.offCanvas;
// If no page was specified, find it.
if (!page) {
/** Array of elements that are / could be "the page". */
let pages =
typeof configs.page.selector == 'string'
? DOM.find(document.body, configs.page.selector)
: DOM.children(document.body, configs.page.nodetype);
// Filter out elements that are absolutely not "the page".
pages = pages.filter(
page => !page.matches('.mm-menu, .mm-wrapper__blocker')
);
// Filter out elements that are configured to not be "the page".
if (configs.page.noSelector.length) {
pages = pages.filter(
page => !page.matches(configs.page.noSelector.join(', '))
);
}
// Wrap multiple pages in a single element.
if (pages.length > 1) {
let wrapper = DOM.create('div');
pages[0].before(wrapper);
pages.forEach(page => {
wrapper.append(page);
});
pages = [wrapper];
}
page = pages[0];
}
page.classList.add('mm-page');
page.classList.add('mm-slideout');
page.id = page.id || uniqueId();
Mmenu.node.page = page;
// Invoke "after" hook.
this.trigger('setPage:after', [page]);
};
/**
* Initialize the window.
*/
const initWindow = function(this: Mmenu) {
// Prevent tabbing
// Because when tabbing outside the menu, the element that gains focus will be centered on the screen.
// In other words: The menu would move out of view.
events.off(document.body, 'keydown.tabguard');
events.on(document.body, 'keydown.tabguard', (evnt: KeyboardEvent) => {
if (evnt.keyCode == 9) {
if (this.node.wrpr.matches('.mm-wrapper_opened')) {
evnt.preventDefault();
}
}
});
};
/**
* Initialize "blocker" node
*/
const initBlocker = function(this: Mmenu) {
// Invoke "before" hook.
this.trigger('initBlocker:before');
var options = this.opts.offCanvas,
configs = this.conf.offCanvas;
if (!options.blockUI) {
return;
}
// Create the blocker node.
if (!Mmenu.node.blck) {
let blck = DOM.create('div.mm-wrapper__blocker.mm-slideout');
blck.innerHTML = '<a></a>';
// Append the blocker node to the body.
document.querySelector(configs.menu.insertSelector).append(blck);
// Store the blocker node.
Mmenu.node.blck = blck;
}
// Close the menu when
// 1) clicking,
// 2) touching or
// 3) dragging the blocker node.
var closeMenu = (evnt: Event) => {
evnt.preventDefault();
evnt.stopPropagation();
if (!this.node.wrpr.matches('.mm-wrapper_modal')) {
this.close();
}
};
Mmenu.node.blck.addEventListener('mousedown', closeMenu); // 1
Mmenu.node.blck.addEventListener('touchstart', closeMenu); // 2
Mmenu.node.blck.addEventListener('touchmove', closeMenu); // 3
// Invoke "after" hook.
this.trigger('initBlocker:after');
};