├── .github └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENCE.md ├── README.md ├── demo ├── advanced.html ├── css │ ├── demo.css │ └── site.css ├── default.html ├── img │ └── iphonex-example-camera.png └── onepage.html ├── dist ├── _modules │ ├── dom.js │ ├── eventlisteners.js │ ├── helpers.js │ ├── i18n.js │ ├── matchmedia.js │ └── support.js ├── addons │ ├── backbutton │ │ ├── mmenu.backbutton.js │ │ └── options.js │ ├── counters │ │ ├── mmenu.counters.css │ │ ├── mmenu.counters.js │ │ └── options.js │ ├── iconbar │ │ ├── mmenu.iconbar.css │ │ ├── mmenu.iconbar.js │ │ └── options.js │ ├── iconpanels │ │ ├── _options.js │ │ ├── mmenu.iconpanels.css │ │ └── mmenu.iconpanels.js │ ├── navbars │ │ ├── configs.js │ │ ├── mmenu.navbars.css │ │ ├── mmenu.navbars.js │ │ ├── navbar.breadcrumbs.js │ │ ├── navbar.close.js │ │ ├── navbar.prev.js │ │ ├── navbar.searchfield.js │ │ ├── navbar.tabs.js │ │ ├── navbar.title.js │ │ └── options.js │ ├── pagescroll │ │ ├── configs.js │ │ ├── mmenu.pagescroll.js │ │ └── options.js │ ├── searchfield │ │ ├── configs.js │ │ ├── mmenu.searchfield.css │ │ ├── mmenu.searchfield.js │ │ ├── options.js │ │ └── translations │ │ │ ├── de.js │ │ │ ├── fa.js │ │ │ ├── index.js │ │ │ ├── nl.js │ │ │ ├── pt_br.js │ │ │ ├── ru.js │ │ │ ├── sk.js │ │ │ └── uk.js │ ├── sectionindexer │ │ ├── mmenu.sectionindexer.css │ │ ├── mmenu.sectionindexer.js │ │ └── options.js │ ├── setselected │ │ ├── mmenu.setselected.css │ │ ├── mmenu.setselected.js │ │ └── options.js │ └── sidebar │ │ ├── mmenu.sidebar.css │ │ ├── mmenu.sidebar.js │ │ └── options.js ├── core │ ├── offcanvas │ │ ├── configs.js │ │ ├── mmenu.offcanvas.css │ │ ├── mmenu.offcanvas.js │ │ ├── options.js │ │ └── translations │ │ │ ├── de.js │ │ │ ├── fa.js │ │ │ ├── index.js │ │ │ ├── nl.js │ │ │ ├── pt_br.js │ │ │ ├── ru.js │ │ │ ├── sk.js │ │ │ └── uk.js │ ├── oncanvas │ │ ├── configs.js │ │ ├── mmenu.oncanvas.css │ │ ├── mmenu.oncanvas.js │ │ ├── options.js │ │ └── translations │ │ │ ├── de.js │ │ │ ├── fa.js │ │ │ ├── index.js │ │ │ ├── nl.js │ │ │ ├── pt_br.js │ │ │ ├── ru.js │ │ │ ├── sk.js │ │ │ └── uk.js │ ├── scrollbugfix │ │ ├── mmenu.scrollbugfix.js │ │ └── options.js │ └── theme │ │ ├── mmenu.theme.css │ │ ├── mmenu.theme.js │ │ └── options.js ├── mmenu.css └── mmenu.js ├── gulp ├── css.js └── js.js ├── gulpfile.js ├── index.html ├── package-lock.json ├── package.json ├── src ├── _mixins.scss ├── _modules │ ├── dom.ts │ ├── eventlisteners.ts │ ├── helpers.ts │ ├── i18n.ts │ ├── matchmedia.ts │ └── support.ts ├── _variables.scss ├── addons │ ├── backbutton │ │ ├── mmenu.backbutton.ts │ │ ├── options.ts │ │ └── typings.d.ts │ ├── counters │ │ ├── mmenu.counters.scss │ │ ├── mmenu.counters.ts │ │ ├── options.ts │ │ └── typings.d.ts │ ├── iconbar │ │ ├── mmenu.iconbar.scss │ │ ├── mmenu.iconbar.ts │ │ ├── options.ts │ │ └── typings.d.ts │ ├── iconpanels │ │ ├── _options.ts │ │ ├── _typings.d.ts │ │ ├── mmenu.iconpanels.scss │ │ └── mmenu.iconpanels.ts │ ├── navbars │ │ ├── _breadcrumbs.scss │ │ ├── _tabs.scss │ │ ├── configs.ts │ │ ├── mmenu.navbars.scss │ │ ├── mmenu.navbars.ts │ │ ├── navbar.breadcrumbs.ts │ │ ├── navbar.close.ts │ │ ├── navbar.prev.ts │ │ ├── navbar.searchfield.ts │ │ ├── navbar.tabs.ts │ │ ├── navbar.title.ts │ │ ├── options.ts │ │ └── typings.d.ts │ ├── pagescroll │ │ ├── configs.ts │ │ ├── mmenu.pagescroll.ts │ │ ├── options.ts │ │ └── typings.d.ts │ ├── searchfield │ │ ├── _panel.scss │ │ ├── configs.ts │ │ ├── mmenu.searchfield.scss │ │ ├── mmenu.searchfield.ts │ │ ├── options.ts │ │ ├── translations │ │ │ ├── de.ts │ │ │ ├── fa.ts │ │ │ ├── index.ts │ │ │ ├── nl.ts │ │ │ ├── pt_br.ts │ │ │ ├── ru.ts │ │ │ ├── sk.ts │ │ │ └── uk.ts │ │ └── typings.d.ts │ ├── sectionindexer │ │ ├── mmenu.sectionindexer.scss │ │ ├── mmenu.sectionindexer.ts │ │ ├── options.ts │ │ └── typings.d.ts │ ├── setselected │ │ ├── mmenu.setselected.scss │ │ ├── mmenu.setselected.ts │ │ ├── options.ts │ │ └── typings.d.ts │ └── sidebar │ │ ├── mmenu.sidebar.scss │ │ ├── mmenu.sidebar.ts │ │ ├── options.ts │ │ └── typings.d.ts ├── core │ ├── offcanvas │ │ ├── _positions.scss │ │ ├── configs.ts │ │ ├── mmenu.offcanvas.scss │ │ ├── mmenu.offcanvas.ts │ │ ├── options.ts │ │ ├── translations │ │ │ ├── de.ts │ │ │ ├── fa.ts │ │ │ ├── index.ts │ │ │ ├── nl.ts │ │ │ ├── pt_br.ts │ │ │ ├── ru.ts │ │ │ ├── sk.ts │ │ │ └── uk.ts │ │ └── typings.d.ts │ ├── oncanvas │ │ ├── _blocker.scss │ │ ├── _button.scss │ │ ├── _divider.scss │ │ ├── _listitem.scss │ │ ├── _listview.scss │ │ ├── _menu.scss │ │ ├── _navbar.scss │ │ ├── _panel.scss │ │ ├── _panels.scss │ │ ├── _toggle.scss │ │ ├── _vertical.scss │ │ ├── configs.ts │ │ ├── mmenu.oncanvas.scss │ │ ├── mmenu.oncanvas.ts │ │ ├── options.ts │ │ ├── translations │ │ │ ├── de.ts │ │ │ ├── fa.ts │ │ │ ├── index.ts │ │ │ ├── nl.ts │ │ │ ├── pt_br.ts │ │ │ ├── ru.ts │ │ │ ├── sk.ts │ │ │ └── uk.ts │ │ └── typings.d.ts │ ├── scrollbugfix │ │ ├── mmenu.scrollbugfix.ts │ │ ├── options.ts │ │ └── typings.d.ts │ └── theme │ │ ├── _black.scss │ │ ├── _dark.scss │ │ ├── _light.scss │ │ ├── _white.scss │ │ ├── mmenu.theme.scss │ │ ├── mmenu.theme.ts │ │ ├── options.ts │ │ └── typings.d.ts ├── mmenu.debugger.js ├── mmenu.js └── mmenu.scss └── tsconfig.json /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '27 23 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'javascript' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Mac system files. 2 | ._* 3 | .DS_Store 4 | 5 | # Ignore sass-cache files. 6 | *.sass-cache* 7 | *.scssc 8 | 9 | # Ignore Gulp modules 10 | node_modules 11 | 12 | # Ignore TODO 13 | TODO.rtf 14 | TODO.txt 15 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to this project 2 | 3 | Please take a moment to review this document in order to make the contribution 4 | process easy and effective for everyone involved. 5 | 6 | 7 | ## Using the issue tracker 8 | 9 | The issue tracker is the preferred channel for [bug reports](#bugs) and 10 | [features requests](#features), but please respect the following restrictions: 11 | 12 | * Please **do not** use the issue tracker for personal support requests. 13 | 14 | * Please keep the discussion **on topic** and respect the opinions of others. 15 | 16 | 17 | 18 | ## Bug reports 19 | 20 | A bug is a _demonstrable problem_ that is caused by the code in the repository. 21 | Good bug reports are extremely helpful - thank you! 22 | 23 | Guidelines for bug reports: 24 | 25 | 1. **Use the GitHub issue search** — check if the issue has already been 26 | reported. 27 | 28 | 2. **Check if the issue has been fixed** — try to reproduce it using the 29 | latest branch in the repository. 30 | 31 | 3. **Isolate the problem** — create a [reduced test 32 | case](http://css-tricks.com/reduced-test-cases/) and a live example. 33 | 34 | A good bug report shouldn't leave others needing to chase you up for more 35 | information. Please try to be as detailed as possible in your report. What is 36 | your environment? What steps will reproduce the issue? What browser(s) and OS 37 | experience the problem? What would you expect to be the outcome? All these 38 | details will help people to fix any potential bugs. 39 | 40 | 41 | 42 | ## Feature requests 43 | 44 | Feature requests are welcome. But take a moment to find out whether your idea 45 | fits with the scope and aims of the project. It's up to *you* to make a strong 46 | case to convince the project's developers of the merits of this feature. Please 47 | provide as much detail and context as possible. -------------------------------------------------------------------------------- /LICENCE.md: -------------------------------------------------------------------------------- 1 | # License information 2 | 3 | The mmenu.js plugin is free to use for personal or non-profit usage. 4 | You can purchase a license if you want to use it in a commercial project. 5 | 6 | 7 | #### For personal or non-profit usage: 8 | The mmenu.js plugin is licensed under [the CC-BY-NC-4.0](http://creativecommons.org/licenses/by-nc/4.0/) license. 9 | 10 | 11 | #### After purchasing a license key: 12 | The mmenu.js plugin is licensed under the [CC-BY-4.0](https://creativecommons.org/licenses/by/4.0/) license. 13 | 14 | For more information, please visit [the documentation](https://mmenujs.com/download.html). 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mmenu.js 2 | 3 | The best javascript plugin for app look-alike on- and off-canvas menus with sliding submenus for your website and webapp. It is very customizable through a wide range of options, extensions and add-ons and it will always fit your needs. 4 | 5 | Need help? Have a look at [the documentation](https://mmenujs.com) for demos, tutorials, documentation and support.
6 | Working on a WordPress site? Check out [the mmenu WordPress plugin](https://mmenujs.com/wordpress-plugin). 7 | 8 | mmenu.js 9 | 10 | ### Licence 11 | 12 | The mmenu javascript plugin is licensed under the [CC-BY-NC-4.0 license](http://creativecommons.org/licenses/by-nc/4.0/).
13 | You can [purchase a license](https://mmenujs.com/download.html) if you want to use it in a commercial project. 14 | 15 | ### Learn more 16 | 17 | - [Tutorial](https://mmenujs.com/tutorials/off-canvas/) 18 | - [Options](https://mmenujs.com/documentation/core/options.html) 19 | - [Add-ons](https://mmenujs.com/documentation/addons/) 20 | - [API](https://mmenujs.com/documentation/core/api.html) 21 | 22 | ### Browser support 23 | 24 | As of version 9, the mmenu.js plugin only supports [ECMAScript 6 compliant browsers](https://kangax.github.io/compat-table/es6/).
25 | For Internet Explorer 11, you can use the latest of version 8 and use polyfills where needed. 26 | -------------------------------------------------------------------------------- /demo/css/demo.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | html, 6 | body { 7 | padding: 0; 8 | margin: 0; 9 | } 10 | body { 11 | background-color: #fff; 12 | font-family: Arial, Helvetica, Verdana; 13 | font-size: 16px; 14 | line-height: 22px; 15 | color: #666; 16 | position: relative; 17 | -webkit-text-size-adjust: none; 18 | } 19 | h1, 20 | h2, 21 | h3, 22 | h4, 23 | h5, 24 | h6 { 25 | margin: 1em 0; 26 | font-size: 22px; 27 | } 28 | p { 29 | margin: 1em 0; 30 | } 31 | a, 32 | a:link, 33 | a:active, 34 | a:visited, 35 | a:hover { 36 | color: inherit; 37 | text-decoration: underline; 38 | } 39 | 40 | nav:not(.mm-menu) { 41 | display: none; 42 | } 43 | 44 | #header { 45 | position: sticky; 46 | height: 50px; 47 | padding: 0 80px; 48 | top: 0; 49 | font-size: 16px; 50 | font-weight: bold; 51 | color: #fff; 52 | line-height: 44px; 53 | text-align: center; 54 | background: #bba6a2; 55 | } 56 | #header a { 57 | display: block; 58 | position: absolute; 59 | top: 0; 60 | left: 0; 61 | width: 80px; 62 | height: 50px; 63 | padding: 15px 25px; 64 | } 65 | #header a:before, 66 | #header a:after { 67 | content: ""; 68 | display: block; 69 | background: #fff; 70 | height: 2px; 71 | } 72 | #header a span { 73 | background: #fff; 74 | display: block; 75 | height: 2px; 76 | margin: 7px 0; 77 | } 78 | 79 | #content { 80 | padding: 150px 50px 50px 50px; 81 | text-align: center; 82 | } 83 | -------------------------------------------------------------------------------- /demo/css/site.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | height: 100%; 6 | } 7 | body { 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | height: 100%; 12 | font-family: Arial, Helvetica, Verdana; 13 | font-size: 18px; 14 | line-height: 26px; 15 | color: #fff; 16 | background-color: #5888aa; 17 | -webkit-text-size-adjust: none; 18 | } 19 | h1 { 20 | text-shadow: 8px 10px 1px rgba(0,0,0,.1); 21 | text-transform: lowercase; 22 | font-family: 'Pacifico', Arial, sans-serif; 23 | font-weight: normal; 24 | font-size: 150px; 25 | line-height: 150px; 26 | letter-spacing: -10px; 27 | margin: 0 0 20px 0; 28 | } 29 | a, 30 | a:hover 31 | { 32 | color: #fff; 33 | text-decoration: underline; 34 | } 35 | 36 | .phone { 37 | display: flex; 38 | flex-direction: column; 39 | position: relative; 40 | height: 600px; 41 | width: 300px; 42 | margin-right: 100px; 43 | overflow: hidden; 44 | overflow-y: auto; 45 | background: #1d2327; 46 | border-radius: 45px; 47 | border: 5px solid #000; 48 | outline: 8px solid #222; 49 | box-shadow: 0 5px 50px #2a6787; 50 | 51 | } 52 | .phone:before { 53 | content: ""; 54 | position: relative; 55 | z-index: 1; 56 | display: block; 57 | height: 35px; 58 | flex-shrink: 0; 59 | margin-top: 0; 60 | border-radius: 30px 30px 0 0; 61 | background: url(../img/iphonex-example-camera.png) center top no-repeat 62 | #bba6a2; 63 | } 64 | .phone:after { 65 | content: ""; 66 | display: block; 67 | position: absolute; 68 | bottom: 8px; 69 | left: calc(50% - 50px); 70 | z-index: 3; 71 | width: 100px; 72 | height: 6px; 73 | border-radius: 3px; 74 | background: rgb(0 0 0 / 30%); 75 | } 76 | .phone iframe { 77 | flex-grow: 1; 78 | display: block; 79 | position: relative; 80 | z-index: 2; 81 | width: 100%; 82 | margin: 0; 83 | border: unset; 84 | } 85 | 86 | .page { 87 | width: 350px; 88 | } 89 | -------------------------------------------------------------------------------- /demo/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | mmenu.js demo 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 23 |
24 |

This is a demo.

25 |

Click the menu icon to open the menu.

26 |
27 | 58 |
59 | 60 | 61 | 62 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /demo/img/iphonex-example-camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FrDH/mmenu-js/17bea9b633b3ae55b80b12a672c1f20215296c5c/demo/img/iphonex-example-camera.png -------------------------------------------------------------------------------- /dist/_modules/eventlisteners.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Make the first letter in a word uppercase. 3 | * @param {string} word The word. 4 | */ 5 | function ucFirst(word) { 6 | if (!word) { 7 | return ''; 8 | } 9 | return word.charAt(0).toUpperCase() + word.slice(1); 10 | } 11 | /** 12 | * Bind an event listener to an element. 13 | * @param {HTMLElement} element The element to bind the event listener to. 14 | * @param {string} evnt The event to listen to. 15 | * @param {funcion} handler The function to invoke. 16 | */ 17 | export const on = (element, evnt, handler) => { 18 | // Extract the event name and space from the event (the event can include a namespace (click.foo)). 19 | const evntParts = evnt.split('.'); 20 | evnt = 'mmEvent' + ucFirst(evntParts[0]) + ucFirst(evntParts[1]); 21 | element[evnt] = element[evnt] || []; 22 | element[evnt].push(handler); 23 | element.addEventListener(evntParts[0], handler); 24 | }; 25 | /** 26 | * Remove an event listener from an element. 27 | * @param {HTMLElement} element The element to remove the event listeners from. 28 | * @param {string} evnt The event to remove. 29 | */ 30 | export const off = (element, evnt) => { 31 | // Extract the event name and space from the event (the event can include a namespace (click.foo)). 32 | const evntParts = evnt.split('.'); 33 | evnt = 'mmEvent' + ucFirst(evntParts[0]) + ucFirst(evntParts[1]); 34 | (element[evnt] || []).forEach((handler) => { 35 | element.removeEventListener(evntParts[0], handler); 36 | }); 37 | }; 38 | /** 39 | * Trigger the bound event listeners on an element. 40 | * @param {HTMLElement} element The element of which to trigger the event listeners from. 41 | * @param {string} evnt The event to trigger. 42 | * @param {object} [options] Options to pass to the handler. 43 | */ 44 | export const trigger = (element, evnt, options) => { 45 | const evntParts = evnt.split('.'); 46 | evnt = 'mmEvent' + ucFirst(evntParts[0]) + ucFirst(evntParts[1]); 47 | (element[evnt] || []).forEach((handler) => { 48 | handler(options || {}); 49 | }); 50 | }; 51 | -------------------------------------------------------------------------------- /dist/_modules/helpers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Deep extend an object with the given defaults. 3 | * Note that the extended object is not a clone, meaning the original object will also be updated. 4 | * 5 | * @param {object} orignl The object to extend to. 6 | * @param {object} dfault The object to extend from. 7 | * @return {object} The extended "orignl" object. 8 | */ 9 | export const extend = (orignl, dfault) => { 10 | if (type(orignl) != 'object') { 11 | orignl = {}; 12 | } 13 | if (type(dfault) != 'object') { 14 | dfault = {}; 15 | } 16 | for (let k in dfault) { 17 | if (!dfault.hasOwnProperty(k)) { 18 | continue; 19 | } 20 | if (typeof orignl[k] == 'undefined') { 21 | orignl[k] = dfault[k]; 22 | } 23 | else if (type(orignl[k]) == 'object') { 24 | extend(orignl[k], dfault[k]); 25 | } 26 | } 27 | return orignl; 28 | }; 29 | /** 30 | * Detect the touch / dragging direction on a touch device. 31 | * 32 | * @param {HTMLElement} surface The element to monitor for touch events. 33 | * @return {object} Object with "get" function. 34 | */ 35 | export const touchDirection = (surface) => { 36 | let direction = ''; 37 | let prevPosition = null; 38 | surface.addEventListener('touchstart', (evnt) => { 39 | if (evnt.touches.length === 1) { 40 | direction = ''; 41 | prevPosition = evnt.touches[0].pageY; 42 | } 43 | }); 44 | surface.addEventListener('touchend', (evnt) => { 45 | if (evnt.touches.length === 0) { 46 | direction = ''; 47 | prevPosition = null; 48 | } 49 | }); 50 | surface.addEventListener('touchmove', (evnt) => { 51 | direction = ''; 52 | if (prevPosition && 53 | evnt.touches.length === 1) { 54 | const currentPosition = evnt.changedTouches[0].pageY; 55 | if (currentPosition > prevPosition) { 56 | direction = 'down'; 57 | } 58 | else if (currentPosition < prevPosition) { 59 | direction = 'up'; 60 | } 61 | prevPosition = currentPosition; 62 | } 63 | }); 64 | return { 65 | get: () => direction, 66 | }; 67 | }; 68 | /** 69 | * Get the type of any given variable. Improvement of "typeof". 70 | * 71 | * @param {any} variable The variable. 72 | * @return {string} The type of the variable in lowercase. 73 | */ 74 | export const type = (variable) => { 75 | return {}.toString 76 | .call(variable) 77 | .match(/\s([a-zA-Z]+)/)[1] 78 | .toLowerCase(); 79 | }; 80 | /** 81 | * Get a (page wide) unique ID. 82 | */ 83 | export const uniqueId = () => { 84 | return `mm-${__id++}`; 85 | }; 86 | let __id = 0; 87 | /** 88 | * Get a prefixed ID from a possibly orifinal ID. 89 | * @param id The possibly original ID. 90 | */ 91 | export const cloneId = (id) => { 92 | if (id.slice(0, 9) == 'mm-clone-') { 93 | return id; 94 | } 95 | return `mm-clone-${id}`; 96 | }; 97 | /** 98 | * Get the original ID from a possibly prefixed ID. 99 | * @param id The possibly prefixed ID. 100 | */ 101 | export const originalId = (id) => { 102 | if (id.slice(0, 9) == 'mm-clone-') { 103 | return id.slice(9); 104 | } 105 | return id; 106 | }; 107 | -------------------------------------------------------------------------------- /dist/_modules/i18n.js: -------------------------------------------------------------------------------- 1 | import { extend } from './helpers'; 2 | const translations = {}; 3 | /** 4 | * Show all translations. 5 | * @return {object} The translations. 6 | */ 7 | export const show = () => { 8 | return translations; 9 | }; 10 | /** 11 | * Add translations to a language. 12 | * @param {object} text Object of key/value translations. 13 | * @param {string} language The translated language. 14 | */ 15 | export const add = (text, language) => { 16 | if (typeof translations[language] === 'undefined') { 17 | translations[language] = {}; 18 | } 19 | extend(translations[language], text); 20 | }; 21 | /** 22 | * Find a translated text in a language. 23 | * @param {string} text The text to find the translation for. 24 | * @param {string} language The language to search in. 25 | * @return {string} The translated text. 26 | */ 27 | export const get = (text, language) => { 28 | if (typeof language === 'string' && 29 | typeof translations[language] !== 'undefined') { 30 | return translations[language][text] || text; 31 | } 32 | return text; 33 | }; 34 | -------------------------------------------------------------------------------- /dist/_modules/matchmedia.js: -------------------------------------------------------------------------------- 1 | /** Collection of callback functions for media querys. */ 2 | let listeners = {}; 3 | /** 4 | * Bind functions to a matchMedia listener (subscriber). 5 | * 6 | * @param {string|number} query Media query to match or number for min-width. 7 | * @param {function} yes Function to invoke when the media query matches. 8 | * @param {function} no Function to invoke when the media query doesn't match. 9 | */ 10 | export const add = (query, yes, no) => { 11 | if (typeof query == 'number') { 12 | query = '(min-width: ' + query + 'px)'; 13 | } 14 | listeners[query] = listeners[query] || []; 15 | listeners[query].push({ yes, no }); 16 | }; 17 | /** 18 | * Initialize the matchMedia listener. 19 | */ 20 | export const watch = () => { 21 | for (let query in listeners) { 22 | let mqlist = window.matchMedia(query); 23 | fire(query, mqlist); 24 | mqlist.onchange = (evnt) => { 25 | fire(query, mqlist); 26 | }; 27 | } 28 | }; 29 | /** 30 | * Invoke the "yes" or "no" function for a matchMedia listener (publisher). 31 | * 32 | * @param {string} query Media query to check for. 33 | * @param {MediaQueryList} mqlist Media query list to check with. 34 | */ 35 | export const fire = (query, mqlist) => { 36 | var fn = mqlist.matches ? 'yes' : 'no'; 37 | for (let m = 0; m < listeners[query].length; m++) { 38 | listeners[query][m][fn](); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /dist/_modules/support.js: -------------------------------------------------------------------------------- 1 | /** Whether or not touch gestures are supported by the browser. */ 2 | export const touch = 'ontouchstart' in window || 3 | (navigator.msMaxTouchPoints ? true : false) || 4 | false; 5 | -------------------------------------------------------------------------------- /dist/addons/backbutton/mmenu.backbutton.js: -------------------------------------------------------------------------------- 1 | import OPTIONS from './options'; 2 | import * as DOM from '../../_modules/dom'; 3 | import { extend } from '../../_modules/helpers'; 4 | export default function () { 5 | this.opts.backButton = this.opts.backButton || {}; 6 | if (!this.opts.offCanvas.use) { 7 | return; 8 | } 9 | // Extend options. 10 | const options = extend(this.opts.backButton, OPTIONS); 11 | const _menu = `#${this.node.menu.id}`; 12 | // Close menu 13 | if (options.close) { 14 | let states = []; 15 | const setStates = () => { 16 | states = [_menu]; 17 | DOM.children(this.node.pnls, '.mm-panel--opened, .mm-panel--parent').forEach((panel) => { 18 | states.push('#' + panel.id); 19 | }); 20 | }; 21 | this.bind('open:after', () => { 22 | history.pushState(null, '', location.pathname + location.search + _menu); 23 | }); 24 | this.bind('open:after', setStates); 25 | this.bind('openPanel:after', setStates); 26 | this.bind('close:after', () => { 27 | states = []; 28 | history.back(); 29 | history.pushState(null, '', location.pathname + location.search); 30 | }); 31 | window.addEventListener('popstate', () => { 32 | if (this.node.menu.matches('.mm-menu--opened')) { 33 | if (states.length) { 34 | states = states.slice(0, -1); 35 | const hash = states[states.length - 1]; 36 | if (hash == _menu) { 37 | this.close(); 38 | } 39 | else { 40 | this.openPanel(this.node.menu.querySelector(hash)); 41 | history.pushState(null, '', location.pathname + location.search + _menu); 42 | } 43 | } 44 | } 45 | }); 46 | } 47 | if (options.open) { 48 | window.addEventListener('popstate', (evnt) => { 49 | if (!this.node.menu.matches('.mm-menu--opened') && location.hash == _menu) { 50 | this.open(); 51 | } 52 | }); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /dist/addons/backbutton/options.js: -------------------------------------------------------------------------------- 1 | const options = { 2 | close: false, 3 | open: false 4 | }; 5 | export default options; 6 | -------------------------------------------------------------------------------- /dist/addons/counters/mmenu.counters.css: -------------------------------------------------------------------------------- 1 | .mm-counter{display:block;-webkit-padding-start:20px;padding-inline-start:20px;float:right;color:var(--mm-color-text-dimmed)}[dir=rtl] .mm-counter{float:left} -------------------------------------------------------------------------------- /dist/addons/counters/mmenu.counters.js: -------------------------------------------------------------------------------- 1 | import OPTIONS from './options'; 2 | import * as DOM from '../../_modules/dom'; 3 | import { extend } from '../../_modules/helpers'; 4 | export default function () { 5 | this.opts.counters = this.opts.counters || {}; 6 | // Extend options. 7 | const options = extend(this.opts.counters, OPTIONS); 8 | if (!options.add) { 9 | return; 10 | } 11 | /** 12 | * Counting the visible listitems and setting it to the counter element. 13 | * @param {HTMLElement} panel Panel to count LIs in. 14 | */ 15 | const count = (panel) => { 16 | /** Parent panel for the mutated listitem. */ 17 | const parent = this.node.pnls.querySelector(`#${panel.dataset.mmParent}`); 18 | if (!parent) { 19 | return; 20 | } 21 | /** The counter for the listitem. */ 22 | const counter = parent.querySelector('.mm-counter'); 23 | if (!counter) { 24 | return; 25 | } 26 | /** The listitems */ 27 | const listitems = []; 28 | DOM.children(panel, '.mm-listview').forEach((listview) => { 29 | listitems.push(...DOM.children(listview, '.mm-listitem')); 30 | }); 31 | counter.innerHTML = DOM.filterLI(listitems).length.toString(); 32 | }; 33 | /** Mutation observer the the listitems. */ 34 | const listitemObserver = new MutationObserver((mutationsList) => { 35 | mutationsList.forEach((mutation) => { 36 | if (mutation.attributeName == 'class') { 37 | count(mutation.target.closest('.mm-panel')); 38 | } 39 | }); 40 | }); 41 | // Add the counters after a listview is initiated. 42 | this.bind('initListview:after', (listview) => { 43 | /** The panel where the listview is in. */ 44 | const panel = listview.closest('.mm-panel'); 45 | /** The parent LI for the panel */ 46 | const parent = this.node.pnls.querySelector(`#${panel.dataset.mmParent}`); 47 | if (!parent) { 48 | return; 49 | } 50 | /** The button inside the parent LI */ 51 | const button = DOM.children(parent, '.mm-btn')[0]; 52 | if (!button) { 53 | return; 54 | } 55 | // Check if no counter already excists. 56 | if (!DOM.children(button, '.mm-counter').length) { 57 | /** The counter for the listitem. */ 58 | const counter = DOM.create('span.mm-counter'); 59 | counter.setAttribute('aria-hidden', 'true'); 60 | button.prepend(counter); 61 | } 62 | // Count immediately. 63 | count(panel); 64 | }); 65 | // Count when LI classname changes. 66 | this.bind('initListitem:after', (listitem) => { 67 | /** The panel where the listitem is in. */ 68 | const panel = listitem.closest('.mm-panel'); 69 | if (!panel) { 70 | return; 71 | } 72 | /** The parent LI for the panel. */ 73 | const parent = this.node.pnls.querySelector(`#${panel.dataset.mmParent}`); 74 | if (!parent) { 75 | return; 76 | } 77 | listitemObserver.observe(listitem, { 78 | attributes: true 79 | }); 80 | }); 81 | } 82 | -------------------------------------------------------------------------------- /dist/addons/counters/options.js: -------------------------------------------------------------------------------- 1 | const options = { 2 | add: false 3 | }; 4 | export default options; 5 | -------------------------------------------------------------------------------- /dist/addons/iconbar/mmenu.iconbar.css: -------------------------------------------------------------------------------- 1 | :root{--mm-iconbar-size:50px}.mm-menu--iconbar-left .mm-navbars,.mm-menu--iconbar-left .mm-panels{margin-left:var(--mm-iconbar-size)}.mm-menu--iconbar-right .mm-navbars,.mm-menu--iconbar-right .mm-panels{margin-right:var(--mm-iconbar-size)}.mm-iconbar{display:none;position:absolute;top:0;bottom:0;z-index:2;width:var(--mm-iconbar-size);overflow:hidden;-webkit-box-sizing:border-box;box-sizing:border-box;border:0 solid;border-color:var(--mm-color-border);background:var(--mm-color-background);color:var(--mm-color-text-dimmed);text-align:center}.mm-menu--iconbar-left .mm-iconbar,.mm-menu--iconbar-right .mm-iconbar{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.mm-menu--iconbar-left .mm-iconbar{border-right-width:1px;left:0}.mm-menu--iconbar-right .mm-iconbar{border-left-width:1px;right:0}.mm-iconbar__bottom,.mm-iconbar__top{width:100%;-webkit-overflow-scrolling:touch;overflow:hidden;overflow-y:auto;-ms-scroll-chaining:none;overscroll-behavior:contain}.mm-iconbar__bottom>*,.mm-iconbar__top>*{-webkit-box-sizing:border-box;box-sizing:border-box;display:block;padding:calc((var(--mm-iconbar-size) - var(--mm-lineheight))/ 2) 0}.mm-iconbar__bottom a,.mm-iconbar__bottom a:hover,.mm-iconbar__top a,.mm-iconbar__top a:hover{text-decoration:none}.mm-iconbar__tab--selected{background:var(--mm-color-background-emphasis)} -------------------------------------------------------------------------------- /dist/addons/iconbar/mmenu.iconbar.js: -------------------------------------------------------------------------------- 1 | import OPTIONS from './options'; 2 | import * as DOM from '../../_modules/dom'; 3 | import * as media from '../../_modules/matchmedia'; 4 | import { type, extend } from '../../_modules/helpers'; 5 | export default function () { 6 | this.opts.iconbar = this.opts.iconbar || {}; 7 | // Extend options. 8 | const options = extend(this.opts.iconbar, OPTIONS); 9 | if (!options.use) { 10 | return; 11 | } 12 | let iconbar; 13 | ['top', 'bottom'].forEach((position, n) => { 14 | let ctnt = options[position]; 15 | // Extend shorthand options 16 | if (type(ctnt) != 'array') { 17 | ctnt = [ctnt]; 18 | } 19 | // Create node 20 | const part = DOM.create('div.mm-iconbar__' + position); 21 | // Add content 22 | for (let c = 0, l = ctnt.length; c < l; c++) { 23 | if (typeof ctnt[c] == 'string') { 24 | part.innerHTML += ctnt[c]; 25 | } 26 | else { 27 | part.append(ctnt[c]); 28 | } 29 | } 30 | if (part.children.length) { 31 | if (!iconbar) { 32 | iconbar = DOM.create('div.mm-iconbar'); 33 | } 34 | iconbar.append(part); 35 | } 36 | }); 37 | // Add to menu 38 | if (iconbar) { 39 | // Add the iconbar. 40 | this.bind('initMenu:after', () => { 41 | this.node.menu.prepend(iconbar); 42 | }); 43 | // En-/disable the iconbar. 44 | let classname = 'mm-menu--iconbar-' + options.position; 45 | let enable = () => { 46 | this.node.menu.classList.add(classname); 47 | }; 48 | let disable = () => { 49 | this.node.menu.classList.remove(classname); 50 | }; 51 | if (typeof options.use == 'boolean') { 52 | this.bind('initMenu:after', enable); 53 | } 54 | else { 55 | media.add(options.use, enable, disable); 56 | } 57 | // Tabs 58 | if (options.type == 'tabs') { 59 | iconbar.classList.add('mm-iconbar--tabs'); 60 | iconbar.addEventListener('click', (evnt) => { 61 | const anchor = evnt.target.closest('.mm-iconbar__tab'); 62 | if (!anchor) { 63 | return; 64 | } 65 | if (anchor.matches('.mm-iconbar__tab--selected')) { 66 | evnt.stopImmediatePropagation(); 67 | return; 68 | } 69 | try { 70 | const panel = DOM.find(this.node.menu, `${anchor.getAttribute('href')}.mm-panel`)[0]; 71 | if (panel) { 72 | evnt.preventDefault(); 73 | evnt.stopImmediatePropagation(); 74 | this.openPanel(panel, false); 75 | } 76 | } 77 | catch (err) { } 78 | }); 79 | const selectTab = (panel) => { 80 | DOM.find(iconbar, 'a').forEach((anchor) => { 81 | anchor.classList.remove('mm-iconbar__tab--selected'); 82 | }); 83 | const anchor = DOM.find(iconbar, '[href="#' + panel.id + '"]')[0]; 84 | if (anchor) { 85 | anchor.classList.add('mm-iconbar__tab--selected'); 86 | } 87 | else { 88 | const parent = DOM.find(this.node.pnls, `#${panel.dataset.mmParent}`)[0]; 89 | if (parent) { 90 | selectTab(parent.closest('.mm-panel')); 91 | } 92 | } 93 | }; 94 | this.bind('openPanel:before', selectTab); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /dist/addons/iconbar/options.js: -------------------------------------------------------------------------------- 1 | const options = { 2 | use: false, 3 | top: [], 4 | bottom: [], 5 | position: 'left', 6 | type: 'default' 7 | }; 8 | export default options; 9 | -------------------------------------------------------------------------------- /dist/addons/iconpanels/_options.js: -------------------------------------------------------------------------------- 1 | const options = { 2 | add: false, 3 | blockPanel: true, 4 | visible: 3 5 | }; 6 | export default options; 7 | -------------------------------------------------------------------------------- /dist/addons/iconpanels/mmenu.iconpanels.css: -------------------------------------------------------------------------------- 1 | :root{--mm-iconpanel-size:50px}.mm-panel--iconpanel-0{inset-inline-start:calc(0 * var(--mm-iconpanel-size))}.mm-panel--iconpanel-1{inset-inline-start:calc(1 * var(--mm-iconpanel-size))}.mm-panel--iconpanel-2{inset-inline-start:calc(2 * var(--mm-iconpanel-size))}.mm-panel--iconpanel-3{inset-inline-start:calc(3 * var(--mm-iconpanel-size))}.mm-panel--iconpanel-4{inset-inline-start:calc(4 * var(--mm-iconpanel-size))}.mm-panel--iconpanel-first~.mm-panel{inset-inline-start:var(--mm-iconpanel-size)}.mm-menu--iconpanel .mm-panel--parent .mm-divider,.mm-menu--iconpanel .mm-panel--parent .mm-navbar{opacity:0}.mm-menu--iconpanel .mm-panels>.mm-panel--parent{overflow-y:hidden;-webkit-transform:unset;-ms-transform:unset;transform:unset}.mm-menu--iconpanel .mm-panels>.mm-panel:not(.mm-panel--iconpanel-first):not(.mm-panel--iconpanel-0){border-inline-start-width:1px;border-inline-start-style:solid} -------------------------------------------------------------------------------- /dist/addons/iconpanels/mmenu.iconpanels.js: -------------------------------------------------------------------------------- 1 | import OPTIONS from './_options'; 2 | import * as DOM from '../../_modules/dom'; 3 | import { extend } from '../../_modules/helpers'; 4 | export default function () { 5 | this.opts.iconPanels = this.opts.iconPanels || {}; 6 | // Extend options. 7 | const options = extend(this.opts.iconPanels, OPTIONS); 8 | let keepFirst = false; 9 | if (options.visible == 'first') { 10 | keepFirst = true; 11 | options.visible = 1; 12 | } 13 | options.visible = Math.min(3, Math.max(1, options.visible)); 14 | options.visible++; 15 | // Add the iconpanels 16 | if (options.add) { 17 | this.bind('initMenu:after', () => { 18 | this.node.menu.classList.add('mm-menu--iconpanel'); 19 | }); 20 | /** The classnames that can be set to a panel */ 21 | const classnames = [ 22 | 'mm-panel--iconpanel-0', 23 | 'mm-panel--iconpanel-1', 24 | 'mm-panel--iconpanel-2', 25 | 'mm-panel--iconpanel-3' 26 | ]; 27 | // Show only the main panel. 28 | if (keepFirst) { 29 | this.bind('initMenu:after', () => { 30 | var _a; 31 | (_a = DOM.children(this.node.pnls, '.mm-panel')[0]) === null || _a === void 0 ? void 0 : _a.classList.add('mm-panel--iconpanel-first'); 32 | }); 33 | // Show parent panel(s). 34 | } 35 | else { 36 | this.bind('openPanel:after', (panel) => { 37 | // Do nothing when opening a vertical submenu 38 | if (panel.closest('.mm-listitem--vertical')) { 39 | return; 40 | } 41 | let panels = DOM.children(this.node.pnls, '.mm-panel'); 42 | // Filter out panels that are not opened. 43 | panels = panels.filter((panel) => panel.matches('.mm-panel--parent')); 44 | // Add the current panel to the list. 45 | panels.push(panel); 46 | // Slice the opened panels to the max visible amount. 47 | panels = panels.slice(-options.visible); 48 | // Add the "iconpanel" classnames. 49 | panels.forEach((panel, p) => { 50 | panel.classList.remove('mm-panel--iconpanel-first', ...classnames); 51 | panel.classList.add(`mm-panel--iconpanel-${p}`); 52 | }); 53 | }); 54 | } 55 | // this.bind('initPanel:after', (panel: HTMLElement) => { 56 | // if (!panel.closest('.mm-listitem--vertical') && 57 | // !DOM.children(panel, '.mm-panel__blocker')[0] 58 | // ) { 59 | // const blocker = DOM.create('div.mm-blocker.mm-panel__blocker') as HTMLElement; 60 | // panel.prepend(blocker); 61 | // } 62 | // }); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /dist/addons/navbars/configs.js: -------------------------------------------------------------------------------- 1 | const configs = { 2 | breadcrumbs: { 3 | separator: '/', 4 | removeFirst: false 5 | } 6 | }; 7 | export default configs; 8 | -------------------------------------------------------------------------------- /dist/addons/navbars/mmenu.navbars.css: -------------------------------------------------------------------------------- 1 | .mm-navbars{-ms-flex-negative:0;flex-shrink:0}.mm-navbars .mm-navbar{position:relative;padding-top:0;border-bottom:none}.mm-navbars--top{border-bottom:1px solid var(--mm-color-border)}.mm-navbars--top .mm-navbar:first-child{padding-top:env(safe-area-inset-top)}.mm-navbars--bottom{border-top:1px solid var(--mm-color-border)}.mm-navbars--bottom .mm-navbar:last-child{padding-bottom:env(safe-area-inset-bottom)}.mm-navbar__breadcrumbs{-o-text-overflow:ellipsis;text-overflow:ellipsis;white-space:nowrap;overflow:hidden;-webkit-box-flex:1;-ms-flex:1 1 50%;flex:1 1 50%;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start;padding:0 20px;overflow-x:auto;-webkit-overflow-scrolling:touch}.mm-navbar__breadcrumbs>*{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-padding-end:6px;padding-inline-end:6px}.mm-navbar__breadcrumbs>a{text-decoration:underline}.mm-navbar__breadcrumbs:not(:last-child){-webkit-padding-end:0;padding-inline-end:0}.mm-btn:not(.mm-hidden)+.mm-navbar__breadcrumbs{-webkit-padding-start:0;padding-inline-start:0}.mm-navbar__tab{padding:0 10px;border:1px solid transparent}.mm-navbar__tab--selected{background:var(--mm-color-background)}.mm-navbar__tab--selected:not(:first-child){border-inline-start-color:var(--mm-color-border)}.mm-navbar__tab--selected:not(:last-child){border-inline-end-color:var(--mm-color-border)}.mm-navbars--top.mm-navbars--has-tabs{border-bottom:none}.mm-navbars--top.mm-navbars--has-tabs .mm-navbar{background:var(--mm-color-background-emphasis)}.mm-navbars--top.mm-navbars--has-tabs .mm-navbar--tabs~.mm-navbar{background:var(--mm-color-background)}.mm-navbars--top.mm-navbars--has-tabs .mm-navbar:not(.mm-navbar--tabs):last-child{border-bottom:1px solid var(--mm-color-border)}.mm-navbars--top .mm-navbar__tab{border-bottom-color:var(--mm-color-border)}.mm-navbars--top .mm-navbar__tab--selected{border-top-color:var(--mm-color-border);border-bottom-color:transparent}.mm-navbars--bottom.mm-navbar--has-tabs{border-top:none}.mm-navbars--bottom.mm-navbar--has-tabs .mm-navbar{background:var(--mm-color-background)}.mm-navbars--bottom.mm-navbar--has-tabs .mm-navbar--tabs,.mm-navbars--bottom.mm-navbar--has-tabs .mm-navbar--tabs~.mm-navbar{background:var(--mm-color-background-emphasis)}.mm-navbars--bottom .mm-navbar__tab{border-top-color:var(--mm-color-border)}.mm-navbars--bottom .mm-navbar__tab--selected{border-bottom-color:var(--mm-color-border);border-top-color:transparent} -------------------------------------------------------------------------------- /dist/addons/navbars/navbar.breadcrumbs.js: -------------------------------------------------------------------------------- 1 | import * as DOM from '../../_modules/dom'; 2 | export default function (navbar) { 3 | // Add content 4 | var breadcrumbs = DOM.create('div.mm-navbar__breadcrumbs'); 5 | navbar.append(breadcrumbs); 6 | this.bind('initNavbar:after', (panel) => { 7 | if (panel.querySelector('.mm-navbar__breadcrumbs')) { 8 | return; 9 | } 10 | DOM.children(panel, '.mm-navbar')[0].classList.add('mm-hidden'); 11 | var crumbs = [], breadcrumbs = DOM.create('span.mm-navbar__breadcrumbs'), current = panel, first = true; 12 | while (current) { 13 | current = current.closest('.mm-panel'); 14 | if (!current.parentElement.matches('.mm-listitem--vertical')) { 15 | let title = DOM.find(current, '.mm-navbar__title span')[0]; 16 | if (title) { 17 | let text = title.textContent; 18 | if (text.length) { 19 | crumbs.unshift(first 20 | ? `${text}` 21 | : `${text}`); 25 | } 26 | } 27 | first = false; 28 | } 29 | current = DOM.find(this.node.pnls, `#${current.dataset.mmParent}`)[0]; 30 | } 31 | if (this.conf.navbars.breadcrumbs.removeFirst) { 32 | crumbs.shift(); 33 | } 34 | breadcrumbs.innerHTML = crumbs.join('' + 35 | this.conf.navbars.breadcrumbs.separator + 36 | ''); 37 | DOM.children(panel, '.mm-navbar')[0].append(breadcrumbs); 38 | }); 39 | // Update for to opened panel 40 | this.bind('openPanel:before', (panel) => { 41 | var crumbs = panel.querySelector('.mm-navbar__breadcrumbs'); 42 | breadcrumbs.innerHTML = crumbs ? crumbs.innerHTML : ''; 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /dist/addons/navbars/navbar.close.js: -------------------------------------------------------------------------------- 1 | import * as DOM from '../../_modules/dom'; 2 | export default function (navbar) { 3 | /** The close button. */ 4 | const close = DOM.create('a.mm-btn.mm-btn--close.mm-navbar__btn'); 5 | close.setAttribute('aria-label', this.i18n(this.conf.offCanvas.screenReader.closeMenu)); 6 | // Add the button to the navbar. 7 | navbar.append(close); 8 | // Update to target the page node. 9 | this.bind('setPage:after', (page) => { 10 | close.href = `#${page.id}`; 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /dist/addons/navbars/navbar.prev.js: -------------------------------------------------------------------------------- 1 | import * as DOM from '../../_modules/dom'; 2 | export default function (navbar) { 3 | /** The prev button. */ 4 | let prev = DOM.create('a.mm-btn.mm-hidden'); 5 | // Add button to navbar. 6 | navbar.append(prev); 7 | // Hide navbar in the panel. 8 | this.bind('initNavbar:after', (panel) => { 9 | DOM.children(panel, '.mm-navbar')[0].classList.add('mm-hidden'); 10 | }); 11 | // Update the button href when opening a panel. 12 | this.bind('openPanel:before', (panel) => { 13 | if (panel.parentElement.matches('.mm-listitem--vertical')) { 14 | return; 15 | } 16 | prev.classList.add('mm-hidden'); 17 | /** Original button in the panel. */ 18 | const original = panel.querySelector('.mm-navbar__btn.mm-btn--prev'); 19 | if (original) { 20 | /** Clone of the original button in the panel. */ 21 | const clone = original.cloneNode(true); 22 | prev.after(clone); 23 | prev.remove(); 24 | prev = clone; 25 | } 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /dist/addons/navbars/navbar.searchfield.js: -------------------------------------------------------------------------------- 1 | import * as DOM from '../../_modules/dom'; 2 | import { uniqueId } from '../../_modules/helpers'; 3 | export default function (navbar) { 4 | /** Empty wrapper for the searchfield. */ 5 | let wrapper = DOM.create('div.mm-navbar__searchfield'); 6 | wrapper.id = uniqueId(); 7 | // Add button to navbar. 8 | navbar.append(wrapper); 9 | this.opts.searchfield = this.opts.searchfield || {}; 10 | this.opts.searchfield.add = true; 11 | this.opts.searchfield.addTo = `#${wrapper.id}`; 12 | } 13 | -------------------------------------------------------------------------------- /dist/addons/navbars/navbar.tabs.js: -------------------------------------------------------------------------------- 1 | import * as DOM from '../../_modules/dom'; 2 | export default function (navbar) { 3 | navbar.classList.add('mm-navbar--tabs'); 4 | navbar.closest('.mm-navbars').classList.add('mm-navbars--has-tabs'); 5 | DOM.children(navbar, 'a').forEach(anchor => { 6 | anchor.classList.add('mm-navbar__tab'); 7 | }); 8 | /** 9 | * Mark a tab as selected. 10 | * @param {HTMLElement} panel Opened panel. 11 | */ 12 | function selectTab(panel) { 13 | /** The tab that links to the opened panel. */ 14 | const anchor = DOM.children(navbar, `.mm-navbar__tab[href="#${panel.id}"]`)[0]; 15 | if (anchor) { 16 | anchor.classList.add('mm-navbar__tab--selected'); 17 | // @ts-ignore 18 | anchor.ariaExpanded = 'true'; 19 | } 20 | else { 21 | /** The parent listitem. */ 22 | const parent = DOM.find(this.node.pnls, `#${panel.dataset.mmParent}`)[0]; 23 | if (parent) { 24 | selectTab.call(this, parent.closest('.mm-panel')); 25 | } 26 | } 27 | } 28 | this.bind('openPanel:before', (panel) => { 29 | // Remove selected class. 30 | DOM.children(navbar, 'a').forEach(anchor => { 31 | anchor.classList.remove('mm-navbar__tab--selected'); 32 | // @ts-ignore 33 | anchor.ariaExpanded = 'false'; 34 | }); 35 | selectTab.call(this, panel); 36 | }); 37 | this.bind('initPanels:after', () => { 38 | // Add animation class to panel. 39 | navbar.addEventListener('click', event => { 40 | var _a, _b, _c; 41 | /** The href for the clicked tab. */ 42 | const href = (_b = (_a = event.target) === null || _a === void 0 ? void 0 : _a.closest('.mm-navbar__tab')) === null || _b === void 0 ? void 0 : _b.getAttribute('href'); 43 | try { 44 | (_c = DOM.find(this.node.pnls, `${href}.mm-panel`)[0]) === null || _c === void 0 ? void 0 : _c.classList.add('mm-panel--noanimation'); 45 | } 46 | catch (err) { } 47 | }, { 48 | // useCapture to ensure the logical order. 49 | capture: true 50 | }); 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /dist/addons/navbars/navbar.title.js: -------------------------------------------------------------------------------- 1 | import * as DOM from '../../_modules/dom'; 2 | export default function (navbar) { 3 | /** The title node in the navbar. */ 4 | let title = DOM.create('a.mm-navbar__title'); 5 | // Add title to the navbar. 6 | navbar.append(title); 7 | // Update the title to the opened panel. 8 | this.bind('openPanel:before', (panel) => { 9 | // Do nothing in a vertically expanding panel. 10 | if (panel.parentElement.matches('.mm-listitem--vertical')) { 11 | return; 12 | } 13 | /** Original title in the panel. */ 14 | const original = panel.querySelector('.mm-navbar__title'); 15 | if (original) { 16 | /** Clone of the original title in the panel. */ 17 | const clone = original.cloneNode(true); 18 | title.after(clone); 19 | title.remove(); 20 | title = clone; 21 | } 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /dist/addons/navbars/options.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Extend shorthand options. 3 | * 4 | * @param {object} options The options to extend. 5 | * @return {object} The extended options. 6 | */ 7 | export function extendShorthandOptions(options) { 8 | if (typeof options == 'boolean' && options) { 9 | options = {}; 10 | } 11 | if (typeof options != 'object') { 12 | options = {}; 13 | } 14 | if (typeof options.content == 'undefined') { 15 | options.content = ['prev', 'title']; 16 | } 17 | if (!(options.content instanceof Array)) { 18 | options.content = [options.content]; 19 | } 20 | if (typeof options.use == 'undefined') { 21 | options.use = true; 22 | } 23 | return options; 24 | } 25 | ; 26 | -------------------------------------------------------------------------------- /dist/addons/pagescroll/configs.js: -------------------------------------------------------------------------------- 1 | const configs = { 2 | scrollOffset: 0, 3 | updateOffset: 50 4 | }; 5 | export default configs; 6 | -------------------------------------------------------------------------------- /dist/addons/pagescroll/mmenu.pagescroll.js: -------------------------------------------------------------------------------- 1 | import Mmenu from '../../core/oncanvas/mmenu.oncanvas'; 2 | import OPTIONS from './options'; 3 | import CONFIGS from './configs'; 4 | import * as DOM from '../../_modules/dom'; 5 | import { extend } from '../../_modules/helpers'; 6 | export default function () { 7 | this.opts.pageScroll = this.opts.pageScroll || {}; 8 | this.conf.pageScroll = this.conf.pageScroll || {}; 9 | // Extend options. 10 | const options = extend(this.opts.pageScroll, OPTIONS); 11 | const configs = extend(this.conf.pageScroll, CONFIGS); 12 | /** The currently "active" section */ 13 | var section; 14 | function scrollTo() { 15 | if (section) { 16 | // section.scrollIntoView({ behavior: 'smooth' }); 17 | window.scrollTo({ 18 | top: section.getBoundingClientRect().top + 19 | document.scrollingElement.scrollTop - 20 | configs.scrollOffset, 21 | behavior: 'smooth' 22 | }); 23 | } 24 | section = null; 25 | } 26 | function anchorInPage(href) { 27 | try { 28 | if (href.slice(0, 1) == '#') { 29 | return DOM.find(Mmenu.node.page, href)[0]; 30 | } 31 | } 32 | catch (err) { } 33 | return null; 34 | } 35 | if (this.opts.offCanvas.use && options.scroll) { 36 | // Scroll to section after clicking menu item. 37 | this.bind('close:after', () => { 38 | scrollTo(); 39 | }); 40 | this.node.menu.addEventListener('click', event => { 41 | var _a, _b; 42 | const href = ((_b = (_a = event.target) === null || _a === void 0 ? void 0 : _a.closest('a[href]')) === null || _b === void 0 ? void 0 : _b.getAttribute('href')) || ''; 43 | section = anchorInPage(href); 44 | if (section) { 45 | event.preventDefault(); 46 | // If the sidebar add-on is "expanded"... 47 | if (this.node.menu.matches('.mm-menu--sidebar-expanded') && 48 | this.node.wrpr.matches('.mm-wrapper--sidebar-expanded')) { 49 | // ... scroll the page to the section. 50 | scrollTo(); 51 | // ... otherwise... 52 | } 53 | else { 54 | // ... close the menu. 55 | this.close(); 56 | } 57 | } 58 | }); 59 | } 60 | // Update selected menu item after scrolling. 61 | if (options.update) { 62 | let scts = []; 63 | this.bind('initListview:after', (listview) => { 64 | const listitems = DOM.children(listview, '.mm-listitem'); 65 | DOM.filterLIA(listitems).forEach(anchor => { 66 | const section = anchorInPage(anchor.getAttribute('href')); 67 | if (section) { 68 | scts.unshift(section); 69 | } 70 | }); 71 | }); 72 | let _selected = -1; 73 | window.addEventListener('scroll', evnt => { 74 | const scrollTop = window.scrollY; 75 | for (var s = 0; s < scts.length; s++) { 76 | if (scts[s].offsetTop < scrollTop + configs.updateOffset) { 77 | if (_selected !== s) { 78 | _selected = s; 79 | let panel = DOM.children(this.node.pnls, '.mm-panel--opened')[0]; 80 | let listitems = DOM.find(panel, '.mm-listitem'); 81 | let anchors = DOM.filterLIA(listitems); 82 | anchors = anchors.filter(anchor => anchor.matches('[href="#' + scts[s].id + '"]')); 83 | if (anchors.length) { 84 | this.setSelected(anchors[0].parentElement); 85 | } 86 | } 87 | break; 88 | } 89 | } 90 | }, { 91 | passive: true 92 | }); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /dist/addons/pagescroll/options.js: -------------------------------------------------------------------------------- 1 | const options = { 2 | scroll: false, 3 | update: false 4 | }; 5 | export default options; 6 | -------------------------------------------------------------------------------- /dist/addons/searchfield/configs.js: -------------------------------------------------------------------------------- 1 | const configs = { 2 | cancel: true, 3 | clear: true, 4 | form: {}, 5 | input: {}, 6 | panel: {}, 7 | submit: false 8 | }; 9 | export default configs; 10 | -------------------------------------------------------------------------------- /dist/addons/searchfield/mmenu.searchfield.css: -------------------------------------------------------------------------------- 1 | .mm-searchfield{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;height:var(--mm-navbar-size);padding:0;overflow:hidden}.mm-searchfield__input{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-flex:1;-ms-flex:1;flex:1;-webkit-box-align:center;-ms-flex-align:center;align-items:center;position:relative;width:100%;max-width:100%;padding:0 10px;-webkit-box-sizing:border-box;box-sizing:border-box}.mm-searchfield__input input{display:block;width:100%;max-width:100%;height:calc(var(--mm-navbar-size) * .7);min-height:auto;max-height:auto;margin:0;padding:0 10px;-webkit-box-sizing:border-box;box-sizing:border-box;border:none;border-radius:4px;line-height:calc(var(--mm-navbar-size) * .7);font:inherit;font-size:inherit}.mm-searchfield__input input,.mm-searchfield__input input:focus,.mm-searchfield__input input:hover{background:var(--mm-color-background-highlight);color:var(--mm-color-text)}.mm-menu[class*=-contrast] .mm-searchfield__input input{border:1px solid var(--mm-color-border)}.mm-searchfield__input input::-ms-clear{display:none}.mm-searchfield__btn{display:none;position:absolute;inset-inline-end:0;top:0;bottom:0}.mm-searchfield--searching .mm-searchfield__btn{display:block}.mm-searchfield__cancel{display:block;position:relative;-webkit-margin-end:-100px;margin-inline-end:-100px;-webkit-padding-start:5px;padding-inline-start:5px;-webkit-padding-end:20px;padding-inline-end:20px;visibility:hidden;line-height:var(--mm-navbar-size);text-decoration:none;-webkit-transition-property:visibility,margin;-o-transition-property:visibility,margin;transition-property:visibility,margin}.mm-searchfield--cancelable .mm-searchfield__cancel{visibility:visible;-webkit-margin-end:0;margin-inline-end:0}.mm-panel--search{left:0!important;right:0!important;width:100%!important;border:none!important}.mm-panel__splash{padding:20px}.mm-panel--searching .mm-panel__splash{display:none}.mm-panel__noresults{display:none;padding:40px 20px;color:var(--mm-color-text-dimmed);text-align:center;font-size:150%;line-height:1.4}.mm-panel--noresults .mm-panel__noresults{display:block} -------------------------------------------------------------------------------- /dist/addons/searchfield/options.js: -------------------------------------------------------------------------------- 1 | const options = { 2 | add: false, 3 | addTo: 'panels', 4 | noResults: 'No results found.', 5 | placeholder: 'Search', 6 | search: true, 7 | searchIn: 'panels', 8 | splash: '', 9 | title: 'Search', 10 | }; 11 | export default options; 12 | -------------------------------------------------------------------------------- /dist/addons/searchfield/translations/de.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'cancel': 'abbrechen', 3 | 'Cancel searching': 'Suche abbrechen', 4 | 'Clear searchfield': 'Suchfeld löschen', 5 | 'No results found.': 'Keine Ergebnisse gefunden.', 6 | 'Search': 'Suche', 7 | }; 8 | -------------------------------------------------------------------------------- /dist/addons/searchfield/translations/fa.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'cancel': 'انصراف', 3 | 'Cancel searching': 'لغو جستجو', 4 | 'Clear searchfield': 'پاک کردن فیلد جستجو', 5 | 'No results found.': 'نتیجه‌ای یافت نشد.', 6 | 'Search': 'جستجو', 7 | }; 8 | -------------------------------------------------------------------------------- /dist/addons/searchfield/translations/index.js: -------------------------------------------------------------------------------- 1 | import { add } from '../../../_modules/i18n'; 2 | import de from './de'; 3 | import fa from './fa'; 4 | import nl from './nl'; 5 | import pt_br from './pt_br'; 6 | import ru from './ru'; 7 | import sk from './sk'; 8 | import uk from './uk'; 9 | export default function () { 10 | add(de, 'de'); 11 | add(fa, 'fa'); 12 | add(nl, 'nl'); 13 | add(pt_br, 'pt_br'); 14 | add(ru, 'ru'); 15 | add(sk, 'sk'); 16 | add(uk, 'uk'); 17 | } 18 | -------------------------------------------------------------------------------- /dist/addons/searchfield/translations/nl.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'cancel': 'annuleren', 3 | 'Cancel searching': 'Zoeken annuleren', 4 | 'Clear searchfield': 'Zoekveld leeg maken', 5 | 'No results found.': 'Geen resultaten gevonden.', 6 | 'Search': 'Zoeken', 7 | }; 8 | -------------------------------------------------------------------------------- /dist/addons/searchfield/translations/pt_br.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'cancel': 'cancelar', 3 | 'Cancel searching': 'Cancelar pesquisa', 4 | 'Clear searchfield': 'Limpar campo de pesquisa', 5 | 'No results found.': 'Nenhum resultado encontrado.', 6 | 'Search': 'Buscar', 7 | }; 8 | -------------------------------------------------------------------------------- /dist/addons/searchfield/translations/ru.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'cancel': 'отменить', 3 | 'Cancel searching': 'Отменить поиск', 4 | 'Clear searchfield': 'Очистить поле поиска', 5 | 'No results found.': 'Ничего не найдено.', 6 | 'Search': 'Найти', 7 | }; 8 | -------------------------------------------------------------------------------- /dist/addons/searchfield/translations/sk.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'cancel': 'zrušiť', 3 | 'Cancel searching': 'Zrušiť vyhľadávanie', 4 | 'Clear searchfield': 'Vymazať pole vyhľadávania', 5 | 'No results found.': 'Neboli nájdené žiadne výsledky.', 6 | 'Search': 'Vyhľadávanie', 7 | }; 8 | -------------------------------------------------------------------------------- /dist/addons/searchfield/translations/uk.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'cancel': 'скасувати', 3 | 'Cancel searching': 'Скасувати пошук', 4 | 'Clear searchfield': 'Очистити поле пошуку', 5 | 'No results found.': 'Нічого не знайдено.', 6 | 'Search': 'Пошук', 7 | }; 8 | -------------------------------------------------------------------------------- /dist/addons/sectionindexer/mmenu.sectionindexer.css: -------------------------------------------------------------------------------- 1 | :root{--mm-sectionindexer-size:20px}.mm-sectionindexer{background:inherit;text-align:center;font-size:12px;-webkit-box-sizing:border-box;box-sizing:border-box;width:var(--mm-sectionindexer-size);position:absolute;top:0;bottom:0;inset-inline-end:calc(-1 * var(--mm-sectionindexer-size));z-index:5;-webkit-transition-property:inset-inline-end;-o-transition-property:inset-inline-end;transition-property:inset-inline-end;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:space-evenly;-ms-flex-pack:space-evenly;justify-content:space-evenly}.mm-sectionindexer a{color:var(--mm-color-text-dimmed);line-height:1;text-decoration:none;display:block}.mm-sectionindexer~.mm-panel{-webkit-padding-end:0;padding-inline-end:0}.mm-sectionindexer--active{right:0}.mm-sectionindexer--active~.mm-panel{-webkit-padding-end:var(--mm-sectionindexer-size);padding-inline-end:var(--mm-sectionindexer-size)} -------------------------------------------------------------------------------- /dist/addons/sectionindexer/mmenu.sectionindexer.js: -------------------------------------------------------------------------------- 1 | import OPTIONS from './options'; 2 | import * as DOM from '../../_modules/dom'; 3 | import * as support from '../../_modules/support'; 4 | import { extend } from '../../_modules/helpers'; 5 | export default function () { 6 | this.opts.sectionIndexer = this.opts.sectionIndexer || {}; 7 | // Extend options. 8 | const options = extend(this.opts.sectionIndexer, OPTIONS); 9 | if (!options.add) { 10 | return; 11 | } 12 | this.bind('initPanels:after', () => { 13 | // Add the indexer, only if it does not allready excists 14 | if (!this.node.indx) { 15 | let buttons = ''; 16 | 'abcdefghijklmnopqrstuvwxyz'.split('').forEach(letter => { 17 | buttons += '' + letter + ''; 18 | }); 19 | let indexer = DOM.create('div.mm-sectionindexer'); 20 | indexer.innerHTML = buttons; 21 | this.node.pnls.prepend(indexer); 22 | this.node.indx = indexer; 23 | // Prevent default behavior when clicking an anchor 24 | this.node.indx.addEventListener('click', evnt => { 25 | const anchor = evnt.target; 26 | if (anchor.matches('a')) { 27 | evnt.preventDefault(); 28 | } 29 | }); 30 | // Scroll onMouseOver / onTouchStart 31 | let mouseOverEvent = evnt => { 32 | if (!evnt.target.matches('a')) { 33 | return; 34 | } 35 | const letter = evnt.target.textContent; 36 | const panel = DOM.children(this.node.pnls, '.mm-panel--opened')[0]; 37 | let newTop = -1, oldTop = panel.scrollTop; 38 | panel.scrollTop = 0; 39 | DOM.find(panel, '.mm-divider') 40 | .filter(divider => !divider.matches('.mm-hidden')) 41 | .forEach(divider => { 42 | if (newTop < 0 && 43 | letter == 44 | divider.textContent 45 | .trim() 46 | .slice(0, 1) 47 | .toLowerCase()) { 48 | newTop = divider.offsetTop; 49 | } 50 | }); 51 | panel.scrollTop = newTop > -1 ? newTop : oldTop; 52 | }; 53 | if (support.touch) { 54 | this.node.indx.addEventListener('touchstart', mouseOverEvent); 55 | this.node.indx.addEventListener('touchmove', mouseOverEvent); 56 | } 57 | else { 58 | this.node.indx.addEventListener('mouseover', mouseOverEvent); 59 | } 60 | } 61 | // Show or hide the indexer 62 | this.bind('openPanel:before', (panel) => { 63 | const active = DOM.find(panel, '.mm-divider').filter(divider => !divider.matches('.mm-hidden')).length; 64 | this.node.indx.classList[active ? 'add' : 'remove']('mm-sectionindexer--active'); 65 | }); 66 | }); 67 | } 68 | -------------------------------------------------------------------------------- /dist/addons/sectionindexer/options.js: -------------------------------------------------------------------------------- 1 | const options = { 2 | add: false, 3 | addTo: 'panels' 4 | }; 5 | export default options; 6 | -------------------------------------------------------------------------------- /dist/addons/setselected/mmenu.setselected.css: -------------------------------------------------------------------------------- 1 | .mm-menu--selected-hover .mm-listitem__btn,.mm-menu--selected-hover .mm-listitem__text,.mm-menu--selected-parent .mm-listitem__btn,.mm-menu--selected-parent .mm-listitem__text{-webkit-transition-property:background-color;-o-transition-property:background-color;transition-property:background-color}@media (hover:hover){.mm-menu--selected-hover .mm-listview:hover>.mm-listitem--selected:not(:hover)>.mm-listitem__text{background:0 0}.mm-menu--selected-hover .mm-listitem__btn:hover,.mm-menu--selected-hover .mm-listitem__text:hover{background:var(--mm-color-background-emphasis)}}.mm-menu--selected-parent .mm-listitem__btn,.mm-menu--selected-parent .mm-listitem__text{-webkit-transition-delay:.2s;-o-transition-delay:.2s;transition-delay:.2s}@media (hover:hover){.mm-menu--selected-parent .mm-listitem__btn:hover,.mm-menu--selected-parent .mm-listitem__text:hover{-webkit-transition-delay:0s;-o-transition-delay:0s;transition-delay:0s}}.mm-menu--selected-parent .mm-panel--parent .mm-listitem:not(.mm-listitem--selected-parent)>.mm-listitem__text{background:0 0}.mm-menu--selected-parent .mm-listitem--selected-parent>.mm-listitem__btn,.mm-menu--selected-parent .mm-listitem--selected-parent>.mm-listitem__text{background:var(--mm-color-background-emphasis)} -------------------------------------------------------------------------------- /dist/addons/setselected/mmenu.setselected.js: -------------------------------------------------------------------------------- 1 | import OPTIONS from './options'; 2 | import * as DOM from '../../_modules/dom'; 3 | import { extend } from '../../_modules/helpers'; 4 | export default function () { 5 | this.opts.setSelected = this.opts.setSelected || {}; 6 | // Extend options. 7 | const options = extend(this.opts.setSelected, OPTIONS); 8 | // Find current by URL 9 | if (options.current == 'detect') { 10 | const findCurrent = (url) => { 11 | url = url.split('?')[0].split('#')[0]; 12 | const anchor = this.node.menu.querySelector('a[href="' + url + '"], a[href="' + url + '/"]'); 13 | if (anchor) { 14 | this.setSelected(anchor.parentElement); 15 | } 16 | else { 17 | const arr = url.split('/').slice(0, -1); 18 | if (arr.length) { 19 | findCurrent(arr.join('/')); 20 | } 21 | } 22 | }; 23 | this.bind('initMenu:after', () => { 24 | findCurrent.call(this, window.location.href); 25 | }); 26 | // Remove current selected item 27 | } 28 | else if (!options.current) { 29 | this.bind('initListview:after', (listview) => { 30 | DOM.children(listview, '.mm-listitem--selected').forEach((listitem) => { 31 | listitem.classList.remove('mm-listitem--selected'); 32 | }); 33 | }); 34 | } 35 | // Add :hover effect on items 36 | if (options.hover) { 37 | this.bind('initMenu:after', () => { 38 | this.node.menu.classList.add('mm-menu--selected-hover'); 39 | }); 40 | } 41 | // Set parent item selected for submenus 42 | if (options.parent) { 43 | this.bind('openPanel:after', (panel) => { 44 | // Remove all 45 | DOM.find(this.node.pnls, '.mm-listitem--selected-parent').forEach((listitem) => { 46 | listitem.classList.remove('mm-listitem--selected-parent'); 47 | }); 48 | // Move up the DOM tree 49 | let current = panel; 50 | while (current) { 51 | let li = DOM.find(this.node.pnls, `#${current.dataset.mmParent}`)[0]; 52 | current = li === null || li === void 0 ? void 0 : li.closest('.mm-panel'); 53 | if (li && !li.matches('.mm-listitem--vertical')) { 54 | li.classList.add('mm-listitem--selected-parent'); 55 | } 56 | } 57 | }); 58 | this.bind('initMenu:after', () => { 59 | this.node.menu.classList.add('mm-menu--selected-parent'); 60 | }); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /dist/addons/setselected/options.js: -------------------------------------------------------------------------------- 1 | const options = { 2 | current: true, 3 | hover: false, 4 | parent: false 5 | }; 6 | export default options; 7 | -------------------------------------------------------------------------------- /dist/addons/sidebar/mmenu.sidebar.css: -------------------------------------------------------------------------------- 1 | :root{--mm-sidebar-collapsed-size:50px;--mm-sidebar-expanded-size:var(--mm-max-size)}.mm-wrapper--sidebar-collapsed .mm-slideout{width:calc(100% - var(--mm-sidebar-collapsed-size));-webkit-transform:translate3d(var(--mm-sidebar-collapsed-size),0,0);transform:translate3d(var(--mm-sidebar-collapsed-size),0,0)}[dir=rtl] .mm-wrapper--sidebar-collapsed .mm-slideout{-webkit-transform:none;-ms-transform:none;transform:none}.mm-wrapper--sidebar-collapsed:not(.mm-wrapper--opened) .mm-menu--sidebar-collapsed .mm-divider,.mm-wrapper--sidebar-collapsed:not(.mm-wrapper--opened) .mm-menu--sidebar-collapsed .mm-navbar{opacity:0}.mm-wrapper--sidebar-expanded .mm-menu--sidebar-expanded{width:var(--mm-sidebar-expanded-size);border-right-width:1px;border-right-style:solid}.mm-wrapper--sidebar-expanded.mm-wrapper--opened{overflow:auto}.mm-wrapper--sidebar-expanded.mm-wrapper--opened .mm-wrapper__blocker{display:none}.mm-wrapper--sidebar-expanded.mm-wrapper--opened .mm-slideout{width:calc(100% - var(--mm-sidebar-expanded-size));-webkit-transform:translate3d(var(--mm-sidebar-expanded-size),0,0);transform:translate3d(var(--mm-sidebar-expanded-size),0,0)}[dir=rtl] .mm-wrapper--sidebar-expanded.mm-wrapper--opened .mm-slideout{-webkit-transform:none;-ms-transform:none;transform:none} -------------------------------------------------------------------------------- /dist/addons/sidebar/mmenu.sidebar.js: -------------------------------------------------------------------------------- 1 | import Mmenu from '../../core/oncanvas/mmenu.oncanvas'; 2 | import OPTIONS from './options'; 3 | import * as media from '../../_modules/matchmedia'; 4 | import { extend } from '../../_modules/helpers'; 5 | export default function () { 6 | // Only for off-canvas menus. 7 | if (!this.opts.offCanvas.use) { 8 | return; 9 | } 10 | this.opts.sidebar = this.opts.sidebar || {}; 11 | // Extend options. 12 | const options = extend(this.opts.sidebar, OPTIONS); 13 | // Collapsed 14 | if (options.collapsed.use) { 15 | // Make the menu collapsable. 16 | this.bind('initMenu:after', () => { 17 | this.node.menu.classList.add('mm-menu--sidebar-collapsed'); 18 | }); 19 | /** Enable the collapsed sidebar */ 20 | let enable = () => { 21 | this.node.wrpr.classList.add('mm-wrapper--sidebar-collapsed'); 22 | }; 23 | /** Disable the collapsed sidebar */ 24 | let disable = () => { 25 | this.node.wrpr.classList.remove('mm-wrapper--sidebar-collapsed'); 26 | }; 27 | if (typeof options.collapsed.use === 'boolean') { 28 | this.bind('initMenu:after', enable); 29 | } 30 | else { 31 | media.add(options.collapsed.use, enable, disable); 32 | } 33 | } 34 | // Expanded 35 | if (options.expanded.use) { 36 | // Make the menu expandable 37 | this.bind('initMenu:after', () => { 38 | this.node.menu.classList.add('mm-menu--sidebar-expanded'); 39 | }); 40 | let expandedEnabled = false; 41 | /** Enable the expanded sidebar */ 42 | let enable = () => { 43 | expandedEnabled = true; 44 | this.node.wrpr.classList.add('mm-wrapper--sidebar-expanded'); 45 | this.node.menu.removeAttribute('aria-modal'); 46 | this.open(); 47 | Mmenu.node.page.removeAttribute('inert'); 48 | }; 49 | /** Disable the expanded sidebar */ 50 | let disable = () => { 51 | expandedEnabled = false; 52 | this.node.wrpr.classList.remove('mm-wrapper--sidebar-expanded'); 53 | this.node.menu.setAttribute('aria-modal', 'true'); 54 | this.close(); 55 | }; 56 | if (typeof options.expanded.use == 'boolean') { 57 | this.bind('initMenu:after', enable); 58 | } 59 | else { 60 | media.add(options.expanded.use, enable, disable); 61 | } 62 | // Store exanded state when opening and closing the menu. 63 | this.bind('close:after', () => { 64 | if (expandedEnabled) { 65 | window.sessionStorage.setItem('mmenuExpandedState', 'closed'); 66 | } 67 | }); 68 | this.bind('open:after', () => { 69 | if (expandedEnabled) { 70 | window.sessionStorage.setItem('mmenuExpandedState', 'open'); 71 | Mmenu.node.page.removeAttribute('inert'); 72 | } 73 | }); 74 | // Set the initial state 75 | let initialState = options.expanded.initial; 76 | const state = window.sessionStorage.getItem('mmenuExpandedState'); 77 | switch (state) { 78 | case 'open': 79 | case 'closed': 80 | initialState = state; 81 | break; 82 | } 83 | if (initialState === 'closed') { 84 | this.bind('init:after', () => { 85 | this.close(); 86 | }); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /dist/addons/sidebar/options.js: -------------------------------------------------------------------------------- 1 | const options = { 2 | collapsed: { 3 | use: false, 4 | }, 5 | expanded: { 6 | use: false, 7 | initial: 'open' 8 | } 9 | }; 10 | export default options; 11 | -------------------------------------------------------------------------------- /dist/core/offcanvas/configs.js: -------------------------------------------------------------------------------- 1 | const configs = { 2 | clone: false, 3 | menu: { 4 | insertMethod: 'append', 5 | insertSelector: 'body' 6 | }, 7 | page: { 8 | nodetype: 'div', 9 | selector: null, 10 | noSelector: [] 11 | }, 12 | screenReader: { 13 | closeMenu: 'Close menu', 14 | openMenu: 'Open menu', 15 | } 16 | }; 17 | export default configs; 18 | -------------------------------------------------------------------------------- /dist/core/offcanvas/mmenu.offcanvas.css: -------------------------------------------------------------------------------- 1 | :root{--mm-size:80%;--mm-min-size:240px;--mm-max-size:440px}.mm-menu--offcanvas{position:fixed;z-index:0}.mm-page{-webkit-box-sizing:border-box;box-sizing:border-box;min-height:100vh;background:inherit}:where(.mm-slideout){position:relative;z-index:1;width:100%;-webkit-transition-duration:.4s;-o-transition-duration:.4s;transition-duration:.4s;-webkit-transition-timing-function:ease;-o-transition-timing-function:ease;transition-timing-function:ease;-webkit-transition-property:width,-webkit-transform;transition-property:width,-webkit-transform;-o-transition-property:width,transform;transition-property:width,transform;transition-property:width,transform,-webkit-transform}.mm-wrapper--opened,.mm-wrapper--opened body{overflow:hidden}.mm-wrapper__blocker{background:rgba(0,0,0,.4)}.mm-wrapper--opened .mm-wrapper__blocker{--mm-blocker-visibility-delay:0s;--mm-blocker-opacity-delay:0.4s;bottom:0;opacity:.5}.mm-menu{--mm-translate-horizontal:0;--mm-translate-vertical:0}.mm-menu--position-left,.mm-menu--position-left-front{right:auto}.mm-menu--position-right,.mm-menu--position-right-front{left:auto}.mm-menu--position-left,.mm-menu--position-left-front,.mm-menu--position-right,.mm-menu--position-right-front{width:clamp(var(--mm-min-size),var(--mm-size),var(--mm-max-size))}.mm-menu--position-left-front{--mm-translate-horizontal:-100%}.mm-menu--position-right-front{--mm-translate-horizontal:100%}.mm-menu--position-top{bottom:auto}.mm-menu--position-bottom{top:auto}.mm-menu--position-bottom,.mm-menu--position-top{width:100%;height:clamp(var(--mm-min-size),var(--mm-size),var(--mm-max-size))}.mm-menu--position-top{--mm-translate-vertical:-100%}.mm-menu--position-bottom{--mm-translate-vertical:100%}.mm-menu--position-bottom,.mm-menu--position-left-front,.mm-menu--position-right-front,.mm-menu--position-top{z-index:2;-webkit-transform:translate3d(var(--mm-translate-horizontal),var(--mm-translate-vertical),0);transform:translate3d(var(--mm-translate-horizontal),var(--mm-translate-vertical),0);-webkit-transition-property:-webkit-transform;transition-property:-webkit-transform;-o-transition-property:transform;transition-property:transform;transition-property:transform,-webkit-transform}.mm-menu--position-bottom.mm-menu--opened,.mm-menu--position-left-front.mm-menu--opened,.mm-menu--position-right-front.mm-menu--opened,.mm-menu--position-top.mm-menu--opened{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.mm-wrapper--position-left{--mm-translate-horizontal:clamp( 2 | var(--mm-min-size), 3 | var(--mm-size), 4 | var(--mm-max-size) 5 | )}.mm-wrapper--position-right{--mm-translate-horizontal:clamp( 6 | calc(-1 * var(--mm-max-size)), 7 | calc(-1 * var(--mm-size)), 8 | calc(-1 * var(--mm-min-size)) 9 | )}.mm-wrapper--position-left .mm-slideout,.mm-wrapper--position-right .mm-slideout{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.mm-wrapper--position-left.mm-wrapper--opened .mm-slideout,.mm-wrapper--position-right.mm-wrapper--opened .mm-slideout{-webkit-transform:translate3d(var(--mm-translate-horizontal),0,0);transform:translate3d(var(--mm-translate-horizontal),0,0)}.mm-wrapper--position-bottom .mm-wrapper__blocker,.mm-wrapper--position-left-front .mm-wrapper__blocker,.mm-wrapper--position-right-front .mm-wrapper__blocker,.mm-wrapper--position-top .mm-wrapper__blocker{z-index:1} -------------------------------------------------------------------------------- /dist/core/offcanvas/options.js: -------------------------------------------------------------------------------- 1 | const options = { 2 | use: true, 3 | position: 'left' 4 | }; 5 | export default options; 6 | -------------------------------------------------------------------------------- /dist/core/offcanvas/translations/de.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'Close menu': 'Menü schließen', 3 | 'Open menu': 'Menü öffnen', 4 | }; 5 | -------------------------------------------------------------------------------- /dist/core/offcanvas/translations/fa.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'Close menu': 'بستن منو', 3 | 'Open menu': 'باز کردن منو', 4 | }; 5 | -------------------------------------------------------------------------------- /dist/core/offcanvas/translations/index.js: -------------------------------------------------------------------------------- 1 | import { add } from '../../../_modules/i18n'; 2 | import de from './de'; 3 | import fa from './fa'; 4 | import nl from './nl'; 5 | import pt_br from './pt_br'; 6 | import ru from './ru'; 7 | import sk from './sk'; 8 | import uk from './uk'; 9 | export default function () { 10 | add(de, 'de'); 11 | add(fa, 'fa'); 12 | add(nl, 'nl'); 13 | add(pt_br, 'pt_br'); 14 | add(ru, 'ru'); 15 | add(sk, 'sk'); 16 | add(uk, 'uk'); 17 | } 18 | -------------------------------------------------------------------------------- /dist/core/offcanvas/translations/nl.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'Close menu': 'Menu sluiten', 3 | 'Open menu': 'Menu openen', 4 | }; 5 | -------------------------------------------------------------------------------- /dist/core/offcanvas/translations/pt_br.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'Close menu': 'Fechar menu', 3 | 'Open menu': 'Abrir menu', 4 | }; 5 | -------------------------------------------------------------------------------- /dist/core/offcanvas/translations/ru.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'Close menu': 'Закрыть меню', 3 | 'Open menu': 'открыть меню', 4 | }; 5 | -------------------------------------------------------------------------------- /dist/core/offcanvas/translations/sk.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'Close menu': 'Zatvoriť menu', 3 | 'Open menu': 'Otvoriť menu', 4 | }; 5 | -------------------------------------------------------------------------------- /dist/core/offcanvas/translations/uk.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'Close menu': 'Закрити меню', 3 | 'Open menu': 'Відкрити меню', 4 | }; 5 | -------------------------------------------------------------------------------- /dist/core/oncanvas/configs.js: -------------------------------------------------------------------------------- 1 | const configs = { 2 | classNames: { 3 | divider: 'Divider', 4 | nolistview: 'NoListview', 5 | nopanel: 'NoPanel', 6 | panel: 'Panel', 7 | selected: 'Selected', 8 | vertical: 'Vertical' 9 | }, 10 | language: null, 11 | panelNodetype: ['ul', 'ol', 'div'], 12 | screenReader: { 13 | closeSubmenu: 'Close submenu', 14 | openSubmenu: 'Open submenu', 15 | toggleSubmenu: 'Toggle submenu' 16 | } 17 | }; 18 | export default configs; 19 | -------------------------------------------------------------------------------- /dist/core/oncanvas/options.js: -------------------------------------------------------------------------------- 1 | const options = { 2 | hooks: {}, 3 | navbar: { 4 | add: true, 5 | title: 'Menu', 6 | titleLink: 'parent' 7 | }, 8 | slidingSubmenus: true 9 | }; 10 | export default options; 11 | -------------------------------------------------------------------------------- /dist/core/oncanvas/translations/de.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'Close submenu': 'Untermenü schließen', 3 | 'Menu': 'Menü', 4 | 'Open submenu': 'Untermenü öffnen', 5 | 'Toggle submenu': 'Untermenü wechseln' 6 | }; 7 | -------------------------------------------------------------------------------- /dist/core/oncanvas/translations/fa.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'Close submenu': 'بستن زیرمنو', 3 | 'Menu': 'منو', 4 | 'Open submenu': 'بازکردن زیرمنو', 5 | 'Toggle submenu': 'سوییچ زیرمنو' 6 | }; 7 | -------------------------------------------------------------------------------- /dist/core/oncanvas/translations/index.js: -------------------------------------------------------------------------------- 1 | import { add } from '../../../_modules/i18n'; 2 | import de from './de'; 3 | import fa from './fa'; 4 | import nl from './nl'; 5 | import pt_br from './pt_br'; 6 | import ru from './ru'; 7 | import sk from './sk'; 8 | import uk from './uk'; 9 | export default function () { 10 | add(de, 'de'); 11 | add(fa, 'fa'); 12 | add(nl, 'nl'); 13 | add(pt_br, 'pt_br'); 14 | add(ru, 'ru'); 15 | add(sk, 'sk'); 16 | add(uk, 'uk'); 17 | } 18 | -------------------------------------------------------------------------------- /dist/core/oncanvas/translations/nl.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'Close submenu': 'Submenu sluiten', 3 | 'Menu': 'Menu', 4 | 'Open submenu': 'Submenu openen', 5 | 'Toggle submenu': 'Submenu wisselen' 6 | }; 7 | -------------------------------------------------------------------------------- /dist/core/oncanvas/translations/pt_br.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'Close submenu': 'Fechar submenu', 3 | 'Menu': 'Menu', 4 | 'Open submenu': 'Abrir submenu', 5 | 'Toggle submenu': 'Alternar submenu' 6 | }; 7 | -------------------------------------------------------------------------------- /dist/core/oncanvas/translations/ru.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'Close submenu': 'Закрыть подменю', 3 | 'Menu': 'Меню', 4 | 'Open submenu': 'Открыть подменю', 5 | 'Toggle submenu': 'Переключить подменю' 6 | }; 7 | -------------------------------------------------------------------------------- /dist/core/oncanvas/translations/sk.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'Close submenu': 'Zatvoriť submenu', 3 | 'Menu': 'Menu', 4 | 'Open submenu': 'Otvoriť submenu', 5 | 'Toggle submenu': 'Prepnúť submenu' 6 | }; 7 | -------------------------------------------------------------------------------- /dist/core/oncanvas/translations/uk.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'Close submenu': 'Закрити підменю', 3 | 'Menu': 'Меню', 4 | 'Open submenu': 'Відкрити підменю', 5 | 'Toggle submenu': 'Перемкнути підменю' 6 | }; 7 | -------------------------------------------------------------------------------- /dist/core/scrollbugfix/mmenu.scrollbugfix.js: -------------------------------------------------------------------------------- 1 | import OPTIONS from './options'; 2 | import * as DOM from '../../_modules/dom'; 3 | import * as support from '../../_modules/support'; 4 | import { extend, touchDirection } from '../../_modules/helpers'; 5 | export default function () { 6 | // The scrollBugFix add-on fixes a scrolling bug 7 | // 1) on touch devices 8 | // 2) in an off-canvas menu 9 | if (!support.touch || // 1 10 | !this.opts.offCanvas.use // 2 11 | ) { 12 | return; 13 | } 14 | this.opts.scrollBugFix = this.opts.scrollBugFix || {}; 15 | // Extend options. 16 | const options = extend(this.opts.scrollBugFix, OPTIONS); 17 | if (!options.fix) { 18 | return; 19 | } 20 | /** The touch-direction instance. */ 21 | const touchDir = touchDirection(this.node.menu); 22 | // Prevent the page from scrolling when scrolling in the menu. 23 | this.node.menu.addEventListener('scroll', evnt => { 24 | evnt.preventDefault(); 25 | evnt.stopPropagation(); 26 | }, { 27 | // Make sure to tell the browser the event will be prevented. 28 | passive: false, 29 | }); 30 | // Prevent the page from scrolling when dragging in the menu. 31 | this.node.menu.addEventListener('touchmove', evnt => { 32 | let wrapper = evnt.target.closest('.mm-panel, .mm-iconbar__top, .mm-iconbar__bottom'); 33 | if (wrapper && wrapper.closest('.mm-listitem--vertical')) { 34 | wrapper = DOM.parents(wrapper, '.mm-panel').pop(); 35 | } 36 | if (wrapper) { 37 | // When dragging a non-scrollable panel/iconbar, 38 | // we can simply stopPropagation. 39 | if (wrapper.scrollHeight === wrapper.offsetHeight) { 40 | evnt.stopPropagation(); 41 | } 42 | // When dragging a scrollable panel/iconbar, 43 | // that is fully scrolled up (or down). 44 | // It will not trigger the scroll event when dragging down (or up) (because you can't scroll up (or down)), 45 | // so we need to match the dragging direction with the scroll position before preventDefault and stopPropagation, 46 | // otherwise the panel would not scroll at all in any direction. 47 | else if ( 48 | // When scrolled up and dragging down 49 | (wrapper.scrollTop == 0 && touchDir.get() == 'down') || 50 | // When scrolled down and dragging up 51 | (wrapper.scrollHeight == 52 | wrapper.scrollTop + wrapper.offsetHeight && 53 | touchDir.get() == 'up')) { 54 | evnt.stopPropagation(); 55 | } 56 | // When dragging anything other than a panel/iconbar. 57 | } 58 | else { 59 | evnt.stopPropagation(); 60 | } 61 | }, { 62 | // Make sure to tell the browser the event can be prevented. 63 | passive: false, 64 | }); 65 | // Some small additional improvements 66 | // Scroll the current opened panel to the top when opening the menu. 67 | this.bind('open:after', () => { 68 | var panel = DOM.children(this.node.pnls, '.mm-panel--opened')[0]; 69 | if (panel) { 70 | panel.scrollTop = 0; 71 | } 72 | }); 73 | // Fix issue after device rotation change. 74 | window.addEventListener('orientationchange', (evnt) => { 75 | var panel = DOM.children(this.node.pnls, '.mm-panel--opened')[0]; 76 | if (panel) { 77 | panel.scrollTop = 0; 78 | // Apparently, changing the overflow-scrolling property triggers some event :) 79 | panel.style['-webkit-overflow-scrolling'] = 'auto'; 80 | panel.style['-webkit-overflow-scrolling'] = 'touch'; 81 | } 82 | }); 83 | } 84 | -------------------------------------------------------------------------------- /dist/core/scrollbugfix/options.js: -------------------------------------------------------------------------------- 1 | const options = { 2 | fix: true 3 | }; 4 | export default options; 5 | -------------------------------------------------------------------------------- /dist/core/theme/mmenu.theme.css: -------------------------------------------------------------------------------- 1 | .mm-menu--theme-light{--mm-color-background:#f3f3f3;--mm-color-border:rgb(0 0 0 / 0.15);--mm-color-icon:rgb(0 0 0 / 0.4);--mm-color-text:rgb(0 0 0 / 0.8);--mm-color-text-dimmed:rgb(0 0 0 / 0.4);--mm-color-background-highlight:rgb(0 0 0 / 0.05);--mm-color-background-emphasis:rgb(255 255 255 / 0.75);--mm-color-focusring:#06c}.mm-menu--theme-light-contrast{--mm-color-background:#f3f3f3;--mm-color-border:rgb(0 0 0 / 0.5);--mm-color-icon:rgb(0 0 0 / 0.5);--mm-color-text:#000;--mm-color-text-dimmed:rgb(0 0 0 / 0.7);--mm-color-background-highlight:rgb(0 0 0 / 0.05);--mm-color-background-emphasis:rgb(255 255 255 / 0.9);--mm-color-focusring:#06c}.mm-menu--theme-dark{--mm-color-background:#333;--mm-color-border:rgb(0, 0, 0, 0.4);--mm-color-icon:rgb(255, 255, 255, 0.4);--mm-color-text:rgb(255, 255, 255, 0.8);--mm-color-text-dimmed:rgb(255, 255, 255, 0.4);--mm-color-background-highlight:rgb(255, 255, 255, 0.08);--mm-color-background-emphasis:rgb(0, 0, 0, 0.1);--mm-color-focusring:#06c}.mm-menu--theme-dark-contrast{--mm-color-background:#333;--mm-color-border:rgb(255 255 255 / 0.5);--mm-color-icon:rgb(255 255 255 / 0.5);--mm-color-text:#fff;--mm-color-text-dimmed:rgb(255 255 255 / 0.7);--mm-color-background-highlight:rgb(255 255 255 / 0.1);--mm-color-background-emphasis:rgb(0 0 0 / 0.3);--mm-color-focusring:#06c}.mm-menu--theme-white{--mm-color-background:#fff;--mm-color-border:rgb(0 0 0 / 0.15);--mm-color-icon:rgb(0 0 0 / 0.3);--mm-color-text:rgb(0 0 0 / 0.8);--mm-color-text-dimmed:rgb(0 0 0 / 0.3);--mm-color-background-highlight:rgb(0 0 0 / 0.06);--mm-color-background-emphasis:rgb(0 0 0 / 0.03);--mm-color-focusring:#06c}.mm-menu--theme-white-contrast{--mm-color-background:#fff;--mm-color-border:rgb(0 0 0 / 0.5);--mm-color-icon:rgb(0 0 0 / 0.5);--mm-color-text:#000;--mm-color-text-dimmed:rgb(0 0 0 / 0.7);--mm-color-background-highlight:rgb(0 0 0 / 0.07);--mm-color-background-emphasis:rgb(0 0 0 / 0.035);--mm-color-focusring:#06c}.mm-menu--theme-black{--mm-color-background:#000;--mm-color-border:rgb(255 255 255 / 0.2);--mm-color-icon:rgb(255 255 255 / 0.4);--mm-color-text:rgb(255 255 255 / 0.7);--mm-color-text-dimmed:rgb(255 255 255 / 0.4);--mm-color-background-highlight:rgb(255 255 255 / 0.1);--mm-color-background-emphasis:rgb(255 255 255 / 0.06);--mm-color-focusring:#06c}.mm-menu--theme-black-contrast{--mm-color-background:#000;--mm-color-border:rgb(255 255 255 / 0.5);--mm-color-icon:rgb(255 255 255 / 0.5);--mm-color-text:#fff;--mm-color-text-dimmed:rgb(255 255 255 / 0.6);--mm-color-background-highlight:rgb(255 255 255 / 0.125);--mm-color-background-emphasis:rgb(255 255 255 / 0.1);--mm-color-focusring:#06c} -------------------------------------------------------------------------------- /dist/core/theme/mmenu.theme.js: -------------------------------------------------------------------------------- 1 | import Mmenu from '../../core/oncanvas/mmenu.oncanvas'; 2 | import OPTIONS from './options'; 3 | /** A list of available themes. */ 4 | const possibleThemes = [ 5 | 'light', 6 | 'dark', 7 | 'white', 8 | 'black', 9 | 'light-contrast', 10 | 'dark-contrast', 11 | 'white-contrast', 12 | 'black-contrast' 13 | ]; 14 | export default function () { 15 | this.opts.theme = this.opts.theme || OPTIONS; 16 | const theme = this.opts.theme; 17 | if (!possibleThemes.includes(theme)) { 18 | this.opts.theme = possibleThemes[0]; 19 | } 20 | this._api.push('theme'); 21 | this.bind('initMenu:after', () => { 22 | this.theme(this.opts.theme); 23 | }); 24 | } 25 | /** 26 | * Get or set the theme for the menu. 27 | * 28 | * @param {string} [position] The theme for the menu. 29 | */ 30 | Mmenu.prototype.theme = function (theme = null) { 31 | /** The previously used theme */ 32 | const oldTheme = this.opts.theme; 33 | if (theme) { 34 | if (possibleThemes.includes(theme)) { 35 | this.node.menu.classList.remove(`mm-menu--theme-${oldTheme}`); 36 | this.node.menu.classList.add(`mm-menu--theme-${theme}`); 37 | this.opts.theme = theme; 38 | } 39 | } 40 | else { 41 | return oldTheme; 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /dist/core/theme/options.js: -------------------------------------------------------------------------------- 1 | const options = 'light'; 2 | export default options; 3 | -------------------------------------------------------------------------------- /gulp/css.js: -------------------------------------------------------------------------------- 1 | const { src, dest, watch, series } = require('gulp'); 2 | 3 | const sass = require('gulp-sass')(require('sass')); 4 | const autoprefixer = require('gulp-autoprefixer'); 5 | const cleancss = require('gulp-clean-css'); 6 | 7 | const dirs = { 8 | input: 'src', 9 | output: 'dist' 10 | }; 11 | 12 | /** Run all scripts. */ 13 | exports.all = CSSall = () => { 14 | return src(dirs.input + '/**/*.scss') 15 | .pipe(sass().on('error', sass.logError)) 16 | .pipe(autoprefixer(['> 5%', 'last 5 versions'])) 17 | .pipe(cleancss()) 18 | .pipe(dest(dirs.output)); 19 | }; 20 | 21 | /** Put a watch on all files. */ 22 | exports.watch = CSSwatch = cb => { 23 | return watch([dirs.input + '/**/*.scss']) 24 | .on('change', path => { 25 | console.log('Change detected to .scss file "' + path + '"'); 26 | series(CSSall)(() => { 27 | console.log('CSS compiled and concatenated.'); 28 | }); 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /gulp/js.js: -------------------------------------------------------------------------------- 1 | const { src, dest, watch, series } = require('gulp'); 2 | 3 | const typescript = require('gulp-typescript'); 4 | const webpack = require('webpack-stream'); 5 | 6 | const dirs = { 7 | input: 'src', 8 | output: 'dist' 9 | }; 10 | 11 | /** Run all scripts. */ 12 | exports.all = JSall = (cb) => { 13 | return series(JStranspile, JSpack)(cb); 14 | }; 15 | 16 | /** Put a watch on all files. */ 17 | exports.watch = JSwatch = (cb) => { 18 | return watch(dirs.input + '/**/*.ts', { 19 | ignored: [ 20 | dirs.input + '/**/*.d.ts', // Exclude all typings. 21 | ], 22 | }).on('change', (path) => { 23 | console.log('Change detected to .ts file "' + path + '"'); 24 | 25 | // Changing any file only affects the files in the same directory: 26 | // - transpile only the directory to js; 27 | // - pack all. 28 | var files = path.split('/'); 29 | files.pop(); 30 | files.shift(); 31 | files = files.join('/'); 32 | 33 | var JStranspileOne = (cb) => JStranspile(cb, 34 | dirs.input + '/' + files + '/*.ts', 35 | dirs.output + '/' + files 36 | ); 37 | 38 | series(JStranspileOne, JSpack)(() => { 39 | console.log('JS transpiled and concatenated.'); 40 | }); 41 | }); 42 | }; 43 | 44 | // Transpile the speicfied TS files (defaults to all TS files) to JS. 45 | const JStranspile = (cb, input, output) => { 46 | return src([ 47 | dirs.input + '/**/*.d.ts', // Include all typings. 48 | input || (dirs.input + '/**/*.ts'), // Include the needed ts files. 49 | ]) 50 | .pipe( 51 | typescript({ 52 | target: 'es2016', 53 | module: 'es6', 54 | moduleResolution: 'node', 55 | resolveJsonModule: true, 56 | }) 57 | ) 58 | .pipe(dest(output || dirs.output)); 59 | }; 60 | 61 | // Pack the files. 62 | const JSpack = () => { 63 | return src(dirs.input + '/mmenu.js') 64 | .pipe( 65 | webpack({ 66 | // mode: 'development', 67 | mode: 'production', 68 | output: { 69 | filename: 'mmenu.js', 70 | }, 71 | // optimization: { 72 | // minimize: false 73 | // } 74 | }) 75 | ) 76 | .pipe(dest(dirs.output)); 77 | }; 78 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /* 2 | Tasks: 3 | 4 | $ gulp : Runs the "js" and "css" tasks. 5 | $ gulp js : Runs the "js" tasks. 6 | $ gulp css : Runs the "css" tasks. 7 | $ gulp watch : Starts a watch on the "js" and "css" tasks. 8 | */ 9 | 10 | const { parallel, series } = require('gulp'); 11 | 12 | const js = require('./gulp/js'); 13 | const css = require('./gulp/css'); 14 | 15 | /* 16 | $ gulp 17 | */ 18 | exports.default = parallel(js.all, css.all); 19 | 20 | /* 21 | $ gulp js 22 | */ 23 | exports.js = js.all; 24 | 25 | /* 26 | $ gulp css 27 | */ 28 | exports.css = css.all; 29 | 30 | /* 31 | $ gulp watch 32 | */ 33 | exports.watch = parallel(series(js.all, js.watch), series(css.all, css.watch)); 34 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | mmenu.js, app look-alike menus with sliding submenus. 10 | 11 | 16 | 17 | 18 | 19 |
20 | 27 |
28 |
29 |

mmenu

30 |

31 | The best javascript plugin for app look-alike on- and off-canvas 32 | menus with sliding submenus for your website and web-app. 33 |

34 |

35 | Check out the example on the left or 36 | play around with the options. 39 |

40 |

41 | For the full documentation please visit: 42 | mmenujs.com 43 |

44 |

45 | There also is a 46 | WordPress plugin available. 49 |

50 |
51 | 52 | 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mmenu-js", 3 | "version": "9.3.0", 4 | "main": "dist/mmenu.js", 5 | "module": "src/mmenu.js", 6 | "author": "Fred Heusschen ", 7 | "license": "CC-BY-NC-4.0", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/FrDH/mmenu-js.git" 11 | }, 12 | "description": "The best javascript plugin for app look-alike on- and off-canvas menus with sliding submenus for your website and webapp.", 13 | "keywords": [ 14 | "app", 15 | "list", 16 | "listview", 17 | "megamenu", 18 | "menu", 19 | "mmenu", 20 | "mobile", 21 | "navigation", 22 | "off-canvas", 23 | "on-canvas", 24 | "curtain", 25 | "panels", 26 | "submenu" 27 | ], 28 | "scripts": { 29 | "build": "gulp default" 30 | }, 31 | "devDependencies": { 32 | "gulp": "^4.0.2", 33 | "gulp-autoprefixer": "^8.0.0", 34 | "gulp-clean-css": "^4.3.0", 35 | "gulp-sass": "^5.1.0", 36 | "gulp-typescript": "^5.0.1", 37 | "sass": "^1.57.1", 38 | "typescript": "^3.9.9", 39 | "webpack": "^5.75.0", 40 | "webpack-stream": "^7.0.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/_mixins.scss: -------------------------------------------------------------------------------- 1 | @use "./variables" as v; 2 | 3 | @mixin ellipsis() { 4 | text-overflow: ellipsis; 5 | white-space: nowrap; 6 | overflow: hidden; 7 | } 8 | -------------------------------------------------------------------------------- /src/_modules/eventlisteners.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Make the first letter in a word uppercase. 3 | * @param {string} word The word. 4 | */ 5 | function ucFirst(word) { 6 | if (!word) { 7 | return ''; 8 | } 9 | return word.charAt(0).toUpperCase() + word.slice(1); 10 | } 11 | 12 | /** 13 | * Bind an event listener to an element. 14 | * @param {HTMLElement} element The element to bind the event listener to. 15 | * @param {string} evnt The event to listen to. 16 | * @param {funcion} handler The function to invoke. 17 | */ 18 | export const on = ( 19 | element: HTMLElement | Window, 20 | evnt: string, 21 | handler: EventListenerOrEventListenerObject 22 | ) => { 23 | // Extract the event name and space from the event (the event can include a namespace (click.foo)). 24 | const evntParts = evnt.split('.'); 25 | evnt = 'mmEvent' + ucFirst(evntParts[0]) + ucFirst(evntParts[1]); 26 | 27 | element[evnt] = element[evnt] || []; 28 | element[evnt].push(handler); 29 | element.addEventListener(evntParts[0], handler); 30 | }; 31 | 32 | /** 33 | * Remove an event listener from an element. 34 | * @param {HTMLElement} element The element to remove the event listeners from. 35 | * @param {string} evnt The event to remove. 36 | */ 37 | export const off = (element: HTMLElement | Window, evnt: string) => { 38 | // Extract the event name and space from the event (the event can include a namespace (click.foo)). 39 | const evntParts = evnt.split('.'); 40 | evnt = 'mmEvent' + ucFirst(evntParts[0]) + ucFirst(evntParts[1]); 41 | 42 | (element[evnt] || []).forEach((handler) => { 43 | element.removeEventListener(evntParts[0], handler); 44 | }); 45 | }; 46 | 47 | /** 48 | * Trigger the bound event listeners on an element. 49 | * @param {HTMLElement} element The element of which to trigger the event listeners from. 50 | * @param {string} evnt The event to trigger. 51 | * @param {object} [options] Options to pass to the handler. 52 | */ 53 | export const trigger = ( 54 | element: HTMLElement | Window, 55 | evnt: string, 56 | options?: mmLooseObject 57 | ) => { 58 | const evntParts = evnt.split('.'); 59 | evnt = 'mmEvent' + ucFirst(evntParts[0]) + ucFirst(evntParts[1]); 60 | 61 | (element[evnt] || []).forEach((handler) => { 62 | handler(options || {}); 63 | }); 64 | }; 65 | -------------------------------------------------------------------------------- /src/_modules/helpers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Deep extend an object with the given defaults. 3 | * Note that the extended object is not a clone, meaning the original object will also be updated. 4 | * 5 | * @param {object} orignl The object to extend to. 6 | * @param {object} dfault The object to extend from. 7 | * @return {object} The extended "orignl" object. 8 | */ 9 | export const extend = (orignl: mmLooseObject, dfault: mmLooseObject) => { 10 | if (type(orignl) != 'object') { 11 | orignl = {}; 12 | } 13 | if (type(dfault) != 'object') { 14 | dfault = {}; 15 | } 16 | 17 | for (let k in dfault) { 18 | if (!dfault.hasOwnProperty(k)) { 19 | continue; 20 | } 21 | 22 | if (typeof orignl[k] == 'undefined') { 23 | orignl[k] = dfault[k]; 24 | } else if (type(orignl[k]) == 'object') { 25 | extend(orignl[k], dfault[k]); 26 | } 27 | } 28 | return orignl; 29 | }; 30 | 31 | /** 32 | * Detect the touch / dragging direction on a touch device. 33 | * 34 | * @param {HTMLElement} surface The element to monitor for touch events. 35 | * @return {object} Object with "get" function. 36 | */ 37 | export const touchDirection = (surface) => { 38 | let direction = ''; 39 | let prevPosition = null; 40 | 41 | surface.addEventListener('touchstart', (evnt) => { 42 | if (evnt.touches.length === 1) { 43 | direction = ''; 44 | prevPosition = evnt.touches[0].pageY; 45 | } 46 | }); 47 | 48 | surface.addEventListener('touchend', (evnt) => { 49 | if (evnt.touches.length === 0) { 50 | direction = ''; 51 | prevPosition = null; 52 | } 53 | }); 54 | 55 | surface.addEventListener('touchmove', (evnt) => { 56 | direction = ''; 57 | 58 | if (prevPosition && 59 | evnt.touches.length === 1 60 | ) { 61 | const currentPosition = evnt.changedTouches[0].pageY; 62 | if (currentPosition > prevPosition) { 63 | direction = 'down'; 64 | } else if (currentPosition < prevPosition) { 65 | direction = 'up'; 66 | } 67 | prevPosition = currentPosition; 68 | } 69 | }); 70 | 71 | return { 72 | get: () => direction, 73 | }; 74 | }; 75 | 76 | /** 77 | * Get the type of any given variable. Improvement of "typeof". 78 | * 79 | * @param {any} variable The variable. 80 | * @return {string} The type of the variable in lowercase. 81 | */ 82 | export const type = (variable: any): string => { 83 | return {}.toString 84 | .call(variable) 85 | .match(/\s([a-zA-Z]+)/)[1] 86 | .toLowerCase(); 87 | }; 88 | 89 | /** 90 | * Get a (page wide) unique ID. 91 | */ 92 | export const uniqueId = () => { 93 | return `mm-${__id++}`; 94 | }; 95 | let __id = 0; 96 | 97 | /** 98 | * Get a prefixed ID from a possibly orifinal ID. 99 | * @param id The possibly original ID. 100 | */ 101 | export const cloneId = (id) => { 102 | if (id.slice(0, 9) == 'mm-clone-') { 103 | return id; 104 | } 105 | return `mm-clone-${id}`; 106 | }; 107 | 108 | /** 109 | * Get the original ID from a possibly prefixed ID. 110 | * @param id The possibly prefixed ID. 111 | */ 112 | export const originalId = (id) => { 113 | if (id.slice(0, 9) == 'mm-clone-') { 114 | return id.slice(9); 115 | } 116 | return id; 117 | }; 118 | -------------------------------------------------------------------------------- /src/_modules/i18n.ts: -------------------------------------------------------------------------------- 1 | import { extend } from './helpers'; 2 | const translations = {}; 3 | 4 | 5 | /** 6 | * Show all translations. 7 | * @return {object} The translations. 8 | */ 9 | export const show = (): {} => { 10 | return translations; 11 | }; 12 | 13 | /** 14 | * Add translations to a language. 15 | * @param {object} text Object of key/value translations. 16 | * @param {string} language The translated language. 17 | */ 18 | export const add = (text: object, language: string) => { 19 | if (typeof translations[language] === 'undefined') { 20 | translations[language] = {}; 21 | } 22 | extend(translations[language], text as object); 23 | } 24 | 25 | /** 26 | * Find a translated text in a language. 27 | * @param {string} text The text to find the translation for. 28 | * @param {string} language The language to search in. 29 | * @return {string} The translated text. 30 | */ 31 | export const get = (text: string, language?: string): string => { 32 | if ( 33 | typeof language === 'string' && 34 | typeof translations[language] !== 'undefined' 35 | ) { 36 | return translations[language][text] || text; 37 | } 38 | return text; 39 | }; 40 | -------------------------------------------------------------------------------- /src/_modules/matchmedia.ts: -------------------------------------------------------------------------------- 1 | /** Collection of callback functions for media querys. */ 2 | let listeners = {}; 3 | 4 | /** 5 | * Bind functions to a matchMedia listener (subscriber). 6 | * 7 | * @param {string|number} query Media query to match or number for min-width. 8 | * @param {function} yes Function to invoke when the media query matches. 9 | * @param {function} no Function to invoke when the media query doesn't match. 10 | */ 11 | export const add = (query: string | number, yes: Function, no: Function) => { 12 | if (typeof query == 'number') { 13 | query = '(min-width: ' + query + 'px)'; 14 | } 15 | listeners[query] = listeners[query] || []; 16 | listeners[query].push({ yes, no }); 17 | }; 18 | 19 | /** 20 | * Initialize the matchMedia listener. 21 | */ 22 | export const watch = () => { 23 | for (let query in listeners) { 24 | let mqlist = window.matchMedia(query); 25 | 26 | fire(query, mqlist); 27 | mqlist.onchange = (evnt) => { 28 | fire(query, mqlist); 29 | }; 30 | } 31 | }; 32 | 33 | /** 34 | * Invoke the "yes" or "no" function for a matchMedia listener (publisher). 35 | * 36 | * @param {string} query Media query to check for. 37 | * @param {MediaQueryList} mqlist Media query list to check with. 38 | */ 39 | export const fire = (query: string, mqlist: MediaQueryList) => { 40 | var fn = mqlist.matches ? 'yes' : 'no'; 41 | for (let m = 0; m < listeners[query].length; m++) { 42 | listeners[query][m][fn](); 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /src/_modules/support.ts: -------------------------------------------------------------------------------- 1 | /** Whether or not touch gestures are supported by the browser. */ 2 | export const touch = 3 | 'ontouchstart' in window || 4 | (navigator.msMaxTouchPoints ? true : false) || 5 | false; 6 | -------------------------------------------------------------------------------- /src/_variables.scss: -------------------------------------------------------------------------------- 1 | // Animations 2 | $transDr: 0.4s !default; 3 | $transFn: ease !default; 4 | 5 | // Sizes 6 | $btnSize: 50px !default; 7 | $listitemIndent: 20px !default; 8 | $panelPadding: 20px !default; 9 | -------------------------------------------------------------------------------- /src/addons/backbutton/mmenu.backbutton.ts: -------------------------------------------------------------------------------- 1 | import Mmenu from '../../core/oncanvas/mmenu.oncanvas'; 2 | import OPTIONS from './options'; 3 | 4 | import * as DOM from '../../_modules/dom'; 5 | import { extend } from '../../_modules/helpers'; 6 | 7 | export default function (this: Mmenu) { 8 | this.opts.backButton = this.opts.backButton || {}; 9 | 10 | if (!this.opts.offCanvas.use) { 11 | return; 12 | } 13 | 14 | // Extend options. 15 | const options = extend(this.opts.backButton, OPTIONS); 16 | 17 | const _menu = `#${this.node.menu.id}`; 18 | 19 | // Close menu 20 | if (options.close) { 21 | let states = []; 22 | 23 | const setStates = () => { 24 | states = [_menu]; 25 | DOM.children( 26 | this.node.pnls, 27 | '.mm-panel--opened, .mm-panel--parent' 28 | ).forEach((panel) => { 29 | states.push('#' + panel.id); 30 | }); 31 | }; 32 | 33 | this.bind('open:after', () => { 34 | history.pushState(null, '', location.pathname + location.search + _menu); 35 | }); 36 | this.bind('open:after', setStates); 37 | this.bind('openPanel:after', setStates); 38 | this.bind('close:after', () => { 39 | states = []; 40 | history.back(); 41 | history.pushState( 42 | null, 43 | '', 44 | location.pathname + location.search 45 | ); 46 | }); 47 | 48 | window.addEventListener('popstate', () => { 49 | if (this.node.menu.matches('.mm-menu--opened')) { 50 | if (states.length) { 51 | states = states.slice(0, -1); 52 | const hash = states[states.length - 1]; 53 | 54 | if (hash == _menu) { 55 | this.close(); 56 | } else { 57 | this.openPanel(this.node.menu.querySelector(hash)); 58 | history.pushState(null, '', location.pathname + location.search + _menu); 59 | } 60 | } 61 | } 62 | }); 63 | } 64 | 65 | if (options.open) { 66 | window.addEventListener('popstate', (evnt) => { 67 | if (!this.node.menu.matches('.mm-menu--opened') && location.hash == _menu) { 68 | this.open(); 69 | } 70 | }); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/addons/backbutton/options.ts: -------------------------------------------------------------------------------- 1 | const options : mmOptionsBackbutton = { 2 | close: false, 3 | open: false 4 | }; 5 | 6 | export default options; 7 | -------------------------------------------------------------------------------- /src/addons/backbutton/typings.d.ts: -------------------------------------------------------------------------------- 1 | /** Options for the backButton add-on. */ 2 | interface mmOptionsBackbutton { 3 | 4 | /** Whether or not to close the menu with the back-( and forth-)button. */ 5 | close ?: boolean 6 | 7 | /** Whether or not to open the menu with the back-( and forth-)button. */ 8 | open ?: boolean 9 | } 10 | -------------------------------------------------------------------------------- /src/addons/counters/mmenu.counters.scss: -------------------------------------------------------------------------------- 1 | $mm_module: ".mm-counter"; 2 | 3 | #{$mm_module} { 4 | display: block; 5 | padding-inline-start: 20px; // left, right for RTL 6 | float: right; 7 | color: var(--mm-color-text-dimmed); 8 | 9 | [dir="rtl"] & { 10 | float: left; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/addons/counters/mmenu.counters.ts: -------------------------------------------------------------------------------- 1 | import Mmenu from '../../core/oncanvas/mmenu.oncanvas'; 2 | import OPTIONS from './options'; 3 | import * as DOM from '../../_modules/dom'; 4 | import { extend } from '../../_modules/helpers'; 5 | 6 | export default function (this: Mmenu) { 7 | this.opts.counters = this.opts.counters || {}; 8 | 9 | // Extend options. 10 | const options = extend(this.opts.counters, OPTIONS); 11 | 12 | if (!options.add) { 13 | return; 14 | } 15 | 16 | /** 17 | * Counting the visible listitems and setting it to the counter element. 18 | * @param {HTMLElement} panel Panel to count LIs in. 19 | */ 20 | const count = (panel: HTMLElement) => { 21 | 22 | /** Parent panel for the mutated listitem. */ 23 | const parent = this.node.pnls.querySelector(`#${panel.dataset.mmParent}`); 24 | 25 | if (!parent) { 26 | return; 27 | } 28 | 29 | /** The counter for the listitem. */ 30 | const counter = parent.querySelector('.mm-counter'); 31 | if (!counter) { 32 | return; 33 | } 34 | 35 | /** The listitems */ 36 | const listitems: HTMLElement[] = []; 37 | DOM.children(panel, '.mm-listview').forEach((listview) => { 38 | listitems.push(...DOM.children(listview, '.mm-listitem')); 39 | }); 40 | 41 | counter.innerHTML = DOM.filterLI(listitems).length.toString(); 42 | }; 43 | 44 | /** Mutation observer the the listitems. */ 45 | const listitemObserver = new MutationObserver((mutationsList) => { 46 | mutationsList.forEach((mutation) => { 47 | if (mutation.attributeName == 'class') { 48 | count((mutation.target as HTMLLIElement).closest('.mm-panel')); 49 | } 50 | }); 51 | }); 52 | 53 | // Add the counters after a listview is initiated. 54 | this.bind('initListview:after', (listview: HTMLUListElement) => { 55 | 56 | /** The panel where the listview is in. */ 57 | const panel: HTMLDivElement = listview.closest('.mm-panel'); 58 | 59 | /** The parent LI for the panel */ 60 | const parent: HTMLLIElement = this.node.pnls.querySelector(`#${panel.dataset.mmParent}`); 61 | 62 | if (!parent) { 63 | return; 64 | } 65 | 66 | /** The button inside the parent LI */ 67 | const button = DOM.children(parent, '.mm-btn')[0]; 68 | 69 | if (!button) { 70 | return; 71 | } 72 | 73 | // Check if no counter already excists. 74 | if (!DOM.children(button, '.mm-counter').length) { 75 | /** The counter for the listitem. */ 76 | const counter = DOM.create('span.mm-counter'); 77 | counter.setAttribute('aria-hidden', 'true'); 78 | 79 | button.prepend(counter); 80 | } 81 | 82 | // Count immediately. 83 | count(panel); 84 | }); 85 | 86 | // Count when LI classname changes. 87 | this.bind('initListitem:after', (listitem: HTMLLIElement) => { 88 | 89 | /** The panel where the listitem is in. */ 90 | const panel: HTMLDivElement = listitem.closest('.mm-panel'); 91 | if (!panel) { 92 | return; 93 | } 94 | 95 | /** The parent LI for the panel. */ 96 | const parent: HTMLLIElement = this.node.pnls.querySelector(`#${panel.dataset.mmParent}`); 97 | if (!parent) { 98 | return; 99 | } 100 | 101 | listitemObserver.observe(listitem, { 102 | attributes: true 103 | }); 104 | }); 105 | } 106 | -------------------------------------------------------------------------------- /src/addons/counters/options.ts: -------------------------------------------------------------------------------- 1 | const options: mmOptionsCounters = { 2 | add: false 3 | }; 4 | 5 | export default options; 6 | -------------------------------------------------------------------------------- /src/addons/counters/typings.d.ts: -------------------------------------------------------------------------------- 1 | /** Options for the counters add-on. */ 2 | interface mmOptionsCounters { 3 | 4 | /** Whether or not to automatically append a counter to each menu item that has a submenu. */ 5 | add?: boolean 6 | 7 | /** Where to add the counters. */ 8 | addTo?: string 9 | 10 | /** Whether or not to automatically count the number of items in the submenu. */ 11 | count?: boolean 12 | } 13 | -------------------------------------------------------------------------------- /src/addons/iconbar/mmenu.iconbar.scss: -------------------------------------------------------------------------------- 1 | $mm_module: ".mm-iconbar"; 2 | 3 | :root { 4 | --mm-iconbar-size: 50px; 5 | } 6 | 7 | .mm-menu--iconbar { 8 | &-left { 9 | .mm-panels, 10 | .mm-navbars { 11 | margin-left: var(--mm-iconbar-size); 12 | } 13 | } 14 | 15 | &-right { 16 | .mm-panels, 17 | .mm-navbars { 18 | margin-right: var(--mm-iconbar-size); 19 | } 20 | } 21 | } 22 | 23 | #{$mm_module} { 24 | display: none; 25 | 26 | .mm-menu--iconbar-left &, 27 | .mm-menu--iconbar-right & { 28 | display: flex; 29 | flex-direction: column; 30 | justify-content: space-between; 31 | } 32 | 33 | .mm-menu--iconbar-left & { 34 | border-right-width: 1px; 35 | left: 0; 36 | } 37 | 38 | .mm-menu--iconbar-right & { 39 | border-left-width: 1px; 40 | right: 0; 41 | } 42 | 43 | position: absolute; 44 | top: 0; 45 | bottom: 0; 46 | z-index: 2; 47 | 48 | width: var(--mm-iconbar-size); 49 | overflow: hidden; 50 | box-sizing: border-box; 51 | 52 | border: 0 solid; 53 | border-color: var(--mm-color-border); 54 | background: var(--mm-color-background); 55 | color: var(--mm-color-text-dimmed); 56 | text-align: center; 57 | } 58 | 59 | #{$mm_module}__top, 60 | #{$mm_module}__bottom { 61 | width: 100%; 62 | 63 | -webkit-overflow-scrolling: touch; 64 | overflow: hidden; 65 | overflow-y: auto; 66 | overscroll-behavior: contain; 67 | 68 | > * { 69 | box-sizing: border-box; 70 | display: block; 71 | padding: calc((var(--mm-iconbar-size) - var(--mm-lineheight)) / 2) 0; 72 | } 73 | 74 | a, 75 | a:hover { 76 | text-decoration: none; 77 | } 78 | } 79 | 80 | #{$mm_module}__tab--selected { 81 | background: var(--mm-color-background-emphasis); 82 | } 83 | -------------------------------------------------------------------------------- /src/addons/iconbar/mmenu.iconbar.ts: -------------------------------------------------------------------------------- 1 | import Mmenu from '../../core/oncanvas/mmenu.oncanvas'; 2 | import OPTIONS from './options'; 3 | import * as DOM from '../../_modules/dom'; 4 | import * as media from '../../_modules/matchmedia'; 5 | import { type, extend } from '../../_modules/helpers'; 6 | 7 | 8 | export default function (this: Mmenu) { 9 | this.opts.iconbar = this.opts.iconbar || {}; 10 | 11 | // Extend options. 12 | const options = extend(this.opts.iconbar, OPTIONS); 13 | 14 | if (!options.use) { 15 | return; 16 | } 17 | 18 | let iconbar: HTMLElement; 19 | 20 | ['top', 'bottom'].forEach((position, n) => { 21 | let ctnt = options[position]; 22 | 23 | // Extend shorthand options 24 | if (type(ctnt) != 'array') { 25 | ctnt = [ctnt]; 26 | } 27 | 28 | // Create node 29 | const part = DOM.create('div.mm-iconbar__' + position); 30 | 31 | // Add content 32 | for (let c = 0, l = ctnt.length; c < l; c++) { 33 | if (typeof ctnt[c] == 'string') { 34 | part.innerHTML += ctnt[c]; 35 | } else { 36 | part.append(ctnt[c]); 37 | } 38 | } 39 | 40 | if (part.children.length) { 41 | if (!iconbar) { 42 | iconbar = DOM.create('div.mm-iconbar'); 43 | } 44 | iconbar.append(part); 45 | } 46 | }); 47 | 48 | // Add to menu 49 | if (iconbar) { 50 | // Add the iconbar. 51 | this.bind('initMenu:after', () => { 52 | this.node.menu.prepend(iconbar); 53 | }); 54 | 55 | // En-/disable the iconbar. 56 | let classname = 'mm-menu--iconbar-' + options.position; 57 | let enable = () => { 58 | this.node.menu.classList.add(classname); 59 | }; 60 | let disable = () => { 61 | this.node.menu.classList.remove(classname); 62 | }; 63 | 64 | if (typeof options.use == 'boolean') { 65 | this.bind('initMenu:after', enable); 66 | } else { 67 | media.add(options.use, enable, disable); 68 | } 69 | 70 | // Tabs 71 | if (options.type == 'tabs') { 72 | iconbar.classList.add('mm-iconbar--tabs'); 73 | iconbar.addEventListener('click', (evnt) => { 74 | const anchor = (evnt.target as HTMLElement).closest('.mm-iconbar__tab'); 75 | 76 | if (!anchor) { 77 | return; 78 | } 79 | 80 | if (anchor.matches('.mm-iconbar__tab--selected')) { 81 | evnt.stopImmediatePropagation(); 82 | return; 83 | } 84 | 85 | try { 86 | const panel = DOM.find(this.node.menu, `${anchor.getAttribute('href')}.mm-panel`)[0]; 87 | if (panel) { 88 | evnt.preventDefault(); 89 | evnt.stopImmediatePropagation(); 90 | 91 | this.openPanel(panel, false); 92 | } 93 | } catch (err) { } 94 | }); 95 | 96 | const selectTab = (panel: HTMLElement) => { 97 | DOM.find(iconbar, 'a').forEach((anchor) => { 98 | anchor.classList.remove('mm-iconbar__tab--selected'); 99 | }); 100 | 101 | const anchor = DOM.find( 102 | iconbar, 103 | '[href="#' + panel.id + '"]' 104 | )[0]; 105 | if (anchor) { 106 | anchor.classList.add('mm-iconbar__tab--selected'); 107 | } else { 108 | const parent: HTMLElement = DOM.find(this.node.pnls, `#${panel.dataset.mmParent}`)[0]; 109 | if (parent) { 110 | selectTab(parent.closest('.mm-panel') as HTMLElement); 111 | } 112 | } 113 | }; 114 | this.bind('openPanel:before', selectTab); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/addons/iconbar/options.ts: -------------------------------------------------------------------------------- 1 | const options: mmOptionsIconbar = { 2 | use: false, 3 | top: [], 4 | bottom: [], 5 | position: 'left', 6 | type: 'default' 7 | }; 8 | 9 | export default options; 10 | -------------------------------------------------------------------------------- /src/addons/iconbar/typings.d.ts: -------------------------------------------------------------------------------- 1 | /** Options for the iconbar add-on. */ 2 | interface mmOptionsIconbar { 3 | 4 | /** Whether or not (and at what breakpoint) to add an iconbar to the menu. */ 5 | use ?: boolean | number | string, 6 | 7 | /** An array of strings (for text or HTML) or HTML elements for icons to put in the top of the iconbar. */ 8 | top ?: string[] | HTMLElement[], 9 | 10 | /** An array of strings (for text or HTML) or HTML elements for icons to put in the bottom of the iconbar. */ 11 | bottom ?: string[] | HTMLElement[], 12 | 13 | /** Where to position the iconbar in the menu. */ 14 | position ?: 'left' | 'right' 15 | 16 | /** The type of iconbar. */ 17 | type ?: 'default' | 'tabs' 18 | } 19 | -------------------------------------------------------------------------------- /src/addons/iconpanels/_options.ts: -------------------------------------------------------------------------------- 1 | const options: mmOptionsIconpanels = { 2 | add: false, 3 | blockPanel: true, 4 | visible: 3 5 | }; 6 | 7 | export default options; 8 | -------------------------------------------------------------------------------- /src/addons/iconpanels/_typings.d.ts: -------------------------------------------------------------------------------- 1 | /** Options for the iconPanels add-on. */ 2 | interface mmOptionsIconpanels { 3 | 4 | /** Whether or not a small part of parent panels should be visible. */ 5 | add?: boolean 6 | 7 | /** Whether or not to block the parent panels from interaction. */ 8 | blockPanel?: boolean 9 | 10 | /** The number of visible parent panels. */ 11 | visible?: number | 'first' 12 | } 13 | -------------------------------------------------------------------------------- /src/addons/iconpanels/mmenu.iconpanels.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --mm-iconpanel-size: 50px; 3 | } 4 | 5 | @for $i from 0 through 4 { 6 | .mm-panel--iconpanel-#{$i} { 7 | inset-inline-start: calc( 8 | #{$i} * var(--mm-iconpanel-size) 9 | ); // left, right for RTL 10 | } 11 | } 12 | 13 | .mm-panel--iconpanel-first { 14 | ~ .mm-panel { 15 | inset-inline-start: var(--mm-iconpanel-size); // left, right for RTL 16 | } 17 | } 18 | 19 | .mm-menu--iconpanel { 20 | // Hide navbars and dividers in parent panels. 21 | .mm-panel--parent { 22 | .mm-navbar, 23 | .mm-divider { 24 | opacity: 0; 25 | } 26 | } 27 | 28 | .mm-panels { 29 | > .mm-panel { 30 | &--parent { 31 | overflow-y: hidden; 32 | transform: unset; 33 | } 34 | 35 | &:not(.mm-panel--iconpanel-first):not(.mm-panel--iconpanel-0) { 36 | border-inline-start-width: 1px; // left, right for RTL 37 | border-inline-start-style: solid; // left, right for RTL 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/addons/iconpanels/mmenu.iconpanels.ts: -------------------------------------------------------------------------------- 1 | import Mmenu from '../../core/oncanvas/mmenu.oncanvas'; 2 | import OPTIONS from './_options'; 3 | import * as DOM from '../../_modules/dom'; 4 | import { extend } from '../../_modules/helpers'; 5 | 6 | export default function (this: Mmenu) { 7 | this.opts.iconPanels = this.opts.iconPanels || {}; 8 | 9 | // Extend options. 10 | const options = extend(this.opts.iconPanels, OPTIONS); 11 | 12 | let keepFirst = false; 13 | 14 | if (options.visible == 'first') { 15 | keepFirst = true; 16 | options.visible = 1; 17 | } 18 | 19 | options.visible = Math.min(3, Math.max(1, options.visible)); 20 | options.visible++; 21 | 22 | // Add the iconpanels 23 | if (options.add) { 24 | this.bind('initMenu:after', () => { 25 | this.node.menu.classList.add('mm-menu--iconpanel'); 26 | }); 27 | 28 | /** The classnames that can be set to a panel */ 29 | const classnames = [ 30 | 'mm-panel--iconpanel-0', 31 | 'mm-panel--iconpanel-1', 32 | 'mm-panel--iconpanel-2', 33 | 'mm-panel--iconpanel-3' 34 | ]; 35 | 36 | // Show only the main panel. 37 | if (keepFirst) { 38 | this.bind('initMenu:after', () => { 39 | DOM.children(this.node.pnls, '.mm-panel')[0]?.classList.add('mm-panel--iconpanel-first'); 40 | }); 41 | 42 | // Show parent panel(s). 43 | } else { 44 | 45 | this.bind('openPanel:after', (panel: HTMLElement) => { 46 | 47 | // Do nothing when opening a vertical submenu 48 | if (panel.closest('.mm-listitem--vertical')) { 49 | return; 50 | } 51 | 52 | let panels = DOM.children(this.node.pnls, '.mm-panel'); 53 | 54 | // Filter out panels that are not opened. 55 | panels = panels.filter((panel) => 56 | panel.matches('.mm-panel--parent') 57 | ); 58 | 59 | // Add the current panel to the list. 60 | panels.push(panel); 61 | 62 | // Slice the opened panels to the max visible amount. 63 | panels = panels.slice(-options.visible); 64 | 65 | // Add the "iconpanel" classnames. 66 | panels.forEach((panel, p) => { 67 | panel.classList.remove('mm-panel--iconpanel-first', ...classnames); 68 | panel.classList.add(`mm-panel--iconpanel-${p}`); 69 | }); 70 | }); 71 | } 72 | 73 | // this.bind('initPanel:after', (panel: HTMLElement) => { 74 | // if (!panel.closest('.mm-listitem--vertical') && 75 | // !DOM.children(panel, '.mm-panel__blocker')[0] 76 | // ) { 77 | // const blocker = DOM.create('div.mm-blocker.mm-panel__blocker') as HTMLElement; 78 | // panel.prepend(blocker); 79 | // } 80 | // }); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/addons/navbars/_breadcrumbs.scss: -------------------------------------------------------------------------------- 1 | @use "../../mixins" as m; 2 | @use "../../variables" as v; 3 | 4 | .mm-navbar__breadcrumbs { 5 | @include m.ellipsis; 6 | 7 | flex: 1 1 50%; 8 | display: flex; 9 | justify-content: flex-start; 10 | padding: 0 v.$panelPadding; 11 | overflow-x: auto; 12 | -webkit-overflow-scrolling: touch; 13 | 14 | > * { 15 | display: flex; 16 | align-items: center; 17 | justify-content: center; 18 | padding-inline-end: 6px; // right, left for RTL 19 | } 20 | 21 | > a { 22 | text-decoration: underline; 23 | } 24 | 25 | &:not(:last-child) { 26 | padding-inline-end: 0; // right, left for RTL 27 | } 28 | 29 | .mm-btn:not(.mm-hidden) + & { 30 | padding-inline-start: 0; // left, right for RTL 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/addons/navbars/_tabs.scss: -------------------------------------------------------------------------------- 1 | // All tabs. 2 | .mm-navbar__tab { 3 | padding: 0 10px; 4 | border: 1px solid transparent; 5 | 6 | // Selected tab. 7 | &--selected { 8 | background: var(--mm-color-background); 9 | 10 | &:not(:first-child) { 11 | border-inline-start-color: var( 12 | --mm-color-border 13 | ); // left, right for RTL 14 | } 15 | 16 | &:not(:last-child) { 17 | border-inline-end-color: var( 18 | --mm-color-border 19 | ); // right, left for RTL 20 | } 21 | } 22 | } 23 | 24 | // Navbars at the top. 25 | .mm-navbars--top { 26 | &.mm-navbars--has-tabs { 27 | border-bottom: none; 28 | 29 | // Darker backgrounds for navbars before the tabs navbar and the tabs navbar. 30 | .mm-navbar { 31 | background: var(--mm-color-background-emphasis); 32 | } 33 | 34 | // Normal backgrounds for the navbars after the tabs navbar. 35 | .mm-navbar--tabs ~ .mm-navbar { 36 | background: var(--mm-color-background); 37 | } 38 | 39 | .mm-navbar:not(.mm-navbar--tabs):last-child { 40 | border-bottom: 1px solid var(--mm-color-border); 41 | } 42 | } 43 | 44 | // Borders for the tabs. 45 | .mm-navbar__tab { 46 | border-bottom-color: var(--mm-color-border); 47 | 48 | &--selected { 49 | border-top-color: var(--mm-color-border); 50 | border-bottom-color: transparent; 51 | } 52 | } 53 | } 54 | 55 | // Navbars at the bottom. 56 | .mm-navbars--bottom { 57 | &.mm-navbar--has-tabs { 58 | border-top: none; 59 | 60 | // Normal backgrounds for navbars before the tabs navbar. 61 | .mm-navbar { 62 | background: var(--mm-color-background); 63 | } 64 | 65 | // Darker backgrounds for the tabs navbar and the navbars after it. 66 | .mm-navbar--tabs, 67 | .mm-navbar--tabs ~ .mm-navbar { 68 | background: var(--mm-color-background-emphasis); 69 | } 70 | } 71 | 72 | // Borders for the tabs. 73 | .mm-navbar__tab { 74 | border-top-color: var(--mm-color-border); 75 | 76 | &--selected { 77 | border-bottom-color: var(--mm-color-border); 78 | border-top-color: transparent; 79 | } 80 | } 81 | 82 | // Backgrounds 83 | } 84 | -------------------------------------------------------------------------------- /src/addons/navbars/configs.ts: -------------------------------------------------------------------------------- 1 | const configs : mmConfigsNavbars = { 2 | breadcrumbs: { 3 | separator: '/', 4 | removeFirst: false 5 | } 6 | }; 7 | export default configs; -------------------------------------------------------------------------------- /src/addons/navbars/mmenu.navbars.scss: -------------------------------------------------------------------------------- 1 | .mm-navbars { 2 | flex-shrink: 0; 3 | 4 | .mm-navbar { 5 | position: relative; 6 | padding-top: 0; 7 | border-bottom: none; 8 | } 9 | 10 | &--top { 11 | border-bottom: 1px solid var(--mm-color-border); 12 | 13 | // safe area inset for iPhones 14 | .mm-navbar:first-child { 15 | padding-top: env(safe-area-inset-top); 16 | } 17 | } 18 | 19 | &--bottom { 20 | border-top: 1px solid var(--mm-color-border); 21 | 22 | // safe area inset for iPhones 23 | .mm-navbar:last-child { 24 | padding-bottom: env(safe-area-inset-bottom); 25 | } 26 | } 27 | } 28 | 29 | @import "breadcrumbs", "tabs"; 30 | -------------------------------------------------------------------------------- /src/addons/navbars/navbar.breadcrumbs.ts: -------------------------------------------------------------------------------- 1 | import Mmenu from '../../core/oncanvas/mmenu.oncanvas'; 2 | import * as DOM from '../../_modules/dom'; 3 | 4 | export default function (this: Mmenu, navbar: HTMLElement) { 5 | // Add content 6 | var breadcrumbs = DOM.create('div.mm-navbar__breadcrumbs'); 7 | navbar.append(breadcrumbs); 8 | 9 | this.bind('initNavbar:after', (panel: HTMLElement) => { 10 | if (panel.querySelector('.mm-navbar__breadcrumbs')) { 11 | return; 12 | } 13 | 14 | DOM.children(panel, '.mm-navbar')[0].classList.add('mm-hidden'); 15 | 16 | var crumbs: string[] = [], 17 | breadcrumbs = DOM.create('span.mm-navbar__breadcrumbs'), 18 | current = panel, 19 | first = true; 20 | 21 | while (current) { 22 | current = current.closest('.mm-panel') as HTMLElement; 23 | 24 | if (!current.parentElement.matches('.mm-listitem--vertical')) { 25 | let title = DOM.find(current, '.mm-navbar__title span')[0]; 26 | if (title) { 27 | let text = title.textContent; 28 | if (text.length) { 29 | crumbs.unshift( 30 | first 31 | ? `${text}` 32 | : `${text}` 36 | ); 37 | } 38 | } 39 | first = false; 40 | } 41 | current = DOM.find(this.node.pnls, `#${current.dataset.mmParent}`)[0]; 42 | } 43 | 44 | if (this.conf.navbars.breadcrumbs.removeFirst) { 45 | crumbs.shift(); 46 | } 47 | 48 | breadcrumbs.innerHTML = crumbs.join( 49 | '' + 50 | this.conf.navbars.breadcrumbs.separator + 51 | '' 52 | ); 53 | DOM.children(panel, '.mm-navbar')[0].append(breadcrumbs); 54 | }); 55 | 56 | // Update for to opened panel 57 | this.bind('openPanel:before', (panel: HTMLElement) => { 58 | var crumbs = panel.querySelector('.mm-navbar__breadcrumbs'); 59 | breadcrumbs.innerHTML = crumbs ? crumbs.innerHTML : ''; 60 | }); 61 | } 62 | -------------------------------------------------------------------------------- /src/addons/navbars/navbar.close.ts: -------------------------------------------------------------------------------- 1 | import Mmenu from '../../core/oncanvas/mmenu.oncanvas'; 2 | import * as DOM from '../../_modules/dom'; 3 | 4 | export default function (this: Mmenu, navbar: HTMLElement) { 5 | /** The close button. */ 6 | const close = DOM.create('a.mm-btn.mm-btn--close.mm-navbar__btn') as HTMLAnchorElement; 7 | close.setAttribute('aria-label', this.i18n(this.conf.offCanvas.screenReader.closeMenu)); 8 | 9 | // Add the button to the navbar. 10 | navbar.append(close); 11 | 12 | // Update to target the page node. 13 | this.bind('setPage:after', (page: HTMLElement) => { 14 | close.href = `#${page.id}`; 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /src/addons/navbars/navbar.prev.ts: -------------------------------------------------------------------------------- 1 | import Mmenu from '../../core/oncanvas/mmenu.oncanvas'; 2 | import * as DOM from '../../_modules/dom'; 3 | 4 | export default function (this: Mmenu, navbar: HTMLElement) { 5 | /** The prev button. */ 6 | let prev = DOM.create('a.mm-btn.mm-hidden') as HTMLAnchorElement; 7 | 8 | // Add button to navbar. 9 | navbar.append(prev); 10 | 11 | // Hide navbar in the panel. 12 | this.bind('initNavbar:after', (panel: HTMLElement) => { 13 | DOM.children(panel, '.mm-navbar')[0].classList.add('mm-hidden'); 14 | }); 15 | 16 | // Update the button href when opening a panel. 17 | this.bind('openPanel:before', (panel: HTMLElement) => { 18 | if (panel.parentElement.matches('.mm-listitem--vertical')) { 19 | return; 20 | } 21 | 22 | prev.classList.add('mm-hidden'); 23 | 24 | /** Original button in the panel. */ 25 | const original = panel.querySelector('.mm-navbar__btn.mm-btn--prev') as HTMLAnchorElement; 26 | if (original) { 27 | 28 | /** Clone of the original button in the panel. */ 29 | const clone = original.cloneNode(true) as HTMLAnchorElement; 30 | prev.after(clone); 31 | prev.remove(); 32 | prev = clone; 33 | } 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /src/addons/navbars/navbar.searchfield.ts: -------------------------------------------------------------------------------- 1 | import Mmenu from '../../core/oncanvas/mmenu.oncanvas'; 2 | import * as DOM from '../../_modules/dom'; 3 | import { uniqueId } from '../../_modules/helpers'; 4 | 5 | export default function (this: Mmenu, navbar: HTMLElement) { 6 | 7 | /** Empty wrapper for the searchfield. */ 8 | let wrapper = DOM.create('div.mm-navbar__searchfield') as HTMLAnchorElement; 9 | wrapper.id = uniqueId(); 10 | 11 | // Add button to navbar. 12 | navbar.append(wrapper); 13 | 14 | this.opts.searchfield = this.opts.searchfield || {}; 15 | this.opts.searchfield.add = true; 16 | this.opts.searchfield.addTo = `#${wrapper.id}`; 17 | } 18 | -------------------------------------------------------------------------------- /src/addons/navbars/navbar.tabs.ts: -------------------------------------------------------------------------------- 1 | import Mmenu from '../../core/oncanvas/mmenu.oncanvas'; 2 | import * as DOM from '../../_modules/dom'; 3 | export default function (this: Mmenu, navbar: HTMLElement) { 4 | navbar.classList.add('mm-navbar--tabs'); 5 | navbar.closest('.mm-navbars').classList.add('mm-navbars--has-tabs'); 6 | 7 | DOM.children(navbar, 'a').forEach(anchor => { 8 | anchor.classList.add('mm-navbar__tab'); 9 | }); 10 | 11 | /** 12 | * Mark a tab as selected. 13 | * @param {HTMLElement} panel Opened panel. 14 | */ 15 | function selectTab(this: Mmenu, panel: HTMLElement) { 16 | /** The tab that links to the opened panel. */ 17 | const anchor = DOM.children(navbar, `.mm-navbar__tab[href="#${panel.id}"]`)[0]; 18 | 19 | if (anchor) { 20 | anchor.classList.add('mm-navbar__tab--selected'); 21 | 22 | // @ts-ignore 23 | anchor.ariaExpanded = 'true'; 24 | 25 | } else { 26 | 27 | /** The parent listitem. */ 28 | const parent = DOM.find(this.node.pnls, `#${panel.dataset.mmParent}`)[0]; 29 | if (parent) { 30 | selectTab.call(this, parent.closest('.mm-panel')); 31 | } 32 | } 33 | } 34 | 35 | this.bind('openPanel:before', (panel) => { 36 | // Remove selected class. 37 | DOM.children(navbar, 'a').forEach(anchor => { 38 | anchor.classList.remove('mm-navbar__tab--selected'); 39 | 40 | // @ts-ignore 41 | anchor.ariaExpanded = 'false'; 42 | }); 43 | 44 | selectTab.call(this, panel); 45 | }); 46 | 47 | this.bind('initPanels:after', () => { 48 | // Add animation class to panel. 49 | navbar.addEventListener('click', event => { 50 | /** The href for the clicked tab. */ 51 | const href = (event.target as HTMLElement)?.closest('.mm-navbar__tab')?.getAttribute('href'); 52 | try { 53 | DOM.find(this.node.pnls, `${href}.mm-panel`)[0]?.classList.add('mm-panel--noanimation'); 54 | } catch (err) { } 55 | }, { 56 | // useCapture to ensure the logical order. 57 | capture: true 58 | }); 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /src/addons/navbars/navbar.title.ts: -------------------------------------------------------------------------------- 1 | import Mmenu from '../../core/oncanvas/mmenu.oncanvas'; 2 | import * as DOM from '../../_modules/dom'; 3 | 4 | export default function (this: Mmenu, navbar: HTMLElement) { 5 | /** The title node in the navbar. */ 6 | let title = DOM.create('a.mm-navbar__title') as HTMLAnchorElement; 7 | 8 | // Add title to the navbar. 9 | navbar.append(title); 10 | 11 | // Update the title to the opened panel. 12 | this.bind('openPanel:before', (panel: HTMLElement) => { 13 | 14 | // Do nothing in a vertically expanding panel. 15 | if (panel.parentElement.matches('.mm-listitem--vertical')) { 16 | return; 17 | } 18 | 19 | /** Original title in the panel. */ 20 | const original = panel.querySelector('.mm-navbar__title') as HTMLAnchorElement; 21 | if (original) { 22 | 23 | /** Clone of the original title in the panel. */ 24 | const clone = original.cloneNode(true) as HTMLAnchorElement; 25 | title.after(clone); 26 | title.remove(); 27 | title = clone; 28 | } 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /src/addons/navbars/options.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Extend shorthand options. 3 | * 4 | * @param {object} options The options to extend. 5 | * @return {object} The extended options. 6 | */ 7 | export function extendShorthandOptions( 8 | options: mmOptionsNavbarsNavbar 9 | ): mmOptionsNavbarsNavbar { 10 | 11 | if (typeof options == 'boolean' && options) { 12 | options = {}; 13 | } 14 | 15 | if (typeof options != 'object') { 16 | options = {}; 17 | } 18 | 19 | if (typeof options.content == 'undefined') { 20 | options.content = ['prev', 'title']; 21 | } 22 | 23 | if (!(options.content instanceof Array)) { 24 | options.content = [options.content]; 25 | } 26 | 27 | if (typeof options.use == 'undefined') { 28 | options.use = true; 29 | } 30 | 31 | return options; 32 | }; -------------------------------------------------------------------------------- /src/addons/navbars/typings.d.ts: -------------------------------------------------------------------------------- 1 | /** "navbar" options for the navbars add-on. */ 2 | interface mmOptionsNavbarsNavbar { 3 | 4 | /** An array of HTML elements or strings (for text or HTML or the keywords: "breadcrumbs", "close", "next", "prev", "searchfield", "title"). */ 5 | content ?: string[] | HTMLElement[] 6 | 7 | /** The size of the navbar. */ 8 | height ?: 1 | 2 | 3 | 4 9 | 10 | /** The position for the navbar. */ 11 | position ?: 'top' | 'bottom' 12 | 13 | /** Whether or not to enable the navbar. */ 14 | use ?: boolean | string | number 15 | 16 | /** The type of navbar. */ 17 | type ?: 'tabs' 18 | } 19 | 20 | 21 | /** Configuration for the navbars add-on. */ 22 | interface mmConfigsNavbars { 23 | 24 | /** Creadcrumbs configuration. */ 25 | breadcrumbs ?: mmConfigsNavbarsBreadcrumbs 26 | } 27 | 28 | /** Breadcrumbs configuration for the navbars add-on. */ 29 | interface mmConfigsNavbarsBreadcrumbs { 30 | 31 | /** The separator between two breadcrumbs. */ 32 | separator ?: string 33 | 34 | /** Whether or not to remove the first breadcrumb. */ 35 | removeFirst ?: boolean 36 | } 37 | -------------------------------------------------------------------------------- /src/addons/pagescroll/configs.ts: -------------------------------------------------------------------------------- 1 | const configs: mmConfigsPagescroll = { 2 | scrollOffset: 0, 3 | updateOffset: 50 4 | }; 5 | export default configs; 6 | -------------------------------------------------------------------------------- /src/addons/pagescroll/mmenu.pagescroll.ts: -------------------------------------------------------------------------------- 1 | import Mmenu from '../../core/oncanvas/mmenu.oncanvas'; 2 | import OPTIONS from './options'; 3 | import CONFIGS from './configs'; 4 | import * as DOM from '../../_modules/dom'; 5 | import { extend } from '../../_modules/helpers'; 6 | 7 | export default function (this: Mmenu) { 8 | this.opts.pageScroll = this.opts.pageScroll || {}; 9 | this.conf.pageScroll = this.conf.pageScroll || {}; 10 | 11 | // Extend options. 12 | const options = extend(this.opts.pageScroll, OPTIONS); 13 | const configs = extend(this.conf.pageScroll, CONFIGS); 14 | 15 | /** The currently "active" section */ 16 | var section: HTMLElement; 17 | 18 | function scrollTo() { 19 | if (section) { 20 | // section.scrollIntoView({ behavior: 'smooth' }); 21 | window.scrollTo({ 22 | top: 23 | section.getBoundingClientRect().top + 24 | document.scrollingElement.scrollTop - 25 | configs.scrollOffset, 26 | behavior: 'smooth' 27 | }); 28 | } 29 | section = null; 30 | } 31 | function anchorInPage(href: string) { 32 | try { 33 | if (href.slice(0, 1) == '#') { 34 | return DOM.find(Mmenu.node.page, href)[0]; 35 | } 36 | } catch (err) { } 37 | 38 | return null; 39 | } 40 | 41 | if (this.opts.offCanvas.use && options.scroll) { 42 | 43 | // Scroll to section after clicking menu item. 44 | this.bind('close:after', () => { 45 | scrollTo(); 46 | }); 47 | 48 | this.node.menu.addEventListener('click', event => { 49 | const href = (event.target as HTMLElement)?.closest('a[href]')?.getAttribute('href') || ''; 50 | 51 | section = anchorInPage(href); 52 | if (section) { 53 | 54 | event.preventDefault(); 55 | 56 | // If the sidebar add-on is "expanded"... 57 | if ( 58 | this.node.menu.matches('.mm-menu--sidebar-expanded') && 59 | this.node.wrpr.matches('.mm-wrapper--sidebar-expanded') 60 | ) { 61 | // ... scroll the page to the section. 62 | scrollTo(); 63 | 64 | // ... otherwise... 65 | } else { 66 | // ... close the menu. 67 | this.close(); 68 | } 69 | } 70 | }); 71 | 72 | } 73 | 74 | // Update selected menu item after scrolling. 75 | if (options.update) { 76 | let scts: HTMLElement[] = []; 77 | 78 | this.bind('initListview:after', (listview: HTMLElement) => { 79 | const listitems = DOM.children(listview, '.mm-listitem'); 80 | DOM.filterLIA(listitems).forEach(anchor => { 81 | const section = anchorInPage(anchor.getAttribute('href')); 82 | 83 | if (section) { 84 | scts.unshift(section); 85 | } 86 | }); 87 | }); 88 | 89 | let _selected = -1; 90 | 91 | window.addEventListener('scroll', evnt => { 92 | const scrollTop = window.scrollY; 93 | 94 | for (var s = 0; s < scts.length; s++) { 95 | if (scts[s].offsetTop < scrollTop + configs.updateOffset) { 96 | if (_selected !== s) { 97 | _selected = s; 98 | 99 | let panel = DOM.children( 100 | this.node.pnls, 101 | '.mm-panel--opened' 102 | )[0]; 103 | 104 | let listitems = DOM.find(panel, '.mm-listitem'); 105 | let anchors = DOM.filterLIA(listitems); 106 | 107 | anchors = anchors.filter(anchor => 108 | anchor.matches('[href="#' + scts[s].id + '"]') 109 | ); 110 | 111 | if (anchors.length) { 112 | this.setSelected(anchors[0].parentElement); 113 | } 114 | } 115 | break; 116 | } 117 | } 118 | }, { 119 | passive: true 120 | }); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/addons/pagescroll/options.ts: -------------------------------------------------------------------------------- 1 | const options : mmOptionsPagescroll = { 2 | scroll: false, 3 | update: false 4 | }; 5 | 6 | export default options; 7 | -------------------------------------------------------------------------------- /src/addons/pagescroll/typings.d.ts: -------------------------------------------------------------------------------- 1 | /** Options for the pageScroll add-on. */ 2 | interface mmOptionsPagescroll { 3 | /** Whether or not to smoothly scroll to a section on the page after clicking a menu item. */ 4 | scroll?: boolean; 5 | 6 | /** Whether or not to automatically make a menu item appear "selected" when scrolling through the section it is linked to. */ 7 | update?: boolean; 8 | } 9 | 10 | /** Configuration for the pageScroll add-on. */ 11 | interface mmConfigsPagescroll { 12 | /** Amount of pixels to scroll past the top of a section after clicking a menu item. */ 13 | scrollOffset?: number; 14 | 15 | /** Amount of pixels to scroll past the top of a section before its menu item will appear "selected". */ 16 | updateOffset?: number; 17 | } 18 | -------------------------------------------------------------------------------- /src/addons/searchfield/_panel.scss: -------------------------------------------------------------------------------- 1 | $mm_module: ".mm-panel"; 2 | 3 | /** 4 | * The searchpanel 5 | */ 6 | #{$mm_module}--search { 7 | left: 0 !important; 8 | right: 0 !important; 9 | width: 100% !important; 10 | border: none !important; 11 | } 12 | 13 | /** 14 | * Splash message 15 | */ 16 | #{$mm_module}__splash { 17 | padding: v.$panelPadding; 18 | 19 | #{$mm_module}--searching & { 20 | display: none; 21 | } 22 | } 23 | 24 | /** 25 | * No results message 26 | */ 27 | #{$mm_module}__noresults { 28 | display: none; 29 | padding: v.$panelPadding * 2 v.$panelPadding; 30 | color: var(--mm-color-text-dimmed); 31 | text-align: center; 32 | font-size: 150%; 33 | line-height: 1.4; 34 | 35 | #{$mm_module}--noresults & { 36 | display: block; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/addons/searchfield/configs.ts: -------------------------------------------------------------------------------- 1 | const configs : mmConfigsSearchfield = { 2 | cancel: true, 3 | clear: true, 4 | form: {}, 5 | input: {}, 6 | panel: {}, 7 | submit: false 8 | }; 9 | export default configs; -------------------------------------------------------------------------------- /src/addons/searchfield/mmenu.searchfield.scss: -------------------------------------------------------------------------------- 1 | @use "../../variables" as v; 2 | 3 | $mm_module: ".mm-searchfield"; 4 | 5 | /** 6 | * The form. 7 | */ 8 | #{$mm_module} { 9 | display: flex; 10 | flex-grow: 1; 11 | height: var(--mm-navbar-size); 12 | padding: 0; 13 | overflow: hidden; 14 | } 15 | /** 16 | * The fieldset 17 | */ 18 | #{$mm_module}__input { 19 | display: flex; 20 | flex: 1; 21 | align-items: center; 22 | position: relative; 23 | width: 100%; 24 | max-width: 100%; 25 | padding: 0 10px; 26 | box-sizing: border-box; 27 | 28 | // Input 29 | input { 30 | display: block; 31 | width: 100%; 32 | max-width: 100%; 33 | height: calc(var(--mm-navbar-size) * 0.7); 34 | min-height: auto; 35 | max-height: auto; 36 | margin: 0; 37 | padding: 0 10px; 38 | box-sizing: border-box; 39 | border: none; 40 | border-radius: 4px; 41 | line-height: calc(var(--mm-navbar-size) * 0.7); 42 | font: inherit; 43 | font-size: inherit; 44 | 45 | &, 46 | &:hover, 47 | &:focus { 48 | background: var(--mm-color-background-highlight); 49 | color: var(--mm-color-text); 50 | } 51 | 52 | .mm-menu[class*="-contrast"] & { 53 | border: 1px solid var(--mm-color-border); 54 | } 55 | } 56 | 57 | input::-ms-clear { 58 | display: none; 59 | } 60 | } 61 | 62 | /** 63 | * Submit and reset buttons. 64 | */ 65 | #{$mm_module}__btn { 66 | display: none; 67 | position: absolute; 68 | inset-inline-end: 0; // right, left for RTL 69 | top: 0; 70 | bottom: 0; 71 | 72 | #{$mm_module}--searching & { 73 | display: block; 74 | } 75 | } 76 | 77 | /** 78 | * Cancel button. 79 | */ 80 | #{$mm_module}__cancel { 81 | display: block; 82 | position: relative; 83 | margin-inline-end: -100px; // right, left for RTL 84 | padding-inline-start: 5px; // left, right for RTL 85 | padding-inline-end: v.$listitemIndent; // right, left for RTL 86 | visibility: hidden; 87 | line-height: var(--mm-navbar-size); 88 | text-decoration: none; 89 | transition-property: visibility, margin; 90 | 91 | #{$mm_module}--cancelable & { 92 | visibility: visible; 93 | margin-inline-end: 0; // right, left for RTL 94 | } 95 | } 96 | 97 | @import "./panel"; 98 | -------------------------------------------------------------------------------- /src/addons/searchfield/options.ts: -------------------------------------------------------------------------------- 1 | const options: mmOptionsSearchfield = { 2 | add: false, 3 | addTo: 'panels', 4 | noResults: 'No results found.', 5 | placeholder: 'Search', 6 | search: true, 7 | searchIn: 'panels', 8 | splash: '', 9 | title: 'Search', 10 | }; 11 | export default options; 12 | -------------------------------------------------------------------------------- /src/addons/searchfield/translations/de.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'cancel': 'abbrechen', 3 | 'Cancel searching': 'Suche abbrechen', 4 | 'Clear searchfield': 'Suchfeld löschen', 5 | 'No results found.': 'Keine Ergebnisse gefunden.', 6 | 'Search': 'Suche', 7 | }; 8 | -------------------------------------------------------------------------------- /src/addons/searchfield/translations/fa.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'cancel': 'انصراف', 3 | 'Cancel searching': 'لغو جستجو', 4 | 'Clear searchfield': 'پاک کردن فیلد جستجو', 5 | 'No results found.': 'نتیجه‌ای یافت نشد.', 6 | 'Search': 'جستجو', 7 | }; 8 | -------------------------------------------------------------------------------- /src/addons/searchfield/translations/index.ts: -------------------------------------------------------------------------------- 1 | import { add } from '../../../_modules/i18n'; 2 | 3 | import de from './de'; 4 | import fa from './fa'; 5 | import nl from './nl'; 6 | import pt_br from './pt_br'; 7 | import ru from './ru'; 8 | import sk from './sk'; 9 | import uk from './uk'; 10 | 11 | export default function () { 12 | add(de, 'de'); 13 | add(fa, 'fa'); 14 | add(nl, 'nl'); 15 | add(pt_br, 'pt_br'); 16 | add(ru, 'ru'); 17 | add(sk, 'sk'); 18 | add(uk, 'uk'); 19 | } 20 | -------------------------------------------------------------------------------- /src/addons/searchfield/translations/nl.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'cancel': 'annuleren', 3 | 'Cancel searching': 'Zoeken annuleren', 4 | 'Clear searchfield': 'Zoekveld leeg maken', 5 | 'No results found.': 'Geen resultaten gevonden.', 6 | 'Search': 'Zoeken', 7 | }; 8 | -------------------------------------------------------------------------------- /src/addons/searchfield/translations/pt_br.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'cancel': 'cancelar', 3 | 'Cancel searching': 'Cancelar pesquisa', 4 | 'Clear searchfield': 'Limpar campo de pesquisa', 5 | 'No results found.': 'Nenhum resultado encontrado.', 6 | 'Search': 'Buscar', 7 | }; 8 | -------------------------------------------------------------------------------- /src/addons/searchfield/translations/ru.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'cancel': 'отменить', 3 | 'Cancel searching': 'Отменить поиск', 4 | 'Clear searchfield': 'Очистить поле поиска', 5 | 'No results found.': 'Ничего не найдено.', 6 | 'Search': 'Найти', 7 | }; 8 | -------------------------------------------------------------------------------- /src/addons/searchfield/translations/sk.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'cancel': 'zrušiť', 3 | 'Cancel searching': 'Zrušiť vyhľadávanie', 4 | 'Clear searchfield': 'Vymazať pole vyhľadávania', 5 | 'No results found.': 'Neboli nájdené žiadne výsledky.', 6 | 'Search': 'Vyhľadávanie', 7 | } -------------------------------------------------------------------------------- /src/addons/searchfield/translations/uk.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'cancel': 'скасувати', 3 | 'Cancel searching': 'Скасувати пошук', 4 | 'Clear searchfield': 'Очистити поле пошуку', 5 | 'No results found.': 'Нічого не знайдено.', 6 | 'Search': 'Пошук', 7 | } -------------------------------------------------------------------------------- /src/addons/searchfield/typings.d.ts: -------------------------------------------------------------------------------- 1 | /** Options for the searchfield add-on. */ 2 | interface mmOptionsSearchfield { 3 | 4 | /** Whether or not to automatically prepend a searchfield to the menu or (some of the) panels. */ 5 | add?: boolean 6 | 7 | /** QuerySelector for the panels to add a searchfield to, or "searchpanel". */ 8 | addTo?: string 9 | 10 | /** The text to show when no results are found. */ 11 | noResults?: string 12 | 13 | /** The placeholder text for the searchfield. */ 14 | placeholder?: string 15 | 16 | /** Whether or not to search. */ 17 | search?: boolean 18 | 19 | /** QuerySelector for the panels to search in. */ 20 | searchIn?: string 21 | 22 | /** Text or HTML to add as splash content. */ 23 | splash?: string 24 | 25 | /** Title for the searchpanel. */ 26 | title?: string 27 | } 28 | 29 | /** Configuration for the searchfield add-on. */ 30 | interface mmConfigsSearchfield { 31 | /** Whether or not to add a cancel button to the searchfield. */ 32 | cancel?: boolean 33 | 34 | /** Whether or not to add a clear button to the searchfield. */ 35 | clear?: boolean 36 | 37 | /** Adds the specified keys/values as attributes to fhe form. */ 38 | form?: mmLooseObject 39 | 40 | /** Adds the specified keys/values as attributes to the input. */ 41 | input?: mmLooseObject 42 | 43 | /** Adds the specified keys/values as attributes to the panel. */ 44 | panel?: mmLooseObject 45 | 46 | /** Whether or not to add a submit button to the searchfield. */ 47 | submit?: boolean 48 | } 49 | -------------------------------------------------------------------------------- /src/addons/sectionindexer/mmenu.sectionindexer.scss: -------------------------------------------------------------------------------- 1 | @use "../../variables"; 2 | 3 | $mm_module: ".mm-sectionindexer"; 4 | 5 | :root { 6 | --mm-sectionindexer-size: 20px; 7 | } 8 | 9 | #{$mm_module} { 10 | background: inherit; 11 | text-align: center; 12 | font-size: 12px; 13 | 14 | box-sizing: border-box; 15 | width: var(--mm-sectionindexer-size); 16 | 17 | position: absolute; 18 | top: 0; 19 | bottom: 0; 20 | inset-inline-end: calc(-1 * var(--mm-sectionindexer-size)); // right, left for RTL 21 | z-index: 5; 22 | 23 | transition-property: inset-inline-end; // right, left for RTL 24 | 25 | display: flex; 26 | flex-direction: column; 27 | justify-content: space-evenly; 28 | 29 | a { 30 | color: var(--mm-color-text-dimmed); 31 | line-height: 1; 32 | text-decoration: none; 33 | display: block; 34 | } 35 | 36 | ~ .mm-panel { 37 | padding-inline-end: 0; // right, left for RTL 38 | } 39 | 40 | &--active { 41 | right: 0; 42 | 43 | ~ .mm-panel { 44 | padding-inline-end: var(--mm-sectionindexer-size); // right, left for RTL 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/addons/sectionindexer/mmenu.sectionindexer.ts: -------------------------------------------------------------------------------- 1 | import Mmenu from '../../core/oncanvas/mmenu.oncanvas'; 2 | import OPTIONS from './options'; 3 | import * as DOM from '../../_modules/dom'; 4 | import * as support from '../../_modules/support'; 5 | import { extend } from '../../_modules/helpers'; 6 | 7 | 8 | export default function (this: Mmenu) { 9 | this.opts.sectionIndexer = this.opts.sectionIndexer || {}; 10 | 11 | // Extend options. 12 | const options = extend(this.opts.sectionIndexer, OPTIONS); 13 | 14 | if (!options.add) { 15 | return; 16 | } 17 | 18 | this.bind('initPanels:after', () => { 19 | // Add the indexer, only if it does not allready excists 20 | if (!this.node.indx) { 21 | let buttons = ''; 22 | 'abcdefghijklmnopqrstuvwxyz'.split('').forEach(letter => { 23 | buttons += '' + letter + ''; 24 | }); 25 | 26 | let indexer = DOM.create('div.mm-sectionindexer'); 27 | indexer.innerHTML = buttons; 28 | 29 | this.node.pnls.prepend(indexer); 30 | this.node.indx = indexer; 31 | 32 | // Prevent default behavior when clicking an anchor 33 | this.node.indx.addEventListener('click', evnt => { 34 | const anchor = evnt.target as HTMLElement; 35 | 36 | if (anchor.matches('a')) { 37 | evnt.preventDefault(); 38 | } 39 | }); 40 | 41 | // Scroll onMouseOver / onTouchStart 42 | let mouseOverEvent = evnt => { 43 | if (!evnt.target.matches('a')) { 44 | return; 45 | } 46 | 47 | const letter = evnt.target.textContent; 48 | const panel = DOM.children(this.node.pnls, '.mm-panel--opened')[0]; 49 | 50 | let newTop = -1, 51 | oldTop = panel.scrollTop; 52 | 53 | panel.scrollTop = 0; 54 | DOM.find(panel, '.mm-divider') 55 | .filter(divider => !divider.matches('.mm-hidden')) 56 | .forEach(divider => { 57 | if ( 58 | newTop < 0 && 59 | letter == 60 | divider.textContent 61 | .trim() 62 | .slice(0, 1) 63 | .toLowerCase() 64 | ) { 65 | newTop = divider.offsetTop; 66 | } 67 | }); 68 | 69 | panel.scrollTop = newTop > -1 ? newTop : oldTop; 70 | }; 71 | 72 | if (support.touch) { 73 | this.node.indx.addEventListener('touchstart', mouseOverEvent); 74 | this.node.indx.addEventListener('touchmove', mouseOverEvent); 75 | } else { 76 | this.node.indx.addEventListener('mouseover', mouseOverEvent); 77 | } 78 | } 79 | 80 | // Show or hide the indexer 81 | this.bind('openPanel:before', (panel: HTMLElement) => { 82 | const active = DOM.find(panel, '.mm-divider').filter( 83 | divider => !divider.matches('.mm-hidden') 84 | ).length; 85 | 86 | this.node.indx.classList[active ? 'add' : 'remove']( 87 | 'mm-sectionindexer--active' 88 | ); 89 | }); 90 | }); 91 | } 92 | -------------------------------------------------------------------------------- /src/addons/sectionindexer/options.ts: -------------------------------------------------------------------------------- 1 | const options : mmOptionsSectionindexer = { 2 | add: false, 3 | addTo: 'panels' 4 | }; 5 | 6 | export default options; 7 | -------------------------------------------------------------------------------- /src/addons/sectionindexer/typings.d.ts: -------------------------------------------------------------------------------- 1 | /** Options for the sectionIndexer add-on. */ 2 | interface mmOptionsSectionindexer { 3 | 4 | /** Whether or not to automatically append a section indexer to the menu. */ 5 | add?: boolean 6 | 7 | /** Where to add the section indexer(s). */ 8 | addTo?: string 9 | } 10 | -------------------------------------------------------------------------------- /src/addons/setselected/mmenu.setselected.scss: -------------------------------------------------------------------------------- 1 | @use "../../variables"; 2 | 3 | .mm-menu--selected { 4 | &-hover, 5 | &-parent { 6 | .mm-listitem__text, 7 | .mm-listitem__btn { 8 | transition-property: background-color; 9 | } 10 | } 11 | 12 | @media (hover: hover) { 13 | &-hover { 14 | .mm-listview:hover > .mm-listitem--selected:not(:hover) { 15 | > .mm-listitem__text { 16 | background: none; 17 | } 18 | } 19 | .mm-listitem__text, 20 | .mm-listitem__btn { 21 | &:hover { 22 | background: var(--mm-color-background-emphasis); 23 | } 24 | } 25 | } 26 | } 27 | 28 | &-parent { 29 | .mm-listitem__text, 30 | .mm-listitem__btn { 31 | transition-delay: variables.$transDr * 0.5; 32 | 33 | @media (hover: hover) { 34 | &:hover { 35 | transition-delay: 0s; 36 | } 37 | } 38 | } 39 | 40 | .mm-panel--parent .mm-listitem:not(.mm-listitem--selected-parent) { 41 | > .mm-listitem__text { 42 | background: none; 43 | } 44 | } 45 | .mm-listitem--selected-parent { 46 | > .mm-listitem__text, 47 | > .mm-listitem__btn { 48 | background: var(--mm-color-background-emphasis); 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/addons/setselected/mmenu.setselected.ts: -------------------------------------------------------------------------------- 1 | import Mmenu from '../../core/oncanvas/mmenu.oncanvas'; 2 | import OPTIONS from './options'; 3 | import * as DOM from '../../_modules/dom'; 4 | import { extend } from '../../_modules/helpers'; 5 | 6 | 7 | export default function (this: Mmenu) { 8 | this.opts.setSelected = this.opts.setSelected || {}; 9 | 10 | // Extend options. 11 | const options = extend(this.opts.setSelected, OPTIONS); 12 | 13 | // Find current by URL 14 | if (options.current == 'detect') { 15 | const findCurrent = (url: string) => { 16 | url = url.split('?')[0].split('#')[0]; 17 | const anchor = this.node.menu.querySelector( 18 | 'a[href="' + url + '"], a[href="' + url + '/"]' 19 | ); 20 | if (anchor) { 21 | this.setSelected(anchor.parentElement); 22 | } else { 23 | const arr = url.split('/').slice(0, -1); 24 | if (arr.length) { 25 | findCurrent(arr.join('/')); 26 | } 27 | } 28 | }; 29 | 30 | this.bind('initMenu:after', () => { 31 | findCurrent.call(this, window.location.href); 32 | }); 33 | 34 | // Remove current selected item 35 | } else if (!options.current) { 36 | this.bind('initListview:after', (listview: HTMLElement) => { 37 | DOM.children(listview, '.mm-listitem--selected').forEach( 38 | (listitem) => { 39 | listitem.classList.remove('mm-listitem--selected'); 40 | } 41 | ); 42 | }); 43 | } 44 | 45 | // Add :hover effect on items 46 | if (options.hover) { 47 | this.bind('initMenu:after', () => { 48 | this.node.menu.classList.add('mm-menu--selected-hover'); 49 | }); 50 | } 51 | 52 | // Set parent item selected for submenus 53 | if (options.parent) { 54 | this.bind('openPanel:after', (panel: HTMLElement) => { 55 | 56 | // Remove all 57 | DOM.find(this.node.pnls, '.mm-listitem--selected-parent').forEach( 58 | (listitem) => { 59 | listitem.classList.remove('mm-listitem--selected-parent'); 60 | } 61 | ); 62 | 63 | // Move up the DOM tree 64 | let current = panel; 65 | while (current) { 66 | let li = DOM.find(this.node.pnls, `#${current.dataset.mmParent}`)[0]; 67 | current = li?.closest('.mm-panel') as HTMLElement; 68 | 69 | if (li && !li.matches('.mm-listitem--vertical')) { 70 | li.classList.add('mm-listitem--selected-parent'); 71 | } 72 | } 73 | }); 74 | 75 | this.bind('initMenu:after', () => { 76 | this.node.menu.classList.add('mm-menu--selected-parent'); 77 | }); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/addons/setselected/options.ts: -------------------------------------------------------------------------------- 1 | const options : mmOptionsSetselected = { 2 | current: true, 3 | hover: false, 4 | parent: false 5 | }; 6 | 7 | export default options; 8 | -------------------------------------------------------------------------------- /src/addons/setselected/typings.d.ts: -------------------------------------------------------------------------------- 1 | /** Options for the setSelected add-on. */ 2 | interface mmOptionsSetselected { 3 | 4 | /** Whether or not to make the current menu item appear "selected". */ 5 | current ?: boolean | 'detect' 6 | 7 | /** Whether or not to make menu item appear "selected" onMouseOver. */ 8 | hover ?: boolean 9 | 10 | /** Whether or not to make menu item appear "selected" while its subpanel is opened. */ 11 | parent ?: boolean 12 | } 13 | -------------------------------------------------------------------------------- /src/addons/sidebar/mmenu.sidebar.scss: -------------------------------------------------------------------------------- 1 | @use "../../mixins" as m; 2 | 3 | :root { 4 | --mm-sidebar-collapsed-size: 50px; 5 | --mm-sidebar-expanded-size: var(--mm-max-size); 6 | } 7 | 8 | .mm-wrapper--sidebar-collapsed { 9 | .mm-slideout { 10 | width: calc(100% - var(--mm-sidebar-collapsed-size)); 11 | transform: translate3d(var(--mm-sidebar-collapsed-size), 0, 0); 12 | 13 | [dir="rtl"] & { 14 | transform: none; 15 | } 16 | } 17 | 18 | &:not(.mm-wrapper--opened) { 19 | .mm-menu--sidebar-collapsed { 20 | .mm-navbar, 21 | .mm-divider { 22 | opacity: 0; 23 | } 24 | } 25 | } 26 | } 27 | 28 | .mm-wrapper--sidebar-expanded { 29 | .mm-menu--sidebar-expanded { 30 | width: var(--mm-sidebar-expanded-size); 31 | border-right-width: 1px; 32 | border-right-style: solid; 33 | 34 | // TODO voor position-right 35 | } 36 | 37 | &.mm-wrapper--opened { 38 | overflow: auto; 39 | 40 | // disable the UI blocker. 41 | .mm-wrapper__blocker { 42 | display: none; 43 | } 44 | 45 | // page next to menu. 46 | .mm-slideout { 47 | width: calc(100% - var(--mm-sidebar-expanded-size)); 48 | transform: translate3d(var(--mm-sidebar-expanded-size), 0, 0); 49 | 50 | [dir="rtl"] & { 51 | transform: none; 52 | } 53 | 54 | // TODO voor position-right 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/addons/sidebar/mmenu.sidebar.ts: -------------------------------------------------------------------------------- 1 | import Mmenu from '../../core/oncanvas/mmenu.oncanvas'; 2 | import OPTIONS from './options'; 3 | import * as DOM from '../../_modules/dom'; 4 | import * as media from '../../_modules/matchmedia'; 5 | import { extend } from '../../_modules/helpers'; 6 | 7 | export default function (this: Mmenu) { 8 | // Only for off-canvas menus. 9 | if (!this.opts.offCanvas.use) { 10 | return; 11 | } 12 | 13 | this.opts.sidebar = this.opts.sidebar || {}; 14 | 15 | // Extend options. 16 | const options = extend(this.opts.sidebar, OPTIONS); 17 | 18 | // Collapsed 19 | if (options.collapsed.use) { 20 | // Make the menu collapsable. 21 | this.bind('initMenu:after', () => { 22 | this.node.menu.classList.add('mm-menu--sidebar-collapsed'); 23 | }); 24 | 25 | /** Enable the collapsed sidebar */ 26 | let enable = () => { 27 | this.node.wrpr.classList.add('mm-wrapper--sidebar-collapsed'); 28 | }; 29 | 30 | /** Disable the collapsed sidebar */ 31 | let disable = () => { 32 | this.node.wrpr.classList.remove('mm-wrapper--sidebar-collapsed'); 33 | }; 34 | 35 | if (typeof options.collapsed.use === 'boolean') { 36 | this.bind('initMenu:after', enable); 37 | } else { 38 | media.add(options.collapsed.use, enable, disable); 39 | } 40 | } 41 | 42 | // Expanded 43 | if (options.expanded.use) { 44 | // Make the menu expandable 45 | this.bind('initMenu:after', () => { 46 | this.node.menu.classList.add('mm-menu--sidebar-expanded'); 47 | }); 48 | 49 | let expandedEnabled = false; 50 | 51 | /** Enable the expanded sidebar */ 52 | let enable = () => { 53 | expandedEnabled = true; 54 | this.node.wrpr.classList.add('mm-wrapper--sidebar-expanded'); 55 | this.node.menu.removeAttribute('aria-modal'); 56 | this.open(); 57 | Mmenu.node.page.removeAttribute('inert'); 58 | }; 59 | 60 | /** Disable the expanded sidebar */ 61 | let disable = () => { 62 | expandedEnabled = false; 63 | this.node.wrpr.classList.remove('mm-wrapper--sidebar-expanded'); 64 | this.node.menu.setAttribute('aria-modal', 'true'); 65 | this.close(); 66 | }; 67 | 68 | if (typeof options.expanded.use == 'boolean') { 69 | this.bind('initMenu:after', enable); 70 | } else { 71 | media.add(options.expanded.use, enable, disable); 72 | } 73 | 74 | // Store exanded state when opening and closing the menu. 75 | this.bind('close:after', () => { 76 | if (expandedEnabled) { 77 | window.sessionStorage.setItem('mmenuExpandedState', 'closed'); 78 | } 79 | }); 80 | 81 | this.bind('open:after', () => { 82 | if (expandedEnabled) { 83 | window.sessionStorage.setItem('mmenuExpandedState', 'open'); 84 | Mmenu.node.page.removeAttribute('inert'); 85 | } 86 | }); 87 | 88 | // Set the initial state 89 | let initialState = options.expanded.initial; 90 | 91 | const state = window.sessionStorage.getItem('mmenuExpandedState'); 92 | switch (state) { 93 | case 'open': 94 | case 'closed': 95 | initialState = state; 96 | break; 97 | } 98 | 99 | if (initialState === 'closed') { 100 | this.bind('init:after', () => { 101 | this.close(); 102 | }); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/addons/sidebar/options.ts: -------------------------------------------------------------------------------- 1 | const options: mmOptionsSidebar = { 2 | collapsed: { 3 | use: false, 4 | }, 5 | expanded: { 6 | use: false, 7 | initial: 'open' 8 | } 9 | }; 10 | export default options; 11 | -------------------------------------------------------------------------------- /src/addons/sidebar/typings.d.ts: -------------------------------------------------------------------------------- 1 | /** Options for the sidebar add-on. */ 2 | interface mmOptionsSidebar { 3 | /** Collapsed options */ 4 | collapsed?: mmOptionsSidebarCollapsed; 5 | 6 | /** Expanded options */ 7 | expanded?: mmOptionsSidebarExpanded; 8 | } 9 | 10 | /** Collapsed options for the searchfield add-on. */ 11 | interface mmOptionsSidebarCollapsed { 12 | /** Whether or not to enable the collapsed menu. */ 13 | use?: boolean | string | number; 14 | } 15 | 16 | /** "expanded" options for the searchfield add-on. */ 17 | interface mmOptionsSidebarExpanded { 18 | /** Whether or not to enable the expanded menu. */ 19 | use?: boolean | string | number; 20 | 21 | /** The initial state */ 22 | initial?: 'open' | 'closed'; 23 | } 24 | -------------------------------------------------------------------------------- /src/core/offcanvas/_positions.scss: -------------------------------------------------------------------------------- 1 | .mm-menu { 2 | /** Horizontal transform */ 3 | --mm-translate-horizontal: 0; 4 | 5 | /** Vertical transform */ 6 | --mm-translate-vertical: 0; 7 | 8 | &--position { 9 | // Left + Right 10 | &-left, 11 | &-left-front { 12 | right: auto; 13 | } 14 | 15 | &-right, 16 | &-right-front { 17 | left: auto; 18 | } 19 | 20 | &-left, 21 | &-right, 22 | &-left-front, 23 | &-right-front { 24 | width: clamp( 25 | var(--mm-min-size), 26 | var(--mm-size), 27 | var(--mm-max-size) 28 | ); 29 | } 30 | 31 | &-left-front { 32 | --mm-translate-horizontal: -100%; 33 | } 34 | 35 | &-right-front { 36 | --mm-translate-horizontal: 100%; 37 | } 38 | 39 | // Top + Bottom 40 | &-top { 41 | bottom: auto; 42 | } 43 | 44 | &-bottom { 45 | top: auto; 46 | } 47 | 48 | &-top, 49 | &-bottom { 50 | width: 100%; 51 | height: clamp( 52 | var(--mm-min-size), 53 | var(--mm-size), 54 | var(--mm-max-size) 55 | ); 56 | } 57 | 58 | &-top { 59 | --mm-translate-vertical: -100%; 60 | } 61 | 62 | &-bottom { 63 | --mm-translate-vertical: 100%; 64 | } 65 | 66 | // All in front 67 | &-left-front, 68 | &-right-front, 69 | &-top, 70 | &-bottom { 71 | z-index: 2; 72 | 73 | transform: translate3d( 74 | var(--mm-translate-horizontal), 75 | var(--mm-translate-vertical), 76 | 0 77 | ); 78 | 79 | transition-property: transform; 80 | 81 | &.mm-menu--opened { 82 | transform: translate3d(0, 0, 0); 83 | } 84 | } 85 | } 86 | } 87 | 88 | .mm-wrapper { 89 | &--position { 90 | // Left + right 91 | &-left { 92 | --mm-translate-horizontal: clamp( 93 | var(--mm-min-size), 94 | var(--mm-size), 95 | var(--mm-max-size) 96 | ); 97 | } 98 | 99 | &-right { 100 | --mm-translate-horizontal: clamp( 101 | calc(-1 * var(--mm-max-size)), 102 | calc(-1 * var(--mm-size)), 103 | calc(-1 * var(--mm-min-size)) 104 | ); 105 | } 106 | 107 | &-left, 108 | &-right { 109 | .mm-slideout { 110 | transform: translate3d(0, 0, 0); 111 | } 112 | 113 | &.mm-wrapper--opened .mm-slideout { 114 | transform: translate3d(var(--mm-translate-horizontal), 0, 0); 115 | } 116 | } 117 | 118 | // All in front 119 | &-left-front, 120 | &-right-front, 121 | &-top, 122 | &-bottom { 123 | .mm-wrapper__blocker { 124 | z-index: 1; 125 | } 126 | } 127 | } 128 | 129 | // TODO RTL met position-right werkt niet 130 | } 131 | -------------------------------------------------------------------------------- /src/core/offcanvas/configs.ts: -------------------------------------------------------------------------------- 1 | const configs: mmConfigsOffcanvas = { 2 | clone: false, 3 | menu: { 4 | insertMethod: 'append', 5 | insertSelector: 'body' 6 | }, 7 | page: { 8 | nodetype: 'div', 9 | selector: null, 10 | noSelector: [] 11 | }, 12 | screenReader: { 13 | closeMenu: 'Close menu', 14 | openMenu: 'Open menu', 15 | } 16 | }; 17 | export default configs; 18 | -------------------------------------------------------------------------------- /src/core/offcanvas/mmenu.offcanvas.scss: -------------------------------------------------------------------------------- 1 | @use "../../variables" as v; 2 | 3 | :root { 4 | --mm-size: 80%; 5 | --mm-min-size: 240px; 6 | --mm-max-size: 440px; 7 | } 8 | 9 | // Menu 10 | .mm-menu--offcanvas { 11 | position: fixed; 12 | z-index: 0; 13 | } 14 | 15 | // Page node 16 | .mm-page { 17 | box-sizing: border-box; 18 | min-height: 100vh; 19 | background: inherit; 20 | } 21 | 22 | // All sliding out nodes 23 | :where(.mm-slideout) { 24 | position: relative; 25 | z-index: 1; 26 | width: 100%; 27 | transition-duration: v.$transDr; 28 | transition-timing-function: v.$transFn; 29 | transition-property: width, transform; 30 | } 31 | 32 | // Wrapper 33 | .mm-wrapper--opened { 34 | &, 35 | body { 36 | overflow: hidden; 37 | } 38 | } 39 | 40 | // UI Blocker 41 | .mm-wrapper__blocker { 42 | background: #00000066; 43 | 44 | .mm-wrapper--opened & { 45 | --mm-blocker-visibility-delay: 0s; 46 | --mm-blocker-opacity-delay: #{v.$transDr}; 47 | 48 | bottom: 0; 49 | opacity: 0.5; 50 | } 51 | } 52 | 53 | @import "positions"; 54 | -------------------------------------------------------------------------------- /src/core/offcanvas/options.ts: -------------------------------------------------------------------------------- 1 | const options: mmOptionsOffcanvas = { 2 | use: true, 3 | position: 'left' 4 | }; 5 | export default options; -------------------------------------------------------------------------------- /src/core/offcanvas/translations/de.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'Close menu': 'Menü schließen', 3 | 'Open menu': 'Menü öffnen', 4 | } -------------------------------------------------------------------------------- /src/core/offcanvas/translations/fa.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'Close menu': 'بستن منو', 3 | 'Open menu': 'باز کردن منو', 4 | } -------------------------------------------------------------------------------- /src/core/offcanvas/translations/index.ts: -------------------------------------------------------------------------------- 1 | import { add } from '../../../_modules/i18n'; 2 | 3 | import de from './de'; 4 | import fa from './fa'; 5 | import nl from './nl'; 6 | import pt_br from './pt_br'; 7 | import ru from './ru'; 8 | import sk from './sk'; 9 | import uk from './uk'; 10 | 11 | export default function () { 12 | add(de, 'de'); 13 | add(fa, 'fa'); 14 | add(nl, 'nl'); 15 | add(pt_br, 'pt_br'); 16 | add(ru, 'ru'); 17 | add(sk, 'sk'); 18 | add(uk, 'uk'); 19 | } -------------------------------------------------------------------------------- /src/core/offcanvas/translations/nl.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'Close menu': 'Menu sluiten', 3 | 'Open menu': 'Menu openen', 4 | } -------------------------------------------------------------------------------- /src/core/offcanvas/translations/pt_br.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'Close menu': 'Fechar menu', 3 | 'Open menu': 'Abrir menu', 4 | } -------------------------------------------------------------------------------- /src/core/offcanvas/translations/ru.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'Close menu': 'Закрыть меню', 3 | 'Open menu': 'открыть меню', 4 | } -------------------------------------------------------------------------------- /src/core/offcanvas/translations/sk.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'Close menu': 'Zatvoriť menu', 3 | 'Open menu': 'Otvoriť menu', 4 | } -------------------------------------------------------------------------------- /src/core/offcanvas/translations/uk.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'Close menu': 'Закрити меню', 3 | 'Open menu': 'Відкрити меню', 4 | } -------------------------------------------------------------------------------- /src/core/offcanvas/typings.d.ts: -------------------------------------------------------------------------------- 1 | // Add-on options interface. 2 | interface mmOptionsOffcanvas { 3 | use?: boolean 4 | position?: mmOptionsOffcanvasPositions 5 | } 6 | 7 | /** Possible positions for the menu. */ 8 | type mmOptionsOffcanvasPositions = 'left' | 'left-front' | 'right' | 'right-front' | 'top' | 'bottom' 9 | 10 | // Add-on configs interfaces. 11 | interface mmConfigsOffcanvas { 12 | /** Whether or not the menu should be cloned (and the original menu kept intact). */ 13 | clone?: boolean; 14 | 15 | /** Menu configuration for the off-canvas add-on. */ 16 | menu?: mmConfigsOffcanvasMenu; 17 | 18 | /** Page configuration for the off-canvas add-on. */ 19 | page?: mmConfigsOffcanvasPage; 20 | 21 | /** Texts for screenreaders. */ 22 | screenReader?: { 23 | openMenu: string, 24 | closeMenu: string, 25 | } 26 | } 27 | interface mmConfigsOffcanvasMenu { 28 | /** How to insert the menu into the DOM. */ 29 | insertMethod?: 'prepend' | 'append'; 30 | 31 | /** Where to insert the menu into the DOM. */ 32 | insertSelector?: string; 33 | } 34 | interface mmConfigsOffcanvasPage { 35 | /** The nodetype for the page. */ 36 | nodetype?: string; 37 | 38 | /** The selector for the page. */ 39 | selector?: string; 40 | 41 | /** List of selectors for nodes to exclude from the page. */ 42 | noSelector?: string[]; 43 | } 44 | -------------------------------------------------------------------------------- /src/core/oncanvas/_blocker.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --mm-blocker-visibility-delay: #{v.$transDr}; 3 | --mm-blocker-opacity-delay: 0s; 4 | } 5 | 6 | .mm-blocker { 7 | display: block; 8 | position: absolute; 9 | bottom: 100%; 10 | top: 0; 11 | right: 0; 12 | left: 0; 13 | z-index: 3; 14 | 15 | opacity: 0; 16 | background: var(--mm-color-background); 17 | 18 | transition: 19 | bottom 0s v.$transFn var(--mm-blocker-visibility-delay), 20 | width v.$transDr v.$transFn, 21 | opacity v.$transDr v.$transFn var(--mm-blocker-opacity-delay), 22 | transform v.$transDr v.$transFn; 23 | 24 | &:focus-visible { 25 | opacity: 0.75; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/core/oncanvas/_button.scss: -------------------------------------------------------------------------------- 1 | $mm_module: ".mm-btn"; 2 | 3 | #{$mm_module} { 4 | flex-grow: 0; 5 | flex-shrink: 0; 6 | display: flex; 7 | align-items: center; 8 | justify-content: center; 9 | position: relative; 10 | width: v.$btnSize; 11 | padding: 0; 12 | 13 | &--next, 14 | [dir="rtl"] &--prev { 15 | --mm-btn-rotate: 135deg; 16 | } 17 | 18 | &--prev, 19 | [dir="rtl"] &--next { 20 | --mm-btn-rotate: -45deg; 21 | } 22 | 23 | &--prev:before, 24 | &--next:after { 25 | content: ""; 26 | display: block; 27 | position: absolute; 28 | top: 0; 29 | bottom: 0; 30 | width: 8px; 31 | height: 8px; 32 | margin: auto; 33 | box-sizing: border-box; 34 | border: 2px solid var(--mm-color-icon); 35 | border-bottom: none; 36 | border-right: none; 37 | transform: rotate(var(--mm-btn-rotate)); 38 | } 39 | 40 | &--prev:before { 41 | inset-inline-start: v.$listitemIndent + 3; // left, right for RTL 42 | } 43 | 44 | &--next:after { 45 | inset-inline-end: v.$listitemIndent + 3; // right, left for RTL 46 | } 47 | 48 | &--close { 49 | &:before { 50 | content: '\d7'; 51 | font-size: 150%; 52 | } 53 | } 54 | 55 | //