├── .stylelintignore ├── docs ├── CNAME ├── images │ ├── icons │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon-96x96.png │ │ ├── ms-icon-144x144.png │ │ ├── apple-icon-114x114.png │ │ ├── apple-icon-120x120.png │ │ ├── apple-icon-144x144.png │ │ ├── apple-icon-152x152.png │ │ ├── apple-icon-180x180.png │ │ ├── apple-icon-57x57.png │ │ ├── apple-icon-60x60.png │ │ ├── apple-icon-72x72.png │ │ ├── apple-icon-76x76.png │ │ ├── android-icon-144x144.png │ │ └── android-icon-192x192.png │ └── logo.svg ├── manifest.json └── index.html ├── .eslintignore ├── env.d.ts ├── dev ├── env.d.ts ├── main.ts ├── pages │ ├── Usage.vue │ ├── Introduction.vue │ ├── Installation.vue │ └── Examples │ │ ├── Manual.vue │ │ ├── Auto.vue │ │ └── Website.vue ├── routes.ts ├── App.scss └── App.vue ├── vue.config.js ├── images ├── icons │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ ├── ms-icon-144x144.png │ ├── apple-icon-114x114.png │ ├── apple-icon-120x120.png │ ├── apple-icon-144x144.png │ ├── apple-icon-152x152.png │ ├── apple-icon-180x180.png │ ├── apple-icon-57x57.png │ ├── apple-icon-60x60.png │ ├── apple-icon-72x72.png │ ├── apple-icon-76x76.png │ ├── android-icon-144x144.png │ └── android-icon-192x192.png └── logo.svg ├── src ├── components │ ├── NavigationLevel │ │ ├── NavigationLevel.scss │ │ ├── NavigationLevel.spec.ts │ │ └── NavigationLevel.vue │ ├── NavigationItem │ │ ├── NavigationItem.scss │ │ └── NavigationItem.vue │ ├── TreeNavigation │ │ ├── TreeNavigation.scss │ │ ├── TreeNavigation.ts │ │ ├── core.ts │ │ └── core.spec.ts │ ├── NavigationToggle │ │ ├── NavigationToggle.scss │ │ ├── NavigationToggle.vue │ │ └── NavigationToggle.spec.ts │ ├── utils.ts │ └── utils.spec.ts └── index.js ├── .gitignore ├── cypress.json ├── .editorconfig ├── .prettierrc.json ├── tsconfig.json ├── tsconfig.vitest.json ├── tsconfig.node.json ├── .stylelintrc ├── tsconfig.app.json ├── vite.config.ts ├── vitest.config.ts ├── cypress └── e2e │ ├── runner.js │ ├── specs │ ├── auto-generated.spec.js │ ├── without-router.spec.js │ └── with-router.spec.js │ └── apps │ ├── auto-generated │ └── index.html │ ├── without-router │ ├── index.html │ └── running │ │ └── barefoot │ │ └── index.html │ └── with-router │ └── index.html ├── .github ├── workflows │ ├── deploy.yaml │ ├── npm-publish-github-packages.yml │ ├── node.js.yml │ └── static.yml └── dependabot.yml ├── .eslintrc.js ├── manifest.json ├── LICENSE ├── index.html ├── package.json └── README.md /.stylelintignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | vue-tree-navigation.j3-tech.com -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | coverage/ 3 | e2e/apps/ 4 | -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /dev/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | publicPath: '/Vue-Tree-Navigation/' 3 | } -------------------------------------------------------------------------------- /images/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J3-Tech/Vue-Tree-Navigation/HEAD/images/icons/favicon-16x16.png -------------------------------------------------------------------------------- /images/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J3-Tech/Vue-Tree-Navigation/HEAD/images/icons/favicon-32x32.png -------------------------------------------------------------------------------- /images/icons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J3-Tech/Vue-Tree-Navigation/HEAD/images/icons/favicon-96x96.png -------------------------------------------------------------------------------- /images/icons/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J3-Tech/Vue-Tree-Navigation/HEAD/images/icons/ms-icon-144x144.png -------------------------------------------------------------------------------- /docs/images/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J3-Tech/Vue-Tree-Navigation/HEAD/docs/images/icons/favicon-16x16.png -------------------------------------------------------------------------------- /docs/images/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J3-Tech/Vue-Tree-Navigation/HEAD/docs/images/icons/favicon-32x32.png -------------------------------------------------------------------------------- /docs/images/icons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J3-Tech/Vue-Tree-Navigation/HEAD/docs/images/icons/favicon-96x96.png -------------------------------------------------------------------------------- /images/icons/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J3-Tech/Vue-Tree-Navigation/HEAD/images/icons/apple-icon-114x114.png -------------------------------------------------------------------------------- /images/icons/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J3-Tech/Vue-Tree-Navigation/HEAD/images/icons/apple-icon-120x120.png -------------------------------------------------------------------------------- /images/icons/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J3-Tech/Vue-Tree-Navigation/HEAD/images/icons/apple-icon-144x144.png -------------------------------------------------------------------------------- /images/icons/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J3-Tech/Vue-Tree-Navigation/HEAD/images/icons/apple-icon-152x152.png -------------------------------------------------------------------------------- /images/icons/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J3-Tech/Vue-Tree-Navigation/HEAD/images/icons/apple-icon-180x180.png -------------------------------------------------------------------------------- /images/icons/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J3-Tech/Vue-Tree-Navigation/HEAD/images/icons/apple-icon-57x57.png -------------------------------------------------------------------------------- /images/icons/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J3-Tech/Vue-Tree-Navigation/HEAD/images/icons/apple-icon-60x60.png -------------------------------------------------------------------------------- /images/icons/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J3-Tech/Vue-Tree-Navigation/HEAD/images/icons/apple-icon-72x72.png -------------------------------------------------------------------------------- /images/icons/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J3-Tech/Vue-Tree-Navigation/HEAD/images/icons/apple-icon-76x76.png -------------------------------------------------------------------------------- /docs/images/icons/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J3-Tech/Vue-Tree-Navigation/HEAD/docs/images/icons/ms-icon-144x144.png -------------------------------------------------------------------------------- /images/icons/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J3-Tech/Vue-Tree-Navigation/HEAD/images/icons/android-icon-144x144.png -------------------------------------------------------------------------------- /images/icons/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J3-Tech/Vue-Tree-Navigation/HEAD/images/icons/android-icon-192x192.png -------------------------------------------------------------------------------- /docs/images/icons/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J3-Tech/Vue-Tree-Navigation/HEAD/docs/images/icons/apple-icon-114x114.png -------------------------------------------------------------------------------- /docs/images/icons/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J3-Tech/Vue-Tree-Navigation/HEAD/docs/images/icons/apple-icon-120x120.png -------------------------------------------------------------------------------- /docs/images/icons/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J3-Tech/Vue-Tree-Navigation/HEAD/docs/images/icons/apple-icon-144x144.png -------------------------------------------------------------------------------- /docs/images/icons/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J3-Tech/Vue-Tree-Navigation/HEAD/docs/images/icons/apple-icon-152x152.png -------------------------------------------------------------------------------- /docs/images/icons/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J3-Tech/Vue-Tree-Navigation/HEAD/docs/images/icons/apple-icon-180x180.png -------------------------------------------------------------------------------- /docs/images/icons/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J3-Tech/Vue-Tree-Navigation/HEAD/docs/images/icons/apple-icon-57x57.png -------------------------------------------------------------------------------- /docs/images/icons/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J3-Tech/Vue-Tree-Navigation/HEAD/docs/images/icons/apple-icon-60x60.png -------------------------------------------------------------------------------- /docs/images/icons/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J3-Tech/Vue-Tree-Navigation/HEAD/docs/images/icons/apple-icon-72x72.png -------------------------------------------------------------------------------- /docs/images/icons/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J3-Tech/Vue-Tree-Navigation/HEAD/docs/images/icons/apple-icon-76x76.png -------------------------------------------------------------------------------- /docs/images/icons/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J3-Tech/Vue-Tree-Navigation/HEAD/docs/images/icons/android-icon-144x144.png -------------------------------------------------------------------------------- /docs/images/icons/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/J3-Tech/Vue-Tree-Navigation/HEAD/docs/images/icons/android-icon-192x192.png -------------------------------------------------------------------------------- /src/components/NavigationLevel/NavigationLevel.scss: -------------------------------------------------------------------------------- 1 | .navigation-level { 2 | &--closed { 3 | ul { 4 | display: none; 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | node_modules/ 4 | coverage/ 5 | cypress/ 6 | 7 | e2e/**/vue-tree-navigation.js 8 | docs/build.js 9 | .vscode 10 | coverage 11 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "integrationFolder": "e2e/specs", 3 | "fixturesFolder": false, 4 | "pluginsFile": false, 5 | "supportFile": false, 6 | "video": false 7 | } 8 | -------------------------------------------------------------------------------- /dev/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import router from './routes'; 3 | import App from './App.vue'; 4 | 5 | const app = createApp(App) 6 | app.use(router); 7 | app.mount('#app'); 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/prettierrc", 3 | "semi": false, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "printWidth": 100, 7 | "trailingComma": "none" 8 | } -------------------------------------------------------------------------------- /src/components/NavigationItem/NavigationItem.scss: -------------------------------------------------------------------------------- 1 | .navigation-item { 2 | display: inline-block; 3 | padding-top: 5px; 4 | padding-bottom: 5px; 5 | 6 | span { 7 | cursor: pointer; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import TreeNavigation from './components/TreeNavigation/TreeNavigation'; 2 | 3 | module.exports = { 4 | install: function (Vue, options) { 5 | Vue.component('vue-tree-navigation', TreeNavigation); 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.node.json" 6 | }, 7 | { 8 | "path": "./tsconfig.app.json" 9 | }, 10 | { 11 | "path": "./tsconfig.vitest.json" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.vitest.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.app.json", 3 | "exclude": [], 4 | "compilerOptions": { 5 | "composite": true, 6 | "lib": [], 7 | "types": ["node", "jsdom"], 8 | "allowJs": true, 9 | "ignoreDeprecations": "5.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/components/TreeNavigation/TreeNavigation.scss: -------------------------------------------------------------------------------- 1 | .tree-navigation { 2 | display: inline-block; 3 | padding: 0; 4 | margin: 0; 5 | 6 | ul { 7 | padding: 0; 8 | margin: 0; 9 | list-style-type: none; 10 | 11 | li { 12 | padding-left: 20px; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.node.json", 3 | "include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "playwright.config.*"], 4 | "compilerOptions": { 5 | "composite": true, 6 | "types": ["node"], 7 | "ignoreDeprecations": "5.0" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "processors": ["stylelint-processor-arbitrary-tags"], 3 | "extends": [ 4 | "stylelint-config-standard", 5 | "stylelint-config-recess-order" 6 | ], 7 | "plugins": [ 8 | "stylelint-scss" 9 | ], 10 | "rules": { 11 | "declaration-colon-space-after": null 12 | } 13 | } -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.web.json", 3 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], 4 | "exclude": ["src/**/__tests__/*"], 5 | "compilerOptions": { 6 | "composite": true, 7 | "baseUrl": ".", 8 | "paths": { 9 | "@/*": ["./src/*"] 10 | }, 11 | "ignoreDeprecations": "5.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | 3 | import { defineConfig } from 'vite' 4 | import vue from '@vitejs/plugin-vue' 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [vue()], 9 | resolve: { 10 | alias: { 11 | '@': fileURLToPath(new URL('./dev', import.meta.url)) 12 | } 13 | } 14 | }) 15 | -------------------------------------------------------------------------------- /src/components/NavigationToggle/NavigationToggle.scss: -------------------------------------------------------------------------------- 1 | .navigation-toggle { 2 | position: relative; 3 | top: -3px; 4 | padding: 5px 5px 5px 3px; 5 | cursor: pointer; 6 | 7 | &__icon { 8 | display: inline-block; 9 | padding: 3px; 10 | border: solid #000; 11 | border-width: 0 2px 2px 0; 12 | transform: rotate(45deg); 13 | } 14 | 15 | &__icon--closed { 16 | transform: rotate(-45deg); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url' 2 | import { mergeConfig } from 'vite' 3 | import { configDefaults, defineConfig } from 'vitest/config' 4 | import viteConfig from './vite.config' 5 | 6 | export default mergeConfig( 7 | viteConfig, 8 | defineConfig({ 9 | test: { 10 | environment: 'jsdom', 11 | exclude: [...configDefaults.exclude, 'e2e/*'], 12 | root: fileURLToPath(new URL('./', import.meta.url)) 13 | } 14 | }) 15 | ) 16 | -------------------------------------------------------------------------------- /cypress/e2e/runner.js: -------------------------------------------------------------------------------- 1 | const shell = require('shelljs'); 2 | 3 | const tests = ['with-router', 'without-router', 'auto-generated']; 4 | 5 | const runTest = test => { 6 | shell.cp(`./dist/vue-tree-navigation.js`, `./e2e/apps/${test}`); 7 | 8 | shell.exec( 9 | `concurrently --kill-others "http-server -p 8000 ./e2e/apps/${test}" "cypress run --spec ./e2e/specs/${test}.spec.js"` 10 | ); 11 | }; 12 | 13 | shell.exec('yarn build'); 14 | tests.forEach(test => runTest(test)); 15 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yaml: -------------------------------------------------------------------------------- 1 | name: Build Vue 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | 7 | jobs: 8 | build_vue: 9 | runs-on: ubuntu-latest 10 | name: Build Vue 11 | steps: 12 | - uses: actions/checkout@v2 13 | - id: Build-Vue 14 | uses: J3-Tech/VuePagesAction@0.0.7 15 | with: 16 | cname: vue-tree-navigation.j3-tech.com 17 | username: 'J3-Tech' 18 | reponame: 'Vue-Tree-Navigation' 19 | token: ${{ secrets.GITHUB_TOKEN }} # Leave this line unchanged 20 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require('@rushstack/eslint-patch/modern-module-resolution') 3 | 4 | module.exports = { 5 | root: true, 6 | 'extends': [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended', 9 | '@vue/eslint-config-typescript', 10 | '@vue/eslint-config-prettier/skip-formatting' 11 | ], 12 | overrides: [ 13 | { 14 | files: [ 15 | 'cypress/e2e/**/*.{cy,spec}.{js,ts,jsx,tsx}' 16 | ], 17 | 'extends': [ 18 | 'plugin:cypress/recommended' 19 | ] 20 | } 21 | ], 22 | parserOptions: { 23 | ecmaVersion: 'latest' 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/components/NavigationToggle/NavigationToggle.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 24 | 25 | 28 | -------------------------------------------------------------------------------- /src/components/NavigationLevel/NavigationLevel.spec.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import { describe, it, expect } from 'vitest'; 3 | 4 | import NavigationLevel from './NavigationLevel.vue'; 5 | 6 | describe('NavigationLevel ', () => { 7 | it('isVueInstance', () => { 8 | const wrapper = mount(NavigationLevel, { 9 | propsData: { 10 | parentItem: { 11 | meta: { 12 | target: 'https://github.com', 13 | }, 14 | }, 15 | level: 1, 16 | defaultOpenLevel: 1, 17 | }, 18 | }); 19 | 20 | expect(wrapper.exists()).toBe(true); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/components/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * First character should be #. 3 | */ 4 | export const sanitizeElement = (element?: String) => { 5 | if (element === undefined || element === '') { 6 | return element; 7 | } 8 | 9 | if (element[0] !== '#') { 10 | element = '#' + element; 11 | } 12 | 13 | return element; 14 | }; 15 | 16 | /** 17 | * First character should be backslash. 18 | * Last character shouldn't be backslash. 19 | */ 20 | export const sanitizePath = (path?: string): string | undefined => { 21 | if (path === undefined) { 22 | return; 23 | } 24 | 25 | if (path[0] !== '/') { 26 | path = '/' + path; 27 | } 28 | 29 | if (path[path.length - 1] === '/') { 30 | path = path.slice(0, -1); 31 | } 32 | 33 | return path; 34 | }; 35 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | {"background_color":"#ffffff","description":"Vue Tree Navigation","display":"standalone","icons":[{"src":"/images/icons/android-icon-36x36.png","sizes":"36x36","type":"image/png","density":"0.75"},{"src":"/images/icons/android-icon-48x48.png","sizes":"48x48","type":"image/png","density":"1.0"},{"src":"/images/icons/android-icon-72x72.png","sizes":"72x72","type":"image/png","density":"1.5"},{"src":"/images/icons/android-icon-96x96.png","sizes":"96x96","type":"image/png","density":"2.0"},{"src":"/images/icons/android-icon-144x144.png","sizes":"144x144","type":"image/png","density":"3.0"},{"src":"/images/icons/android-icon-192x192.png","sizes":"192x192","type":"image/png","density":"4.0"}],"lang":"en-US","name":"Vue.js","short_name":"Vue","start_url":"./menu","theme_color":"#4fc08d"} -------------------------------------------------------------------------------- /docs/manifest.json: -------------------------------------------------------------------------------- 1 | {"background_color":"#ffffff","description":"Vue Tree Navigation","display":"standalone","icons":[{"src":"/images/icons/android-icon-36x36.png","sizes":"36x36","type":"image/png","density":"0.75"},{"src":"/images/icons/android-icon-48x48.png","sizes":"48x48","type":"image/png","density":"1.0"},{"src":"/images/icons/android-icon-72x72.png","sizes":"72x72","type":"image/png","density":"1.5"},{"src":"/images/icons/android-icon-96x96.png","sizes":"96x96","type":"image/png","density":"2.0"},{"src":"/images/icons/android-icon-144x144.png","sizes":"144x144","type":"image/png","density":"3.0"},{"src":"/images/icons/android-icon-192x192.png","sizes":"192x192","type":"image/png","density":"4.0"}],"lang":"en-US","name":"Vue.js","short_name":"Vue","start_url":"./menu","theme_color":"#4fc08d"} -------------------------------------------------------------------------------- /cypress/e2e/specs/auto-generated.spec.js: -------------------------------------------------------------------------------- 1 | describe('with items automatically generated from vue-router routes', () => { 2 | beforeEach(() => { 3 | cy.visit('http://127.0.0.1:8000'); 4 | }); 5 | 6 | it('renders all menu items with a correct targets', () => { 7 | cy.contains('Home').should('have.attr', 'href', '#/'); 8 | cy.contains('Running').should('have.attr', 'href', '#/running'); 9 | cy.contains('Barefoot').should('have.attr', 'href', '#/running/barefoot'); 10 | cy.contains('Yoga').should('have.attr', 'href', '#/yoga'); 11 | cy.contains('Mats').should('have.attr', 'href', '#/yoga/mats'); 12 | cy.contains('Tops').should('have.attr', 'href', '#/yoga/tops'); 13 | cy.contains('About').should('have.attr', 'href', '#/about'); 14 | cy.contains('Career').should('have.attr', 'href', '#/about/career'); 15 | cy.contains('Design').should('have.attr', 'href', '#/about/career/design'); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/components/NavigationToggle/NavigationToggle.spec.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils'; 2 | import { describe, it, expect } from 'vitest'; 3 | 4 | import NavigationToggle from './NavigationToggle.vue'; 5 | 6 | describe('NavigationToggle ', () => { 7 | it('isVueInstance', () => { 8 | const wrapper = mount(NavigationToggle); 9 | 10 | expect(wrapper.exists()).toBe(true); 11 | }); 12 | 13 | describe('when closed', () => { 14 | it('is assigned closed class', () => { 15 | const wrapper = mount(NavigationToggle, { 16 | propsData: { 17 | open: false, 18 | }, 19 | }); 20 | 21 | expect(wrapper.classes()).toContain('navigation-toggle--closed'); 22 | }); 23 | }); 24 | 25 | describe('when opened', () => { 26 | it('is not assigned closed class', () => { 27 | const wrapper = mount(NavigationToggle, { 28 | propsData: { 29 | open: true, 30 | }, 31 | }); 32 | 33 | expect(wrapper.classes()).not.toContain('navigation-toggle--closed'); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish-github-packages.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | push: 8 | tags: 9 | - '*' 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: actions/setup-node@v3 17 | with: 18 | node-version: 16 19 | - run: npm ci 20 | - run: npm test 21 | 22 | publish-gpr: 23 | needs: build 24 | runs-on: ubuntu-latest 25 | permissions: 26 | contents: read 27 | packages: write 28 | steps: 29 | - uses: actions/checkout@v3 30 | - uses: actions/setup-node@v3 31 | with: 32 | node-version: 16 33 | registry-url: https://npm.pkg.github.com/ 34 | - run: npm ci 35 | - run: npm publish 36 | env: 37 | NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} 38 | -------------------------------------------------------------------------------- /src/components/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | 3 | import { sanitizeElement, sanitizePath } from './utils'; 4 | 5 | describe('utils', () => { 6 | describe('sanitizeElement', () => { 7 | [ 8 | { expected: undefined, element: undefined }, 9 | { expected: '', element: '' }, 10 | { expected: '#element', element: '#element' }, 11 | { expected: '#element', element: 'element' }, 12 | ].forEach((item, i) => { 13 | it('returns %s for path %s', () => { 14 | expect(sanitizeElement(item.element)).toBe(item.expected); 15 | }); 16 | }); 17 | }); 18 | 19 | describe('sanitizePath', () => { 20 | [ 21 | { expected: undefined, path: undefined }, 22 | { expected: '', path: '' }, 23 | { expected: '/path', path: '/path' }, 24 | { expected: '/path', path: 'path' }, 25 | { expected: '/path', path: 'path/' }, 26 | ].forEach((item) => { 27 | it('returns %s for path %s', () => { 28 | expect(sanitizePath(item.path)).toBe(item.expected); 29 | }); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-present, Michaela Robosova 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /dev/pages/Usage.vue: -------------------------------------------------------------------------------- 1 | 32 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [16.x, 18.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - run: npm ci 30 | - run: npm run build --if-present 31 | - run: npm test 32 | 33 | - name: Upload coverage reports to Codecov 34 | uses: codecov/codecov-action@v3 35 | with: 36 | token: ${{ secrets.CODE_COV_TOKEN }} 37 | files: ./coverage/clover.xml 38 | verbose: true 39 | -------------------------------------------------------------------------------- /.github/workflows/static.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["gh-pages"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 20 | concurrency: 21 | group: "pages" 22 | cancel-in-progress: false 23 | 24 | jobs: 25 | # Single deploy job since we're just deploying 26 | deploy: 27 | environment: 28 | name: github-pages 29 | url: ${{ steps.deployment.outputs.page_url }} 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v3 34 | - name: Setup Pages 35 | uses: actions/configure-pages@v3 36 | - name: Upload artifact 37 | uses: actions/upload-pages-artifact@v1 38 | with: 39 | # Upload entire repository 40 | path: '.' 41 | - name: Deploy to GitHub Pages 42 | id: deployment 43 | uses: actions/deploy-pages@v1 44 | -------------------------------------------------------------------------------- /dev/pages/Introduction.vue: -------------------------------------------------------------------------------- 1 | 43 | -------------------------------------------------------------------------------- /dev/routes.ts: -------------------------------------------------------------------------------- 1 | import ExamplesAuto from './pages/Examples/Auto.vue' 2 | import ExamplesManual from './pages/Examples/Manual.vue' 3 | import ExamplesWebsite from './pages/Examples/Website.vue' 4 | 5 | import Introduction from './pages/Introduction.vue'; 6 | import Installation from './pages/Installation.vue'; 7 | import Usage from './pages/Usage.vue'; 8 | 9 | import { createRouter, createWebHistory } from 'vue-router' 10 | 11 | const routes = [ 12 | { 13 | path: '/', 14 | component: Introduction, 15 | }, 16 | { 17 | path: '/installation', 18 | component: Installation, 19 | }, 20 | { 21 | path: '/installation/usage', 22 | component: Usage, 23 | }, 24 | { 25 | path: '/examples', 26 | component: ExamplesWebsite, 27 | children: [ 28 | { 29 | path: '/examples/auto', 30 | component: ExamplesAuto, 31 | }, 32 | { 33 | path: '/examples/manually-defined', 34 | component: ExamplesManual, 35 | }, 36 | { 37 | path: '/examples/this-website', 38 | component: ExamplesWebsite, 39 | }, 40 | ], 41 | }, 42 | ]; 43 | 44 | const router = createRouter({ 45 | history: createWebHistory(import.meta.env.BASE_URL), 46 | routes: routes, 47 | scrollBehavior(to, from, savedPosition) { 48 | if (to.hash) { 49 | return { 50 | el: to.hash, 51 | behavior: 'smooth', 52 | top: -10, 53 | } 54 | } 55 | } 56 | }) 57 | 58 | export default router 59 | -------------------------------------------------------------------------------- /src/components/TreeNavigation/TreeNavigation.ts: -------------------------------------------------------------------------------- 1 | import { insertMetadataToNavItems, generateLevel } from './core'; 2 | import { h } from 'vue'; 3 | import './TreeNavigation.scss'; 4 | 5 | const TreeNavigation = { 6 | props: { 7 | items: { 8 | type: Array, 9 | required: false, 10 | default: () => [], 11 | }, 12 | defaultOpenLevel: { 13 | type: Number, 14 | default: 0, 15 | }, 16 | }, 17 | computed: { 18 | navItems(props: any): any { 19 | if (props.items && props.items.length) { 20 | return props.items; 21 | } 22 | 23 | if ( 24 | props.$router && 25 | props.$router.options && 26 | props.$router.options.routes && 27 | props.$router.options.routes.length 28 | ) { 29 | return props.$router.options.routes; 30 | } 31 | 32 | console.warn( 33 | "[VueTreeNavigation]: Haven't you forget to provide items or define vue-router routes?" 34 | ); 35 | return []; 36 | }, 37 | navItemsWithMetadata() { 38 | const navItems = JSON.parse(JSON.stringify(this.navItems)); 39 | return insertMetadataToNavItems(navItems); 40 | }, 41 | }, 42 | 43 | render(props: any): any { 44 | const level = 1; 45 | const tree: any = h( 46 | 'ul', 47 | generateLevel(props.navItemsWithMetadata, level, props.defaultOpenLevel) 48 | ); 49 | 50 | const level0 = h( 51 | 'div', 52 | { 53 | class: ['navigation-level', 'navigation-level--level-0'], 54 | }, 55 | [tree] 56 | ); 57 | 58 | const treeNavigation: any = h( 59 | 'div', 60 | { 61 | class: 'tree-navigation', 62 | }, 63 | [level0] 64 | ); 65 | 66 | return treeNavigation; 67 | }, 68 | }; 69 | 70 | export default TreeNavigation; 71 | -------------------------------------------------------------------------------- /cypress/e2e/apps/auto-generated/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vue-tree-navigation 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 |
20 | 21 | 22 |
23 | 24 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /dev/pages/Installation.vue: -------------------------------------------------------------------------------- 1 | 46 | -------------------------------------------------------------------------------- /dev/App.scss: -------------------------------------------------------------------------------- 1 | body, 2 | html { 3 | height: 100%; 4 | overflow: hidden; 5 | } 6 | body { 7 | color: #4e4e4e; 8 | } 9 | h1, 10 | h2, 11 | h3, 12 | h4, 13 | h5, 14 | h6 { 15 | color: #35495e; 16 | } 17 | a { 18 | color: #42b883; 19 | } 20 | main, 21 | nav { 22 | height: 100%; 23 | overflow-y: scroll; 24 | } 25 | nav { 26 | padding: 0; 27 | width: 0; 28 | } 29 | main { 30 | -webkit-box-flex: 1; 31 | -ms-flex: 1; 32 | flex: 1; 33 | padding: 25px; 34 | } 35 | main section { 36 | margin-top: 40px; 37 | } 38 | code { 39 | font-family: Roboto Mono, monospace; 40 | font-weight: 400; 41 | } 42 | pre { 43 | border-left-color: #42b883; 44 | } 45 | ul ul { 46 | font-size: 100%; 47 | } 48 | .hljs-number, 49 | .hljs-string { 50 | color: #af00cd; 51 | } 52 | .tag { 53 | color: #b3b3b3; 54 | } 55 | .additional { 56 | font-size: 0.8em; 57 | } 58 | .container { 59 | height: 100%; 60 | display: -webkit-box; 61 | display: -ms-flexbox; 62 | display: flex; 63 | max-width: 1400px; 64 | padding: 0; 65 | } 66 | .tree-navigation { 67 | margin-top: 42px; 68 | margin-right: 20px; 69 | font-size: 0.9em; 70 | a { 71 | color: inherit; 72 | } 73 | .navigation-level__children { 74 | padding-left: 10px; 75 | } 76 | .navigation-level__parent { 77 | font-weight: 600; 78 | padding-bottom: 5px; 79 | } 80 | .navigation-item { 81 | color: #545454; 82 | padding: 0; 83 | } 84 | .navigation-item--active { 85 | color: #42b883; 86 | } 87 | .navigation-toggle__icon { 88 | border-color: #42b883; 89 | } 90 | } 91 | 92 | #app { 93 | height: 100%; 94 | } 95 | #app.navOpen { 96 | nav { 97 | padding: 20px; 98 | width: 100vw; 99 | } 100 | main { 101 | display: none; 102 | } 103 | } 104 | #hamburger { 105 | position: absolute; 106 | right: 20px; 107 | top: 10px; 108 | z-index: 1; 109 | padding: 10px; 110 | cursor: pointer; 111 | } 112 | #github, 113 | #hamburger { 114 | color: #b3b3b3; 115 | } 116 | #github:hover, 117 | #hamburger:hover { 118 | color: #7f7f7f; 119 | } 120 | #installation div { 121 | margin-top: 50px; 122 | } 123 | 124 | @media only screen and (min-width: 900px) { 125 | #hamburger { 126 | display: none; 127 | } 128 | #github { 129 | float: right; 130 | } 131 | nav { 132 | padding: 20px; 133 | width: auto; 134 | } 135 | main { 136 | padding: 30px 60px 60px; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/components/NavigationLevel/NavigationLevel.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 94 | 95 | 98 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Vue Tree Navigation 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |
37 | 38 | 39 | -------------------------------------------------------------------------------- /dev/App.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 86 | 87 | 90 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Vue Tree Navigation 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 43 | 44 | 45 |
46 | 47 | 48 | -------------------------------------------------------------------------------- /src/components/NavigationItem/NavigationItem.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 103 | 104 | 107 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-tree-navigation", 3 | "version": "5.0.0", 4 | "description": "Vue.js 2 tree navigation", 5 | "keywords": [ 6 | "tree", 7 | "navigation", 8 | "vue", 9 | "vuejs", 10 | "component", 11 | "plugin", 12 | "menu" 13 | ], 14 | "homepage": "https://github.com/J3-Tech/vue-tree-navigation", 15 | "bugs": "https://github.com/J3-Tech/vue-tree-navigation/issues", 16 | "license": "MIT", 17 | "author": "Michaela Robošová ", 18 | "files": [ 19 | "dist" 20 | ], 21 | "main": "dist/vue-tree-navigation.js", 22 | "husky": { 23 | "hooks": { 24 | "pre-commit": "npm run lint && npm run unit" 25 | } 26 | }, 27 | "engines": { 28 | "npm": ">=7.10.0 <=9.5.0", 29 | "node": ">=16.13.0 <=19.7.0" 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "https://github.com/J3-Tech/Vue-Tree-Navigation.git" 34 | }, 35 | "scripts": { 36 | "dev": "vite", 37 | "build": "run-p type-check build-only", 38 | "build:docs": "run-p type-check build-only", 39 | "lint:ts": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", 40 | "lint:scss": "stylelint './**/*.scss'", 41 | "lint": "npm run lint:ts & npm run lint:scss", 42 | "prettier-list": "prettier --list-different '**/*.{js,vue}' --ignore-path .eslintignore", 43 | "prettier": "prettier --write '**/*.{js,vue}' --ignore-path .eslintignore", 44 | "test": "vitest", 45 | "e2e": "node ./e2e/runner.js", 46 | "build-only": "vite build", 47 | "type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false", 48 | "format": "prettier --write src/" 49 | }, 50 | "dependencies": { 51 | "husky": "^9.1.5", 52 | "vue": "^3.2.47", 53 | "vue-router": "^4.1.6" 54 | }, 55 | "devDependencies": { 56 | "@rushstack/eslint-patch": "^1.2.0", 57 | "@types/jsdom": "^21.1.0", 58 | "@types/node": "^22.5.2", 59 | "@vitejs/plugin-vue": "^4.0.0", 60 | "@vue/eslint-config-prettier": "^7.1.0", 61 | "@vue/eslint-config-typescript": "^13.0.0", 62 | "@vue/compiler-sfc": "^3.1.0", 63 | "@vue/test-utils": "^2.3.1", 64 | "@vue/tsconfig": "^0.1.3", 65 | "cypress": "^13.1.0", 66 | "eslint": "^8.34.0", 67 | "eslint-config-prettier": "^9.1.0", 68 | "eslint-config-standard": "^17.0.0", 69 | "eslint-plugin-cypress": "^2.0.1", 70 | "eslint-plugin-html": "^8.1.1", 71 | "eslint-plugin-import": "^2.14.0", 72 | "eslint-plugin-node": "^11.1.0", 73 | "eslint-plugin-promise": "^6.1.1", 74 | "eslint-plugin-standard": "^5.0.0", 75 | "eslint-plugin-vue": "^9.9.0", 76 | "jsdom": "^22.1.0", 77 | "npm-run-all": "^4.1.5", 78 | "prettier": "^2.8.4", 79 | "sass": "^1.59.3", 80 | "shelljs": "^0.8.2", 81 | "start-server-and-test": "^2.0.0", 82 | "stylelint": "~15.11.0", 83 | "stylelint-config-recess-order": "~4.6.0", 84 | "stylelint-config-standard": "~31.0.0", 85 | "stylelint-processor-arbitrary-tags": "^0.1.0", 86 | "stylelint-scss": "~5.3.2", 87 | "typescript": "^5.0.4", 88 | "vite": "^4.1.4", 89 | "vitest": "^0.32.2", 90 | "vue-tsc": "^1.2.0" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /cypress/e2e/apps/without-router/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vue-tree-navigation 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 |
18 | 19 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /cypress/e2e/apps/without-router/running/barefoot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vue-tree-navigation 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 |
18 | 19 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /dev/pages/Examples/Manual.vue: -------------------------------------------------------------------------------- 1 | 61 | -------------------------------------------------------------------------------- /cypress/e2e/specs/without-router.spec.js: -------------------------------------------------------------------------------- 1 | describe('without router', () => { 2 | describe('when loaded first time', () => { 3 | beforeEach(() => { 4 | cy.visit('http://127.0.0.1:8000'); 5 | }); 6 | 7 | it('renders all menu items with a correct targets', () => { 8 | cy.contains('Home').should('have.attr', 'href', ''); 9 | cy.contains('Products').should('have.attr', 'href', '/#products'); 10 | cy.contains('Running').should('have.attr', 'href', '/running'); 11 | cy.contains('Shoes').should('have.attr', 'href', '/running#shoes'); 12 | cy.contains('Barefoot').should('have.attr', 'href', '/running/barefoot'); 13 | cy.contains('Minimal').should('have.attr', 'href', '/running#minimal'); 14 | cy.contains('Yoga').should('have.attr', 'href', '/yoga'); 15 | cy.contains('Mats').should('have.attr', 'href', '/yoga/mats'); 16 | cy.contains('Clothing').should( 17 | 'have.attr', 18 | 'href', 19 | 'https://www.yogarebel.com' 20 | ); 21 | cy.contains('Tops').should('have.attr', 'href', '/yoga/tops'); 22 | cy.contains('About').should('have.attr', 'href', '/about'); 23 | cy.contains('Company').should('have.attr', 'href', '/about#company'); 24 | cy.contains('Career').should('have.attr', 'href', '/about/career'); 25 | cy.contains('Design').should('have.attr', 'href', '/about/career/design'); 26 | cy.contains('Development').should( 27 | 'have.attr', 28 | 'href', 29 | '/about/career#development' 30 | ); 31 | cy.contains('Github').should('have.attr', 'href', 'https://github.com'); 32 | cy.contains('Press').should('have.attr', 'href', '/#press'); 33 | }); 34 | 35 | it('all root items are visible', () => { 36 | cy.contains('Home').should('be.visible'); 37 | cy.contains('Products').should('be.visible'); 38 | cy.contains('About').should('be.visible'); 39 | cy.contains('Github').should('be.visible'); 40 | cy.contains('Press').should('be.visible'); 41 | }); 42 | 43 | it('first level is open', () => { 44 | // "Products" is open 45 | cy.contains('Running').should('be.visible'); 46 | cy.contains('Yoga').should('be.visible'); 47 | 48 | // "About" is open 49 | cy.contains('Company').should('be.visible'); 50 | cy.contains('Career').should('be.visible'); 51 | }); 52 | 53 | it('second level is open', () => { 54 | // "Running" is open 55 | cy.contains('Shoes').should('be.visible'); 56 | 57 | // "Yoga" is open 58 | cy.contains('Mats').should('be.visible'); 59 | cy.contains('Clothing').should('be.visible'); 60 | 61 | // "Career" is open 62 | cy.contains('Design').should('be.visible'); 63 | cy.contains('Development').should('be.visible'); 64 | }); 65 | 66 | it('third level is closed', () => { 67 | // "Shoes" is closed 68 | cy.contains('Barefoot').should('not.be.visible'); 69 | cy.contains('Minimal').should('not.be.visible'); 70 | 71 | // "Clothing" is closed 72 | cy.contains('Tops').should('not.be.visible'); 73 | }); 74 | }); 75 | 76 | describe('when a target URL of menu item visited', () => { 77 | beforeEach(() => { 78 | cy.visit('http://127.0.0.1:8000/running/barefoot'); 79 | }); 80 | 81 | it('opens a corresponding level so the item is visible in menu', () => { 82 | cy.contains('Barefoot').should('be.visible'); 83 | }); 84 | 85 | it('assigns active class to an item which target URL is visited', () => { 86 | cy.contains('Barefoot') 87 | .parent() 88 | .should('have.class', 'NavigationItem--active'); 89 | }); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /cypress/e2e/specs/with-router.spec.js: -------------------------------------------------------------------------------- 1 | describe('with router', () => { 2 | describe('when loaded first time', () => { 3 | beforeEach(() => { 4 | cy.visit('http://127.0.0.1:8000'); 5 | }); 6 | 7 | it('renders all menu items with a correct targets', () => { 8 | cy.contains('Home').should('have.attr', 'href', '#/'); 9 | cy.contains('Products').should('have.attr', 'href', '#/#products'); 10 | cy.contains('Running').should('have.attr', 'href', '#/running'); 11 | cy.contains('Shoes').should('have.attr', 'href', '#/running#shoes'); 12 | cy.contains('Barefoot').should('have.attr', 'href', '#/running/barefoot'); 13 | cy.contains('Minimal').should('have.attr', 'href', '#/running#minimal'); 14 | cy.contains('Yoga').should('have.attr', 'href', '#/yoga'); 15 | cy.contains('Mats').should('have.attr', 'href', '#/yoga/mats'); 16 | cy.contains('Clothing').should( 17 | 'have.attr', 18 | 'href', 19 | 'https://www.yogarebel.com' 20 | ); 21 | cy.contains('Tops').should('have.attr', 'href', '#/yoga/tops'); 22 | cy.contains('About').should('have.attr', 'href', '#/about'); 23 | cy.contains('Company').should('have.attr', 'href', '#/about#company'); 24 | cy.contains('Career').should('have.attr', 'href', '#/about/career'); 25 | cy.contains('Design').should( 26 | 'have.attr', 27 | 'href', 28 | '#/about/career/design' 29 | ); 30 | cy.contains('Development').should( 31 | 'have.attr', 32 | 'href', 33 | '#/about/career#development' 34 | ); 35 | cy.contains('Github').should('have.attr', 'href', 'https://github.com'); 36 | cy.contains('Press').should('have.attr', 'href', '#/#press'); 37 | }); 38 | 39 | it('all root items are visible', () => { 40 | cy.contains('Home').should('be.visible'); 41 | cy.contains('Products').should('be.visible'); 42 | cy.contains('About').should('be.visible'); 43 | cy.contains('Github').should('be.visible'); 44 | cy.contains('Press').should('be.visible'); 45 | }); 46 | 47 | it('first level is open', () => { 48 | // "Products" is open 49 | cy.contains('Running').should('be.visible'); 50 | cy.contains('Yoga').should('be.visible'); 51 | 52 | // "About" is open 53 | cy.contains('Company').should('be.visible'); 54 | cy.contains('Career').should('be.visible'); 55 | }); 56 | 57 | it('second level is open', () => { 58 | // "Running" is open 59 | cy.contains('Shoes').should('be.visible'); 60 | 61 | // "Yoga" is open 62 | cy.contains('Mats').should('be.visible'); 63 | cy.contains('Clothing').should('be.visible'); 64 | 65 | // "Career" is open 66 | cy.contains('Design').should('be.visible'); 67 | cy.contains('Development').should('be.visible'); 68 | }); 69 | 70 | it('third level is closed', () => { 71 | // "Shoes" is closed 72 | cy.contains('Barefoot').should('not.be.visible'); 73 | cy.contains('Minimal').should('not.be.visible'); 74 | 75 | // "Clothing" is closed 76 | cy.contains('Tops').should('not.be.visible'); 77 | }); 78 | }); 79 | 80 | describe('when a target URL of menu item visited', () => { 81 | beforeEach(() => { 82 | cy.visit('http://127.0.0.1:8000/#/running/barefoot'); 83 | }); 84 | 85 | it('opens a corresponding level so the item is visible in menu', () => { 86 | cy.contains('Barefoot').should('be.visible'); 87 | }); 88 | 89 | it('assigns active class to an item which target URL is visited', () => { 90 | cy.contains('Barefoot') 91 | .parent() 92 | .should('have.class', 'NavigationItem--active'); 93 | }); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /src/components/TreeNavigation/core.ts: -------------------------------------------------------------------------------- 1 | import NavigationLevel from '../NavigationLevel/NavigationLevel.vue'; 2 | import NavigationItem from '../NavigationItem/NavigationItem.vue'; 3 | 4 | import { sanitizeElement, sanitizePath } from '../utils'; 5 | import { h } from 'vue'; 6 | 7 | export interface ItemMetaData { 8 | name?: string; 9 | element?: string; 10 | path?: string; 11 | external?: string; 12 | target?: string; 13 | meta?: ItemMetaData; 14 | children?: ItemMetaData[]; 15 | } 16 | 17 | /** 18 | * Recursive function. 19 | * One call generates one level of the tree. 20 | */ 21 | export const generateLevel = ( 22 | //createElement: (el: String | Object | Function, prop: Object, children: VNode[]) => VNode, 23 | items: ItemMetaData[] | undefined, 24 | level: number, 25 | defaultOpenLevel: number 26 | ): any[] => { 27 | const children: any[] = []; 28 | 29 | items?.forEach((item) => { 30 | if (item.children) { 31 | const navLevel = h( 32 | NavigationLevel as never, 33 | { 34 | parentItem: item, 35 | level, 36 | defaultOpenLevel, 37 | }, 38 | () => [...generateLevel(item.children, level + 1, defaultOpenLevel)] 39 | ); 40 | 41 | children.push(h('li', {}, [navLevel])); 42 | } else { 43 | const navItem = h( 44 | NavigationItem as never, 45 | { 46 | item: item, 47 | }, 48 | () => [] 49 | ); 50 | 51 | children.push(h('li', {}, [navItem])); 52 | } 53 | }); 54 | 55 | return children; 56 | }; 57 | 58 | /** 59 | * Recursive function. 60 | * Insert metadata containing the navigation path and its type to each item. 61 | **/ 62 | export const insertMetadataToNavItems = ( 63 | items: ItemMetaData[], 64 | parent?: ItemMetaData 65 | ): ItemMetaData[] => { 66 | items.forEach((item) => { 67 | item.meta = getItemMetadata(item, parent); 68 | 69 | if (item.children) { 70 | item.children = insertMetadataToNavItems(item.children, item); 71 | } 72 | }); 73 | 74 | return items; 75 | }; 76 | 77 | /** 78 | * Return item metadata object: { path: ..., target: ... } 79 | */ 80 | export const getItemMetadata = ( 81 | item: ItemMetaData, 82 | parent?: ItemMetaData 83 | ): ItemMetaData => { 84 | const element = sanitizeElement(item.element); 85 | const path = sanitizePath(item.path); 86 | const external = item.external; 87 | 88 | // item is its own parent 89 | if (parent === undefined) { 90 | if (element === undefined && path === undefined && external === undefined) { 91 | return { 92 | path: '', 93 | target: '', 94 | }; 95 | } 96 | 97 | if (external !== undefined) { 98 | return { 99 | path: '', 100 | target: external, 101 | }; 102 | } 103 | 104 | if (path !== undefined) { 105 | return { 106 | path, 107 | target: path, 108 | }; 109 | } 110 | 111 | if (element !== undefined) { 112 | return { 113 | path: '', 114 | target: '/' + element, 115 | }; 116 | } 117 | } 118 | 119 | const parentPath = sanitizePath(parent?.meta?.path); 120 | 121 | if (external !== undefined) { 122 | return { 123 | path: parentPath, 124 | target: external, 125 | }; 126 | } 127 | 128 | if (path !== undefined) { 129 | return { 130 | path: parentPath + path, 131 | target: parentPath + path, 132 | }; 133 | } 134 | 135 | if (element !== undefined && parentPath !== undefined) { 136 | return { 137 | path: parentPath, 138 | target: sanitizePath(parentPath + element), 139 | }; 140 | } 141 | 142 | return { 143 | path: parentPath, 144 | target: '', 145 | }; 146 | }; 147 | -------------------------------------------------------------------------------- /cypress/e2e/apps/with-router/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vue-tree-navigation 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 |
20 | 21 | 22 |
23 | 24 | 158 | 159 | 160 | -------------------------------------------------------------------------------- /dev/pages/Examples/Auto.vue: -------------------------------------------------------------------------------- 1 | 101 | -------------------------------------------------------------------------------- /src/components/TreeNavigation/core.spec.ts: -------------------------------------------------------------------------------- 1 | import { getItemMetadata } from './core'; 2 | import { describe, it, expect } from 'vitest'; 3 | 4 | describe('TreeNavigation', () => { 5 | describe('core', () => { 6 | describe('getItemMetadata', () => { 7 | describe('without a parent', () => { 8 | describe('with a label item', () => { 9 | it('returns a correct metadata', () => { 10 | const item = { 11 | name: 'item', 12 | }; 13 | 14 | const expected = { 15 | path: '', 16 | target: '', 17 | }; 18 | 19 | expect(getItemMetadata(item)).toEqual(expected); 20 | }); 21 | }); 22 | 23 | describe('with an external item', () => { 24 | it('returns a correct metadata', () => { 25 | const item = { 26 | external: 'https://github.com', 27 | }; 28 | 29 | const expected = { 30 | path: '', 31 | target: 'https://github.com', 32 | }; 33 | 34 | expect(getItemMetadata(item)).toEqual(expected); 35 | }); 36 | }); 37 | 38 | describe('with a path item', () => { 39 | it('returns a correct metadata', () => { 40 | const item = { 41 | path: 'path', 42 | }; 43 | 44 | const expected = { 45 | path: '/path', 46 | target: '/path', 47 | }; 48 | 49 | expect(getItemMetadata(item)).toEqual(expected); 50 | }); 51 | }); 52 | 53 | describe('with an element item', () => { 54 | it('returns a correct metadata', () => { 55 | const item = { 56 | element: 'element', 57 | }; 58 | 59 | const expected = { 60 | path: '', 61 | target: '/#element', 62 | }; 63 | 64 | expect(getItemMetadata(item)).toEqual(expected); 65 | }); 66 | }); 67 | }); 68 | 69 | describe('with a parent', () => { 70 | describe('with a label item', () => { 71 | it('returns correct metadata', () => { 72 | const parent = { 73 | meta: { 74 | path: '/home', 75 | target: '/home#element', 76 | }, 77 | }; 78 | 79 | const item = { 80 | name: 'item', 81 | }; 82 | 83 | const expected = { 84 | path: '/home', 85 | target: '', 86 | }; 87 | 88 | expect(getItemMetadata(item, parent)).toEqual(expected); 89 | }); 90 | }); 91 | 92 | describe('with an external item', () => { 93 | it('returns a correct metadata', () => { 94 | const parent = { 95 | meta: { 96 | path: '/home', 97 | target: '/home#element', 98 | }, 99 | }; 100 | 101 | const item = { 102 | external: 'https://github.com', 103 | }; 104 | 105 | const expected = { 106 | path: '/home', 107 | target: 'https://github.com', 108 | }; 109 | 110 | expect(getItemMetadata(item, parent)).toEqual(expected); 111 | }); 112 | }); 113 | 114 | describe('with a path item', () => { 115 | it('returns a correct metadata', () => { 116 | const parent = { 117 | meta: { 118 | path: '/home', 119 | target: '/home#element', 120 | }, 121 | }; 122 | 123 | const item = { 124 | path: 'path', 125 | }; 126 | 127 | const expected = { 128 | path: '/home/path', 129 | target: '/home/path', 130 | }; 131 | 132 | expect(getItemMetadata(item, parent)).toEqual(expected); 133 | }); 134 | }); 135 | 136 | describe('with an element item', () => { 137 | it('returns a correct metadata', () => { 138 | const parent = { 139 | meta: { 140 | path: '/home', 141 | target: '/home#element', 142 | }, 143 | }; 144 | 145 | const item = { 146 | element: 'contact', 147 | }; 148 | 149 | const expected = { 150 | path: '/home', 151 | target: '/home#contact', 152 | }; 153 | 154 | expect(getItemMetadata(item, parent)).toEqual(expected); 155 | }); 156 | }); 157 | }); 158 | }); 159 | }); 160 | }); 161 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-tree-navigation 2 | [![Version](https://img.shields.io/npm/v/vue-tree-navigation.svg)](https://www.npmjs.com/package/vue-tree-navigation) 3 | [![Node.js CI](https://github.com/J3-Tech/Vue-Tree-Navigation/actions/workflows/node.js.yml/badge.svg)](https://github.com/J3-Tech/Vue-Tree-Navigation/actions/workflows/node.js.yml) 4 | [![Build Vue](https://github.com/J3-Tech/Vue-Tree-Navigation/actions/workflows/deploy.yaml/badge.svg)](https://github.com/J3-Tech/Vue-Tree-Navigation/actions/workflows/deploy.yaml) 5 | [![codecov](https://codecov.io/gh/J3-Tech/Vue-Tree-Navigation/branch/master/graph/badge.svg?token=UMg49v76h5)](https://codecov.io/gh/J3-Tech/Vue-Tree-Navigation) 6 | 7 | > Vue.js tree navigation with vue-router support 8 | 9 | For more detailed information see [documentation/demo](https://vue-tree-navigation.j3-tech.com) 10 | 11 | ## Features 12 | 13 | - unlimited number of levels 14 | - optional [vue-router](https://router.vuejs.org/en/) support (v2.0.0 or higher) 15 | - generate navigation items automatically from _vue-router_ routes or define them manually 16 | - define a default open level 17 | - auto-open a level when a corresponding URL visited 18 | - focused on core functionality, only necessary styles included 19 | - elements are provided with meaningful classes to make customizations comfortable (for example `NavigationItem--active`, `NavigationLevel--level-1`, `NavigationLevel--closed`) 20 | 21 | ## Example 22 | 23 | ### 1. Navigation items generated from _vue-router_ routes 24 | 25 | Let's suppose you use _vue-router_ with the following routes definition 26 | 27 | ```javascript 28 | const routes = [ 29 | { 30 | name: 'Home', 31 | path: '/', 32 | }, 33 | { 34 | name: 'Running', 35 | path: '/running', 36 | children: [ 37 | { 38 | name: 'Barefoot', 39 | path: 'barefoot', 40 | }, 41 | ], 42 | }, 43 | { 44 | name: 'Yoga', 45 | path: '/yoga', 46 | children: [ 47 | { 48 | name: 'Mats', 49 | path: 'mats', 50 | }, 51 | { 52 | name: 'Tops', 53 | path: 'tops', 54 | }, 55 | ], 56 | }, 57 | { 58 | name: 'About', 59 | path: '/about', 60 | children: [ 61 | { 62 | name: 'Career', 63 | path: 'career', 64 | children: [ 65 | { 66 | name: 'Design', 67 | path: 'design', 68 | }, 69 | ], 70 | }, 71 | ], 72 | }, 73 | ]; 74 | ``` 75 | 76 | Then simply include _vue-tree-navigation_ 77 | 78 | ```html 79 | 82 | ``` 83 | 84 | and it will generate the following menu: 85 | 86 | ``` 87 | - Home // --> / 88 | - Running // --> /running 89 | - Barefoot // --> /running/barefoot 90 | - Yoga // --> /yoga 91 | - Mats // --> /yoga/mats 92 | - Tops // --> /yoga/tops 93 | - About // --> /about 94 | - Career // --> /about/career 95 | - Design // --> /about/career/design 96 | ``` 97 | 98 | Do not forget to use named routes since _vue-tree-navigation_ uses `name` field to label navigation items. 99 | 100 | ### 2. Menu items defined manually 101 | 102 | The following configuration 103 | 104 | ```html 105 | 108 | 109 | 129 | ``` 130 | 131 | will generate 132 | 133 | ``` 134 | - Products // category label 135 | - Shoes // --> /shoes 136 | - About // --> /about 137 | - Contact // --> /about/contact 138 | - E-mail // --> /about/contact#email 139 | - Phone // --> /about/contact#phone 140 | - Github // --> https://github.com 141 | ``` 142 | 143 | For more examples see [documentation/demo](https://vue-tree-navigation.j3-tech.com) 144 | 145 | ## Installation 146 | 147 | ### NPM 148 | 149 | ```console 150 | $ npm install vue-tree-navigation 151 | ``` 152 | 153 | _main.js_ 154 | 155 | ```javascript 156 | import VueTreeNavigation from 'vue-tree-navigation'; 157 | 158 | Vue.use(VueTreeNavigation); 159 | ``` 160 | 161 | ### Include with a script tag 162 | 163 | ```html 164 | 165 | 166 | 169 | ``` 170 | 171 | _Example_ 172 | 173 | ```html 174 |
175 | 176 |
177 | 178 | 190 | ``` 191 | 192 | ## Requirements 193 | 194 | - [Vue.js](https://v2.vuejs.org/) 195 | 196 | ## Developers 197 | 198 | ```console 199 | $ yarn dev 200 | 201 | $ yarn build 202 | 203 | $ yarn prettier 204 | $ yarn lint 205 | 206 | $ yarn unit 207 | $ yarn unit --verbose 208 | 209 | $ yarn e2e 210 | ``` 211 | -------------------------------------------------------------------------------- /dev/pages/Examples/Website.vue: -------------------------------------------------------------------------------- 1 | 116 | --------------------------------------------------------------------------------