├── .babelrc ├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── README.ru.md ├── assets ├── figure1.png ├── figure2.png ├── figure3.png └── figure4.png ├── dist ├── global │ ├── vue-simple-menu.js │ └── vue-simple-menu.min.js ├── styles │ ├── vue-simple-menu.default.css │ └── vue-simple-menu.default.min.css └── vue-simple-menu.js ├── docs ├── index.html └── main.js ├── karma.conf.js ├── package-lock.json ├── package.json ├── src ├── index.html ├── scripts │ ├── index.js │ ├── lib │ │ ├── VueSimpleMenu.vue │ │ └── VueSimpleMenuItem.vue │ ├── plugin.js │ ├── rawMenuData.js │ └── rawMenuData4Test.js └── styles │ ├── default.sass │ └── index.sass ├── test └── index.js ├── webpack.config.js ├── webpack.docs.config.js └── webpack.test.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"], 3 | "plugins": [ 4 | "@babel/plugin-syntax-dynamic-import", 5 | "@babel/plugin-proposal-object-rest-spread" 6 | ], 7 | } 8 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "plugins": ["html"] 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm-debug.log 4 | dist/styles/*.js 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | addons: 5 | chrome: stable 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 RG team 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue simple menu 2 | 3 | Vue component for fast create simple menu block 4 | 5 | > I will be glad to correct the inaccuracy of the my English 😄 6 | 7 | [Описание на русском языке](README.ru.md) 8 | 9 | [![Build Status](https://travis-ci.org/RGRU/vue-simple-menu.svg?branch=master)](https://travis-ci.org/RGRU/vue-simple-menu) 10 | [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) 11 | [![npm version](https://badge.fury.io/js/vue-simple-menu.svg)](https://badge.fury.io/js/vue-simple-menu) 12 | 13 | ## For whom? 14 | 15 | Simple and easy menu with a set of basic functionality, which is enought in 80% of cases: 16 | * Menu items with direct link (href="/url.html") 17 | * Compatibility with vue-router 18 | * Menu items can be toggle expand 19 | * Menu items with infinity nesting 20 | * Stylize as you want (you can select default or make and require own style) 21 | 22 | # Installation and usage 23 | 24 | ## ES6 via npm 25 | 26 | ```sh 27 | npm i vue-simple-menu -D 28 | ``` 29 | 30 | ### Usage 31 | 32 | For example, we have app container, and menu component inside 33 | 34 | ```html 35 |
36 | 37 |
38 | ``` 39 | 40 | For building menu, you need pass to `raw-menu-data` data of menu, must be of a certain format 41 | 42 | Params 43 | 44 | | Name | Type | Description | 45 | |:-- |:-- |:-- | 46 | | id | string | ID for item. It is link to itself id key (figure 1)
ID format as you want | 47 | | name | string | Name or title for menu item element (figure 2) | 48 | | uri | string | Add link to item element (figure 3) | 49 | | list | array: object | Add children elements to item (figure 4)
The structure of nesting objects repeats the main parent | 50 | 51 | __Pictures for data params__ 52 | 53 | __figure 1__ Identificator for item. It is link to itself id key 54 | ![figure1](./assets/figure1.png) 55 | 56 | __figure 2__ Name or title for menu item element 57 | ![figure2](./assets/figure2.png) 58 | 59 | __figure 3__ Add link to item element 60 | ![figure3](./assets/figure3.png) 61 | 62 | __figure 4__ Add children elements to item 63 | ![figure4](./assets/figure4.png) 64 | 65 | For example file rawMenuData.js 66 | 67 | ```js 68 | export default { 69 | item1: { 70 | id: 'item1', 71 | name: 'Item 1', 72 | 73 | // Item can be as link 74 | uri: '//rg.ru' 75 | }, 76 | item2: { 77 | id: 'item1', 78 | name: 'Item 1', 79 | uri: '//rg.ru', 80 | 81 | // Item can have as child items list 82 | list: { 83 | item1_1: { 84 | id: 'item1_1', 85 | name: 'Item 1_1', 86 | 87 | // List items may be endless 88 | list: { 89 | item1_1_1: { 90 | id: 'item1_1_1', 91 | name: 'Item 1_1_1', 92 | uri: '//rg.ru' 93 | } 94 | } 95 | } 96 | ... 97 | } 98 | } 99 | ... 100 | } 101 | ``` 102 | 103 | And add imported menu component to Vue app. Menu data pass as component 104 | 105 | ```js 106 | import Vue from 'vue' 107 | import VueSimpleMenu from 'vue-simple-menu' 108 | 109 | // Data for menu, may get by APi or somehow else 110 | import rawMenuData from './rawMenuData' 111 | 112 | // Add style for menu 113 | require('../styles/default.sass') 114 | 115 | // Init vue application 116 | new Vue({ 117 | el: '#app', 118 | data () { 119 | return { 120 | 121 | // Init default data for menu 122 | rawMenuData: {} 123 | } 124 | }, 125 | components: { 126 | 'vue-simple-menu': VueSimpleMenu 127 | } 128 | }) 129 | 130 | // Emulate async getting menu data 131 | setTimeout(function () { 132 | app.rawMenuData = rawMenuData 133 | }, 1000) 134 | ``` 135 | 136 | ## Component as global in browser 137 | 138 | Pass to your html page scripts below 139 | 140 | ```html 141 | 142 | 143 | 144 | 145 | 146 | ``` 147 | 148 | ### Usage 149 | 150 | Add element for our application with menu 151 | 152 | ```html 153 | 154 |
155 | 156 |
157 | ``` 158 | 159 | And use in you scripts some as: 160 | 161 | ```js 162 | // Data for menu, may get by APi or somehow else 163 | import rawMenuData from './rawMenuData' 164 | 165 | // Init vue app with menu component in template 166 | new Vue({ 167 | el: '#app', 168 | data () { 169 | return { 170 | rawMenuData: menuData 171 | } 172 | } 173 | }) 174 | ``` 175 | 176 | ## Usage with Vue Router 177 | 178 | You can use simple menu with [vue router](https://router.vuejs.org/en/essentials/getting-started.html) links 179 | 180 | Just add value `vueRouter: true` in rawMenuData, and items with this value will be work as vue-router link 181 | 182 | Is implied, the vue router is already connected in your app script 183 | 184 | Example below 185 | 186 | ```js 187 | articles: { 188 | id: 'articles', 189 | name: 'Статьи', 190 | uri: '/articles/list', 191 | 192 | // Add value for associate this item with vue-router 193 | vueRouter: true, 194 | ... 195 | } 196 | ``` 197 | 198 | Done! 199 | 200 | ## Stylize 201 | 202 | You can use default styles for menu. Just require sass or css file to your project from styles folder in src. You should setup webpack config for processing styles (for example [css-loader](https://github.com/webpack-contrib/css-loader)) 203 | 204 | Example 205 | 206 | ```js 207 | // Path where put styles in your own project 208 | require('../styles/default.sass') 209 | ``` 210 | 211 | Or pass default style from CDN 212 | 213 | Example 214 | 215 | ```html 216 | 217 | ``` 218 | -------------------------------------------------------------------------------- /README.ru.md: -------------------------------------------------------------------------------- 1 | # Vue simple menu 2 | 3 | Компонент vue для быстрого создания блока с меню 4 | 5 | [![Build Status](https://travis-ci.org/RGRU/vue-simple-menu.svg?branch=master)](https://travis-ci.org/RGRU/vue-simple-menu) 6 | [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) 7 | [![npm version](https://badge.fury.io/js/vue-simple-menu.svg)](https://badge.fury.io/js/vue-simple-menu) 8 | 9 | ## Для кого? 10 | 11 | Самое легкое меню, покрывающее своей функциональностью 80% случаев его использования: 12 | * По клику на элемент меню - переход на другую страницу 13 | * Совместимость с vue router (по клику на элемент, срабатывает хук роутера) 14 | * Элемент меню как раскрывающийся список подменю (без перехода по клику) 15 | * Вложенность подменю может быть бесконечной 16 | * Можно стилизовать блок как вам хочется или использовать как отправную точку стилизацию из этого пакета 17 | 18 | # Установка и использование 19 | 20 | ## ES6 через npm 21 | 22 | ```sh 23 | npm i vue-simple-menu -D 24 | ``` 25 | 26 | ### Использование 27 | 28 | Например у нас есть контейнер `app` и компонент с меню внутри него 29 | 30 | ```html 31 |
32 | 33 |
34 | ``` 35 | 36 | Для создания меню нам нужно передать в свойство `raw-menu-data` данные для меню. Причем они должны быть определенного формата, описанного ниже 37 | 38 | Параметры данных для меню 39 | 40 | | Название | Тип | Описание | 41 | |:-- |:-- |:-- | 42 | | id | string | ID для элемента, который нужно продублировать в свойство id (изображение 1)
Формат id может быть любой, какой вы сами выберете | 43 | | name | string | Название элемента меню (изображение 2) | 44 | | uri | string | Ссылка с элемента меню (изображение 3) | 45 | | list | array: object | Дочерние подуровни у элемента меню (изображение 4)
Структура дочерних объектов полностью повторяет основную родительскую | 46 | 47 | __Изображения к параметрам данных для меню__ 48 | 49 | __Изображение 1__ ID для элемента меню 50 | ![figure1](./assets/figure1.png) 51 | 52 | __Изображение 2__ Название элемента меню 53 | ![figure2](./assets/figure2.png) 54 | 55 | __Изображение 3__ Добавление ссылки к элементу меню 56 | ![figure3](./assets/figure3.png) 57 | 58 | __Изображение 4__ Добавление дочерних элементов 59 | ![figure4](./assets/figure4.png) 60 | 61 | В итоге файл с данными получится примерно таким rawMenuData.js 62 | 63 | ```js 64 | export default { 65 | item1: { 66 | id: 'item1', 67 | name: 'Item 1', 68 | 69 | // Элемент меню как кликабельная ссылка, 70 | // из-за указания этого свойства 71 | uri: '//rg.ru' 72 | }, 73 | item2: { 74 | id: 'item1', 75 | name: 'Item 1', 76 | uri: '//rg.ru', 77 | 78 | // Элемент имеет дочерние подуровни 79 | list: { 80 | item1_1: { 81 | id: 'item1_1', 82 | name: 'Item 1_1', 83 | 84 | // Подуровни могут быть бесконечными 85 | list: { 86 | item1_1_1: { 87 | id: 'item1_1_1', 88 | name: 'Item 1_1_1', 89 | uri: '//rg.ru' 90 | } 91 | } 92 | } 93 | ... 94 | } 95 | } 96 | ... 97 | } 98 | ``` 99 | 100 | Добавляем компонент меню в приложение, как компонент и передаем в него заранее подготовленные данные 101 | 102 | ```js 103 | import Vue from 'vue' 104 | import VueSimpleMenu from 'vue-simple-menu' 105 | 106 | // Подготовленные данные для меню 107 | import rawMenuData from './rawMenuData' 108 | 109 | // Добавляем стили для меню 110 | require('../styles/default.sass') 111 | 112 | // Инициализация приложения 113 | new Vue({ 114 | el: '#app', 115 | data () { 116 | return { 117 | 118 | // Инициализация данных для меню по-умолчанию 119 | rawMenuData: {} 120 | } 121 | }, 122 | 123 | // Добавляем компонент 124 | components: { 125 | 'vue-simple-menu': VueSimpleMenu 126 | } 127 | }) 128 | 129 | // Эмулируем асинхронное получение данных для меню 130 | setTimeout(function () { 131 | app.rawMenuData = rawMenuData 132 | }, 1000) 133 | ``` 134 | 135 | ## Использование компонента напрямую в брузере 136 | 137 | Просто поместите на вашу страницу скрипты с библиотекой vue и самим компонентом меню 138 | 139 | ```html 140 | 141 | 142 | 143 | 144 | 145 | ``` 146 | 147 | ### Использование 148 | 149 | Добавьте компонент в ваше приложение 150 | 151 | ```html 152 | 153 |
154 | 155 |
156 | ``` 157 | 158 | В скриптах используйте так: 159 | 160 | ```js 161 | // Подготовленные данные для меню 162 | import rawMenuData from './rawMenuData' 163 | 164 | // Инициализируем компонент меню вместе с данными (он уже подключен глобально, отдельно его никак подключать не надо) 165 | new Vue({ 166 | el: '#app', 167 | data () { 168 | return { 169 | rawMenuData: menuData 170 | } 171 | } 172 | }) 173 | ``` 174 | > Важное примечание. При использовании vue-simple-menu, подключая через тег script, он подключается глобально и поэтому может быть использован корректно только один на странице 175 | 176 | ## Использование вместе с vue router 177 | 178 | Вы можете использовать vue-simple-menu вместе с [vue router](https://router.vuejs.org/en/essentials/getting-started.html) 179 | 180 | Просто добавьте свойство `vueRouter: true` в подготавливаемые данные для меню, и тогда по клику на этот элемент, будет срабатывать событие, перехваченное vue router. 181 | 182 | > Подразумевается, что vue router уже подключен у вас на странице 183 | 184 | Пример 185 | 186 | ```js 187 | articles: { 188 | id: 'articles', 189 | name: 'Статьи', 190 | uri: '/articles/list', 191 | 192 | // Добавим свойство в данных, чтобы связать элемент меню с vue router 193 | vueRouter: true, 194 | ... 195 | } 196 | ``` 197 | 198 | Готово! 199 | 200 | ## Стилизация 201 | 202 | Вы можете использовать стандартные стили для меню. Просто подключите sass или css файл в ваш проект из соответствующих папок репозитория vue-simple-menu. Так же вам нужно будет настроить сборку для корректной обработки стилей внутри js файла (например [css-loader](https://github.com/webpack-contrib/css-loader)) 203 | 204 | Пример 205 | 206 | ```js 207 | // Подключаем стили для меню на страницу 208 | require('../styles/default.sass') 209 | ``` 210 | 211 | Или подключить стандартные стили через CDN 212 | 213 | Пример 214 | 215 | ```html 216 | 217 | ``` 218 | -------------------------------------------------------------------------------- /assets/figure1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RGRU/vue-simple-menu/c08e15b20565696eb2912ba4f0c978e00be8406c/assets/figure1.png -------------------------------------------------------------------------------- /assets/figure2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RGRU/vue-simple-menu/c08e15b20565696eb2912ba4f0c978e00be8406c/assets/figure2.png -------------------------------------------------------------------------------- /assets/figure3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RGRU/vue-simple-menu/c08e15b20565696eb2912ba4f0c978e00be8406c/assets/figure3.png -------------------------------------------------------------------------------- /assets/figure4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RGRU/vue-simple-menu/c08e15b20565696eb2912ba4f0c978e00be8406c/assets/figure4.png -------------------------------------------------------------------------------- /dist/global/vue-simple-menu.js: -------------------------------------------------------------------------------- 1 | window["VueSimpleMenu"] = 2 | /******/ (function(modules) { // webpackBootstrap 3 | /******/ // The module cache 4 | /******/ var installedModules = {}; 5 | /******/ 6 | /******/ // The require function 7 | /******/ function __webpack_require__(moduleId) { 8 | /******/ 9 | /******/ // Check if module is in cache 10 | /******/ if(installedModules[moduleId]) { 11 | /******/ return installedModules[moduleId].exports; 12 | /******/ } 13 | /******/ // Create a new module (and put it into the cache) 14 | /******/ var module = installedModules[moduleId] = { 15 | /******/ i: moduleId, 16 | /******/ l: false, 17 | /******/ exports: {} 18 | /******/ }; 19 | /******/ 20 | /******/ // Execute the module function 21 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 22 | /******/ 23 | /******/ // Flag the module as loaded 24 | /******/ module.l = true; 25 | /******/ 26 | /******/ // Return the exports of the module 27 | /******/ return module.exports; 28 | /******/ } 29 | /******/ 30 | /******/ 31 | /******/ // expose the modules object (__webpack_modules__) 32 | /******/ __webpack_require__.m = modules; 33 | /******/ 34 | /******/ // expose the module cache 35 | /******/ __webpack_require__.c = installedModules; 36 | /******/ 37 | /******/ // define getter function for harmony exports 38 | /******/ __webpack_require__.d = function(exports, name, getter) { 39 | /******/ if(!__webpack_require__.o(exports, name)) { 40 | /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); 41 | /******/ } 42 | /******/ }; 43 | /******/ 44 | /******/ // define __esModule on exports 45 | /******/ __webpack_require__.r = function(exports) { 46 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { 47 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); 48 | /******/ } 49 | /******/ Object.defineProperty(exports, '__esModule', { value: true }); 50 | /******/ }; 51 | /******/ 52 | /******/ // create a fake namespace object 53 | /******/ // mode & 1: value is a module id, require it 54 | /******/ // mode & 2: merge all properties of value into the ns 55 | /******/ // mode & 4: return value when already ns object 56 | /******/ // mode & 8|1: behave like require 57 | /******/ __webpack_require__.t = function(value, mode) { 58 | /******/ if(mode & 1) value = __webpack_require__(value); 59 | /******/ if(mode & 8) return value; 60 | /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; 61 | /******/ var ns = Object.create(null); 62 | /******/ __webpack_require__.r(ns); 63 | /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); 64 | /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); 65 | /******/ return ns; 66 | /******/ }; 67 | /******/ 68 | /******/ // getDefaultExport function for compatibility with non-harmony modules 69 | /******/ __webpack_require__.n = function(module) { 70 | /******/ var getter = module && module.__esModule ? 71 | /******/ function getDefault() { return module['default']; } : 72 | /******/ function getModuleExports() { return module; }; 73 | /******/ __webpack_require__.d(getter, 'a', getter); 74 | /******/ return getter; 75 | /******/ }; 76 | /******/ 77 | /******/ // Object.prototype.hasOwnProperty.call 78 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 79 | /******/ 80 | /******/ // __webpack_public_path__ 81 | /******/ __webpack_require__.p = ""; 82 | /******/ 83 | /******/ 84 | /******/ // Load entry module and return exports 85 | /******/ return __webpack_require__(__webpack_require__.s = 0); 86 | /******/ }) 87 | /************************************************************************/ 88 | /******/ ([ 89 | /* 0 */ 90 | /***/ (function(module, __webpack_exports__, __webpack_require__) { 91 | 92 | "use strict"; 93 | __webpack_require__.r(__webpack_exports__); 94 | 95 | // CONCATENATED MODULE: ./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib??vue-loader-options!./src/scripts/lib/VueSimpleMenuItem.vue?vue&type=template&id=52a158e0& 96 | var render = function() { 97 | var _vm = this 98 | var _h = _vm.$createElement 99 | var _c = _vm._self._c || _h 100 | return _c( 101 | "ul", 102 | { staticClass: "vue-simple-menu" }, 103 | _vm._l(_vm.menu, function(item) { 104 | return _c( 105 | "li", 106 | { 107 | key: item.id, 108 | staticClass: "vue-simple-menu__item", 109 | class: { 110 | "vue-simple-menu__item_expand": item.expand, 111 | expanded: item.expanded 112 | } 113 | }, 114 | [ 115 | item.vueRouter 116 | ? [ 117 | _c( 118 | "router-link", 119 | { 120 | staticClass: "vue-simple-menu__title vue-simple-menu__link", 121 | attrs: { to: item.uri } 122 | }, 123 | [_vm._v(_vm._s(item.name))] 124 | ) 125 | ] 126 | : [ 127 | item.uri 128 | ? _c( 129 | "a", 130 | { 131 | staticClass: 132 | "vue-simple-menu__title vue-simple-menu__link", 133 | attrs: { href: item.uri } 134 | }, 135 | [_vm._v(_vm._s(item.name))] 136 | ) 137 | : _c( 138 | "span", 139 | { 140 | staticClass: "vue-simple-menu__title", 141 | on: { 142 | click: function($event) { 143 | _vm.expandTrigger(item) 144 | } 145 | } 146 | }, 147 | [_vm._v(_vm._s(item.name))] 148 | ) 149 | ], 150 | _vm._v(" "), 151 | item.list 152 | ? _c( 153 | "div", 154 | { staticClass: "vue-simple-menu__child" }, 155 | [_c("vue-simple-menu-item", { attrs: { menu: item.list } })], 156 | 1 157 | ) 158 | : _vm._e() 159 | ], 160 | 2 161 | ) 162 | }), 163 | 0 164 | ) 165 | } 166 | var staticRenderFns = [] 167 | render._withStripped = true 168 | 169 | 170 | // CONCATENATED MODULE: ./src/scripts/lib/VueSimpleMenuItem.vue?vue&type=template&id=52a158e0& 171 | 172 | // CONCATENATED MODULE: ./node_modules/babel-loader/lib!./node_modules/vue-loader/lib??vue-loader-options!./src/scripts/lib/VueSimpleMenuItem.vue?vue&type=script&lang=js& 173 | // 174 | // 175 | // 176 | // 177 | // 178 | // 179 | // 180 | // 181 | // 182 | // 183 | // 184 | // 185 | // 186 | // 187 | // 188 | // 189 | // 190 | // 191 | /* harmony default export */ var VueSimpleMenuItemvue_type_script_lang_js_ = ({ 192 | name: 'VueSimpleMenuItem', 193 | props: { 194 | menu: { 195 | required: true, 196 | type: Array 197 | } 198 | }, 199 | methods: { 200 | expandTrigger: function expandTrigger(item) { 201 | if (item.expand) item.expanded = !item.expanded; 202 | } 203 | } 204 | }); 205 | // CONCATENATED MODULE: ./src/scripts/lib/VueSimpleMenuItem.vue?vue&type=script&lang=js& 206 | /* harmony default export */ var lib_VueSimpleMenuItemvue_type_script_lang_js_ = (VueSimpleMenuItemvue_type_script_lang_js_); 207 | // CONCATENATED MODULE: ./node_modules/vue-loader/lib/runtime/componentNormalizer.js 208 | /* globals __VUE_SSR_CONTEXT__ */ 209 | 210 | // IMPORTANT: Do NOT use ES2015 features in this file (except for modules). 211 | // This module is a runtime utility for cleaner component module output and will 212 | // be included in the final webpack user bundle. 213 | 214 | function normalizeComponent ( 215 | scriptExports, 216 | render, 217 | staticRenderFns, 218 | functionalTemplate, 219 | injectStyles, 220 | scopeId, 221 | moduleIdentifier, /* server only */ 222 | shadowMode /* vue-cli only */ 223 | ) { 224 | // Vue.extend constructor export interop 225 | var options = typeof scriptExports === 'function' 226 | ? scriptExports.options 227 | : scriptExports 228 | 229 | // render functions 230 | if (render) { 231 | options.render = render 232 | options.staticRenderFns = staticRenderFns 233 | options._compiled = true 234 | } 235 | 236 | // functional template 237 | if (functionalTemplate) { 238 | options.functional = true 239 | } 240 | 241 | // scopedId 242 | if (scopeId) { 243 | options._scopeId = 'data-v-' + scopeId 244 | } 245 | 246 | var hook 247 | if (moduleIdentifier) { // server build 248 | hook = function (context) { 249 | // 2.3 injection 250 | context = 251 | context || // cached call 252 | (this.$vnode && this.$vnode.ssrContext) || // stateful 253 | (this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext) // functional 254 | // 2.2 with runInNewContext: true 255 | if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') { 256 | context = __VUE_SSR_CONTEXT__ 257 | } 258 | // inject component styles 259 | if (injectStyles) { 260 | injectStyles.call(this, context) 261 | } 262 | // register component module identifier for async chunk inferrence 263 | if (context && context._registeredComponents) { 264 | context._registeredComponents.add(moduleIdentifier) 265 | } 266 | } 267 | // used by ssr in case component is cached and beforeCreate 268 | // never gets called 269 | options._ssrRegister = hook 270 | } else if (injectStyles) { 271 | hook = shadowMode 272 | ? function () { injectStyles.call(this, this.$root.$options.shadowRoot) } 273 | : injectStyles 274 | } 275 | 276 | if (hook) { 277 | if (options.functional) { 278 | // for template-only hot-reload because in that case the render fn doesn't 279 | // go through the normalizer 280 | options._injectStyles = hook 281 | // register for functioal component in vue file 282 | var originalRender = options.render 283 | options.render = function renderWithStyleInjection (h, context) { 284 | hook.call(context) 285 | return originalRender(h, context) 286 | } 287 | } else { 288 | // inject component registration as beforeCreate hook 289 | var existing = options.beforeCreate 290 | options.beforeCreate = existing 291 | ? [].concat(existing, hook) 292 | : [hook] 293 | } 294 | } 295 | 296 | return { 297 | exports: scriptExports, 298 | options: options 299 | } 300 | } 301 | 302 | // CONCATENATED MODULE: ./src/scripts/lib/VueSimpleMenuItem.vue 303 | 304 | 305 | 306 | 307 | 308 | /* normalize component */ 309 | 310 | var component = normalizeComponent( 311 | lib_VueSimpleMenuItemvue_type_script_lang_js_, 312 | render, 313 | staticRenderFns, 314 | false, 315 | null, 316 | null, 317 | null 318 | 319 | ) 320 | 321 | /* hot reload */ 322 | if (false) { var api; } 323 | component.options.__file = "src/scripts/lib/VueSimpleMenuItem.vue" 324 | /* harmony default export */ var VueSimpleMenuItem = (component.exports); 325 | // CONCATENATED MODULE: ./node_modules/babel-loader/lib!./node_modules/vue-loader/lib??vue-loader-options!./src/scripts/lib/VueSimpleMenu.vue?vue&type=script&lang=js& 326 | function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; var ownKeys = Object.keys(source); if (typeof Object.getOwnPropertySymbols === 'function') { ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { return Object.getOwnPropertyDescriptor(source, sym).enumerable; })); } ownKeys.forEach(function (key) { _defineProperty(target, key, source[key]); }); } return target; } 327 | 328 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 329 | 330 | 331 | /* harmony default export */ var VueSimpleMenuvue_type_script_lang_js_ = ({ 332 | name: 'VueSimpleMenu', 333 | components: { 334 | 'vue-simple-menu-item': VueSimpleMenuItem 335 | }, 336 | props: { 337 | rawMenuData: { 338 | type: Object, 339 | required: true 340 | }, 341 | defaultName: { 342 | type: String, 343 | default: '' 344 | } 345 | }, 346 | data: function data() { 347 | return { 348 | list: [] 349 | }; 350 | }, 351 | mounted: function mounted() { 352 | if (this.rawMenuData) { 353 | this.list = this.generateBranch(this.rawMenuData); 354 | } 355 | }, 356 | watch: { 357 | rawMenuData: function rawMenuData() { 358 | this.list = this.generateBranch(this.rawMenuData); 359 | } 360 | }, 361 | methods: { 362 | /** 363 | * generateBranch - recursive function for generate menu branch 364 | * 365 | * @param {object} menuBranch branc menu for precessing 366 | * @return {array} complete menu data 367 | */ 368 | generateBranch: function generateBranch(menuBranch) { 369 | var _this = this; 370 | 371 | return Object.keys(menuBranch).reduce(function (acc, item) { 372 | var menuItem = _objectSpread({}, menuBranch[item]); // If have child list items, 373 | // generate child branch 374 | 375 | 376 | if (menuItem.list) menuItem.list = _this.generateBranch(menuItem.list); // If item need expand behavoir 377 | // add property 378 | 379 | if (menuItem.list && !menuItem.uri) { 380 | menuItem.expand = true; 381 | menuItem.expanded = typeof menuItem.expanded === 'boolean' ? menuItem.expanded : true; 382 | } 383 | 384 | return acc.concat(menuItem); 385 | }, []); 386 | } 387 | }, 388 | template: '' 389 | }); 390 | // CONCATENATED MODULE: ./src/scripts/lib/VueSimpleMenu.vue?vue&type=script&lang=js& 391 | /* harmony default export */ var lib_VueSimpleMenuvue_type_script_lang_js_ = (VueSimpleMenuvue_type_script_lang_js_); 392 | // CONCATENATED MODULE: ./src/scripts/lib/VueSimpleMenu.vue 393 | var VueSimpleMenu_render, VueSimpleMenu_staticRenderFns 394 | 395 | 396 | 397 | 398 | /* normalize component */ 399 | 400 | var VueSimpleMenu_component = normalizeComponent( 401 | lib_VueSimpleMenuvue_type_script_lang_js_, 402 | VueSimpleMenu_render, 403 | VueSimpleMenu_staticRenderFns, 404 | false, 405 | null, 406 | null, 407 | null 408 | 409 | ) 410 | 411 | /* hot reload */ 412 | if (false) { var VueSimpleMenu_api; } 413 | VueSimpleMenu_component.options.__file = "src/scripts/lib/VueSimpleMenu.vue" 414 | /* harmony default export */ var VueSimpleMenu = (VueSimpleMenu_component.exports); 415 | // CONCATENATED MODULE: ./src/scripts/plugin.js 416 | /** 417 | * File use for create component for global element, include as script tag 418 | */ 419 | 420 | var VueSimpleMenuPlugin = { 421 | install: function install(Vue) { 422 | Vue.component('vue-simple-menu', VueSimpleMenu); 423 | } 424 | }; 425 | 426 | if (typeof window !== 'undefined' && window.Vue) { 427 | window.Vue.use(VueSimpleMenuPlugin); 428 | } 429 | 430 | /* harmony default export */ var scripts_plugin = __webpack_exports__["default"] = (VueSimpleMenuPlugin); 431 | 432 | /***/ }) 433 | /******/ ])["default"]; -------------------------------------------------------------------------------- /dist/global/vue-simple-menu.min.js: -------------------------------------------------------------------------------- 1 | window.VueSimpleMenu=function(n){var r={};function i(e){if(r[e])return r[e].exports;var t=r[e]={i:e,l:!1,exports:{}};return n[e].call(t.exports,t,t.exports,i),t.l=!0,t.exports}return i.m=n,i.c=r,i.d=function(e,t,n){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(t,e){if(1&e&&(t=i(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var n=Object.create(null);if(i.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var r in t)i.d(n,r,function(e){return t[e]}.bind(null,r));return n},i.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(t,"a",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p="",i(i.s=0)}([function(e,t,n){"use strict";n.r(t);var r=function(){var n=this,e=n.$createElement,r=n._self._c||e;return r("ul",{staticClass:"vue-simple-menu"},n._l(n.menu,function(t){return r("li",{key:t.id,staticClass:"vue-simple-menu__item",class:{"vue-simple-menu__item_expand":t.expand,expanded:t.expanded}},[t.vueRouter?[r("router-link",{staticClass:"vue-simple-menu__title vue-simple-menu__link",attrs:{to:t.uri}},[n._v(n._s(t.name))])]:[t.uri?r("a",{staticClass:"vue-simple-menu__title vue-simple-menu__link",attrs:{href:t.uri}},[n._v(n._s(t.name))]):r("span",{staticClass:"vue-simple-menu__title",on:{click:function(e){n.expandTrigger(t)}}},[n._v(n._s(t.name))])],n._v(" "),t.list?r("div",{staticClass:"vue-simple-menu__child"},[r("vue-simple-menu-item",{attrs:{menu:t.list}})],1):n._e()],2)}),0)};function i(e,t,n,r,i,u,o,a){var s,l="function"==typeof e?e.options:e;if(t&&(l.render=t,l.staticRenderFns=n,l._compiled=!0),r&&(l.functional=!0),u&&(l._scopeId="data-v-"+u),o?(s=function(e){(e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext)||"undefined"==typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),i&&i.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(o)},l._ssrRegister=s):i&&(s=a?function(){i.call(this,this.$root.$options.shadowRoot)}:i),s)if(l.functional){l._injectStyles=s;var c=l.render;l.render=function(e,t){return s.call(t),c(e,t)}}else{var p=l.beforeCreate;l.beforeCreate=p?[].concat(p,s):[s]}return{exports:e,options:l}}var u=i({name:"VueSimpleMenuItem",props:{menu:{required:r._withStripped=!0,type:Array}},methods:{expandTrigger:function(e){e.expand&&(e.expanded=!e.expanded)}}},r,[],!1,null,null,null);u.options.__file="src/scripts/lib/VueSimpleMenuItem.vue";var o=i({name:"VueSimpleMenu",components:{"vue-simple-menu-item":u.exports},props:{rawMenuData:{type:Object,required:!0},defaultName:{type:String,default:""}},data:function(){return{list:[]}},mounted:function(){this.rawMenuData&&(this.list=this.generateBranch(this.rawMenuData))},watch:{rawMenuData:function(){this.list=this.generateBranch(this.rawMenuData)}},methods:{generateBranch:function(r){var i=this;return Object.keys(r).reduce(function(e,t){var n=function(i){for(var e=1;e'},void 0,void 0,!1,null,null,null);o.options.__file="src/scripts/lib/VueSimpleMenu.vue";var a=o.exports,s={install:function(e){e.component("vue-simple-menu",a)}};"undefined"!=typeof window&&window.Vue&&window.Vue.use(s);t.default=s}]).default; -------------------------------------------------------------------------------- /dist/styles/vue-simple-menu.default.css: -------------------------------------------------------------------------------- 1 | .vue-simple-menu { 2 | font-family: Helvetica, sans-serif; 3 | line-height: 1.2; 4 | margin: 0; 5 | padding: 0; 6 | list-style-type: none; } 7 | .vue-simple-menu__title { 8 | font-size: 14px; 9 | min-height: 14px; 10 | padding-top: 16px; 11 | padding-bottom: 16px; 12 | padding-left: 10px; 13 | display: block; 14 | color: white; 15 | cursor: pointer; } 16 | .vue-simple-menu__link { 17 | text-decoration: none; } 18 | .vue-simple-menu__child { 19 | padding-left: 10px; } 20 | .vue-simple-menu__item_expand.expanded { 21 | background: rgba(100, 0, 0, 0.1); } 22 | .vue-simple-menu__item_expand > .vue-simple-menu__title { 23 | position: relative; 24 | background: rgba(100, 0, 0, 0.1); } 25 | .vue-simple-menu__item_expand > .vue-simple-menu__title:after { 26 | content: '\203A'; 27 | margin: auto; 28 | position: absolute; 29 | right: 10px; 30 | color: white; 31 | transition: transform .3s ease; } 32 | .vue-simple-menu__item_expand.expanded > .vue-simple-menu__title:after { 33 | transform: rotate(90deg); } 34 | .vue-simple-menu__item_expand > .vue-simple-menu__child { 35 | display: none; } 36 | .vue-simple-menu__item_expand.expanded > .vue-simple-menu__child { 37 | display: block; } 38 | 39 | -------------------------------------------------------------------------------- /dist/styles/vue-simple-menu.default.min.css: -------------------------------------------------------------------------------- 1 | .vue-simple-menu{font-family:Helvetica,sans-serif;line-height:1.2;margin:0;padding:0;list-style-type:none}.vue-simple-menu__title{font-size:14px;min-height:14px;padding-top:16px;padding-bottom:16px;padding-left:10px;display:block;color:#fff;cursor:pointer}.vue-simple-menu__link{text-decoration:none}.vue-simple-menu__child{padding-left:10px}.vue-simple-menu__item_expand.expanded{background:rgba(100,0,0,.1)}.vue-simple-menu__item_expand>.vue-simple-menu__title{position:relative;background:rgba(100,0,0,.1)}.vue-simple-menu__item_expand>.vue-simple-menu__title:after{content:"\203A";margin:auto;position:absolute;right:10px;color:#fff;transition:transform .3s ease}.vue-simple-menu__item_expand.expanded>.vue-simple-menu__title:after{transform:rotate(90deg)}.vue-simple-menu__item_expand>.vue-simple-menu__child{display:none}.vue-simple-menu__item_expand.expanded>.vue-simple-menu__child{display:block} -------------------------------------------------------------------------------- /dist/vue-simple-menu.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define("VueSimpleMenu",[],t):"object"==typeof exports?exports.VueSimpleMenu=t():e.VueSimpleMenu=t()}(window,function(){return function(e){var t={};function n(r){if(t[r])return t[r].exports;var i=t[r]={i:r,l:!1,exports:{}};return e[r].call(i.exports,i,i.exports,n),i.l=!0,i.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)n.d(r,i,function(t){return e[t]}.bind(null,i));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t,n){"use strict";n.r(t);var r=function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("ul",{staticClass:"vue-simple-menu"},e._l(e.menu,function(t){return n("li",{key:t.id,staticClass:"vue-simple-menu__item",class:{"vue-simple-menu__item_expand":t.expand,expanded:t.expanded}},[t.vueRouter?[n("router-link",{staticClass:"vue-simple-menu__title vue-simple-menu__link",attrs:{to:t.uri}},[e._v(e._s(t.name))])]:[t.uri?n("a",{staticClass:"vue-simple-menu__title vue-simple-menu__link",attrs:{href:t.uri}},[e._v(e._s(t.name))]):n("span",{staticClass:"vue-simple-menu__title",on:{click:function(n){e.expandTrigger(t)}}},[e._v(e._s(t.name))])],e._v(" "),t.list?n("div",{staticClass:"vue-simple-menu__child"},[n("vue-simple-menu-item",{attrs:{menu:t.list}})],1):e._e()],2)}),0)};function i(e,t,n,r,i,u,o,a){var s,l="function"==typeof e?e.options:e;if(t&&(l.render=t,l.staticRenderFns=n,l._compiled=!0),r&&(l.functional=!0),u&&(l._scopeId="data-v-"+u),o?(s=function(e){(e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext)||"undefined"==typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),i&&i.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(o)},l._ssrRegister=s):i&&(s=a?function(){i.call(this,this.$root.$options.shadowRoot)}:i),s)if(l.functional){l._injectStyles=s;var c=l.render;l.render=function(e,t){return s.call(t),c(e,t)}}else{var p=l.beforeCreate;l.beforeCreate=p?[].concat(p,s):[s]}return{exports:e,options:l}}r._withStripped=!0;var u=i({name:"VueSimpleMenuItem",props:{menu:{required:!0,type:Array}},methods:{expandTrigger:function(e){e.expand&&(e.expanded=!e.expanded)}}},r,[],!1,null,null,null);function o(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}u.options.__file="src/scripts/lib/VueSimpleMenuItem.vue";var a=i({name:"VueSimpleMenu",components:{"vue-simple-menu-item":u.exports},props:{rawMenuData:{type:Object,required:!0},defaultName:{type:String,default:""}},data:function(){return{list:[]}},mounted:function(){this.rawMenuData&&(this.list=this.generateBranch(this.rawMenuData))},watch:{rawMenuData:function(){this.list=this.generateBranch(this.rawMenuData)}},methods:{generateBranch:function(e){var t=this;return Object.keys(e).reduce(function(n,r){var i=function(e){for(var t=1;t'},void 0,void 0,!1,null,null,null);a.options.__file="src/scripts/lib/VueSimpleMenu.vue";t.default=a.exports}])}); -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vue simple menu demo 8 | 9 | 10 |
11 | 14 |
15 |
Demo example Vue Simple menu
16 |
    17 |
  • Элементы с пометкой with vue router привязаны к раутингу через vue router
  • 18 |
  • Элементы в данных которых не указано поле name выводят пустой блок
  • 19 |
20 |
21 | 22 |
23 |
24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | const webpackConfig = require('./webpack.test.config') 2 | 3 | module.exports = function(config) { 4 | config.set({ 5 | browsers: ['ChromeHeadless'], 6 | frameworks: ['mocha', 'chai'], 7 | singleRun: true, 8 | autoWatch: false, 9 | reporters: ['mocha'], 10 | files: ['./test/index.js'], 11 | preprocessors: { 12 | './test/index.js': ['webpack'] 13 | }, 14 | webpack: webpackConfig, 15 | webpackMiddleware: { 16 | noInfo: true 17 | } 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-simple-menu", 3 | "version": "0.1.0", 4 | "description": "vue component for fast create simple menu block", 5 | "main": "dist/vue-simple-menu.js", 6 | "unpkg": "dist/global/vue-simple-menu.min.js", 7 | "files": [ 8 | "dist" 9 | ], 10 | "scripts": { 11 | "test": "karma start karma.conf.js", 12 | "build": "npm run test && webpack --mode production && npm run docs", 13 | "build:watch": "webpack --mode production --watch && webpack --config webpack.docs.config.js", 14 | "docs": "webpack --config webpack.docs.config.js", 15 | "dev": "webpack-dev-server --config webpack.docs.config.js --watch" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/RGRU/vue-simple-menu.git" 20 | }, 21 | "keywords": [ 22 | "vue", 23 | "menu", 24 | "vue component", 25 | "simple menu" 26 | ], 27 | "author": "RGRU team | nanomen (https://github.com/RGRU/)", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/RGRU/vue-simple-menu/issues" 31 | }, 32 | "homepage": "https://github.com/RGRU/vue-simple-menu#readme", 33 | "devDependencies": { 34 | "@babel/core": "^7.2.2", 35 | "@babel/plugin-proposal-object-rest-spread": "^7.3.1", 36 | "@babel/plugin-syntax-dynamic-import": "^7.2.0", 37 | "@babel/preset-env": "^7.3.1", 38 | "babel-loader": "^8.0.5", 39 | "chai": "^4.1.2", 40 | "clean-webpack-plugin": "^1.0.1", 41 | "css-loader": "^2.1.0", 42 | "eslint": "^4.12.1", 43 | "eslint-config-standard": "^10.2.1", 44 | "eslint-loader": "^2.1.1", 45 | "eslint-plugin-html": "^4.0.1", 46 | "eslint-plugin-import": "^2.8.0", 47 | "eslint-plugin-node": "^5.2.1", 48 | "eslint-plugin-promise": "^3.6.0", 49 | "eslint-plugin-standard": "^3.0.1", 50 | "html-webpack-plugin": "^3.2.0", 51 | "karma": "^4.0.0", 52 | "karma-chai": "^0.1.0", 53 | "karma-chrome-launcher": "^2.2.0", 54 | "karma-mocha": "^1.3.0", 55 | "karma-mocha-reporter": "^2.2.5", 56 | "karma-webpack": "^3.0.5", 57 | "mini-css-extract-plugin": "^0.5.0", 58 | "mocha": "^4.0.1", 59 | "node-sass": "^4.11.0", 60 | "optimize-css-assets-webpack-plugin": "^5.0.1", 61 | "sass-loader": "^7.1.0", 62 | "style-loader": "^0.23.1", 63 | "uglifyjs-webpack-plugin": "^2.1.1", 64 | "vue-loader": "^15.6.2", 65 | "vue-router": "^3.0.1", 66 | "vue-template-compiler": "^2.5.9", 67 | "webpack": "^4.29.0", 68 | "webpack-cli": "^3.2.1", 69 | "webpack-dev-server": "^3.1.14" 70 | }, 71 | "dependencies": { 72 | "vue": "^2.5.9" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= htmlWebpackPlugin.options.title %> 8 | 9 | 10 |
11 | 14 |
15 |
Demo example Vue Simple menu
16 |
    17 |
  • Элементы с пометкой with vue router привязаны к раутингу через vue router
  • 18 |
  • Элементы в данных которых не указано поле name выводят пустой блок
  • 19 |
20 |
21 | 22 |
23 |
24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /src/scripts/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueSimpleMenu from './lib/VueSimpleMenu.vue' 3 | import rawMenuData from './rawMenuData' 4 | 5 | import VueRouter from 'vue-router' 6 | 7 | Vue.use(VueRouter) 8 | 9 | // Init vue application 10 | const app = new Vue({ 11 | el: '#app', 12 | router: new VueRouter({ 13 | routes: [ 14 | { path: '/articles/list', component: { template: '

Статьи

' } }, 15 | { path: '/rubrics/org', component: { template: '

Организации

' } }, 16 | { path: '/rubrics/reg', component: { template: '

Регионы

' } } 17 | ] 18 | }), 19 | data () { 20 | return { 21 | rawMenuData: {} 22 | } 23 | }, 24 | components: { 25 | 'vue-simple-menu': VueSimpleMenu 26 | } 27 | }) 28 | 29 | // Add global style 30 | require('../styles/index.sass') 31 | 32 | // Add style for menu 33 | require('../styles/default.sass') 34 | 35 | // Emulate async 36 | setTimeout(function () { 37 | app.rawMenuData = rawMenuData 38 | }, 1000) 39 | -------------------------------------------------------------------------------- /src/scripts/lib/VueSimpleMenu.vue: -------------------------------------------------------------------------------- 1 | 63 | -------------------------------------------------------------------------------- /src/scripts/lib/VueSimpleMenuItem.vue: -------------------------------------------------------------------------------- 1 | 19 | 35 | -------------------------------------------------------------------------------- /src/scripts/plugin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * File use for create component for global element, include as script tag 3 | */ 4 | 5 | import VueSimpleMenu from './lib/VueSimpleMenu.vue' 6 | 7 | const VueSimpleMenuPlugin = { 8 | install (Vue) { 9 | Vue.component('vue-simple-menu', VueSimpleMenu) 10 | } 11 | } 12 | 13 | if (typeof window !== 'undefined' && window.Vue) { 14 | window.Vue.use(VueSimpleMenuPlugin) 15 | } 16 | 17 | export default VueSimpleMenuPlugin 18 | -------------------------------------------------------------------------------- /src/scripts/rawMenuData.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 3 | // Элементы меню 4 | articles: { 5 | 6 | // Параметры элемента 7 | id: 'articles', 8 | name: 'Статьи - with vue Router', 9 | vueRouter: true, 10 | uri: '/articles/list', 11 | 12 | // Если есть вложенность 13 | list: { 14 | item1: { 15 | id: 'item1', 16 | name: 'Вложенность 1.1' 17 | }, 18 | item2: { 19 | id: 'item2', 20 | name: 'Вложенность 2.1', 21 | uri: '/test', 22 | list: { 23 | i1: { 24 | id: 'i1', 25 | name: 'Вложенность 2.1' 26 | }, 27 | i2: { 28 | id: 'i2', 29 | name: 'Вложенность 2.2 - vue Router', 30 | list: { 31 | i1: { 32 | id: 'i1', 33 | name: 'Вложенность 3.1' 34 | }, 35 | i2: { 36 | id: 'i2', 37 | name: 'Вложенность 3.2', 38 | uri: '/test2' 39 | }, 40 | i3: { 41 | id: 'i3', 42 | name: 'Вложенность 3.3' 43 | } 44 | } 45 | }, 46 | i3: { 47 | id: 'i3', 48 | name: 'Вложенность 2.3' 49 | } 50 | } 51 | } 52 | } 53 | }, 54 | 55 | blocks: { 56 | id: 'blocks', 57 | name: 'Блоки', 58 | uri: '/blocks/list' 59 | }, 60 | 61 | auth: { 62 | id: 'auth', 63 | list: { 64 | roles: { 65 | id: 'roles', 66 | name: 'Роли', 67 | uri: '/roles/list' 68 | }, 69 | users: { 70 | id: 'users', 71 | name: 'Пользователи', 72 | uri: '/users/list' 73 | } 74 | } 75 | }, 76 | 77 | masks: { 78 | id: 'masks', 79 | name: 'Маски' 80 | }, 81 | 82 | sujets: { 83 | id: 'sujets', 84 | name: 'Сюжеты', 85 | uri: '/sujets/list' 86 | }, 87 | 88 | rubrics: { 89 | id: 'rubrics', 90 | name: 'Рубрики', 91 | expanded: false, 92 | list: { 93 | thema: { 94 | id: 'thema', 95 | name: 'Тематический рубрикатор', 96 | uri: '/rubrics/thema', 97 | list: { 98 | item1: { 99 | id: 'item1', 100 | name: 'Вложенность 1.1' 101 | }, 102 | item2: { 103 | id: 'item2', 104 | name: 'Вложенность 2.1', 105 | uri: '/test', 106 | list: { 107 | i1: { 108 | id: 'i1', 109 | name: 'Вложенность 2.1' 110 | }, 111 | i2: { 112 | id: 'i2', 113 | name: 'Вложенность 2.2', 114 | list: { 115 | i1: { 116 | id: 'i1', 117 | name: 'Вложенность 3.1' 118 | }, 119 | i2: { 120 | id: 'i2', 121 | name: 'Вложенность 3.2', 122 | uri: '/test2' 123 | }, 124 | i3: { 125 | id: 'i3', 126 | name: 'Вложенность 3.3' 127 | } 128 | } 129 | }, 130 | i3: { 131 | id: 'i3', 132 | name: 'Вложенность 2.3' 133 | } 134 | } 135 | } 136 | } 137 | }, 138 | org: { 139 | id: 'org', 140 | name: 'Организации - with vue Router', 141 | vueRouter: true, 142 | uri: '/rubrics/org' 143 | }, 144 | reg: { 145 | id: 'reg', 146 | name: 'Регионы - with vue Router', 147 | vueRouter: true, 148 | uri: '/rubrics/reg' 149 | } 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/scripts/rawMenuData4Test.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 3 | // Элементы меню 4 | articles: { 5 | 6 | // Параметры элемента 7 | id: 'articles', 8 | name: 'Статьи', 9 | uri: '/articles/list', 10 | 11 | // Если есть вложенность 12 | list: { 13 | item1: { 14 | id: 'item1', 15 | name: 'Вложенность 1.1' 16 | }, 17 | item2: { 18 | id: 'item2', 19 | name: 'Вложенность 2.1', 20 | uri: '/test', 21 | list: { 22 | i1: { 23 | id: 'i1', 24 | name: 'Вложенность 2.1' 25 | }, 26 | i2: { 27 | id: 'i2', 28 | name: 'Вложенность 2.2', 29 | list: { 30 | i1: { 31 | id: 'i1', 32 | name: 'Вложенность 3.1' 33 | }, 34 | i2: { 35 | id: 'i2', 36 | name: 'Вложенность 3.2', 37 | uri: '/test2' 38 | }, 39 | i3: { 40 | id: 'i3', 41 | name: 'Вложенность 3.3' 42 | } 43 | } 44 | }, 45 | i3: { 46 | id: 'i3', 47 | name: 'Вложенность 2.3' 48 | } 49 | } 50 | } 51 | } 52 | }, 53 | 54 | blocks: { 55 | id: 'blocks', 56 | name: 'Блоки', 57 | uri: '/blocks/list' 58 | }, 59 | 60 | auth: { 61 | id: 'auth', 62 | list: { 63 | roles: { 64 | id: 'roles', 65 | name: 'Роли', 66 | uri: '/roles/list' 67 | }, 68 | users: { 69 | id: 'users', 70 | name: 'Пользователи', 71 | uri: '/users/list' 72 | } 73 | } 74 | }, 75 | 76 | masks: { 77 | id: 'masks', 78 | name: 'Маски' 79 | }, 80 | 81 | sujets: { 82 | id: 'sujets', 83 | name: 'Сюжеты', 84 | uri: '/sujets/list' 85 | }, 86 | 87 | rubrics: { 88 | id: 'rubrics', 89 | name: 'Рубрики', 90 | expanded: false, 91 | list: { 92 | thema: { 93 | id: 'thema', 94 | name: 'Тематический рубрикатор', 95 | uri: '/rubrics/thema', 96 | list: { 97 | item1: { 98 | id: 'item1', 99 | name: 'Вложенность 1.1' 100 | }, 101 | item2: { 102 | id: 'item2', 103 | name: 'Вложенность 2.1', 104 | uri: '/test', 105 | list: { 106 | i1: { 107 | id: 'i1', 108 | name: 'Вложенность 2.1' 109 | }, 110 | i2: { 111 | id: 'i2', 112 | name: 'Вложенность 2.2', 113 | list: { 114 | i1: { 115 | id: 'i1', 116 | name: 'Вложенность 3.1' 117 | }, 118 | i2: { 119 | id: 'i2', 120 | name: 'Вложенность 3.2', 121 | uri: '/test2' 122 | }, 123 | i3: { 124 | id: 'i3', 125 | name: 'Вложенность 3.3' 126 | } 127 | } 128 | }, 129 | i3: { 130 | id: 'i3', 131 | name: 'Вложенность 2.3' 132 | } 133 | } 134 | } 135 | } 136 | }, 137 | org: { 138 | id: 'org', 139 | name: 'Организации', 140 | uri: '/rubrics/org' 141 | }, 142 | reg: { 143 | id: 'reg', 144 | name: 'Регионы', 145 | uri: '/rubrics/reg' 146 | } 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/styles/default.sass: -------------------------------------------------------------------------------- 1 | $block: '.vue-simple-menu' 2 | 3 | $ff: 'Helvetica, sans-serif' 4 | $c-expand: rgba(100, 0, 0, .1) 5 | $c-expanded: rgba(100, 0, 0, .1) 6 | $c-text: white 7 | 8 | $height: 14px 9 | $indent-v: 16px 10 | $indent-g: 10px 11 | 12 | #{$block} 13 | font-family: #{$ff} 14 | line-height: 1.2 15 | margin: 0 16 | padding: 0 17 | list-style-type: none 18 | 19 | &__title 20 | font-size: $height 21 | min-height: $height 22 | padding-top: $indent-v 23 | padding-bottom: $indent-v 24 | padding-left: $indent-g 25 | display: block 26 | color: $c-text 27 | cursor: pointer 28 | 29 | &__link 30 | text-decoration: none 31 | 32 | &__item 33 | 34 | &__child 35 | padding-left: $indent-g 36 | 37 | &__item_expand 38 | 39 | &.expanded 40 | background: $c-expanded 41 | 42 | &__item_expand > &__title 43 | position: relative 44 | background: $c-expand 45 | 46 | &:after 47 | content: '\203A' 48 | margin: auto 49 | position: absolute 50 | right: $indent-g 51 | color: $c-text 52 | transition: transform .3s ease 53 | 54 | &__item_expand.expanded > &__title 55 | 56 | &:after 57 | transform: rotate(90deg) 58 | 59 | &__item_expand > &__child 60 | display: none 61 | 62 | &__item_expand.expanded > &__child 63 | display: block 64 | -------------------------------------------------------------------------------- /src/styles/index.sass: -------------------------------------------------------------------------------- 1 | /** 2 | * This styles for demo page (docs) 3 | */ 4 | 5 | html, 6 | body, 7 | .app 8 | font-family: sans-serif 9 | height: 100% 10 | margin: 0 11 | padding: 0 12 | 13 | .sidebar 14 | width: 300px 15 | float: left 16 | background: #35353b 17 | background: #b388ff 18 | 19 | .main 20 | width: calc(100% - 340px) 21 | min-height: 100% 22 | padding: 20px 23 | float: right 24 | background: #e4e9f0 25 | 26 | .title 27 | font-size: 24px 28 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | import VueSimpleMenu from '../src/scripts/lib/VueSimpleMenu.vue' 4 | import rawMenuData from '../src/scripts/rawMenuData4Test' 5 | 6 | describe('VueSimpleMenu.vue', () => { 7 | describe('# Init', () => { 8 | it('Component has registered in components list', done => { 9 | 10 | const vm = new Vue({ 11 | template: '
', 12 | data () { 13 | return { 14 | rawMenuData: {} 15 | } 16 | }, 17 | components: { 18 | 'vue-simple-menu': VueSimpleMenu 19 | } 20 | }).$mount() 21 | 22 | // Find component in components list 23 | vm.$nextTick(() => { 24 | expect(typeof vm.$options.components['vue-simple-menu']).to.equal('object') 25 | 26 | done() 27 | }) 28 | }) 29 | 30 | it('Render after mount', done => { 31 | 32 | const vm = new Vue({ 33 | template: '
', 34 | data() { 35 | return { 36 | rawMenuData: {} 37 | } 38 | }, 39 | components: { 40 | 'vue-simple-menu': VueSimpleMenu 41 | } 42 | }).$mount() 43 | 44 | vm.$nextTick(() => { 45 | // Find rendered menu component 46 | expect(!!vm.$el.querySelector('.vue-simple-menu')).to.be.true 47 | 48 | done() 49 | }) 50 | }) 51 | 52 | it('Render empty component when no raw-menu-data', done => { 53 | 54 | const vm = new Vue({ 55 | template: '
', 56 | data () { 57 | return { 58 | rawMenuData: {} 59 | } 60 | }, 61 | components: { 62 | 'vue-simple-menu': VueSimpleMenu 63 | } 64 | }).$mount() 65 | 66 | vm.$nextTick(() => { 67 | // Find rendered menu component 68 | expect(!!vm.$el.querySelector('.vue-simple-menu')).to.be.true 69 | 70 | // If raw data no pass, item elements will not create 71 | expect(vm.$el.querySelectorAll('.vue-simple-menu__item').length === 0).to.be.true 72 | 73 | done() 74 | }) 75 | }) 76 | }) 77 | 78 | describe('# Temlpate structure', () => { 79 | it('Expect list items when pass raw data when created app', done => { 80 | 81 | const vm = new Vue({ 82 | template: '
', 83 | data () { 84 | return { 85 | rawMenuData 86 | } 87 | }, 88 | components: { 89 | 'vue-simple-menu': VueSimpleMenu 90 | } 91 | }).$mount() 92 | 93 | vm.$nextTick(() => { 94 | // If raw data pass, item elements exist 95 | expect(vm.$el.querySelectorAll('.vue-simple-menu__item').length > 0).to.be.true 96 | 97 | done() 98 | }) 99 | }) 100 | 101 | it('Expect list items when pass raw data async', done => { 102 | 103 | const vm = new Vue({ 104 | template: '
', 105 | data () { 106 | return { 107 | rawMenuData: {} 108 | } 109 | }, 110 | components: { 111 | 'vue-simple-menu': VueSimpleMenu 112 | } 113 | }).$mount() 114 | 115 | vm.$nextTick(() => { 116 | // If raw data no pass, item elements will not create 117 | expect(vm.$el.querySelectorAll('.vue-simple-menu__item').length === 0).to.be.true 118 | 119 | // Set raw data 120 | setTimeout(() => { 121 | vm.rawMenuData = rawMenuData 122 | 123 | vm.$nextTick(() => { 124 | // If raw data pass, item elements exist 125 | expect(vm.$el.querySelectorAll('.vue-simple-menu__item').length > 0).to.be.true 126 | 127 | done() 128 | }) 129 | }, 1000) 130 | }) 131 | }) 132 | 133 | it('Check create child branch', done => { 134 | 135 | const vm = new Vue({ 136 | template: '
', 137 | data () { 138 | return { 139 | rawMenuData 140 | } 141 | }, 142 | components: { 143 | 'vue-simple-menu': VueSimpleMenu 144 | } 145 | }).$mount() 146 | 147 | vm.$nextTick(() => { 148 | // Find child elements 149 | expect(vm.$el.querySelectorAll('.vue-simple-menu__child').length > 0).to.be.true 150 | 151 | done() 152 | }) 153 | }) 154 | 155 | it('Check create current item name', done => { 156 | 157 | const vm = new Vue({ 158 | template: '
', 159 | data () { 160 | return { 161 | rawMenuData 162 | } 163 | }, 164 | components: { 165 | 'vue-simple-menu': VueSimpleMenu 166 | } 167 | }).$mount() 168 | 169 | vm.$nextTick(() => { 170 | // Check item name first level (articles) 171 | expect( 172 | vm.$el 173 | .querySelectorAll('.vue-simple-menu > .vue-simple-menu__item')[0] 174 | .querySelector('.vue-simple-menu__link').innerHTML 175 | ).to.equal(rawMenuData.articles.name) 176 | 177 | // Check item name from child level (rubrics -> thema -> item2 -> i2 -> i1 === Вложенность 3.1) 178 | expect( 179 | vm.$el 180 | .querySelector('.vue-simple-menu > .vue-simple-menu__item:nth-child(6)') 181 | .querySelector('.vue-simple-menu__child > .vue-simple-menu > .vue-simple-menu__item:nth-child(1)') 182 | .querySelector('.vue-simple-menu__child > .vue-simple-menu > .vue-simple-menu__item:nth-child(2)') 183 | .querySelector('.vue-simple-menu__child > .vue-simple-menu > .vue-simple-menu__item:nth-child(2)') 184 | .querySelector('.vue-simple-menu__child > .vue-simple-menu > .vue-simple-menu__item:nth-child(1)') 185 | .querySelector('.vue-simple-menu__title') 186 | .innerHTML 187 | ).to.equal(rawMenuData.rubrics.list.thema.list.item2.list.i2.list.i1.name) 188 | 189 | done() 190 | }) 191 | }) 192 | 193 | it('Check create link in item name', done => { 194 | const vm = new Vue({ 195 | template: '
', 196 | data () { 197 | return { 198 | rawMenuData 199 | } 200 | }, 201 | components: { 202 | 'vue-simple-menu': VueSimpleMenu 203 | } 204 | }).$mount() 205 | 206 | vm.$nextTick(() => { 207 | // Check item link first level (articles) 208 | expect( 209 | vm.$el 210 | .querySelectorAll('.vue-simple-menu > .vue-simple-menu__item')[0] 211 | .querySelector('.vue-simple-menu__link') 212 | .getAttribute('href') 213 | ).to.equal(rawMenuData.articles.uri) 214 | 215 | // Check item link from child level (articles -> thema -> item2 -> i2 -> i2 === /test2) 216 | expect( 217 | vm.$el 218 | .querySelector('.vue-simple-menu > .vue-simple-menu__item:nth-child(1)') 219 | .querySelector('.vue-simple-menu__child > .vue-simple-menu > .vue-simple-menu__item:nth-child(2)') 220 | .querySelector('.vue-simple-menu__child > .vue-simple-menu > .vue-simple-menu__item:nth-child(2)') 221 | .querySelector('.vue-simple-menu__link') 222 | .getAttribute('href') 223 | ).to.equal(rawMenuData.articles.list.item2.list.i2.list.i2.uri) 224 | 225 | done() 226 | }) 227 | }) 228 | 229 | it('Items with vue-router', done => { 230 | Vue.use(VueRouter) 231 | 232 | const router = new VueRouter({ 233 | routes: [ 234 | { 235 | path: '/articles/list', 236 | component: { template: '
' } 237 | } 238 | ] 239 | }) 240 | 241 | const vm = new Vue({ 242 | template: ` 243 |
244 | 245 | 246 |
`, 247 | router, 248 | data () { 249 | return { 250 | rawMenuData 251 | } 252 | }, 253 | components: { 254 | 'vue-simple-menu': VueSimpleMenu 255 | } 256 | }).$mount() 257 | 258 | // Emulate click event 259 | router.push('/articles/list') 260 | 261 | vm.$nextTick(() => { 262 | expect(!!vm.$el.querySelector('#childMenu')).to.be.true 263 | 264 | // go back (to default route) 265 | router.go(-1) 266 | 267 | done() 268 | }) 269 | }) 270 | 271 | it('Several menu components in page', done => { 272 | Vue.use(VueRouter) 273 | 274 | const ArticlesList = { 275 | name: 'ArticlesList', 276 | data () { 277 | return { 278 | rawMenuDataTwo: { 279 | item1: { 280 | id: 'item1', 281 | name: 'Item 1', 282 | uri: '//rg.ru' 283 | }, 284 | item2: { 285 | id: 'item2', 286 | name: 'Item 1', 287 | list: { 288 | item1_1: { 289 | id: 'item1_1', 290 | name: 'Item 1_1', 291 | list: { 292 | item1_1_1: { 293 | id: 'item1_1_1', 294 | name: 'Item 1_1_1', 295 | uri: '//rg.ru' 296 | } 297 | } 298 | } 299 | } 300 | } 301 | } 302 | } 303 | }, 304 | components: { 305 | 'vue-simple-menu': VueSimpleMenu 306 | }, 307 | template: ` 308 |
309 | ARTICLES LIST 310 |
311 | 312 |
313 |
` 314 | } 315 | 316 | const router = new VueRouter({ 317 | routes: [ 318 | { 319 | path: '/articles/list', 320 | component: ArticlesList 321 | } 322 | ] 323 | }) 324 | 325 | const vm = new Vue({ 326 | template: ` 327 |
328 | 329 | 330 |
`, 331 | router, 332 | data () { 333 | return { 334 | rawMenuData 335 | } 336 | }, 337 | components: { 338 | 'vue-simple-menu': VueSimpleMenu 339 | } 340 | }).$mount() 341 | 342 | // Emulate click event 343 | router.push('/articles/list') 344 | 345 | vm.$nextTick(() => { 346 | expect(!!vm.$el.querySelector('#childMenu .vue-simple-menu')).to.be.true 347 | 348 | // go back (to default route) 349 | router.go(-1) 350 | 351 | done() 352 | }) 353 | }) 354 | }) 355 | 356 | describe('# Behavior', () => { 357 | it('Trigger expand menu on click first level items', done => { 358 | 359 | const vm = new Vue({ 360 | template: '
', 361 | data () { 362 | return { 363 | rawMenuData 364 | } 365 | }, 366 | components: { 367 | 'vue-simple-menu': VueSimpleMenu 368 | } 369 | }).$mount() 370 | 371 | // Emulate click event 372 | // https://developer.mozilla.org/ru/docs/Web/API/Document/createEvent 373 | const event = document.createEvent('Event') 374 | event.initEvent('click', true, true) 375 | 376 | vm.$nextTick(() => { 377 | let targetItem = vm.$el.querySelector('.wrapper > .vue-simple-menu > .vue-simple-menu__item:nth-child(3)') 378 | let expandStartState = targetItem.classList.contains('expanded') 379 | 380 | // Emulate click event 381 | targetItem.querySelector('.vue-simple-menu__title').dispatchEvent(event) 382 | 383 | // Expand off 384 | vm.$nextTick(() => { 385 | expect(targetItem.classList.contains('expanded')).to.not.equal(expandStartState) 386 | 387 | done() 388 | }) 389 | }) 390 | }) 391 | 392 | it('Trigger expand menu on click children items', done => { 393 | 394 | const vm = new Vue({ 395 | template: '
', 396 | data () { 397 | return { 398 | rawMenuData 399 | } 400 | }, 401 | components: { 402 | 'vue-simple-menu': VueSimpleMenu 403 | } 404 | }).$mount() 405 | 406 | // Emulate click event 407 | // https://developer.mozilla.org/ru/docs/Web/API/Document/createEvent 408 | const event = document.createEvent('Event') 409 | event.initEvent('click', true, true) 410 | 411 | vm.$nextTick(() => { 412 | let targetItem = vm.$el 413 | .querySelector('.wrapper > .vue-simple-menu > .vue-simple-menu__item:nth-child(1)') 414 | .querySelector('.vue-simple-menu__child > .vue-simple-menu > .vue-simple-menu__item:nth-child(2)') 415 | .querySelector('.vue-simple-menu__child > .vue-simple-menu > .vue-simple-menu__item:nth-child(2)') 416 | 417 | let expandStartState = targetItem.classList.contains('expanded') 418 | 419 | // Emulate click event 420 | targetItem.querySelector('.vue-simple-menu__title').dispatchEvent(event) 421 | 422 | // Expand off 423 | vm.$nextTick(() => { 424 | expect(targetItem.classList.contains('expanded')).to.not.equal(expandStartState) 425 | 426 | done() 427 | }) 428 | }) 429 | }) 430 | 431 | it('Turn off menu item if its data has a expanded:false property', done => { 432 | 433 | const vm = new Vue({ 434 | template: '
', 435 | data() { 436 | return { 437 | rawMenuData 438 | } 439 | }, 440 | components: { 441 | 'vue-simple-menu': VueSimpleMenu 442 | } 443 | }).$mount() 444 | 445 | vm.$nextTick(() => { 446 | let itemEl = vm.$el.querySelectorAll('.vue-simple-menu')[0].lastChild 447 | 448 | expect(itemEl.classList.contains('vue-simple-menu__item_expand')).to.be.true 449 | expect(itemEl.classList.contains('expanded')).to.be.false 450 | 451 | done() 452 | }) 453 | }) 454 | }) 455 | }) 456 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin') 2 | const path = require('path') 3 | const VueLoaderPlugin = require('vue-loader/lib/plugin') 4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 5 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin') 6 | 7 | const config = { 8 | module: { 9 | rules: [ 10 | { 11 | test: /\.(js|vue)&/, 12 | enforce: 'pre', 13 | use: ['eslint-loader'] 14 | }, 15 | { 16 | test: /\.js$/, 17 | loader: 'babel-loader' 18 | }, 19 | { 20 | test: /\.vue$/, 21 | loader: 'vue-loader' 22 | } 23 | ] 24 | } 25 | } 26 | 27 | const config4Styles = { 28 | output: { 29 | path: path.join(__dirname, 'dist/styles'), 30 | filename: '[name].js' 31 | }, 32 | plugins: [ 33 | new MiniCssExtractPlugin({ 34 | filename: '[name].css' 35 | }) 36 | ] 37 | } 38 | 39 | module.exports = [ 40 | 41 | // Build as UMD module 42 | Object.assign( 43 | {}, 44 | config, 45 | { 46 | entry: path.join(__dirname, 'src/scripts/lib/VueSimpleMenu.vue'), 47 | output: { 48 | path: path.join(__dirname, 'dist'), 49 | filename: 'vue-simple-menu.js', 50 | libraryTarget: 'umd', 51 | library: 'VueSimpleMenu', 52 | umdNamedDefine: true 53 | }, 54 | plugins: [ 55 | new VueLoaderPlugin() 56 | ] 57 | } 58 | ), 59 | 60 | // Build for using in browser 61 | // as