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, | ... | ... |