UNPKG

mmenu-js

Version:

The best javascript plugin for app look-alike on- and off-canvas menus with sliding submenus for your website and webapp.

662 lines (549 loc) 21.1 kB
import Mmenu from '../../core/oncanvas/mmenu.oncanvas'; import options from './_options'; import configs from './_configs'; import translate from './translations/translate'; import { extendShorthandOptions } from './_options'; import * as DOM from '../../_modules/dom'; import * as events from '../../_modules/eventlisteners'; import { type, extend } from '../../_modules/helpers'; // Add the translations. translate(); // Add the options and configs. Mmenu.options.searchfield = options; Mmenu.configs.searchfield = configs; export default function(this: Mmenu) { var options = extendShorthandOptions(this.opts.searchfield); this.opts.searchfield = extend(options, Mmenu.options.searchfield); var configs = this.conf.searchfield; if (!options.add) { return; } // Blur searchfield this.bind('close:start', () => { DOM.find(this.node.menu, '.mm-searchfield').forEach(input => { input.blur(); }); }); this.bind('initPanel:after', (panel: HTMLElement) => { var searchpanel: HTMLElement = null; // Add the search panel if (options.panel.add) { searchpanel = initSearchPanel.call(this); } // Add the searchfield var addTo: HTMLElement[] = null; switch (options.addTo) { case 'panels': addTo = [panel]; break; case 'panel': addTo = [searchpanel]; break; default: if (typeof options.addTo == 'string') { addTo = DOM.find(this.node.menu, options.addTo); } else if (type(options.addTo) == 'array') { addTo = options.addTo; } break; } addTo.forEach(form => { form = initSearchfield.call(this, form); if (options.search && form) { initSearching.call(this, form); } }); // Add the no-results message if (options.noResults) { initNoResultsMsg.call( this, options.panel.add ? searchpanel : panel ); } }); // Add click behavior. // Prevents default behavior when clicking an anchor this.clck.push((anchor: HTMLElement, args: mmClickArguments) => { if (args.inMenu) { if (anchor.matches('.mm-searchfield__btn')) { // Clicking the clear button if (anchor.matches('.mm-btn_close')) { let form = anchor.closest('.mm-searchfield') as HTMLElement, input = DOM.find(form, 'input')[0] as HTMLInputElement; input.value = ''; this.search(input); return true; } // Clicking the submit button if (anchor.matches('.mm-btn_next')) { let form = anchor.closest('form'); if (form) { form.submit(); } return true; } } } }); } const initSearchPanel = function(this: Mmenu): HTMLElement { var options = this.opts.searchfield, configs = this.conf.searchfield; var searchpanel = DOM.children(this.node.pnls, '.mm-panel_search')[0]; // Only once if (searchpanel) { return searchpanel; } searchpanel = DOM.create('div.mm-panel.mm-panel_search.mm-hidden'); if (options.panel.id) { searchpanel.id = options.panel.id; } if (options.panel.title) { searchpanel.setAttribute('data-mm-title', options.panel.title); // searchpanel.dataset.mmTitle = options.panel.title; // IE10 has no dataset :( } var listview = DOM.create('ul'); searchpanel.append(listview); this.node.pnls.append(searchpanel); this.initListview(listview); this._initNavbar(searchpanel); switch (options.panel.fx) { case false: break; case 'none': searchpanel.classList.add('mm-panel_noanimation'); break; default: searchpanel.classList.add('mm-panel_fx-' + options.panel.fx); break; } // Add splash content if (options.panel.splash) { let splash = DOM.create('div.mm-panel__content'); splash.innerHTML = options.panel.splash; searchpanel.append(splash); } searchpanel.classList.add('mm-panel'); searchpanel.classList.add('mm-hidden'); this.node.pnls.append(searchpanel); return searchpanel; }; const initSearchfield = function( this: Mmenu, wrapper: HTMLElement ): HTMLElement { var options = this.opts.searchfield, configs = this.conf.searchfield; // No searchfield in vertical submenus if (wrapper.parentElement.matches('.mm-listitem_vertical')) { return null; } // Only one searchfield per panel var form = DOM.find(wrapper, '.mm-searchfield')[0]; if (form) { return form; } function addAttributes( element: HTMLElement, attr: mmLooseObject | boolean ) { if (attr) { for (var a in attr as mmLooseObject) { element.setAttribute(a, attr[a]); } } } var form = DOM.create((configs.form ? 'form' : 'div') + '.mm-searchfield'), field = DOM.create('div.mm-searchfield__input'), input = DOM.create('input') as HTMLInputElement; input.type = 'text'; input.autocomplete = 'off'; input.placeholder = this.i18n(options.placeholder) as string; field.append(input); form.append(field); wrapper.prepend(form); // Add attributes to the input addAttributes(input, configs.input); // Add the clear button if (configs.clear) { let anchor = DOM.create('a.mm-btn.mm-btn_close.mm-searchfield__btn'); anchor.setAttribute('href', '#'); field.append(anchor); } // Add attributes and submit to the form addAttributes(form, configs.form); if (configs.form && configs.submit && !configs.clear) { let anchor = DOM.create('a.mm-btn.mm-btn_next.mm-searchfield__btn'); anchor.setAttribute('href', '#'); field.append(anchor); } if (options.cancel) { let anchor = DOM.create('a.mm-searchfield__cancel'); anchor.setAttribute('href', '#'); anchor.textContent = this.i18n('cancel') as string; form.append(anchor); } return form; }; const initSearching = function(this: Mmenu, form: HTMLElement) { var options = this.opts.searchfield, configs = this.conf.searchfield; var data: mmLooseObject = {}; // In the searchpanel. if (form.closest('.mm-panel_search')) { data.panels = DOM.find(this.node.pnls, '.mm-panel'); data.noresults = [form.closest('.mm-panel')]; // In a panel } else if (form.closest('.mm-panel')) { data.panels = [form.closest('.mm-panel')]; data.noresults = data.panels; // Not in a panel, global } else { data.panels = DOM.find(this.node.pnls, '.mm-panel'); data.noresults = [this.node.menu]; } // Filter out search panel data.panels = data.panels.filter( panel => !panel.matches('.mm-panel_search') ); // Filter out vertical submenus data.panels = data.panels.filter( panel => !panel.parentElement.matches('.mm-listitem_vertical') ); // Find listitems and dividers. data.listitems = []; data.dividers = []; data.panels.forEach(panel => { data.listitems.push(...DOM.find(panel, '.mm-listitem')); data.dividers.push(...DOM.find(panel, '.mm-divider')); }); var searchpanel = DOM.children(this.node.pnls, '.mm-panel_search')[0], input = DOM.find(form, 'input')[0], cancel = DOM.find(form, '.mm-searchfield__cancel')[0]; input['mmSearchfield'] = data; // Open the splash panel when focussing the input. if (options.panel.add && options.panel.splash) { events.off(input, 'focus.splash'); events.on(input, 'focus.splash', evnt => { this.openPanel(searchpanel); }); } if (options.cancel) { // Show the cancel button when focussing the input. events.off(input, 'focus.cancel'); events.on(input, 'focus.cancel', evnt => { cancel.classList.add('mm-searchfield__cancel-active'); }); // Close the splash panel when clicking the cancel button. events.off(cancel, 'click.splash'); events.on(cancel, 'click.splash', evnt => { evnt.preventDefault(); cancel.classList.remove('mm-searchfield__cancel-active'); if (searchpanel.matches('.mm-panel_opened')) { let parents = DOM.children( this.node.pnls, '.mm-panel_opened-parent' ); if (parents.length) { this.openPanel(parents[parents.length - 1]); } } }); } // Focus the input in the searchpanel when opening the searchpanel. if (options.panel.add && options.addTo == 'panel') { this.bind('openPanel:finish', (panel: HTMLElement) => { if (panel === searchpanel) { input.focus(); } }); } // Search while typing. events.off(input, 'input.search'); events.on(input, 'input.search', (evnt: KeyboardEvent) => { switch (evnt.keyCode) { case 9: // tab case 16: // shift case 17: // control case 18: // alt case 37: // left case 38: // top case 39: // right case 40: // bottom break; default: this.search(input); break; } }); // Search initially. this.search(input); }; const initNoResultsMsg = function(this: Mmenu, wrapper: HTMLElement) { if (!wrapper) { return; } var options = this.opts.searchfield, configs = this.conf.searchfield; // Not in a panel if (!wrapper.closest('.mm-panel')) { wrapper = DOM.children(this.node.pnls, '.mm-panel')[0]; } // Only once if (DOM.children(wrapper, '.mm-panel__noresultsmsg').length) { return; } // Add no-results message var message = DOM.create('div.mm-panel__noresultsmsg.mm-hidden'); message.innerHTML = this.i18n(options.noResults) as string; wrapper.append(message); }; Mmenu.prototype.search = function( this: Mmenu, input: HTMLInputElement, query: string ) { var options = this.opts.searchfield, configs = this.conf.searchfield; query = query || '' + input.value; query = query.toLowerCase().trim(); var data = input['mmSearchfield']; var form: HTMLElement = input.closest('.mm-searchfield') as HTMLElement, buttons: HTMLElement[] = DOM.find(form as HTMLElement, '.mm-btn'), searchpanel: HTMLElement = DOM.children( this.node.pnls, '.mm-panel_search' )[0]; /** The panels. */ var panels: HTMLElement[] = data.panels; /** The "no results" messages in a cloned array. */ var noresults: HTMLElement[] = data.noresults; /** The listitems in a cloned array. */ var listitems: HTMLElement[] = data.listitems; /** Tje dividers in a cloned array. */ var dividers: HTMLElement[] = data.dividers; // Reset previous results listitems.forEach(listitem => { listitem.classList.remove('mm-listitem_nosubitems'); listitem.classList.remove('mm-listitem_onlysubitems'); listitem.classList.remove('mm-hidden'); }); if (searchpanel) { DOM.children(searchpanel, '.mm-listview')[0].innerHTML = ''; } panels.forEach(panel => { panel.scrollTop = 0; }); // Search if (query.length) { // Initially hide all dividers. dividers.forEach(divider => { divider.classList.add('mm-hidden'); }); // Hide listitems that do not match. listitems.forEach(listitem => { var text = DOM.children(listitem, '.mm-listitem__text')[0]; var add = false; // The listitem should be shown if: // 1) The text matches the query and // 2a) The text is a open-button and // 2b) the option showSubPanels is set to true. // or 3a) The text is not an anchor and // 3b) the option showTextItems is set to true. // or 4) The text is an anchor. // 1 if ( text && DOM.text(text) .toLowerCase() .indexOf(query) > -1 ) { // 2a if (text.matches('.mm-listitem__btn')) { // 2b if (options.showSubPanels) { add = true; } } // 3a else if (!text.matches('a')) { // 3b if (options.showTextItems) { add = true; } } // 4 else { add = true; } } if (!add) { listitem.classList.add('mm-hidden'); } }); /** Whether or not the query yielded results. */ var hasResults = listitems.filter( listitem => !listitem.matches('.mm-hidden') ).length; // Show all mached listitems in the search panel if (options.panel.add) { // Clone all matched listitems into the search panel let allitems: HTMLElement[] = []; panels.forEach(panel => { let listitems = DOM.filterLI(DOM.find(panel, '.mm-listitem')); listitems = listitems.filter( listitem => !listitem.matches('.mm-hidden') ); if (listitems.length) { // Add a divider to indicate in what panel the listitems were. if (options.panel.dividers) { let divider = DOM.create('li.mm-divider'); let title = DOM.find(panel, '.mm-navbar__title')[0]; if (title) { divider.innerHTML = title.innerHTML; allitems.push(divider); } } listitems.forEach(listitem => { allitems.push(listitem.cloneNode(true) as HTMLElement); }); } }); // Remove toggles and checks. allitems.forEach(listitem => { listitem .querySelectorAll('.mm-toggle, .mm-check') .forEach(element => { element.remove(); }); }); // Add to the search panel. DOM.children(searchpanel, '.mm-listview')[0].append(...allitems); // Open the search panel. this.openPanel(searchpanel); } else { // Also show listitems in sub-panels for matched listitems if (options.showSubPanels) { panels.forEach(panel => { let listitems = DOM.find(panel, '.mm-listitem'); DOM.filterLI(listitems).forEach(listitem => { let child: HTMLElement = listitem['mmChild']; if (child) { DOM.find(child, '.mm-listitem').forEach( listitem => { listitem.classList.remove('mm-hidden'); } ); } }); }); } // Update parent for sub-panel // .reverse() mutates the original array, therefor we "clone" it first using [...panels]. [...panels].reverse().forEach((panel, p) => { let parent: HTMLElement = panel['mmParent']; if (parent) { // The current panel has mached listitems let listitems = DOM.find(panel, '.mm-listitem'); if (DOM.filterLI(listitems).length) { // Show parent if (parent.matches('.mm-hidden')) { parent.classList.remove('mm-hidden'); } parent.classList.add('mm-listitem_onlysubitems'); } else if (!input.closest('.mm-panel')) { if ( panel.matches('.mm-panel_opened') || panel.matches('.mm-panel_opened-parent') ) { // Compensate the timeout for the opening animation setTimeout(() => { this.openPanel( parent.closest('.mm-panel') as HTMLElement ); }, (p + 1) * (this.conf.openingInterval * 1.5)); } parent.classList.add('mm-listitem_nosubitems'); } } }); // Show parent panels of vertical submenus panels.forEach(panel => { let listitems = DOM.find(panel, '.mm-listitem'); DOM.filterLI(listitems).forEach(listitem => { DOM.parents(listitem, '.mm-listitem_vertical').forEach( parent => { if (parent.matches('.mm-hidden')) { parent.classList.remove('mm-hidden'); parent.classList.add( 'mm-listitem_onlysubitems' ); } } ); }); }); // Show first preceeding divider of parent panels.forEach(panel => { let listitems = DOM.find(panel, '.mm-listitem'); DOM.filterLI(listitems).forEach(listitem => { let divider = DOM.prevAll(listitem, '.mm-divider')[0]; if (divider) { divider.classList.remove('mm-hidden'); } }); }); } // Show submit / clear button buttons.forEach(button => button.classList.remove('mm-hidden')); // Show/hide no results message noresults.forEach(wrapper => { DOM.find(wrapper, '.mm-panel__noresultsmsg').forEach(message => message.classList[hasResults ? 'add' : 'remove']('mm-hidden') ); }); if (options.panel.add) { // Hide splash if (options.panel.splash) { DOM.find(searchpanel, '.mm-panel__content').forEach(splash => splash.classList.add('mm-hidden') ); } // Re-show original listitems when in search panel listitems.forEach(listitem => listitem.classList.remove('mm-hidden') ); dividers.forEach(divider => divider.classList.remove('mm-hidden')); } // Don't search } else { // Show all items listitems.forEach(listitem => listitem.classList.remove('mm-hidden')); dividers.forEach(divider => divider.classList.remove('mm-hidden')); // Hide submit / clear button buttons.forEach(button => button.classList.add('mm-hidden')); // Hide no results message noresults.forEach(wrapper => { DOM.find(wrapper, '.mm-panel__noresultsmsg').forEach(message => message.classList.add('mm-hidden') ); }); if (options.panel.add) { // Show splash if (options.panel.splash) { DOM.find(searchpanel, '.mm-panel__content').forEach(splash => splash.classList.remove('mm-hidden') ); // Close panel } else if (!input.closest('.mm-panel_search')) { let opened = DOM.children( this.node.pnls, '.mm-panel_opened-parent' ); this.openPanel(opened.slice(-1)[0]); } } } // Update for other addons this.trigger('updateListview'); };