import version from '../../_version'; import options from './_options'; import configs from './_configs'; import translate from './translations/translate'; import * as DOM from '../../_modules/dom'; import * as i18n from '../../_modules/i18n'; import * as media from '../../_modules/matchmedia'; import { type, extend, transitionend, uniqueId, valueOrFn } from '../../_modules/helpers'; // Add the translations. translate(); /** * Class for a mobile menu. */ var Mmenu = /** @class */ (function () { /** * Create a mobile menu. * @param {HTMLElement|string} menu The menu node. * @param {object} [options=Mmenu.options] Options for the menu. * @param {object} [configs=Mmenu.configs] Configuration options for the menu. */ function Mmenu(menu, options, configs) { // Extend options and configuration from defaults. this.opts = extend(options, Mmenu.options); this.conf = extend(configs, Mmenu.configs); // Methods to expose in the API. this._api = [ 'bind', 'initPanel', 'initListview', 'openPanel', 'closePanel', 'closeAllPanels', 'setSelected' ]; // Storage objects for nodes, variables, hooks and click handlers. this.node = {}; this.vars = {}; this.hook = {}; this.clck = []; // Get menu node from string or element. this.node.menu = typeof menu == 'string' ? document.querySelector(menu) : menu; if (typeof this._deprecatedWarnings == 'function') { this._deprecatedWarnings(); } this._initWrappers(); this._initAddons(); this._initExtensions(); this._initHooks(); this._initAPI(); this._initMenu(); this._initPanels(); this._initOpened(); this._initAnchors(); media.watch(); return this; } /** * Open a panel. * @param {HTMLElement} panel Panel to open. * @param {boolean} [animation=true] Whether or not to open the panel with an animation. */ Mmenu.prototype.openPanel = function (panel, animation) { var _this = this; // Invoke "before" hook. this.trigger('openPanel:before', [panel]); // Find panel. if (!panel) { return; } if (!panel.matches('.mm-panel')) { panel = panel.closest('.mm-panel'); } if (!panel) { return; } // /Find panel. if (typeof animation != 'boolean') { animation = true; } // Open a "vertical" panel. if (panel.parentElement.matches('.mm-listitem_vertical')) { // Open current and all vertical parent panels. DOM.parents(panel, '.mm-listitem_vertical').forEach(function (listitem) { listitem.classList.add('mm-listitem_opened'); DOM.children(listitem, '.mm-panel').forEach(function (panel) { panel.classList.remove('mm-hidden'); }); }); // Open first non-vertical parent panel. var parents = DOM.parents(panel, '.mm-panel').filter(function (panel) { return !panel.parentElement.matches('.mm-listitem_vertical'); }); this.trigger('openPanel:start', [panel]); if (parents.length) { this.openPanel(parents[0]); } this.trigger('openPanel:finish', [panel]); // Open a "horizontal" panel. } else { if (panel.matches('.mm-panel_opened')) { return; } var panels = DOM.children(this.node.pnls, '.mm-panel'), current_1 = DOM.children(this.node.pnls, '.mm-panel_opened')[0]; // Close all child panels. panels .filter(function (parent) { return parent !== panel; }) .forEach(function (parent) { parent.classList.remove('mm-panel_opened-parent'); }); // Open all parent panels. var parent_1 = panel['mmParent']; while (parent_1) { parent_1 = parent_1.closest('.mm-panel'); if (parent_1) { if (!parent_1.parentElement.matches('.mm-listitem_vertical')) { parent_1.classList.add('mm-panel_opened-parent'); } parent_1 = parent_1['mmParent']; } } // Add classes for animation. panels.forEach(function (panel) { panel.classList.remove('mm-panel_highest'); }); panels .filter(function (hidden) { return hidden !== current_1; }) .filter(function (hidden) { return hidden !== panel; }) .forEach(function (hidden) { hidden.classList.add('mm-hidden'); }); panel.classList.remove('mm-hidden'); /** Start opening the panel. */ var openPanelStart_1 = function () { if (current_1) { current_1.classList.remove('mm-panel_opened'); } panel.classList.add('mm-panel_opened'); if (panel.matches('.mm-panel_opened-parent')) { if (current_1) { current_1.classList.add('mm-panel_highest'); } panel.classList.remove('mm-panel_opened-parent'); } else { if (current_1) { current_1.classList.add('mm-panel_opened-parent'); } panel.classList.add('mm-panel_highest'); } // Invoke "start" hook. _this.trigger('openPanel:start', [panel]); }; /** Finish opening the panel. */ var openPanelFinish_1 = function () { if (current_1) { current_1.classList.remove('mm-panel_highest'); current_1.classList.add('mm-hidden'); } panel.classList.remove('mm-panel_highest'); // Invoke "finish" hook. _this.trigger('openPanel:finish', [panel]); }; if (animation && !panel.matches('.mm-panel_noanimation')) { // Without the timeout the animation will not work because the element had display: none; setTimeout(function () { // Callback transitionend(panel, function () { openPanelFinish_1(); }, _this.conf.transitionDuration); openPanelStart_1(); }, this.conf.openingInterval); } else { openPanelStart_1(); openPanelFinish_1(); } } // Invoke "after" hook. this.trigger('openPanel:after', [panel]); }; /** * Close a panel. * @param {HTMLElement} panel Panel to close. */ Mmenu.prototype.closePanel = function (panel) { // Invoke "before" hook. this.trigger('closePanel:before', [panel]); var li = panel.parentElement; // Only works for "vertical" panels. if (li.matches('.mm-listitem_vertical')) { li.classList.remove('mm-listitem_opened'); panel.classList.add('mm-hidden'); // Invoke main hook. this.trigger('closePanel', [panel]); } // Invoke "after" hook. this.trigger('closePanel:after', [panel]); }; /** * Close all opened panels. * @param {HTMLElement} panel Panel to open after closing all other panels. */ Mmenu.prototype.closeAllPanels = function (panel) { // Invoke "before" hook. this.trigger('closeAllPanels:before'); // Close all "vertical" panels. var listitems = this.node.pnls.querySelectorAll('.mm-listitem'); listitems.forEach(function (listitem) { listitem.classList.remove('mm-listitem_selected'); listitem.classList.remove('mm-listitem_opened'); }); // Close all "horizontal" panels. var panels = DOM.children(this.node.pnls, '.mm-panel'), opened = panel ? panel : panels[0]; DOM.children(this.node.pnls, '.mm-panel').forEach(function (panel) { if (panel !== opened) { panel.classList.remove('mm-panel_opened'); panel.classList.remove('mm-panel_opened-parent'); panel.classList.remove('mm-panel_highest'); panel.classList.add('mm-hidden'); } }); // Open first panel. this.openPanel(opened, false); // Invoke "after" hook. this.trigger('closeAllPanels:after'); }; /** * Toggle a panel opened/closed. * @param {HTMLElement} panel Panel to open or close. */ Mmenu.prototype.togglePanel = function (panel) { var listitem = panel.parentElement; // Only works for "vertical" panels. if (listitem.matches('.mm-listitem_vertical')) { this[listitem.matches('.mm-listitem_opened') ? 'closePanel' : 'openPanel'](panel); } }; /** * Display a listitem as being "selected". * @param {HTMLElement} listitem Listitem to mark. */ Mmenu.prototype.setSelected = function (listitem) { // Invoke "before" hook. this.trigger('setSelected:before', [listitem]); // First, remove the selected class from all listitems. DOM.find(this.node.menu, '.mm-listitem_selected').forEach(function (li) { li.classList.remove('mm-listitem_selected'); }); // Next, add the selected class to the provided listitem. listitem.classList.add('mm-listitem_selected'); // Invoke "after" hook. this.trigger('setSelected:after', [listitem]); }; /** * Bind functions to a hook (subscriber). * @param {string} hook The hook. * @param {function} func The function. */ Mmenu.prototype.bind = function (hook, func) { // Create an array for the hook if it does not yet excist. this.hook[hook] = this.hook[hook] || []; // Push the function to the array. this.hook[hook].push(func); }; /** * Invoke the functions bound to a hook (publisher). * @param {string} hook The hook. * @param {array} [args] Arguments for the function. */ Mmenu.prototype.trigger = function (hook, args) { if (this.hook[hook]) { for (var h = 0, l = this.hook[hook].length; h < l; h++) { this.hook[hook][h].apply(this, args); } } }; /** * Create the API. */ Mmenu.prototype._initAPI = function () { var _this = this; // We need this=that because: // 1) the "arguments" object can not be referenced in an arrow function in ES3 and ES5. var that = this; this.API = {}; this._api.forEach(function (fn) { _this.API[fn] = function () { var re = that[fn].apply(that, arguments); // 1) return typeof re == 'undefined' ? that.API : re; }; }); // Store the API in the HTML node for external usage. this.node.menu['mmApi'] = this.API; }; /** * Bind the hooks specified in the options (publisher). */ Mmenu.prototype._initHooks = function () { for (var hook in this.opts.hooks) { this.bind(hook, this.opts.hooks[hook]); } }; /** * Initialize the wrappers specified in the options. */ Mmenu.prototype._initWrappers = function () { // Invoke "before" hook. this.trigger('initWrappers:before'); for (var w = 0; w < this.opts.wrappers.length; w++) { var wrpr = Mmenu.wrappers[this.opts.wrappers[w]]; if (typeof wrpr == 'function') { wrpr.call(this); } } // Invoke "after" hook. this.trigger('initWrappers:after'); }; /** * Initialize all available add-ons. */ Mmenu.prototype._initAddons = function () { // Invoke "before" hook. this.trigger('initAddons:before'); for (var addon in Mmenu.addons) { Mmenu.addons[addon].call(this); } // Invoke "after" hook. this.trigger('initAddons:after'); }; /** * Initialize the extensions specified in the options. */ Mmenu.prototype._initExtensions = function () { var _this = this; // Invoke "before" hook. this.trigger('initExtensions:before'); // Convert array to object with array. if (type(this.opts.extensions) == 'array') { this.opts.extensions = { all: this.opts.extensions }; } // Loop over object. Object.keys(this.opts.extensions).forEach(function (query) { var classnames = _this.opts.extensions[query].map(function (extension) { return 'mm-menu_' + extension; }); if (classnames.length) { media.add(query, function () { // IE11: classnames.forEach(function (classname) { _this.node.menu.classList.add(classname); }); // Better browsers: // this.node.menu.classList.add(...classnames); }, function () { // IE11: classnames.forEach(function (classname) { _this.node.menu.classList.remove(classname); }); // Better browsers: // this.node.menu.classList.remove(...classnames); }); } }); // Invoke "after" hook. this.trigger('initExtensions:after'); }; /** * Initialize the menu. */ Mmenu.prototype._initMenu = function () { var _this = this; // Invoke "before" hook. this.trigger('initMenu:before'); // Add class to the wrapper. this.node.wrpr = this.node.wrpr || this.node.menu.parentElement; this.node.wrpr.classList.add('mm-wrapper'); // Add an ID to the menu if it does not yet have one. this.node.menu.id = this.node.menu.id || uniqueId(); // Wrap the panels in a node. var panels = DOM.create('div.mm-panels'); DOM.children(this.node.menu).forEach(function (panel) { if (_this.conf.panelNodetype.indexOf(panel.nodeName.toLowerCase()) > -1) { panels.append(panel); } }); this.node.menu.append(panels); this.node.pnls = panels; // Add class to the menu. this.node.menu.classList.add('mm-menu'); // Invoke "after" hook. this.trigger('initMenu:after'); }; /** * Initialize panels. */ Mmenu.prototype._initPanels = function () { var _this = this; // Invoke "before" hook. this.trigger('initPanels:before'); // Open / close panels. this.clck.push(function (anchor, args) { if (args.inMenu) { var href = anchor.getAttribute('href'); if (href && href.length > 1 && href.slice(0, 1) == '#') { try { var panel = DOM.find(_this.node.menu, href)[0]; if (panel && panel.matches('.mm-panel')) { if (anchor.parentElement.matches('.mm-listitem_vertical')) { _this.togglePanel(panel); } else { _this.openPanel(panel); } return true; } } catch (err) { } } } }); /** The panels to initiate */ var panels = DOM.children(this.node.pnls); panels.forEach(function (panel) { _this.initPanel(panel); }); // Invoke "after" hook. this.trigger('initPanels:after'); }; /** * Initialize a single panel and its children. * @param {HTMLElement} panel The panel to initialize. */ Mmenu.prototype.initPanel = function (panel) { var _this = this; /** Query selector for possible node-types for panels. */ var panelNodetype = this.conf.panelNodetype.join(', '); if (panel.matches(panelNodetype)) { // Only once if (!panel.matches('.mm-panel')) { panel = this._initPanel(panel); } if (panel) { /** The sub panels. */ var children_1 = []; // Find panel > panel children_1.push.apply(children_1, DOM.children(panel, '.' + this.conf.classNames.panel)); // Find panel listitem > panel DOM.children(panel, '.mm-listview').forEach(function (listview) { DOM.children(listview, '.mm-listitem').forEach(function (listitem) { children_1.push.apply(children_1, DOM.children(listitem, panelNodetype)); }); }); // Initiate subpanel(s). children_1.forEach(function (child) { _this.initPanel(child); }); } } }; /** * Initialize a single panel. * @param {HTMLElement} panel Panel to initialize. * @return {HTMLElement|null} Initialized panel. */ Mmenu.prototype._initPanel = function (panel) { var _this = this; // Invoke "before" hook. this.trigger('initPanel:before', [panel]); // Refactor panel classnames DOM.reClass(panel, this.conf.classNames.panel, 'mm-panel'); DOM.reClass(panel, this.conf.classNames.nopanel, 'mm-nopanel'); DOM.reClass(panel, this.conf.classNames.inset, 'mm-listview_inset'); if (panel.matches('.mm-listview_inset')) { panel.classList.add('mm-nopanel'); } // Stop if not supposed to be a panel. if (panel.matches('.mm-nopanel')) { return null; } /** The original ID on the node. */ var id = panel.id || uniqueId(); // Vertical panel. var vertical = panel.matches('.' + this.conf.classNames.vertical) || !this.opts.slidingSubmenus; panel.classList.remove(this.conf.classNames.vertical); // Wrap UL/OL in DIV if (panel.matches('ul, ol')) { panel.removeAttribute('id'); /** The panel. */ var wrapper = DOM.create('div'); // Wrap the listview in the panel. panel.before(wrapper); wrapper.append(panel); panel = wrapper; } panel.id = id; panel.classList.add('mm-panel'); panel.classList.add('mm-hidden'); /** The parent listitem. */ var parent = [panel.parentElement].filter(function (listitem) { return listitem.matches('li'); })[0]; if (vertical) { if (parent) { parent.classList.add('mm-listitem_vertical'); } } else { this.node.pnls.append(panel); } if (parent) { // Store parent/child relation. parent['mmChild'] = panel; panel['mmParent'] = parent; // Add open link to parent listitem if (parent && parent.matches('.mm-listitem')) { if (!DOM.children(parent, '.mm-btn').length) { /** The text node. */ var item = DOM.children(parent, '.mm-listitem__text')[0]; if (item) { /** The open link. */ var button = DOM.create('a.mm-btn.mm-btn_next.mm-listitem__btn'); button.setAttribute('href', '#' + panel.id); // If the item has no link, // Replace the item with the open link. if (item.matches('span')) { button.classList.add('mm-listitem__text'); button.innerHTML = item.innerHTML; parent.insertBefore(button, item.nextElementSibling); item.remove(); } // Otherwise, insert the button after the text. else { parent.insertBefore(button, DOM.children(parent, '.mm-panel')[0]); } } } } } this._initNavbar(panel); DOM.children(panel, 'ul, ol').forEach(function (listview) { _this.initListview(listview); }); // Invoke "after" hook. this.trigger('initPanel:after', [panel]); return panel; }; /** * Initialize a navbar. * @param {HTMLElement} panel Panel for the navbar. */ Mmenu.prototype._initNavbar = function (panel) { // Invoke "before" hook. this.trigger('initNavbar:before', [panel]); // Only one navbar per panel. if (DOM.children(panel, '.mm-navbar').length) { return; } /** The parent listitem. */ var parentListitem = null; /** The parent panel. */ var parentPanel = null; // The parent panel was specified in the data-mm-parent attribute. if (panel.getAttribute('data-mm-parent')) { parentPanel = DOM.find(this.node.pnls, panel.getAttribute('data-mm-parent'))[0]; } // if (panel.dataset.mmParent) { // IE10 has no dataset // parentPanel = DOM.find(this.node.pnls, panel.dataset.mmParent)[0]; // } // The parent panel from a listitem. else { parentListitem = panel['mmParent']; if (parentListitem) { parentPanel = parentListitem.closest('.mm-panel'); } } // No navbar needed for vertical submenus. if (parentListitem && parentListitem.matches('.mm-listitem_vertical')) { return; } /** The navbar element. */ var navbar = DOM.create('div.mm-navbar'); // Hide navbar if specified in options. if (!this.opts.navbar.add) { navbar.classList.add('mm-hidden'); } // Sticky navbars. else if (this.opts.navbar.sticky) { navbar.classList.add('mm-navbar_sticky'); } // Add the back button. if (parentPanel) { /** The back button. */ var prev = DOM.create('a.mm-btn.mm-btn_prev.mm-navbar__btn'); prev.setAttribute('href', '#' + parentPanel.id); navbar.append(prev); } /** The anchor that opens the panel. */ var opener = null; // The anchor is in a listitem. if (parentListitem) { opener = DOM.children(parentListitem, '.mm-listitem__text')[0]; } // The anchor is in a panel. else if (parentPanel) { opener = DOM.find(parentPanel, 'a[href="#' + panel.id + '"]')[0]; } // Add the title. var title = DOM.create('a.mm-navbar__title'); var titleText = DOM.create('span'); title.append(titleText); titleText.innerHTML = // panel.dataset.mmTitle || // IE10 has no dataset :( panel.getAttribute('data-mm-title') || (opener ? opener.textContent : '') || this.i18n(this.opts.navbar.title) || this.i18n('Menu'); switch (this.opts.navbar.titleLink) { case 'anchor': if (opener) { title.setAttribute('href', opener.getAttribute('href')); } break; case 'parent': if (parentPanel) { title.setAttribute('href', '#' + parentPanel.id); } break; } navbar.append(title); panel.prepend(navbar); // Invoke "after" hook. this.trigger('initNavbar:after', [panel]); }; /** * Initialize a listview. * @param {HTMLElement} listview Listview to initialize. */ Mmenu.prototype.initListview = function (listview) { var _this = this; // Invoke "before" hook. this.trigger('initListview:before', [listview]); DOM.reClass(listview, this.conf.classNames.nolistview, 'mm-nolistview'); if (!listview.matches('.mm-nolistview')) { listview.classList.add('mm-listview'); DOM.children(listview).forEach(function (listitem) { listitem.classList.add('mm-listitem'); DOM.reClass(listitem, _this.conf.classNames.selected, 'mm-listitem_selected'); DOM.children(listitem, 'a, span').forEach(function (item) { if (!item.matches('.mm-btn')) { item.classList.add('mm-listitem__text'); } }); }); } // Invoke "after" hook. this.trigger('initListview:after', [listview]); }; /** * Find and open the correct panel after creating the menu. */ Mmenu.prototype._initOpened = function () { // Invoke "before" hook. this.trigger('initOpened:before'); /** The selected listitem(s). */ var listitems = this.node.pnls.querySelectorAll('.mm-listitem_selected'); /** The last selected listitem. */ var lastitem = null; // Deselect the listitems. listitems.forEach(function (listitem) { lastitem = listitem; listitem.classList.remove('mm-listitem_selected'); }); // Re-select the last listitem. if (lastitem) { lastitem.classList.add('mm-listitem_selected'); } /** The current opened panel. */ var current = lastitem ? lastitem.closest('.mm-panel') : DOM.children(this.node.pnls, '.mm-panel')[0]; // Open the current opened panel. this.openPanel(current, false); // Invoke "after" hook. this.trigger('initOpened:after'); }; /** * Initialize anchors in / for the menu. */ Mmenu.prototype._initAnchors = function () { var _this = this; // Invoke "before" hook. this.trigger('initAnchors:before'); document.addEventListener('click', function (evnt) { /** The clicked element. */ var target = evnt.target.closest('a[href]'); if (!target) { return; } /** Arguments passed to the bound methods. */ var args = { inMenu: target.closest('.mm-menu') === _this.node.menu, inListview: target.matches('.mm-listitem > a'), toExternal: target.matches('[rel="external"]') || target.matches('[target="_blank"]') }; var onClick = { close: null, setSelected: null, preventDefault: target.getAttribute('href').slice(0, 1) == '#' }; // Find hooked behavior. for (var c = 0; c < _this.clck.length; c++) { var click = _this.clck[c].call(_this, target, args); if (click) { if (typeof click == 'boolean') { evnt.preventDefault(); return; } if (type(click) == 'object') { onClick = extend(click, onClick); } } } // Default behavior for anchors in lists. if (args.inMenu && args.inListview && !args.toExternal) { // Set selected item, Default: true if (valueOrFn(target, _this.opts.onClick.setSelected, onClick.setSelected)) { _this.setSelected(target.parentElement); } // Prevent default / don't follow link. Default: false. if (valueOrFn(target, _this.opts.onClick.preventDefault, onClick.preventDefault)) { evnt.preventDefault(); } // Close menu. Default: false if (valueOrFn(target, _this.opts.onClick.close, onClick.close)) { if (_this.opts.offCanvas && typeof _this.close == 'function') { _this.close(); } } } }, true); // Invoke "after" hook. this.trigger('initAnchors:after'); }; /** * Get the translation for a text. * @param {string} text Text to translate. * @return {string} The translated text. */ Mmenu.prototype.i18n = function (text) { return i18n.get(text, this.conf.language); }; /** Plugin version. */ Mmenu.version = version; /** Default options for menus. */ Mmenu.options = options; /** Default configuration for menus. */ Mmenu.configs = configs; /** Available add-ons for the plugin. */ Mmenu.addons = {}; /** Available wrappers for the plugin. */ Mmenu.wrappers = {}; /** Globally used HTML elements. */ Mmenu.node = {}; /** Globally used variables. */ Mmenu.vars = {}; return Mmenu; }()); export default Mmenu;