├── .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 [](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 |
86 |
87 |
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 |
2 |
3 |
4 |
5 |
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 |
2 |
46 |
47 |
48 |
207 |
--------------------------------------------------------------------------------
/src/components/MenuBurger.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
22 |
23 |
31 |
--------------------------------------------------------------------------------
/src/components/MenuPanel.vue:
--------------------------------------------------------------------------------
1 |
2 |
35 |
36 |
37 |
84 |
85 |
147 |
--------------------------------------------------------------------------------
/src/components/MenuShadow.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |
2 |
9 |
10 |
11 |
12 |
18 |
19 |
26 |
--------------------------------------------------------------------------------
/src/icons/MenuIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
20 |
21 |
28 |
--------------------------------------------------------------------------------
/src/icons/RightArrowIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
10 |
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 |
--------------------------------------------------------------------------------