Commit b9851cf0e930d1b1ad3858aad8041d799835b08e
1 parent
c39c688c
mew-menu
Showing
10 changed files
with
501 additions
and
0 deletions
Show diff stats
1 | +import Menu from './menu.vue'; | ||
2 | +import MenuGroup from './menu-group.vue'; | ||
3 | +import MenuItem from './menu-item.vue'; | ||
4 | +import Submenu from './submenu.vue'; | ||
5 | + | ||
6 | +Menu.Group = MenuGroup; | ||
7 | +Menu.Item = MenuItem; | ||
8 | +Menu.Sub = Submenu; | ||
9 | + | ||
10 | +export default Menu; | ||
0 | \ No newline at end of file | 11 | \ No newline at end of file |
1 | +<template> | ||
2 | + <li :class="[prefixCls + '-item-group']"> | ||
3 | + <div :class="[prefixCls + '-item-group-title']" :style="groupStyle">{{ title }}</div> | ||
4 | + <ul><slot></slot></ul> | ||
5 | + </li> | ||
6 | +</template> | ||
7 | +<script> | ||
8 | + import mixin from './mixin'; | ||
9 | + const prefixCls = 'ivu-menu'; | ||
10 | + | ||
11 | + export default { | ||
12 | + name: 'MenuGroup', | ||
13 | + mixins: [ mixin ], | ||
14 | + props: { | ||
15 | + title: { | ||
16 | + type: String, | ||
17 | + default: '' | ||
18 | + } | ||
19 | + }, | ||
20 | + data () { | ||
21 | + return { | ||
22 | + prefixCls: prefixCls | ||
23 | + }; | ||
24 | + }, | ||
25 | + computed: { | ||
26 | + groupStyle () { | ||
27 | + return this.hasParentSubmenu && this.mode !== 'horizontal' ? { | ||
28 | + paddingLeft: 43 + (this.parentSubmenuNum - 1) * 28 + 'px' | ||
29 | + } : {}; | ||
30 | + } | ||
31 | + } | ||
32 | + }; | ||
33 | +</script> |
1 | +<template> | ||
2 | + <a | ||
3 | + v-if="to" | ||
4 | + :href="linkUrl" | ||
5 | + :target="target" | ||
6 | + :class="classes" | ||
7 | + @click.exact="handleClickItem($event, false)" | ||
8 | + @click.ctrl="handleClickItem($event, true)" | ||
9 | + @click.meta="handleClickItem($event, true)" | ||
10 | + :style="itemStyle"><slot></slot></a> | ||
11 | + <li v-else :class="classes" @click.stop="handleClickItem" :style="itemStyle"><slot></slot></li> | ||
12 | +</template> | ||
13 | +<script> | ||
14 | + import Emitter from '../../mixins/emitter'; | ||
15 | + import { findComponentUpward } from '../../utils/assist'; | ||
16 | + import mixin from './mixin'; | ||
17 | + import mixinsLink from '../../mixins/link'; | ||
18 | + | ||
19 | + const prefixCls = 'ivu-menu'; | ||
20 | + | ||
21 | + export default { | ||
22 | + name: 'MenuItem', | ||
23 | + mixins: [ Emitter, mixin, mixinsLink ], | ||
24 | + props: { | ||
25 | + name: { | ||
26 | + type: [String, Number], | ||
27 | + required: true | ||
28 | + }, | ||
29 | + disabled: { | ||
30 | + type: Boolean, | ||
31 | + default: false | ||
32 | + }, | ||
33 | + }, | ||
34 | + data () { | ||
35 | + return { | ||
36 | + active: false | ||
37 | + }; | ||
38 | + }, | ||
39 | + computed: { | ||
40 | + classes () { | ||
41 | + return [ | ||
42 | + `${prefixCls}-item`, | ||
43 | + { | ||
44 | + [`${prefixCls}-item-active`]: this.active, | ||
45 | + [`${prefixCls}-item-selected`]: this.active, | ||
46 | + [`${prefixCls}-item-disabled`]: this.disabled | ||
47 | + } | ||
48 | + ]; | ||
49 | + }, | ||
50 | + itemStyle () { | ||
51 | + return this.hasParentSubmenu && this.mode !== 'horizontal' ? { | ||
52 | + paddingLeft: 43 + (this.parentSubmenuNum - 1) * 24 + 'px' | ||
53 | + } : {}; | ||
54 | + } | ||
55 | + }, | ||
56 | + methods: { | ||
57 | + handleClickItem (event, new_window = false) { | ||
58 | + if (this.disabled) return; | ||
59 | + | ||
60 | + if (new_window || this.target === '_blank') { | ||
61 | + // 如果是 new_window,直接新开窗口就行,无需发送状态 | ||
62 | + this.handleCheckClick(event, new_window); | ||
63 | + let parentMenu = findComponentUpward(this, 'Menu'); | ||
64 | + if (parentMenu) parentMenu.handleEmitSelectEvent(this.name); | ||
65 | + } else { | ||
66 | + let parent = findComponentUpward(this, 'Submenu'); | ||
67 | + | ||
68 | + if (parent) { | ||
69 | + this.dispatch('Submenu', 'on-menu-item-select', this.name); | ||
70 | + } else { | ||
71 | + this.dispatch('Menu', 'on-menu-item-select', this.name); | ||
72 | + } | ||
73 | + | ||
74 | + this.handleCheckClick(event, new_window); | ||
75 | + } | ||
76 | + } | ||
77 | + }, | ||
78 | + mounted () { | ||
79 | + this.$on('on-update-active-name', (name) => { | ||
80 | + if (this.name === name) { | ||
81 | + this.active = true; | ||
82 | + this.dispatch('Submenu', 'on-update-active-name', name); | ||
83 | + } else { | ||
84 | + this.active = false; | ||
85 | + } | ||
86 | + }); | ||
87 | + } | ||
88 | + }; | ||
89 | +</script> |
1 | +<template> | ||
2 | + <ul :class="classes" :style="styles"><slot></slot></ul> | ||
3 | +</template> | ||
4 | +<script> | ||
5 | + import { oneOf, findComponentsDownward, findComponentsUpward } from '../../utils/assist'; | ||
6 | + import Emitter from '../../mixins/emitter'; | ||
7 | + | ||
8 | + const prefixCls = 'ivu-menu'; | ||
9 | + | ||
10 | + export default { | ||
11 | + name: 'Menu', | ||
12 | + mixins: [ Emitter ], | ||
13 | + props: { | ||
14 | + mode: { | ||
15 | + validator (value) { | ||
16 | + return oneOf(value, ['horizontal', 'vertical']); | ||
17 | + }, | ||
18 | + default: 'vertical' | ||
19 | + }, | ||
20 | + theme: { | ||
21 | + validator (value) { | ||
22 | + return oneOf(value, ['light', 'dark', 'primary']); | ||
23 | + }, | ||
24 | + default: 'light' | ||
25 | + }, | ||
26 | + activeName: { | ||
27 | + type: [String, Number] | ||
28 | + }, | ||
29 | + openNames: { | ||
30 | + type: Array, | ||
31 | + default () { | ||
32 | + return []; | ||
33 | + } | ||
34 | + }, | ||
35 | + accordion: { | ||
36 | + type: Boolean, | ||
37 | + default: false | ||
38 | + }, | ||
39 | + width: { | ||
40 | + type: String, | ||
41 | + default: '240px' | ||
42 | + } | ||
43 | + }, | ||
44 | + data () { | ||
45 | + return { | ||
46 | + currentActiveName: this.activeName, | ||
47 | + openedNames: [] | ||
48 | + }; | ||
49 | + }, | ||
50 | + computed: { | ||
51 | + classes () { | ||
52 | + let theme = this.theme; | ||
53 | + if (this.mode === 'vertical' && this.theme === 'primary') theme = 'light'; | ||
54 | + | ||
55 | + return [ | ||
56 | + `${prefixCls}`, | ||
57 | + `${prefixCls}-${theme}`, | ||
58 | + { | ||
59 | + [`${prefixCls}-${this.mode}`]: this.mode | ||
60 | + } | ||
61 | + ]; | ||
62 | + }, | ||
63 | + styles () { | ||
64 | + let style = {}; | ||
65 | + | ||
66 | + if (this.mode === 'vertical') style.width = this.width; | ||
67 | + | ||
68 | + return style; | ||
69 | + } | ||
70 | + }, | ||
71 | + methods: { | ||
72 | + updateActiveName () { | ||
73 | + if (this.currentActiveName === undefined) { | ||
74 | + this.currentActiveName = -1; | ||
75 | + } | ||
76 | + this.broadcast('Submenu', 'on-update-active-name', false); | ||
77 | + this.broadcast('MenuItem', 'on-update-active-name', this.currentActiveName); | ||
78 | + }, | ||
79 | + updateOpenKeys (name) { | ||
80 | + let names = [...this.openedNames]; | ||
81 | + const index = names.indexOf(name); | ||
82 | + if (this.accordion) findComponentsDownward(this, 'Submenu').forEach(item => { | ||
83 | + item.opened = false; | ||
84 | + }); | ||
85 | + if (index >= 0) { | ||
86 | + let currentSubmenu = null; | ||
87 | + findComponentsDownward(this, 'Submenu').forEach(item => { | ||
88 | + if (item.name === name) { | ||
89 | + currentSubmenu = item; | ||
90 | + item.opened = false; | ||
91 | + } | ||
92 | + }); | ||
93 | + findComponentsUpward(currentSubmenu, 'Submenu').forEach(item => { | ||
94 | + item.opened = true; | ||
95 | + }); | ||
96 | + findComponentsDownward(currentSubmenu, 'Submenu').forEach(item => { | ||
97 | + item.opened = false; | ||
98 | + }); | ||
99 | + } else { | ||
100 | + if (this.accordion) { | ||
101 | + let currentSubmenu = null; | ||
102 | + findComponentsDownward(this, 'Submenu').forEach(item => { | ||
103 | + if (item.name === name) { | ||
104 | + currentSubmenu = item; | ||
105 | + item.opened = true; | ||
106 | + } | ||
107 | + }); | ||
108 | + findComponentsUpward(currentSubmenu, 'Submenu').forEach(item => { | ||
109 | + item.opened = true; | ||
110 | + }); | ||
111 | + } else { | ||
112 | + findComponentsDownward(this, 'Submenu').forEach(item => { | ||
113 | + if (item.name === name) item.opened = true; | ||
114 | + }); | ||
115 | + } | ||
116 | + } | ||
117 | + let openedNames = findComponentsDownward(this, 'Submenu').filter(item => item.opened).map(item => item.name); | ||
118 | + this.openedNames = [...openedNames]; | ||
119 | + this.$emit('on-open-change', openedNames); | ||
120 | + }, | ||
121 | + updateOpened () { | ||
122 | + const items = findComponentsDownward(this, 'Submenu'); | ||
123 | + | ||
124 | + if (items.length) { | ||
125 | + items.forEach(item => { | ||
126 | + if (this.openedNames.indexOf(item.name) > -1) item.opened = true; | ||
127 | + else item.opened = false; | ||
128 | + }); | ||
129 | + } | ||
130 | + }, | ||
131 | + handleEmitSelectEvent (name) { | ||
132 | + this.$emit('on-select', name); | ||
133 | + } | ||
134 | + }, | ||
135 | + mounted () { | ||
136 | + this.openedNames = [...this.openNames]; | ||
137 | + this.updateOpened(); | ||
138 | + this.$nextTick(() => this.updateActiveName()); | ||
139 | + this.$on('on-menu-item-select', (name) => { | ||
140 | + this.currentActiveName = name; | ||
141 | + this.$emit('on-select', name); | ||
142 | + }); | ||
143 | + }, | ||
144 | + watch: { | ||
145 | + openNames (names) { | ||
146 | + this.openedNames = names; | ||
147 | + }, | ||
148 | + activeName (val) { | ||
149 | + this.currentActiveName = val; | ||
150 | + }, | ||
151 | + currentActiveName () { | ||
152 | + this.updateActiveName(); | ||
153 | + } | ||
154 | + } | ||
155 | + }; | ||
156 | +</script> |
1 | +import { findComponentUpward, findComponentsUpward } from '../../utils/assist'; | ||
2 | +export default { | ||
3 | + data () { | ||
4 | + return { | ||
5 | + menu: findComponentUpward(this, 'Menu') | ||
6 | + }; | ||
7 | + }, | ||
8 | + computed: { | ||
9 | + hasParentSubmenu () { | ||
10 | + return !!findComponentUpward(this, 'Submenu'); | ||
11 | + }, | ||
12 | + parentSubmenuNum () { | ||
13 | + return findComponentsUpward(this, 'Submenu').length; | ||
14 | + }, | ||
15 | + mode () { | ||
16 | + return this.menu.mode; | ||
17 | + } | ||
18 | + } | ||
19 | +}; |
1 | +<template> | ||
2 | + <li :class="classes" @mouseenter="handleMouseenter" @mouseleave="handleMouseleave"> | ||
3 | + <div :class="[prefixCls + '-submenu-title']" ref="reference" @click.stop="handleClick" :style="titleStyle"> | ||
4 | + <slot name="title"></slot> | ||
5 | + <Icon :type="arrowType" :custom="customArrowType" :size="arrowSize" :class="[prefixCls + '-submenu-title-icon']" /> | ||
6 | + </div> | ||
7 | + <collapse-transition v-if="mode === 'vertical'"> | ||
8 | + <ul :class="[prefixCls]" v-show="opened"><slot></slot></ul> | ||
9 | + </collapse-transition> | ||
10 | + <transition name="slide-up" v-else> | ||
11 | + <Drop | ||
12 | + v-show="opened" | ||
13 | + placement="bottom" | ||
14 | + ref="drop" | ||
15 | + :style="dropStyle"><ul :class="[prefixCls + '-drop-list']"><slot></slot></ul> | ||
16 | + </Drop> | ||
17 | + </transition> | ||
18 | + </li> | ||
19 | +</template> | ||
20 | +<script> | ||
21 | + import Drop from '../select/dropdown.vue'; | ||
22 | + import Icon from '../icon/icon.vue'; | ||
23 | + import CollapseTransition from '../base/collapse-transition'; | ||
24 | + import { getStyle, findComponentUpward, findComponentsDownward } from '../../utils/assist'; | ||
25 | + import Emitter from '../../mixins/emitter'; | ||
26 | + import mixin from './mixin'; | ||
27 | + | ||
28 | + const prefixCls = 'ivu-menu'; | ||
29 | + | ||
30 | + export default { | ||
31 | + name: 'Submenu', | ||
32 | + mixins: [ Emitter, mixin ], | ||
33 | + components: { Icon, Drop, CollapseTransition }, | ||
34 | + props: { | ||
35 | + name: { | ||
36 | + type: [String, Number], | ||
37 | + required: true | ||
38 | + }, | ||
39 | + disabled: { | ||
40 | + type: Boolean, | ||
41 | + default: false | ||
42 | + } | ||
43 | + }, | ||
44 | + data () { | ||
45 | + return { | ||
46 | + prefixCls: prefixCls, | ||
47 | + active: false, | ||
48 | + opened: false, | ||
49 | + dropWidth: parseFloat(getStyle(this.$el, 'width')) | ||
50 | + }; | ||
51 | + }, | ||
52 | + computed: { | ||
53 | + classes () { | ||
54 | + return [ | ||
55 | + `${prefixCls}-submenu`, | ||
56 | + { | ||
57 | + [`${prefixCls}-item-active`]: this.active && !this.hasParentSubmenu, | ||
58 | + [`${prefixCls}-opened`]: this.opened, | ||
59 | + [`${prefixCls}-submenu-disabled`]: this.disabled, | ||
60 | + [`${prefixCls}-submenu-has-parent-submenu`]: this.hasParentSubmenu, | ||
61 | + [`${prefixCls}-child-item-active`]: this.active | ||
62 | + } | ||
63 | + ]; | ||
64 | + }, | ||
65 | + accordion () { | ||
66 | + return this.menu.accordion; | ||
67 | + }, | ||
68 | + dropStyle () { | ||
69 | + let style = {}; | ||
70 | + | ||
71 | + if (this.dropWidth) style.minWidth = `${this.dropWidth}px`; | ||
72 | + return style; | ||
73 | + }, | ||
74 | + titleStyle () { | ||
75 | + return this.hasParentSubmenu && this.mode !== 'horizontal' ? { | ||
76 | + paddingLeft: 43 + (this.parentSubmenuNum - 1) * 24 + 'px' | ||
77 | + } : {}; | ||
78 | + }, | ||
79 | + // 3.4.0, global setting customArrow 有值时,arrow 赋值空 | ||
80 | + arrowType () { | ||
81 | + let type = 'ios-arrow-down'; | ||
82 | + | ||
83 | + if (this.$IVIEW) { | ||
84 | + if (this.$IVIEW.menu.customArrow) { | ||
85 | + type = ''; | ||
86 | + } else if (this.$IVIEW.menu.arrow) { | ||
87 | + type = this.$IVIEW.menu.arrow; | ||
88 | + } | ||
89 | + } | ||
90 | + return type; | ||
91 | + }, | ||
92 | + // 3.4.0, global setting | ||
93 | + customArrowType () { | ||
94 | + let type = ''; | ||
95 | + | ||
96 | + if (this.$IVIEW) { | ||
97 | + if (this.$IVIEW.menu.customArrow) { | ||
98 | + type = this.$IVIEW.menu.customArrow; | ||
99 | + } | ||
100 | + } | ||
101 | + return type; | ||
102 | + }, | ||
103 | + // 3.4.0, global setting | ||
104 | + arrowSize () { | ||
105 | + let size = ''; | ||
106 | + | ||
107 | + if (this.$IVIEW) { | ||
108 | + if (this.$IVIEW.menu.arrowSize) { | ||
109 | + size = this.$IVIEW.menu.arrowSize; | ||
110 | + } | ||
111 | + } | ||
112 | + return size; | ||
113 | + } | ||
114 | + }, | ||
115 | + methods: { | ||
116 | + handleMouseenter () { | ||
117 | + if (this.disabled) return; | ||
118 | + if (this.mode === 'vertical') return; | ||
119 | + | ||
120 | + clearTimeout(this.timeout); | ||
121 | + this.timeout = setTimeout(() => { | ||
122 | + this.menu.updateOpenKeys(this.name); | ||
123 | + this.opened = true; | ||
124 | + }, 250); | ||
125 | + }, | ||
126 | + handleMouseleave () { | ||
127 | + if (this.disabled) return; | ||
128 | + if (this.mode === 'vertical') return; | ||
129 | + | ||
130 | + clearTimeout(this.timeout); | ||
131 | + this.timeout = setTimeout(() => { | ||
132 | + this.menu.updateOpenKeys(this.name); | ||
133 | + this.opened = false; | ||
134 | + }, 150); | ||
135 | + }, | ||
136 | + handleClick () { | ||
137 | + if (this.disabled) return; | ||
138 | + if (this.mode === 'horizontal') return; | ||
139 | + const opened = this.opened; | ||
140 | + if (this.accordion) { | ||
141 | + this.$parent.$children.forEach(item => { | ||
142 | + if (item.$options.name === 'Submenu') item.opened = false; | ||
143 | + }); | ||
144 | + } | ||
145 | + this.opened = !opened; | ||
146 | + this.menu.updateOpenKeys(this.name); | ||
147 | + } | ||
148 | + }, | ||
149 | + watch: { | ||
150 | + mode (val) { | ||
151 | + if (val === 'horizontal') { | ||
152 | + this.$refs.drop.update(); | ||
153 | + } | ||
154 | + }, | ||
155 | + opened (val) { | ||
156 | + if (this.mode === 'vertical') return; | ||
157 | + if (val) { | ||
158 | + // set drop a width to fixed when menu has fixed position | ||
159 | + this.dropWidth = parseFloat(getStyle(this.$el, 'width')); | ||
160 | + this.$refs.drop.update(); | ||
161 | + } else { | ||
162 | + this.$refs.drop.destroy(); | ||
163 | + } | ||
164 | + } | ||
165 | + }, | ||
166 | + mounted () { | ||
167 | + this.$on('on-menu-item-select', (name) => { | ||
168 | + if (this.mode === 'horizontal') this.opened = false; | ||
169 | + this.dispatch('Menu', 'on-menu-item-select', name); | ||
170 | + return true; | ||
171 | + }); | ||
172 | + this.$on('on-update-active-name', (status) => { | ||
173 | + if (findComponentUpward(this, 'Submenu')) this.dispatch('Submenu', 'on-update-active-name', status); | ||
174 | + if (findComponentsDownward(this, 'Submenu')) findComponentsDownward(this, 'Submenu').forEach(item => { | ||
175 | + item.active = false; | ||
176 | + }); | ||
177 | + this.active = status; | ||
178 | + }); | ||
179 | + } | ||
180 | + }; | ||
181 | +</script> |
src/index.js
@@ -36,6 +36,7 @@ import Menu from './components/menu'; | @@ -36,6 +36,7 @@ import Menu from './components/menu'; | ||
36 | import Message from './components/message'; | 36 | import Message from './components/message'; |
37 | import MewMap from './components/mew-map'; | 37 | import MewMap from './components/mew-map'; |
38 | import MewMapSelector from './components/mew-map-selector'; | 38 | import MewMapSelector from './components/mew-map-selector'; |
39 | +import MewMenu from './components/mew-menu'; | ||
39 | import Modal from './components/modal'; | 40 | import Modal from './components/modal'; |
40 | import Notice from './components/notice'; | 41 | import Notice from './components/notice'; |
41 | import Page from './components/page'; | 42 | import Page from './components/page'; |
@@ -113,6 +114,9 @@ const components = { | @@ -113,6 +114,9 @@ const components = { | ||
113 | Message, | 114 | Message, |
114 | MewMap, | 115 | MewMap, |
115 | MewMapSelector, | 116 | MewMapSelector, |
117 | + MewMenu, | ||
118 | + MewMenuGroup: MewMenu.Group, | ||
119 | + MewMenuItem: MewMenu.Item, | ||
116 | Modal, | 120 | Modal, |
117 | Notice, | 121 | Notice, |
118 | Option: Option, | 122 | Option: Option, |