├── .babelrc ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── .storybook ├── addons.js └── config.js ├── README.md ├── config └── buble.config.js ├── dist └── vue-nested-menu.js ├── example ├── install │ └── index.html └── single-file-component │ └── MyMenu.vue ├── package.json ├── rollup.config.js ├── src ├── App.vue ├── components │ ├── MenuBurger.vue │ ├── MenuPanel.vue │ └── MenuShadow.vue ├── demo-data.js ├── icons │ ├── LeftArrowIcon.vue │ ├── MenuIcon.vue │ └── RightArrowIcon.vue ├── index.js └── mixins │ ├── contentControl.mixin.js │ ├── functionalityStyle.mixin.js │ └── panelControl.mixin.js ├── stories ├── Menu.js ├── Welcome.js └── index.stories.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "modules": false 7 | } 8 | ], 9 | "vue" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; EditorConfig is awesome: http://EditorConfig.org 2 | 3 | ; top-most EditorConfig file 4 | root = true 5 | 6 | ; Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | indent_style = space 12 | indent_size = 4 13 | 14 | charset = utf-8 15 | 16 | trim_trailing_whitespace = true 17 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'env': { 3 | 'browser': true, 4 | 'es6': true, 5 | }, 6 | 'extends': [ 7 | // 'eslint:recommended', 8 | 'plugin:vue/strongly-recommended', 9 | ], 10 | 'parserOptions': { 11 | 'ecmaFeatures': { 12 | 'jsx': true 13 | }, 14 | 'sourceType': 'module', 15 | }, 16 | 'rules': { 17 | 'indent': [ 18 | 'error', 19 | 4, 20 | ], 21 | 'linebreak-style': [ 22 | 'error', 23 | 'unix', 24 | ], 25 | 'quotes': [ 26 | 'error', 27 | 'single', 28 | { 29 | 'allowTemplateLiterals': true, 30 | }, 31 | ], 32 | 'semi': [ 33 | 'error', 34 | 'always' 35 | ], 36 | 'comma-dangle': [ 37 | 'error', 38 | 'always' 39 | ], 40 | 'object-shorthand' : [ 41 | 'error', 42 | ], 43 | 'prefer-arrow-callback': [ 44 | 'error', 45 | { 46 | 'allowUnboundThis': true, 47 | }, 48 | ], 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | npm-debug.log 4 | coverage/ 5 | /node_modules 6 | /.idea 7 | .env 8 | .yarn-error.log -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register'; 2 | import '@storybook/addon-links/register'; 3 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from '@storybook/vue'; 2 | 3 | // automatically import all files ending in *.stories.js 4 | const req = require.context('../stories', true, /.stories.js$/); 5 | function loadStories() { 6 | req.keys().forEach(filename => req(filename)); 7 | } 8 | 9 | configure(loadStories, module); 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-nested-menu [![Version](https://img.shields.io/npm/v/vue-nested-menu.svg)](https://www.npmjs.com/package/vue-nested-menu) 2 | A simple hands-on mobile nested menu UI component with a smooth slide animation. 3 | 4 | [demo](https://guansunyata.me/guansunyata/vue-nested-menu) 5 | 6 | 7 | ## Installation 8 | 9 | #### Yarn / NPM 10 | 11 | ```console 12 | $ yarn add vue-nested-menu 13 | ``` 14 | 15 | *main.js* 16 | 17 | ```javascript 18 | import VueNestedMenu from 'vue-nested-menu'; 19 | 20 | Vue.use(VueNestedMenu); 21 | ``` 22 | 23 | 24 | ## Usage 25 | 26 | #### Basic 27 | *index.html* 28 | ```html 29 |
30 | 31 |
32 | ``` 33 | 34 | *main.js* 35 | ```js 36 | import VueNestedMenu from 'vue-nested-menu'; 37 | 38 | Vue.use(VueNestedMenu) 39 | 40 | new Vue({ 41 | el: '#app', 42 | data: { 43 | menu: { 44 | title: '首頁', 45 | children: [ 46 | { 47 | title: `Today's Deals`, 48 | link: `/today`, 49 | children: [], 50 | }, 51 | { 52 | title: `Shop By Department`, 53 | children: [ 54 | { 55 | title: `Amazon Music`, 56 | link: `/music`, 57 | children: [], 58 | }, 59 | { 60 | title: `CDs and Vinyl`, 61 | link: `/cds`, 62 | children: [], 63 | }, 64 | ], 65 | }, 66 | ], 67 | }, 68 | }, 69 | }); 70 | 71 | ``` 72 | 73 | #### Single File Component 74 | *app.js* 75 | ```js 76 | import VueNestedMenu from 'vue-nested-menu'; 77 | 78 | Vue.use(VueNestedMenu); 79 | 80 | // ... 81 | ``` 82 | 83 | *MyMenu.vue* 84 | ```html 85 | 88 | 89 | 100 | ``` 101 | 102 | ## Styling 103 | You can use following classes for your own customizations 104 | 105 | *default style* 106 | ```scss 107 | 108 | .Menu__header { 109 | display: flex; 110 | align-items: center; 111 | padding-left: 35px; 112 | height: 50px; 113 | color: #fff; 114 | font-size: 16px; 115 | background-color: #232f3e; 116 | cursor: pointer; 117 | 118 | .arrow { 119 | padding-top: 2px; 120 | fill: #fff; 121 | margin-right: 10px; 122 | width: 10px; 123 | height: 100%; 124 | display: flex; 125 | align-items: center; 126 | } 127 | } 128 | 129 | .Menu__list { 130 | list-style: none; 131 | padding-bottom: 2px; 132 | 133 | .separator { 134 | border-bottom: 1px solid #d5dbdb; 135 | padding: 2px 0 0 0; 136 | margin: 0; 137 | } 138 | } 139 | 140 | .Menu__item { 141 | color: #4a4a4a; 142 | padding-left: 35px; 143 | height: 45px; 144 | display: flex; 145 | align-items: center; 146 | cursor: pointer; 147 | 148 | a { 149 | color: #4a4a4a; 150 | text-decoration: none; 151 | } 152 | 153 | .arrow { 154 | padding-top: 2px; 155 | padding-left: 15px; 156 | display: flex; 157 | align-items: center; 158 | width: 10px; 159 | height: 100%; 160 | } 161 | } 162 | ``` 163 | -------------------------------------------------------------------------------- /config/buble.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | objectAssign: 'Object.assign', 3 | }; 4 | -------------------------------------------------------------------------------- /dist/vue-nested-menu.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.VueNestedMenu=t()}(this,function(){var e={title:"首頁",children:[{title:"Today's Deals",children:[]},{title:"Interesting Finds",children:[]},{title:"Your Recommendations",children:[]},{title:"Shop By Department",children:[{title:"Amazon Music",children:[{title:"Amazon Music Unlimited",children:[]},{title:"Prime Music",children:[]},{title:"CDs and Vinyl",children:[]}]},{title:"Prime Vedios",children:[{title:"All Vedio",children:[]},{title:"Included with Prime",children:[]},{title:"Rent or Buy",children:[]}]},{title:"Treasure Truck",children:[]},{title:"Amazon Restaurants",children:[{title:"Thai",children:[]},{title:"Chinese",children:[]},{title:"American",children:[]},{title:"Indian",children:[]},{title:"Popular Restaurants",children:[{title:"Popular Restaurants 1",children:[]},{title:"Popular Restaurants 2",children:[]}]}]}]}]},t={data:function(){return{style_wrapperStyle:{},style_wrapperActiveStyle:{},style_panelStyle:{},style_transitionStyle:{}}},mounted:function(){var e=this.panelWidth,t=this.menuOpenSpeed,n=this.menuSwitchSpeed,i="."+t/10+"s",a="."+n/10+"s",s={width:e+"px",position:"absolute",top:0,left:"-"+e+"px",zIndex:99999,height:"100vh",overflow:"hidden",transition:"left "+i},l={left:0},o={position:"absolute",top:0,zIndex:99999,height:"100vh",width:e+"px",backgroundColor:"#fff"},r={transition:"left "+a};this.style_wrapperStyle=s,this.style_wrapperActiveStyle=l,this.style_panelStyle=o,this.style_transitionStyle=r}},n={data:function(){return{panel_prevPositionStyle:{},panel_stagingPositionStyle:{},panel_nextPositionStyle:{}}},mounted:function(){this.panel_prevPositionStyle=this.$_panelControl_positionSet.prev,this.panel_stagingPositionStyle=this.$_panelControl_positionSet.staging,this.panel_nextPositionStyle=this.$_panelControl_positionSet.next},computed:{$_panelControl_positionSet:function(){return{staging:{left:0},prev:{left:"-"+this.panelWidth+"px"},next:{left:this.panelWidth+"px"}}}},methods:{panel_slideNext:function(){this.panel_stagingPositionStyle=this.$_panelControl_positionSet.prev,this.panel_nextPositionStyle=this.$_panelControl_positionSet.staging},panel_slideBack:function(){this.panel_stagingPositionStyle=this.$_panelControl_positionSet.next,this.panel_prevPositionStyle=this.$_panelControl_positionSet.staging},panel_homingPosition:function(){this.panel_prevPositionStyle=this.$_panelControl_positionSet.prev,this.panel_nextPositionStyle=this.$_panelControl_positionSet.next,this.panel_stagingPositionStyle=this.$_panelControl_positionSet.staging}}},i={data:function(){return{content_prevItem:{},content_currentItem:{},content_nextItem:{},content_parentStack:[]}},methods:{content_setNextItem:function(e){this.content_nextItem=e},content_setPrevItem:function(){this.content_prevItem=this.content_parentStack[this.content_parentStack.length-1]},content_homingItemAfterNext:function(){this.content_prevItem=this.content_currentItem,this.content_currentItem=this.content_nextItem,this.content_nextItem={}},content_homingItemAfterBack:function(){this.content_parentStack.pop(),this.content_currentItem=this.content_prevItem,this.content_nextItem={}},content_pushCurrentToParentStack:function(e){var t=this.content_currentItem;this.content_parentStack.push(t)}}};!function(){if("undefined"!=typeof document){var e=document.head||document.getElementsByTagName("head")[0],t=document.createElement("style"),n=".svg[data-v-674f72f4] { width: 100%; height: 100%; } ";t.type="text/css",t.styleSheet?t.styleSheet.cssText=n:t.appendChild(document.createTextNode(n)),e.appendChild(t)}}();var a={render:function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("svg",{staticClass:"svg",staticStyle:{"enable-background":"new 0 0 492.004 492.004"},attrs:{version:"1.1",id:"Layer_1",xmlns:"http://www.w3.org/2000/svg","xmlns:xlink":"http://www.w3.org/1999/xlink",x:"0px",y:"0px",viewBox:"0 0 492.004 492.004","xml:space":"preserve"}},[n("g",[n("g",[n("path",{attrs:{d:"M382.678,226.804L163.73,7.86C158.666,2.792,151.906,0,144.698,0s-13.968,2.792-19.032,7.86l-16.124,16.12 c-10.492,10.504-10.492,27.576,0,38.064L293.398,245.9l-184.06,184.06c-5.064,5.068-7.86,11.824-7.86,19.028 c0,7.212,2.796,13.968,7.86,19.04l16.124,16.116c5.068,5.068,11.824,7.86,19.032,7.86s13.968-2.792,19.032-7.86L382.678,265 c5.076-5.084,7.864-11.872,7.848-19.088C390.542,238.668,387.754,231.884,382.678,226.804z"}})])])])},staticRenderFns:[],_scopeId:"data-v-674f72f4"};!function(){if("undefined"!=typeof document){var e=document.head||document.getElementsByTagName("head")[0],t=document.createElement("style"),n=".svg[data-v-0671583d] { width: 100%; height: 100%; } ";t.type="text/css",t.styleSheet?t.styleSheet.cssText=n:t.appendChild(document.createTextNode(n)),e.appendChild(t)}}();var s={render:function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("svg",{staticClass:"svg",staticStyle:{"enable-background":"new 0 0 492 492"},attrs:{version:"1.1",id:"Layer_1",xmlns:"http://www.w3.org/2000/svg","xmlns:xlink":"http://www.w3.org/1999/xlink",x:"0px",y:"0px",viewBox:"0 0 492 492","xml:space":"preserve"}},[n("g",[n("g",[n("path",{attrs:{d:"M198.608,246.104L382.664,62.04c5.068-5.056,7.856-11.816,7.856-19.024c0-7.212-2.788-13.968-7.856-19.032l-16.128-16.12 C361.476,2.792,354.712,0,347.504,0s-13.964,2.792-19.028,7.864L109.328,227.008c-5.084,5.08-7.868,11.868-7.848,19.084 c-0.02,7.248,2.76,14.028,7.848,19.112l218.944,218.932c5.064,5.072,11.82,7.864,19.032,7.864c7.208,0,13.964-2.792,19.032-7.864 l16.124-16.12c10.492-10.492,10.492-27.572,0-38.06L198.608,246.104z"}})])])])},staticRenderFns:[],_scopeId:"data-v-0671583d"};!function(){if("undefined"!=typeof document){var e=document.head||document.getElementsByTagName("head")[0],t=document.createElement("style"),n=".svg[data-v-d9c658f4] { width: 100%; height: 100%; } ";t.type="text/css",t.styleSheet?t.styleSheet.cssText=n:t.appendChild(document.createTextNode(n)),e.appendChild(t)}}();var l={render:function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("svg",{staticStyle:{"enable-background":"new 0 0 53 53"},attrs:{version:"1.1",id:"Capa_1",xmlns:"http://www.w3.org/2000/svg","xmlns:xlink":"http://www.w3.org/1999/xlink",x:"0px",y:"0px",viewBox:"0 0 53 53","xml:space":"preserve"}},[n("g",[n("g",[n("path",{attrs:{d:"M2,13.5h49c1.104,0,2-0.896,2-2s-0.896-2-2-2H2c-1.104,0-2,0.896-2,2S0.896,13.5,2,13.5z"}}),e._v(" "),n("path",{attrs:{d:"M2,28.5h49c1.104,0,2-0.896,2-2s-0.896-2-2-2H2c-1.104,0-2,0.896-2,2S0.896,28.5,2,28.5z"}}),e._v(" "),n("path",{attrs:{d:"M2,43.5h49c1.104,0,2-0.896,2-2s-0.896-2-2-2H2c-1.104,0-2,0.896-2,2S0.896,43.5,2,43.5z"}})])])])},staticRenderFns:[],_scopeId:"data-v-d9c658f4"};!function(){if("undefined"!=typeof document){var e=document.head||document.getElementsByTagName("head")[0],t=document.createElement("style"),n=".Menu__burger[data-v-510beae4] { width: 30px; height: 30px; cursor: pointer; } ";t.type="text/css",t.styleSheet?t.styleSheet.cssText=n:t.appendChild(document.createTextNode(n)),e.appendChild(t)}}();var o={render:function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",{staticClass:"Menu__burger",on:{click:e.handleBurgerClicked}},[n("MenuIcon")],1)},staticRenderFns:[],_scopeId:"data-v-510beae4",components:{MenuIcon:l},props:{handleBurgerClicked:{type:Function,required:!0}}};!function(){if("undefined"!=typeof document){var e=document.head||document.getElementsByTagName("head")[0],t=document.createElement("style"),n=".vmenu-shadow[data-v-e1b9ebe2] { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: #333; opacity: 0.5; z-index: 99997; } .vmenu-fade-enter-active[data-v-e1b9ebe2], .vmenu-fade-leave-active[data-v-e1b9ebe2] { transition: opacity .5s; } .vmenu-fade-enter[data-v-e1b9ebe2], .vmenu-fade-leave-to[data-v-e1b9ebe2] { opacity: 0; } ";t.type="text/css",t.styleSheet?t.styleSheet.cssText=n:t.appendChild(document.createTextNode(n)),e.appendChild(t)}}();var r={render:function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("transition",{attrs:{name:"vmenu-fade"}},[e.isActive?n("div",{staticClass:"vmenu-shadow",on:{click:e.handleShadowClicked}}):e._e()])},staticRenderFns:[],_scopeId:"data-v-e1b9ebe2",props:{isActive:{type:Boolean,default:!1},handleShadowClicked:{type:Function,default:function(){}}}};!function(){if("undefined"!=typeof document){var e=document.head||document.getElementsByTagName("head")[0],t=document.createElement("style"),n="ul[data-v-2a060565], li[data-v-2a060565] { padding: 0; margin: 0; } .Menu__header[data-v-2a060565] { display: flex; align-items: center; padding-left: 35px; height: 50px; color: #fff; font-size: 16px; background-color: #232f3e; cursor: pointer; } .Menu__header .arrow[data-v-2a060565] { padding-top: 2px; fill: #fff; margin-right: 10px; width: 10px; height: 100%; display: flex; align-items: center; } .Menu__list[data-v-2a060565] { list-style: none; padding-bottom: 2px; } .Menu__list .separator[data-v-2a060565] { border-bottom: 1px solid #d5dbdb; padding: 2px 0 0 0; margin: 0; } .Menu__item[data-v-2a060565] { padding-left: 35px; height: 45px; display: flex; align-items: center; cursor: pointer; } .Menu__item .arrow[data-v-2a060565] { padding-top: 2px; padding-left: 15px; display: flex; align-items: center; width: 10px; height: 100%; } ";t.type="text/css",t.styleSheet?t.styleSheet.cssText=n:t.appendChild(document.createTextNode(n)),e.appendChild(t)}}();var c={render:function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",{staticClass:"MenuPanel"},[n("div",{staticClass:"Menu__panel",style:[e.functionalityStyle,e.positionStyle,e.isTranslating?e.transitionStyle:{}]},[e.list.title?n("div",{staticClass:"Menu__header",on:{click:e.handleHeaderClicked}},[n("span",{directives:[{name:"show",rawName:"v-show",value:e.showHeaderArrow,expression:"showHeaderArrow"}],staticClass:"arrow"},[n("LeftArrowIcon")],1),e._v(" "+e._s(e.list.title)+" ")]):e._e(),e._v(" "),n("ul",{staticClass:"Menu__list"},e._l(e.list.children,function(t){return n("li",{staticClass:"Menu__item",on:{click:function(n){e.handleItemClicked(t)}}},[n("div",{staticClass:"text"},[e._v(e._s(t.title))]),e._v(" "),n("span",{directives:[{name:"show",rawName:"v-show",value:t.children.length>0,expression:"item.children.length > 0"}],staticClass:"arrow"},[n("RightArrowIcon")],1)])}))])])},staticRenderFns:[],_scopeId:"data-v-2a060565",components:{RightArrowIcon:a,LeftArrowIcon:s},props:{list:{type:Object,required:!0},positionStyle:{type:Object,required:!0},showHeaderArrow:{type:Boolean,default:!1},isTranslating:{type:Boolean,default:!1},handleHeaderClicked:{type:Function,default:function(){}},handleItemClicked:{type:Function,default:function(){}},functionalityStyle:{type:Object,required:!0},transitionStyle:{type:Object,required:!0}}};!function(){if("undefined"!=typeof document){var e=document.head||document.getElementsByTagName("head")[0],t=document.createElement("style");t.type="text/css",t.styleSheet?t.styleSheet.cssText="":t.appendChild(document.createTextNode("")),e.appendChild(t)}}();var d={render:function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",{staticClass:"Menu"},[n("MenuBurger",{attrs:{handleBurgerClicked:e.clickBurger}}),e._v(" "),n("MenuShadow",{attrs:{isActive:e.isActive,handleShadowClicked:e.clickShadow}}),e._v(" "),n("div",{staticClass:"Menu__panel-wrapper",class:{isActive:e.isActive},style:[e.style_wrapperStyle,e.isActive?e.style_wrapperActiveStyle:{}]},[n("MenuPanel",{attrs:{list:e.content_prevItem,functionalityStyle:e.style_panelStyle,positionStyle:e.panel_prevPositionStyle,isTranslating:e.isTranslating,transitionStyle:e.style_transitionStyle,showHeaderArrow:e.prevItemHasParent}}),e._v(" "),n("MenuPanel",{attrs:{list:e.content_currentItem,functionalityStyle:e.style_panelStyle,positionStyle:e.panel_stagingPositionStyle,isTranslating:e.isTranslating,transitionStyle:e.style_transitionStyle,showHeaderArrow:e.currentItemHasParent,handleHeaderClicked:e.clickPrevItem,handleItemClicked:e.clickNextItem}}),e._v(" "),n("MenuPanel",{attrs:{list:e.content_nextItem,functionalityStyle:e.style_panelStyle,positionStyle:e.panel_nextPositionStyle,isTranslating:e.isTranslating,transitionStyle:e.style_transitionStyle,showHeaderArrow:!0}})],1)],1)},staticRenderFns:[],mixins:[t,n,i],components:{RightArrowIcon:a,LeftArrowIcon:s,MenuBurger:o,MenuShadow:r,MenuPanel:c},props:{panelWidth:{type:Number,default:300},menuOpenSpeed:{type:Number,default:350},menuSwitchSpeed:{type:Number,default:300}},data:function(){return{data:e,isActive:!1,isTranslating:!1}},mounted:function(){this.content_currentItem=this.data},computed:{currentItemHasParent:function(){return this.content_parentStack.length>=1},prevItemHasParent:function(){return this.content_parentStack.length>=2}},methods:{clickBurger:function(){this.isActive=!this.isActive},clickShadow:function(){this.isActive=!1},clickNextItem:function(e){this.isTranslating||e.children.length<=0||this.slideToNext(e)},clickPrevItem:function(){!this.isTranslating&&this.currentItemHasParent&&this.slideToPrev()},slideToNext:function(e){var t=this;this.content_setNextItem(e),this.setTranslating(!0),this.$nextTick(function(){t.panel_slideNext()}),this.homingAfterTranslatingNext()},slideToPrev:function(){var e=this;this.content_setPrevItem(),this.setTranslating(!0),this.$nextTick(function(){e.panel_slideBack()}),this.homingAfterTranslatingBack()},homingAfterTranslatingNext:function(){var e=this;setTimeout(function(){e.setTranslating(!1),e.content_pushCurrentToParentStack(),e.panel_homingPosition(),e.content_homingItemAfterNext()},this.menuSwitchSpeed)},homingAfterTranslatingBack:function(){var e=this;setTimeout(function(){e.setTranslating(!1),e.panel_homingPosition(),e.content_homingItemAfterBack()},this.menuSwitchSpeed)},setTranslating:function(e){this.isTranslating=e}}};return{install:function(e,t){e.component("vue-nested-menu",d)}}}); 2 | -------------------------------------------------------------------------------- /example/install/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Document 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /example/single-file-component/MyMenu.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-nested-menu", 3 | "version": "0.0.2", 4 | "description": "A simple, hands-on slide nested menu with a smooth slide effect.", 5 | "main": "dist/vue-nested-menu.js", 6 | "devDependencies": { 7 | "@storybook/addon-actions": "^4.0.0-alpha.4", 8 | "@storybook/addon-links": "^4.0.0-alpha.4", 9 | "@storybook/addons": "^4.0.0-alpha.4", 10 | "@storybook/vue": "^4.0.0-alpha.4", 11 | "babel-core": "^6.26.3", 12 | "babel-preset-vue": "^2.0.2", 13 | "cross-env": "^5.1.5", 14 | "mime": "^2.3.1", 15 | "node-sass": "^4.9.0", 16 | "react": "^16.3.2", 17 | "react-dom": "^16.3.2", 18 | "rollup": "^0.36.1", 19 | "rollup-plugin-buble": "^0.14.0", 20 | "rollup-plugin-commonjs": "^5.0.4", 21 | "rollup-plugin-eslint": "^3.0.0", 22 | "rollup-plugin-node-globals": "^1.0.9", 23 | "rollup-plugin-node-resolve": "^2.0.0", 24 | "rollup-plugin-uglify": "^1.0.1", 25 | "rollup-plugin-vue": "^2.2.3", 26 | "sass-loader": "^7.0.1", 27 | "vue": "^2.5.16", 28 | "vue-loader": "14.2.2", 29 | "vue-template-compiler": "^2.5.16" 30 | }, 31 | "scripts": { 32 | "storybook": "start-storybook -p 6006", 33 | "build-storybook": "build-storybook", 34 | "build": "cross-env NODE_ENV=production rollup -c" 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "git+https://github.com/guAnsunyata/vue-nested-menu.git" 39 | }, 40 | "keywords": [ 41 | "Vue", 42 | "navigation", 43 | "slide-menu", 44 | "nested", 45 | "mobile", 46 | "responsive" 47 | ], 48 | "author": "guansunyata@gmail.com", 49 | "license": "MIT", 50 | "bugs": { 51 | "url": "https://github.com/guAnsunyata/vue-nested-menu/issues" 52 | }, 53 | "homepage": "https://github.com/guAnsunyata/vue-nested-menu#readme" 54 | } 55 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import buble from 'rollup-plugin-buble'; 2 | import eslint from 'rollup-plugin-eslint'; 3 | import resolve from 'rollup-plugin-node-resolve'; 4 | import commonjs from 'rollup-plugin-commonjs'; 5 | import uglify from 'rollup-plugin-uglify'; 6 | import vue from 'rollup-plugin-vue'; 7 | import nodeGlobals from 'rollup-plugin-node-globals'; 8 | import bubleConfig from './config/buble.config.js'; 9 | 10 | export default { 11 | entry: 'src/index.js', 12 | dest: 'dist/vue-nested-menu.js', 13 | format: 'umd', 14 | moduleName: 'VueNestedMenu', 15 | sourceMap: false, 16 | useStrict: false, 17 | plugins: [ 18 | vue({ css: true, }), 19 | buble(bubleConfig), 20 | resolve({ 21 | jsnext: true, 22 | main: true, 23 | browser: true, 24 | }), 25 | commonjs(), 26 | nodeGlobals(), 27 | (process.env.NODE_ENV === 'production' && uglify()), 28 | ], 29 | }; 30 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 207 | -------------------------------------------------------------------------------- /src/components/MenuBurger.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 22 | 23 | 31 | -------------------------------------------------------------------------------- /src/components/MenuPanel.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 84 | 85 | 147 | -------------------------------------------------------------------------------- /src/components/MenuShadow.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 22 | 23 | 44 | -------------------------------------------------------------------------------- /src/demo-data.js: -------------------------------------------------------------------------------- 1 | 2 | const data = { 3 | title: '首頁', 4 | children: [ 5 | { 6 | title: `Today's Deals`, 7 | link: `/`, 8 | children: [], 9 | }, 10 | { 11 | title: `Your Recommendations`, 12 | link: '/', 13 | children: [], 14 | }, 15 | { 16 | title: `Shop By Department`, 17 | children: [ 18 | { 19 | title: `Amazon Music`, 20 | children: [ 21 | { 22 | title: `Amazon Music Unlimited`, 23 | link: `/`, 24 | children: [], 25 | }, 26 | { 27 | title: `Prime Music`, 28 | link: `/`, 29 | children: [], 30 | }, 31 | { 32 | title: `CDs and Vinyl`, 33 | link: `/`, 34 | children: [], 35 | }, 36 | ], 37 | }, 38 | { 39 | title: `Prime Vedios`, 40 | children: [ 41 | { 42 | title: `All Vedio`, 43 | link: '/', 44 | children: [], 45 | }, 46 | { 47 | title: `Included with Prime`, 48 | link: '/', 49 | children: [], 50 | }, 51 | { 52 | title: `Rent or Buy`, 53 | link: '/', 54 | children: [], 55 | }, 56 | ], 57 | }, 58 | { 59 | title: `Treasure Truck`, 60 | link: '/', 61 | children: [], 62 | }, 63 | { 64 | title: `Amazon Restaurants`, 65 | children: [ 66 | { 67 | title: `Thai`, 68 | link: '/', 69 | children: [], 70 | }, 71 | { 72 | title: `Chinese`, 73 | link: '/', 74 | children: [], 75 | }, 76 | { 77 | title: `American`, 78 | link: '/', 79 | children: [], 80 | }, 81 | { 82 | title: `Indian`, 83 | link: '/', 84 | children: [], 85 | }, 86 | { 87 | title: `Popular Restaurants`, 88 | children: [ 89 | { 90 | title: `Popular Restaurants 1`, 91 | link: '/', 92 | children: [], 93 | }, 94 | { 95 | title: `Popular Restaurants 2`, 96 | link: '/', 97 | children: [], 98 | }, 99 | ], 100 | }, 101 | ], 102 | }, 103 | ], 104 | }, 105 | ], 106 | }; 107 | 108 | export default data; 109 | -------------------------------------------------------------------------------- /src/icons/LeftArrowIcon.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 18 | 19 | 26 | -------------------------------------------------------------------------------- /src/icons/MenuIcon.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 20 | 21 | 28 | -------------------------------------------------------------------------------- /src/icons/RightArrowIcon.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 18 | 19 | 26 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import App from './App.vue'; 2 | 3 | export default { 4 | install(Vue, options) { 5 | Vue.component('vue-nested-menu', App); 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /src/mixins/contentControl.mixin.js: -------------------------------------------------------------------------------- 1 | 2 | const contentControl = { 3 | 4 | // menu content control 5 | data() { 6 | return { 7 | content_prevItem: {}, 8 | content_currentItem: {}, 9 | content_nextItem: {}, 10 | content_parentStack: [], 11 | }; 12 | }, 13 | 14 | // menu content s 15 | methods: { 16 | content_setNextItem(targetItem) { 17 | this.content_nextItem = targetItem; 18 | }, 19 | content_setPrevItem() { 20 | this.content_prevItem = this.content_parentStack[this.content_parentStack.length - 1]; // the prev content is the parent of the current item. 21 | }, 22 | content_homingItemAfterNext() { // reset item after panel homing 23 | this.content_prevItem = this.content_currentItem; 24 | this.content_currentItem = this.content_nextItem; 25 | this.content_nextItem = {}; 26 | }, 27 | content_homingItemAfterBack() { 28 | this.content_parentStack.pop(); // update parent stack 29 | this.content_currentItem = this.content_prevItem; 30 | this.content_nextItem = {}; 31 | }, 32 | content_pushCurrentToParentStack(item) { 33 | const parent = this.content_currentItem; 34 | this.content_parentStack.push(parent); 35 | }, 36 | }, 37 | }; 38 | 39 | export default contentControl; 40 | -------------------------------------------------------------------------------- /src/mixins/functionalityStyle.mixin.js: -------------------------------------------------------------------------------- 1 | 2 | const functionalityStyle = { 3 | data() { 4 | return { 5 | style_wrapperStyle: {}, 6 | style_wrapperActiveStyle: {}, 7 | style_panelStyle: {}, 8 | style_transitionStyle: {}, 9 | }; 10 | }, 11 | mounted() { 12 | const panelWidth = this.panelWidth; 13 | const menuOpenSpeed = this.menuOpenSpeed; 14 | const menuSwitchSpeed = this.menuSwitchSpeed; 15 | 16 | const menuOpenTransitionSecond = `.${menuOpenSpeed / 10}s`; 17 | const menuSwitchTransitionSecond = `.${menuSwitchSpeed / 10}s`; 18 | 19 | const wrapperStyle = { 20 | width: `${panelWidth}px`, 21 | position: `absolute`, 22 | top: 0, 23 | left: `-${panelWidth}px`, 24 | zIndex: 99999, 25 | height: `100vh`, 26 | overflow: `hidden`, 27 | transition: `left ${menuOpenTransitionSecond}`, 28 | }; 29 | 30 | const wrapperActiveStyle = { 31 | left: 0, 32 | }; 33 | 34 | const panelStyle = { 35 | position: `absolute`, 36 | top: 0, 37 | zIndex: 99999, 38 | height: `100vh`, 39 | width: `${panelWidth}px`, 40 | backgroundColor: `#fff`, 41 | }; 42 | 43 | const transitionStyle = { 44 | transition: `left ${menuSwitchTransitionSecond}`, 45 | }; 46 | 47 | this.style_wrapperStyle = wrapperStyle; 48 | this.style_wrapperActiveStyle = wrapperActiveStyle; 49 | this.style_panelStyle = panelStyle; 50 | this.style_transitionStyle = transitionStyle; 51 | }, 52 | }; 53 | 54 | export default functionalityStyle; 55 | -------------------------------------------------------------------------------- /src/mixins/panelControl.mixin.js: -------------------------------------------------------------------------------- 1 | 2 | const panelControl = { 3 | 4 | // panel position style 5 | data() { 6 | return { 7 | panel_prevPositionStyle: {}, 8 | panel_stagingPositionStyle: {}, 9 | panel_nextPositionStyle: {}, 10 | }; 11 | }, 12 | mounted() { 13 | this.panel_prevPositionStyle = this.$_panelControl_positionSet['prev']; 14 | this.panel_stagingPositionStyle = this.$_panelControl_positionSet['staging']; 15 | this.panel_nextPositionStyle = this.$_panelControl_positionSet['next']; 16 | }, 17 | computed: { 18 | $_panelControl_positionSet() { 19 | return { 20 | staging: { 21 | left: 0, 22 | }, 23 | prev: { 24 | left: `-${this.panelWidth}px`, 25 | }, 26 | next: { 27 | left: `${this.panelWidth}px`, 28 | }, 29 | }; 30 | }, 31 | }, 32 | 33 | // change panel position change 34 | methods: { 35 | panel_slideNext() { 36 | this.panel_stagingPositionStyle = this.$_panelControl_positionSet['prev']; 37 | this.panel_nextPositionStyle = this.$_panelControl_positionSet['staging']; 38 | }, 39 | panel_slideBack() { 40 | this.panel_stagingPositionStyle = this.$_panelControl_positionSet['next'];; 41 | this.panel_prevPositionStyle = this.$_panelControl_positionSet['staging']; 42 | }, 43 | panel_homingPosition() { 44 | this.panel_prevPositionStyle = this.$_panelControl_positionSet['prev']; 45 | this.panel_nextPositionStyle = this.$_panelControl_positionSet['next']; 46 | this.panel_stagingPositionStyle = this.$_panelControl_positionSet['staging']; 47 | }, 48 | }, 49 | }; 50 | 51 | export default panelControl; 52 | -------------------------------------------------------------------------------- /stories/Menu.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'menu', 3 | data() { 4 | return {}; 5 | }, 6 | template: `
`, 7 | methods: {}, 8 | }; 9 | -------------------------------------------------------------------------------- /stories/Welcome.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-console 2 | const log = () => console.log('Welcome to storybook!'); 3 | 4 | export default { 5 | name: 'welcome', 6 | 7 | props: { 8 | showApp: { 9 | type: Function, 10 | default: log, 11 | }, 12 | }, 13 | 14 | data() { 15 | return { 16 | main: { 17 | margin: 15, 18 | maxWidth: 600, 19 | lineHeight: 1.4, 20 | fontFamily: '"Helvetica Neue", Helvetica, "Segoe UI", Arial, freesans, sans-serif', 21 | }, 22 | 23 | logo: { 24 | width: 200, 25 | }, 26 | 27 | link: { 28 | color: '#1474f3', 29 | textDecoration: 'none', 30 | borderBottom: '1px solid #1474f3', 31 | paddingBottom: 2, 32 | }, 33 | 34 | code: { 35 | fontSize: 15, 36 | fontWeight: 600, 37 | padding: '2px 5px', 38 | border: '1px solid #eae9e9', 39 | borderRadius: 4, 40 | backgroundColor: '#f3f2f2', 41 | color: '#3a3a3a', 42 | }, 43 | 44 | note: { 45 | opacity: 0.5, 46 | }, 47 | }; 48 | }, 49 | 50 | template: ` 51 |
52 |

Welcome to vue-nested-menu

53 |
54 | `, 55 | 56 | methods: { 57 | onClick(event) { 58 | event.preventDefault(); 59 | this.showApp(); 60 | }, 61 | }, 62 | }; 63 | -------------------------------------------------------------------------------- /stories/index.stories.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/react-in-jsx-scope */ 2 | 3 | import { storiesOf } from '@storybook/vue'; 4 | import { action } from '@storybook/addon-actions'; 5 | import { linkTo } from '@storybook/addon-links'; 6 | 7 | import Welcome from './Welcome'; 8 | import App from '../src/App'; 9 | 10 | storiesOf('Welcome', module).add('to Storybook', () => ({ 11 | components: { Welcome }, 12 | })); 13 | 14 | storiesOf('Menu', module) 15 | .add('menu', () => ({ 16 | components: { App }, 17 | template: '', 18 | })); 19 | 20 | // .add('with JSX', () => ({ 21 | // components: { MyButton }, 22 | // render() { 23 | // return With JSX; 24 | // }, 25 | // methods: { action: linkTo('clicked') }, 26 | // })) 27 | // .add('with some emoji', () => ({ 28 | // components: { MyButton }, 29 | // template: '😀 😎 👍 💯', 30 | // methods: { action: action('clicked') }, 31 | // })); 32 | 33 | /* eslint-enable react/react-in-jsx-scope */ 34 | --------------------------------------------------------------------------------