├── .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 |
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 | //