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 | 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 | 36 | import Message from './components/message'; |
37 | 37 | import MewMap from './components/mew-map'; |
38 | 38 | import MewMapSelector from './components/mew-map-selector'; |
39 | +import MewMenu from './components/mew-menu'; | |
39 | 40 | import Modal from './components/modal'; |
40 | 41 | import Notice from './components/notice'; |
41 | 42 | import Page from './components/page'; |
... | ... | @@ -113,6 +114,9 @@ const components = { |
113 | 114 | Message, |
114 | 115 | MewMap, |
115 | 116 | MewMapSelector, |
117 | + MewMenu, | |
118 | + MewMenuGroup: MewMenu.Group, | |
119 | + MewMenuItem: MewMenu.Item, | |
116 | 120 | Modal, |
117 | 121 | Notice, |
118 | 122 | Option: Option, | ... | ... |