mirror of
https://github.com/tusen-ai/naive-ui.git
synced 2025-04-12 14:40:47 +08:00
refactor(menu): support vue3
This commit is contained in:
parent
88d142d64b
commit
1684139614
@ -27,6 +27,12 @@ module.exports = {
|
||||
describe: 'readonly',
|
||||
it: 'readonly'
|
||||
}
|
||||
},
|
||||
{
|
||||
files: '*',
|
||||
globals: {
|
||||
__DEV__: 'readonly'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
const path = require('path')
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||
const { DefinePlugin } = require('webpack')
|
||||
|
||||
exports.alias = {
|
||||
'naive-ui/lib/icons': path.resolve(__dirname, '../src/_icons'),
|
||||
@ -68,3 +69,9 @@ exports.docLoaders = (env) => [
|
||||
loader: '@intlify/vue-i18n-loader'
|
||||
}
|
||||
]
|
||||
|
||||
exports.plugins = [
|
||||
new DefinePlugin({
|
||||
__DEV__: JSON.stringify(process.env.NODE_ENV === 'development')
|
||||
})
|
||||
]
|
||||
|
@ -51,7 +51,8 @@ const webpackConfig = {
|
||||
filename: '[name].css',
|
||||
chunkFilename: '[id].css',
|
||||
ignoreOrder: false
|
||||
})
|
||||
}),
|
||||
...config.plugins
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -48,7 +48,8 @@ const webpackConfig = {
|
||||
preserveWhitespace: false
|
||||
}
|
||||
}
|
||||
})
|
||||
}),
|
||||
...config.plugins
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -48,7 +48,8 @@ const webpackConfig = {
|
||||
preserveWhitespace: false
|
||||
}
|
||||
}
|
||||
})
|
||||
}),
|
||||
...config.plugins
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -44,7 +44,8 @@ const webpackConfig = {
|
||||
filename: '[name].css',
|
||||
chunkFilename: '[id].css',
|
||||
ignoreOrder: false
|
||||
})
|
||||
}),
|
||||
...config.plugins
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Delay
|
||||
```html
|
||||
<n-popover :delay="500" :duration="500" :width="240">
|
||||
<n-popover :delay="500" :duration="500">
|
||||
<template v-slot:trigger>
|
||||
<n-button>
|
||||
Delay 500, Duration 500
|
||||
|
@ -2,7 +2,6 @@
|
||||
```html
|
||||
<n-popover
|
||||
placement="bottom"
|
||||
:width="200"
|
||||
trigger="hover"
|
||||
@show="handleShow"
|
||||
@hide="handleHide"
|
||||
@ -18,7 +17,6 @@
|
||||
</n-popover>
|
||||
<n-popover
|
||||
placement="bottom"
|
||||
:width="200"
|
||||
trigger="click"
|
||||
@show="handleShow"
|
||||
@hide="handleHide"
|
||||
@ -35,7 +33,6 @@
|
||||
<n-popover
|
||||
:show="showPopover"
|
||||
placement="bottom"
|
||||
:width="200"
|
||||
@show="handleShow"
|
||||
@hide="handleHide"
|
||||
>
|
||||
|
@ -1,6 +1,6 @@
|
||||
# 基础用法
|
||||
```html
|
||||
<n-popover>
|
||||
<n-popover trigger="hover">
|
||||
<template v-slot:trigger>
|
||||
<n-button>
|
||||
悬浮
|
||||
|
@ -1,6 +1,10 @@
|
||||
# 延迟
|
||||
```html
|
||||
<n-popover :delay="500" :duration="500" :width="240">
|
||||
<n-popover
|
||||
trigger="hover"
|
||||
:delay="500"
|
||||
:duration="500"
|
||||
>
|
||||
<template v-slot:trigger>
|
||||
<n-button>
|
||||
延迟 500ms, 持续 500ms
|
||||
|
@ -2,10 +2,8 @@
|
||||
```html
|
||||
<n-popover
|
||||
placement="bottom"
|
||||
:width="200"
|
||||
trigger="hover"
|
||||
@show="handleShow"
|
||||
@hide="handleHide"
|
||||
@update:show="handleUpdateShow"
|
||||
>
|
||||
<template v-slot:trigger>
|
||||
<n-button>
|
||||
@ -18,10 +16,8 @@
|
||||
</n-popover>
|
||||
<n-popover
|
||||
placement="bottom"
|
||||
:width="200"
|
||||
trigger="click"
|
||||
@show="handleShow"
|
||||
@hide="handleHide"
|
||||
@update:show="handleUpdateShow"
|
||||
>
|
||||
<template v-slot:trigger>
|
||||
<n-button>
|
||||
@ -35,9 +31,7 @@
|
||||
<n-popover
|
||||
:show="showPopover"
|
||||
placement="bottom"
|
||||
:width="200"
|
||||
@show="handleShow"
|
||||
@hide="handleHide"
|
||||
@update:show="handleUpdateShow"
|
||||
>
|
||||
<template v-slot:trigger>
|
||||
<n-button @click="showPopover = !showPopover">
|
||||
@ -52,20 +46,18 @@
|
||||
|
||||
```js
|
||||
export default {
|
||||
inject: ['message'],
|
||||
data() {
|
||||
return {
|
||||
showPopover: false
|
||||
};
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleShow() {
|
||||
this.$NMessage.success("show popover");
|
||||
},
|
||||
handleHide() {
|
||||
this.$NMessage.success("hide popover");
|
||||
handleUpdateShow (value) {
|
||||
this.message.success(`Update show: ${value}`)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
```css
|
||||
|
@ -35,7 +35,7 @@ manual-position
|
||||
|show-arrow|`boolean`|`true`||
|
||||
|show|`boolean`|-|是否展示 popover|
|
||||
|theme|`'light' \| 'dark' \| null \| string`|`null`||
|
||||
|trigger|`'hover' \| 'click'`|`'hover'`||
|
||||
|trigger|`'hover' \| 'click' \| null`|`null`||
|
||||
|x|`number`|-|手动控制位置时填出内容的 CSS `left` 的像素值|
|
||||
|y|`number`|-|手动控制位置时填出内容的 CSS `top` 的像素值||
|
||||
|
||||
|
@ -1,6 +1,9 @@
|
||||
# 不要箭头
|
||||
```html
|
||||
<n-popover :show-arrow="false">
|
||||
<n-popover
|
||||
trigger="hover"
|
||||
:show-arrow="false"
|
||||
>
|
||||
<template v-slot:trigger>
|
||||
<n-button>
|
||||
悬浮
|
||||
|
@ -1,6 +1,6 @@
|
||||
# 不用基础样式
|
||||
```html
|
||||
<n-popover raw :show-arrow="false">
|
||||
<n-popover trigger="hover" raw :show-arrow="false">
|
||||
<template v-slot:trigger>
|
||||
<n-button style="margin:0;">
|
||||
悬浮
|
||||
|
@ -1,6 +1,6 @@
|
||||
# 主体样式
|
||||
```html
|
||||
<n-popover :body-style="{ width: '500px' }">
|
||||
<n-popover :body-style="{ width: '500px' }" trigger="hover">
|
||||
<template v-slot:trigger>
|
||||
<n-button style="margin:0;">
|
||||
宽度 500px
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import { h } from 'vue'
|
||||
import { h, resolveComponent } from 'vue'
|
||||
|
||||
export default {
|
||||
name: 'NNimbusServiceLayoutSiderMenu',
|
||||
@ -9,10 +9,6 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
content () {
|
||||
const ServiceLayout = this.NNimbusServiceLayout
|
||||
return this.createMenu(h, ServiceLayout.items)
|
||||
},
|
||||
subMenuNames () {
|
||||
const ServiceLayout = this.NNimbusServiceLayout
|
||||
const subMenuNames = []
|
||||
@ -29,60 +25,42 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
createMenu (h, items) {
|
||||
createItems (items) {
|
||||
return items.map(item => {
|
||||
const props = {
|
||||
return {
|
||||
title: item.title || item.name,
|
||||
titleExtra: item.titleExtra,
|
||||
name: item.name,
|
||||
disabled: !!item.disabled
|
||||
}
|
||||
if (item.group) {
|
||||
return h('NMenuItemGroup', {
|
||||
props
|
||||
},
|
||||
this.createMenu(h, item.childItems)
|
||||
)
|
||||
}
|
||||
if (item.childItems) {
|
||||
return h('NSubmenu', {
|
||||
props
|
||||
},
|
||||
this.createMenu(h, item.childItems)
|
||||
)
|
||||
} else {
|
||||
return h('NMenuItem', {
|
||||
props: props,
|
||||
on: {
|
||||
click: () => {
|
||||
if (this.$router && item.path) {
|
||||
Promise.resolve(
|
||||
this.$router.push(item.path)
|
||||
).catch(() => {})
|
||||
}
|
||||
}
|
||||
disabled: !!item.disabled,
|
||||
children: item.childItems ? this.createItems(item.childItems) : undefined,
|
||||
group: item.group,
|
||||
onClick: !(item.group && item.childItems) ? () => {
|
||||
console.log('item click')
|
||||
console.log(this.$router, item.path)
|
||||
if (this.$router && item.path) {
|
||||
Promise.resolve(
|
||||
this.$router.push(item.path)
|
||||
).catch(err => {
|
||||
console.log(err)
|
||||
})
|
||||
}
|
||||
})
|
||||
} : undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
render () {
|
||||
const ServiceLayout = this.NNimbusServiceLayout
|
||||
return null
|
||||
// return h('NMenu',
|
||||
// {
|
||||
// ...ServiceLayout.$attrs,
|
||||
// value: ServiceLayout.value || ServiceLayout.activeItem,
|
||||
// expandedNames: ServiceLayout.expandedNames,
|
||||
// defaultExpandedNames: ServiceLayout.defaultExpandedNames || this.subMenuNames,
|
||||
// rootIndent: 36,
|
||||
// indent: 40
|
||||
// },
|
||||
// {
|
||||
// default: () => this.content
|
||||
// }
|
||||
// )
|
||||
return h(resolveComponent('NMenu'),
|
||||
{
|
||||
modelValue: ServiceLayout.value || ServiceLayout.activeItem,
|
||||
expandedNames: ServiceLayout.expandedNames,
|
||||
defaultExpandedNames: ServiceLayout.defaultExpandedNames || this.subMenuNames,
|
||||
rootIndent: 36,
|
||||
indent: 40,
|
||||
items: this.createItems(ServiceLayout.items)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -3,7 +3,6 @@
|
||||
import Scrollbar from '../../../scrollbar'
|
||||
import withapp from '../../../_mixins/withapp'
|
||||
import themeable from '../../../_mixins/themeable'
|
||||
import getDefaultSlot from '../../../_utils/vue/getDefaultSlot'
|
||||
import SiderMenu from './SiderMenu'
|
||||
import NLayout from '../../../layout/src/Layout.vue'
|
||||
import NLayoutSider from '../../../layout/src/LayoutSider'
|
||||
@ -108,43 +107,6 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
createMenu (h, items) {
|
||||
return items.map(item => {
|
||||
const props = {
|
||||
title: item.title || item.name,
|
||||
titleExtra: item.titleExtra,
|
||||
name: item.name,
|
||||
disabled: !!item.disabled
|
||||
}
|
||||
if (item.group) {
|
||||
return h('NMenuItemGroup', {
|
||||
props
|
||||
},
|
||||
this.createMenu(h, item.childItems)
|
||||
)
|
||||
}
|
||||
if (item.childItems) {
|
||||
return h('NSubmenu', {
|
||||
props
|
||||
},
|
||||
this.createMenu(h, item.childItems)
|
||||
)
|
||||
} else {
|
||||
return h('NMenuItem', {
|
||||
props: props,
|
||||
on: {
|
||||
click: () => {
|
||||
if (this.$router && item.path) {
|
||||
Promise.resolve(
|
||||
this.$router.push(item.path)
|
||||
).catch(() => {})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
scrollTo (...args) {
|
||||
this.$refs.body.scrollTo(...args)
|
||||
},
|
||||
@ -201,50 +163,6 @@ export default {
|
||||
themedStyle: this.bodyThemedStyle,
|
||||
position: 'absolute'
|
||||
}, {
|
||||
// [
|
||||
// this.name ? h('div', {
|
||||
// staticStyle: {
|
||||
// alignItems: 'center',
|
||||
// height: '64px',
|
||||
// paddingLeft: '36px',
|
||||
// fontSize: '16px',
|
||||
// fontWeight: '500',
|
||||
// display: 'flex',
|
||||
// position: 'relative'
|
||||
// }
|
||||
// }, [
|
||||
// this.$slots['drawer-header-icon'] ? h(
|
||||
// 'NConfigConsumer', {
|
||||
// props: {
|
||||
// abstract: true
|
||||
// },
|
||||
// scopedSlots: {
|
||||
// default: ({ styleScheme }) => {
|
||||
// return h('NIcon', {
|
||||
// props: { size: 20 },
|
||||
// staticStyle: {
|
||||
// position: 'absolute',
|
||||
// left: '10px',
|
||||
// top: '50%',
|
||||
// transform: 'translateY(-50%)'
|
||||
// },
|
||||
// style: {
|
||||
// fill: (styleScheme && styleScheme.secondaryTextColor) || null
|
||||
// }
|
||||
// }, this.$slots['drawer-header-icon'])
|
||||
// }
|
||||
// }
|
||||
// }) : null,
|
||||
// h('span', {}, this.name)
|
||||
// ]) : null,
|
||||
// this.name ? h('n-divider', {
|
||||
// staticStyle: {
|
||||
// margin: '0',
|
||||
// padding: '0 20px 0 4px'
|
||||
// }
|
||||
// }) : null,
|
||||
// h(SiderMenu)]
|
||||
// ),
|
||||
default: () => [
|
||||
h(NLayoutSider, {
|
||||
...siderProps,
|
||||
@ -259,6 +177,51 @@ export default {
|
||||
onExpand: () => {
|
||||
this.collapsed = false
|
||||
}
|
||||
}, {
|
||||
default: () => [
|
||||
// this.name ? h('div', {
|
||||
// style: {
|
||||
// alignItems: 'center',
|
||||
// height: '64px',
|
||||
// paddingLeft: '36px',
|
||||
// fontSize: '16px',
|
||||
// fontWeight: '500',
|
||||
// display: 'flex',
|
||||
// position: 'relative'
|
||||
// }
|
||||
// }, [
|
||||
// this.$slots['drawer-header-icon'] ? h(
|
||||
// 'NConfigConsumer', {
|
||||
// props: {
|
||||
// abstract: true
|
||||
// },
|
||||
// scopedSlots: {
|
||||
// default: ({ styleScheme }) => {
|
||||
// return h('NIcon', {
|
||||
// props: { size: 20 },
|
||||
// staticStyle: {
|
||||
// position: 'absolute',
|
||||
// left: '10px',
|
||||
// top: '50%',
|
||||
// transform: 'translateY(-50%)'
|
||||
// },
|
||||
// style: {
|
||||
// fill: (styleScheme && styleScheme.secondaryTextColor) || null
|
||||
// }
|
||||
// }, this.$slots['drawer-header-icon'])
|
||||
// }
|
||||
// }
|
||||
// }) : null,
|
||||
// h('span', {}, this.name)
|
||||
// ]) : null,
|
||||
// this.name ? h('n-divider', {
|
||||
// staticStyle: {
|
||||
// margin: '0',
|
||||
// padding: '0 20px 0 4px'
|
||||
// }
|
||||
// }) : null,
|
||||
h(SiderMenu)
|
||||
]
|
||||
}),
|
||||
h(NLayout, {
|
||||
ref: 'body',
|
||||
|
@ -273,7 +273,7 @@ export default {
|
||||
}
|
||||
let adjustedPlacement = this.placement
|
||||
let contentBoundingClientRect = null
|
||||
if (this.zindexableFlip) {
|
||||
if (this.placeableFlip) {
|
||||
contentBoundingClientRect = {
|
||||
width: trackingElement.offsetWidth,
|
||||
height: trackingElement.offsetHeight
|
||||
|
@ -2,7 +2,7 @@ import {
|
||||
ref,
|
||||
computed,
|
||||
watch,
|
||||
onMounted
|
||||
onMounted, inject, toRef
|
||||
} from 'vue'
|
||||
|
||||
export function useFalseUntilTruthy (valueRef) {
|
||||
@ -19,6 +19,11 @@ export function useMergedState (
|
||||
controlledStateRef,
|
||||
uncontrolledStateRef
|
||||
) {
|
||||
watch(controlledStateRef, value => {
|
||||
if (value !== undefined) {
|
||||
uncontrolledStateRef.value = value
|
||||
}
|
||||
})
|
||||
return computed(() => {
|
||||
if (controlledStateRef.value === undefined) {
|
||||
return uncontrolledStateRef.value
|
||||
@ -43,4 +48,18 @@ export function useIsMounted () {
|
||||
return isMounted
|
||||
}
|
||||
|
||||
export function useMemo (valueGenerator, deps) {
|
||||
const valueRef = ref(valueGenerator())
|
||||
watch(deps, () => {
|
||||
valueRef.value = valueGenerator()
|
||||
})
|
||||
return valueRef
|
||||
}
|
||||
|
||||
export function useInjectionRef (injectionName, key, fallback) {
|
||||
const injection = inject(injectionName)
|
||||
if (!injection && arguments.length > 2) return fallback
|
||||
return toRef(injection, key)
|
||||
}
|
||||
|
||||
export { default as useLastClickPosition } from './use-last-click-position'
|
||||
|
11
src/_utils/naive/warn.js
Normal file
11
src/_utils/naive/warn.js
Normal file
@ -0,0 +1,11 @@
|
||||
const warnedMessages = new Set()
|
||||
|
||||
export function warnOnce (location, message) {
|
||||
const mergedMessage = `[naive/${location}]: ${message}`
|
||||
if (warnedMessages.has(mergedMessage)) return
|
||||
warnedMessages.add(mergedMessage)
|
||||
}
|
||||
|
||||
export function warn (location, message) {
|
||||
console.error(`[naive/${location}]: ${message}`)
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { h, createTextVNode } from 'vue'
|
||||
import { createTextVNode } from 'vue'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@ -10,7 +10,7 @@ export default {
|
||||
render () {
|
||||
const { render } = this
|
||||
if (typeof render === 'function') {
|
||||
return render(h)
|
||||
return render()
|
||||
} else if (typeof render === 'string') {
|
||||
return createTextVNode(render)
|
||||
} else if (typeof render === 'number') {
|
||||
|
@ -1,16 +1,7 @@
|
||||
import Menu from './src/MenuAdapter.vue'
|
||||
import MenuItem from './src/MenuItem.vue'
|
||||
import Submenu from './src/Submenu.vue'
|
||||
import MenuItemGroup from './src/MenuItemGroup.vue'
|
||||
import Menu from './src/Menu.js'
|
||||
|
||||
Menu.install = function (app, naive) {
|
||||
app.component(naive.componentPrefix + Menu.name, Menu)
|
||||
// just keep them
|
||||
// they shouldn't be removed since vue can't resolve those components by
|
||||
// local register
|
||||
app.component(MenuItem.name, MenuItem)
|
||||
app.component(Submenu.name, Submenu)
|
||||
app.component(MenuItemGroup.name, MenuItemGroup)
|
||||
}
|
||||
|
||||
export default Menu
|
||||
|
164
src/menu/src/Menu.js
Normal file
164
src/menu/src/Menu.js
Normal file
@ -0,0 +1,164 @@
|
||||
import { h, nextTick, ref, toRef, computed, onMounted } from 'vue'
|
||||
import withapp from '../../_mixins/withapp'
|
||||
import themeable from '../../_mixins/themeable'
|
||||
import usecssr from '../../_mixins/usecssr'
|
||||
import styles from './styles/index'
|
||||
import { useMergedState } from '../../_utils/composition'
|
||||
import {
|
||||
getActivePath,
|
||||
getWrappedItems,
|
||||
itemRenderer
|
||||
} from './utils'
|
||||
|
||||
export default {
|
||||
name: 'Menu',
|
||||
provide () {
|
||||
return {
|
||||
NMenu: this,
|
||||
NSubmenu: null
|
||||
}
|
||||
},
|
||||
mixins: [
|
||||
withapp,
|
||||
themeable,
|
||||
usecssr(styles)
|
||||
],
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
collapsedWidth: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
iconSize: {
|
||||
type: Number,
|
||||
default: 20
|
||||
},
|
||||
collapsedIconSize: {
|
||||
type: Number,
|
||||
default: 20
|
||||
},
|
||||
rootIndent: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
indent: {
|
||||
type: Number,
|
||||
default: 32
|
||||
},
|
||||
defaultExpandedNames: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
expandedNames: {
|
||||
type: Array,
|
||||
default: undefined
|
||||
},
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
mode: {
|
||||
validator (value) {
|
||||
return ['vertical', 'horizontal'].includes(value)
|
||||
},
|
||||
default: 'vertical'
|
||||
},
|
||||
onExpandedNamesChange: {
|
||||
type: Function,
|
||||
default: () => {}
|
||||
},
|
||||
'onUpdate:modelValue': {
|
||||
type: Function,
|
||||
default: () => {}
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// deprecated
|
||||
onOpenNamesChange: {
|
||||
type: Function,
|
||||
default: () => {}
|
||||
},
|
||||
onSelect: {
|
||||
type: Function,
|
||||
default: () => {}
|
||||
},
|
||||
overlayWidth: {
|
||||
type: [Number, String],
|
||||
default: null
|
||||
},
|
||||
overlayMinWidth: {
|
||||
type: [Number, String],
|
||||
default: 180
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
const uncontrolledExpandedNamesRef = ref(props.defaultExpandedNames)
|
||||
const controlledExpandedNamesRef = toRef(props, 'expandedNames')
|
||||
const mergedExpandedNamesRef = useMergedState(
|
||||
controlledExpandedNamesRef,
|
||||
uncontrolledExpandedNamesRef
|
||||
)
|
||||
const itemsRef = toRef(props, 'items')
|
||||
const valueRef = toRef(props, 'modelValue')
|
||||
const activePathRef = computed(() => getActivePath(itemsRef.value, valueRef.value))
|
||||
const transitionDisabledRef = ref(true)
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
transitionDisabledRef.value = false
|
||||
})
|
||||
})
|
||||
return {
|
||||
uncontrolledExpanededNames: uncontrolledExpandedNamesRef,
|
||||
mergedExpandedNames: mergedExpandedNamesRef,
|
||||
activePath: activePathRef,
|
||||
menuItems: getWrappedItems(props.items),
|
||||
transitionDisabled: transitionDisabledRef
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleSelect (value) {
|
||||
this['onUpdate:modelValue'](value)
|
||||
// deprecated
|
||||
this.onSelect(value)
|
||||
},
|
||||
toggleExpand (name) {
|
||||
const currentExpandedNames = Array.from(this.mergedExpandedNames)
|
||||
const index = currentExpandedNames.findIndex(
|
||||
expanededName => expanededName === name
|
||||
)
|
||||
if (~index) {
|
||||
currentExpandedNames.splice(index, 1)
|
||||
} else {
|
||||
currentExpandedNames.push(name)
|
||||
}
|
||||
if (this.expandedNames === undefined) {
|
||||
this.uncontrolledExpanededNames = currentExpandedNames
|
||||
}
|
||||
this.onExpandedNamesChange(currentExpandedNames)
|
||||
// deprecated
|
||||
this.onOpenNamesChange(currentExpandedNames)
|
||||
}
|
||||
},
|
||||
render () {
|
||||
return h('div', {
|
||||
class: [
|
||||
'n-menu',
|
||||
`n-menu--${this.mode}`,
|
||||
{
|
||||
[`n-${this.syntheticTheme}-theme`]: this.syntheticTheme,
|
||||
'n-menu--collapsed': this.collapsed,
|
||||
'n-menu--transition-disabled': this.transitionDisabled
|
||||
}
|
||||
]
|
||||
}, this.menuItems.map(item => itemRenderer(item)))
|
||||
}
|
||||
}
|
@ -1,146 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="n-menu"
|
||||
:class="{
|
||||
[`n-${syntheticTheme}-theme`]: syntheticTheme,
|
||||
[`n-menu--${mode}`]: mode,
|
||||
'n-menu--collapsed': collapsed,
|
||||
'n-menu--transition-disabled': transitionDisabled
|
||||
}"
|
||||
>
|
||||
<ul class="n-menu-list">
|
||||
<slot />
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import withapp from '../../_mixins/withapp'
|
||||
import themeable from '../../_mixins/themeable'
|
||||
import usecssr from '../../_mixins/usecssr'
|
||||
import styles from './styles/index'
|
||||
|
||||
export default {
|
||||
name: 'Menu',
|
||||
provide () {
|
||||
return {
|
||||
NMenu: this,
|
||||
NSubmenu: null
|
||||
}
|
||||
},
|
||||
mixins: [
|
||||
withapp,
|
||||
themeable,
|
||||
usecssr(styles)
|
||||
],
|
||||
model: {
|
||||
prop: 'value',
|
||||
model: 'select'
|
||||
},
|
||||
props: {
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
collapsedWidth: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
iconSize: {
|
||||
type: Number,
|
||||
default: 20
|
||||
},
|
||||
collapsedIconSize: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
overlayWidth: {
|
||||
type: [Number, String],
|
||||
default: null
|
||||
},
|
||||
overlayMinWidth: {
|
||||
type: [Number, String],
|
||||
default: 180
|
||||
},
|
||||
rootIndent: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
indent: {
|
||||
type: Number,
|
||||
default: 32
|
||||
},
|
||||
defaultExpandedNames: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
expandedNames: {
|
||||
type: Array,
|
||||
default: undefined
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'vertical'
|
||||
},
|
||||
/** private */
|
||||
insidePopover: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
submenuCollapsable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
transitionDisabled: true,
|
||||
internalExpandedNames: this.expandedNames || this.defaultExpandedNames
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
syntheticExpandedNames () {
|
||||
if (this.expandedNames !== undefined) return this.expandedNames || []
|
||||
else return this.internalExpandedNames
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.disableTransitionOneTick()
|
||||
},
|
||||
methods: {
|
||||
handleSelect (value) {
|
||||
this.$emit('select', value)
|
||||
this.$emit('input', value)
|
||||
},
|
||||
toggleOpenName (name) {
|
||||
const currentExpandedNames = Array.from(this.syntheticExpandedNames)
|
||||
const index = currentExpandedNames.findIndex(openName => openName === name)
|
||||
if (~index) {
|
||||
currentExpandedNames.splice(index, 1)
|
||||
} else {
|
||||
currentExpandedNames.push(name)
|
||||
}
|
||||
if (this.expandedNames === undefined) {
|
||||
this.internalExpandedNames = currentExpandedNames
|
||||
}
|
||||
this.$emit('open-names-change', currentExpandedNames)
|
||||
},
|
||||
handleExpandedNamesChange (names) {
|
||||
if (this.openName === undefined) {
|
||||
this.internalExpandedNames = names
|
||||
}
|
||||
this.$emit('open-names-change', names)
|
||||
},
|
||||
disableTransitionOneTick () {
|
||||
this.transitionDisabled = true
|
||||
this.$nextTick().then(() => {
|
||||
this.transitionDisabled = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,77 +0,0 @@
|
||||
|
||||
<script>
|
||||
import { h } from 'vue'
|
||||
import Menu from './Menu.vue'
|
||||
import MenuItem from './MenuItem.vue'
|
||||
import Submenu from './Submenu.vue'
|
||||
import MenuItemGroup from './MenuItemGroup.vue'
|
||||
|
||||
// Todo remove unnecessary attrs
|
||||
// Todo refactor to remove slot
|
||||
export default {
|
||||
name: 'Menu',
|
||||
props: Menu.props,
|
||||
render () {
|
||||
if (this.$props.items) {
|
||||
const createItems = items => {
|
||||
return items.map(item => {
|
||||
const props = {
|
||||
title: item.title,
|
||||
titleExtra: item.titleExtra,
|
||||
name: item.name,
|
||||
disabled: !!item.disabled
|
||||
}
|
||||
if (item.children) {
|
||||
const scopedSlots = {}
|
||||
if (typeof item.title === 'function') {
|
||||
delete props.title
|
||||
scopedSlots.header = item.title
|
||||
}
|
||||
if (typeof item.icon === 'function') {
|
||||
scopedSlots.icon = () => item.icon(h)
|
||||
}
|
||||
if (item.group || item.type === 'group') {
|
||||
return h(MenuItemGroup, {
|
||||
props,
|
||||
scopedSlots
|
||||
}, createItems(item.children))
|
||||
} else {
|
||||
return h(Submenu, {
|
||||
props,
|
||||
scopedSlots
|
||||
}, createItems(item.children))
|
||||
}
|
||||
} else {
|
||||
const scopedSlots = {}
|
||||
if (typeof item.title === 'function') {
|
||||
delete props.title
|
||||
scopedSlots.default = item.title
|
||||
}
|
||||
if (typeof item.icon === 'function') {
|
||||
scopedSlots.icon = () => item.icon(h)
|
||||
}
|
||||
return h(MenuItem, {
|
||||
props,
|
||||
scopedSlots
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
return h(Menu,
|
||||
{
|
||||
props: this.$props,
|
||||
scopedSlots: { ...this.$slots },
|
||||
on: this.$listeners,
|
||||
attrs: this.$data.attrs
|
||||
},
|
||||
createItems(this.$props.items)
|
||||
)
|
||||
} else {
|
||||
return h(Menu, {
|
||||
...this.$props,
|
||||
...this.$attrs
|
||||
}, this.$slots)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,143 +1,92 @@
|
||||
<template>
|
||||
<li
|
||||
<div
|
||||
class="n-menu-item"
|
||||
:class="{
|
||||
'n-menu-item--selected': selected,
|
||||
'n-menu-item--disabled': disabled
|
||||
'n-menu-item--disabled': mergedDisabled
|
||||
}"
|
||||
>
|
||||
<template v-if="renderContentAsPopover">
|
||||
<!-- <n-tooltip
|
||||
:placement="menuItemPopoverPlacement"
|
||||
:disabled="rootMenuIsHorizontal || !rootMenuCollapsed"
|
||||
>
|
||||
<template v-slot:activator>
|
||||
<n-menu-item-content
|
||||
:padding-left="delayedPaddingLeft"
|
||||
:max-icon-size="maxIconSize"
|
||||
:active-icon-size="activeIconSize"
|
||||
:title="title"
|
||||
:disabled="syntheticDisabled"
|
||||
:title-extra="titleExtra"
|
||||
@click="handleClick"
|
||||
>
|
||||
<template v-if="$slots.icon" v-slot:icon>
|
||||
<slot name="icon" />
|
||||
</template>
|
||||
<template v-if="$slots['header-extra']" v-slot:header-extra>
|
||||
<slot name="extra" />
|
||||
</template>
|
||||
<slot />
|
||||
</n-menu-item-content>
|
||||
</template>
|
||||
{{ title }}
|
||||
</n-tooltip> -->
|
||||
</template>
|
||||
<n-menu-item-content
|
||||
v-else
|
||||
:max-icon-size="maxIconSize"
|
||||
:active-icon-size="activeIconSize"
|
||||
:padding-left="delayedPaddingLeft"
|
||||
:title="title"
|
||||
:title-extra="titleExtra"
|
||||
:disabled="syntheticDisabled"
|
||||
@click="handleClick"
|
||||
<n-tooltip
|
||||
trigger="hover"
|
||||
:placement="popoverPlacement"
|
||||
:disabled="!popoverEnabled"
|
||||
>
|
||||
<template v-if="$slots.icon" v-slot:icon>
|
||||
<slot name="icon" />
|
||||
<template v-slot:trigger>
|
||||
<n-menu-item-content
|
||||
:padding-left="delayedPaddingLeft"
|
||||
:max-icon-size="maxIconSize"
|
||||
:active-icon-size="activeIconSize"
|
||||
:title="title"
|
||||
:disabled="mergedDisabled"
|
||||
:title-extra="titleExtra"
|
||||
:icon="icon"
|
||||
@click="handleClick"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="$slots['header-extra']" v-slot:header-extra>
|
||||
<slot name="header-extra" />
|
||||
</template>
|
||||
<slot />
|
||||
</n-menu-item-content>
|
||||
</li>
|
||||
{{ title }}
|
||||
</n-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import collectable from '../../_mixins/collectable'
|
||||
import withapp from '../../_mixins/withapp'
|
||||
import themeable from '../../_mixins/themeable'
|
||||
import simulatedComputed from '../../_mixins/simulatedComputed'
|
||||
import { toRef, computed } from 'vue'
|
||||
import NMenuItemContent from './MenuItemContent'
|
||||
import NTooltip from '../../tooltip'
|
||||
import menuContentMixin from './menuContentMixin'
|
||||
import menuChildMixin from './menu-child-mixin'
|
||||
import { useMemo, useInjectionRef } from '../../_utils/composition'
|
||||
|
||||
export default {
|
||||
name: 'NMenuItem',
|
||||
name: 'MenuItem',
|
||||
components: {
|
||||
NMenuItemContent,
|
||||
NTooltip
|
||||
},
|
||||
mixins: [
|
||||
collectable('PenetratedNSubmenu', 'menuItemNames', 'name', true, function (injectedNSubmenu) {
|
||||
const injectedNMenu = this.NMenu
|
||||
if (injectedNMenu !== injectedNSubmenu.NMenu) {
|
||||
if (injectedNSubmenu.rootMenuIsHorizontal) return false
|
||||
return true
|
||||
}
|
||||
}),
|
||||
simulatedComputed({
|
||||
selected: {
|
||||
get () {
|
||||
if (this.rootMenuValue === this.name) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
},
|
||||
deps: ['rootMenuValue'],
|
||||
default: false
|
||||
}
|
||||
}),
|
||||
withapp,
|
||||
themeable,
|
||||
menuContentMixin
|
||||
menuChildMixin
|
||||
],
|
||||
props: {
|
||||
title: {
|
||||
type: [ String, Function ],
|
||||
default: null
|
||||
},
|
||||
titleExtra: {
|
||||
type: [ String, Function ],
|
||||
default: null
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: undefined
|
||||
default: false
|
||||
},
|
||||
icon: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
onClick: {
|
||||
type: Function,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data () {
|
||||
setup (props) {
|
||||
const rootMenuValueRef = useInjectionRef('NMenu', 'modelValue')
|
||||
const submenuDisabledRef = useInjectionRef('NSubmenu', 'mergedDisabled', false)
|
||||
const nameRef = toRef(props, 'name')
|
||||
const mergedDisabledRef = computed(() => {
|
||||
return submenuDisabledRef.value || props.disabled
|
||||
})
|
||||
return {
|
||||
delayedPaddingLeft: null
|
||||
selected: useMemo(() => {
|
||||
if (rootMenuValueRef.value === props.name) return true
|
||||
return false
|
||||
}, [rootMenuValueRef, nameRef]),
|
||||
mergedDisabled: mergedDisabledRef
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
syntheticDisabled () {
|
||||
if (this.disabled !== undefined) return this.disabled
|
||||
return this.PenetratedNSubmenu && this.PenetratedNSubmenu.syntheticDisabled
|
||||
popoverEnabled () {
|
||||
return !this.horizontal && this.root && this.menuCollapsed && !this.mergedDisabled
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
paddingLeft (value) {
|
||||
this.$nextTick().then(() => {
|
||||
this.delayedPaddingLeft = value
|
||||
})
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.delayedPaddingLeft = this.paddingLeft
|
||||
},
|
||||
methods: {
|
||||
handleClick () {
|
||||
if (!this.syntheticDisabled) {
|
||||
handleClick (e) {
|
||||
if (!this.mergedDisabled) {
|
||||
this.NMenu.handleSelect(this.name)
|
||||
this.$emit('click', this)
|
||||
this.onClick(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
class="n-menu-item-content"
|
||||
:style="{ paddingLeft: paddingLeft && (paddingLeft + 'px') }"
|
||||
:style="style"
|
||||
:class="{
|
||||
'n-menu-item-content--collapsed': collapsed,
|
||||
'n-menu-item-content--child-selected': childSelected,
|
||||
@ -12,25 +12,17 @@
|
||||
@click="handleClick"
|
||||
>
|
||||
<div
|
||||
v-if="$slots.icon"
|
||||
v-if="icon"
|
||||
class="n-menu-item-content__icon"
|
||||
:style="{
|
||||
width: maxIconSize && (maxIconSize + 'px'),
|
||||
height: maxIconSize && (maxIconSize + 'px'),
|
||||
fontSize: activeIconSize && (activeIconSize + 'px'),
|
||||
}"
|
||||
:style="iconStyle"
|
||||
>
|
||||
<slot name="icon" />
|
||||
<render :render="icon" />
|
||||
</div>
|
||||
<div class="n-menu-item-content-header">
|
||||
<slot name="header">
|
||||
<render :render="title" />
|
||||
</slot>
|
||||
<slot name="header-extra">
|
||||
<span v-if="titleExtra" class="n-menu-item-content-header__extra">
|
||||
<render v-if="titleExtra" :render="titleExtra" />
|
||||
</span>
|
||||
</slot>
|
||||
<render :render="title" />
|
||||
<span v-if="titleExtra" class="n-menu-item-content-header__extra">
|
||||
<render :render="titleExtra" />
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="showArrow" class="n-menu-item-content__arrow" />
|
||||
</div>
|
||||
@ -40,7 +32,7 @@
|
||||
import render from '../../_utils/vue/render'
|
||||
|
||||
export default {
|
||||
name: 'NMenuItemContent',
|
||||
name: 'MenuItemContent',
|
||||
components: {
|
||||
render
|
||||
},
|
||||
@ -73,6 +65,10 @@ export default {
|
||||
type: [String, Function],
|
||||
default: null
|
||||
},
|
||||
icon: {
|
||||
type: [String, Function],
|
||||
default: null
|
||||
},
|
||||
showArrow: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
@ -88,11 +84,32 @@ export default {
|
||||
uncollapsable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
onClick: {
|
||||
type: Function,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
style () {
|
||||
const { paddingLeft } = this
|
||||
return { paddingLeft: paddingLeft && (paddingLeft + 'px') }
|
||||
},
|
||||
iconStyle () {
|
||||
const {
|
||||
maxIconSize,
|
||||
activeIconSize
|
||||
} = this
|
||||
return {
|
||||
width: maxIconSize + 'px',
|
||||
height: maxIconSize + 'px',
|
||||
fontSize: activeIconSize + 'px'
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClick () {
|
||||
this.$emit('click')
|
||||
this.onClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
38
src/menu/src/MenuItemGroup.js
Normal file
38
src/menu/src/MenuItemGroup.js
Normal file
@ -0,0 +1,38 @@
|
||||
import { h } from 'vue'
|
||||
import render from '../../_utils/vue/render'
|
||||
import { itemRenderer } from './utils'
|
||||
import menuChildMixin from './menu-child-mixin'
|
||||
|
||||
export default {
|
||||
name: 'MenuItemGroup',
|
||||
mixins: [
|
||||
menuChildMixin
|
||||
],
|
||||
provide () {
|
||||
return {
|
||||
NMenuItemGroup: this,
|
||||
NSubmenu: null
|
||||
}
|
||||
},
|
||||
props: {
|
||||
children: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
render () {
|
||||
return h('div', {
|
||||
class: 'n-menu-item-group'
|
||||
}, [
|
||||
h('span', {
|
||||
class: 'n-menu-item-group-title',
|
||||
style: `padding-left: ${this.delayedPaddingLeft}px;`
|
||||
}, [
|
||||
h(render, {
|
||||
render: this.title
|
||||
})
|
||||
]),
|
||||
h('div', this.children.map(item => itemRenderer(item)))
|
||||
])
|
||||
}
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
<template>
|
||||
<li class="n-menu-item-group">
|
||||
<span class="n-menu-item-group-title" :style="{ paddingLeft: delayedPaddingLeft && delayedPaddingLeft + 'px' }">
|
||||
<slot name="header"><render :render="title" /></slot>
|
||||
</span>
|
||||
<div>
|
||||
<slot />
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
<script>
|
||||
import render from '../../_utils/vue/render'
|
||||
|
||||
export default {
|
||||
name: 'NMenuItemGroup',
|
||||
components: {
|
||||
render
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: [String, Function],
|
||||
default: null
|
||||
}
|
||||
},
|
||||
provide () {
|
||||
return {
|
||||
NMenuItemGroup: this,
|
||||
NSubmenu: null
|
||||
}
|
||||
},
|
||||
inject: {
|
||||
NMenuItemGroup: {
|
||||
default: null
|
||||
},
|
||||
NMenu: {
|
||||
default: null
|
||||
},
|
||||
NSubmenu: {
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
delayedPaddingLeft: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
atRoot () {
|
||||
return !this.NSubmenu && !this.NMenuItemGroup
|
||||
},
|
||||
paddingLeft () {
|
||||
if (this.atRoot) {
|
||||
return this.NMenu.rootIndent === null ? this.NMenu.indent : this.NMenu.rootIndent
|
||||
}
|
||||
if (this.NMenuItemGroup) {
|
||||
return this.NMenu.indent / 2 + this.NMenuItemGroup.paddingLeft
|
||||
} else if (this.NSubmenu) {
|
||||
return this.NMenu.indent / 2 + this.NSubmenu.paddingLeft
|
||||
} else {
|
||||
return this.NMenu.indent / 2
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
paddingLeft (value) {
|
||||
this.$nextTick().then(() => {
|
||||
this.delayedPaddingLeft = value
|
||||
})
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.delayedPaddingLeft = this.paddingLeft
|
||||
}
|
||||
}
|
||||
</script>
|
176
src/menu/src/Submenu.js
Normal file
176
src/menu/src/Submenu.js
Normal file
@ -0,0 +1,176 @@
|
||||
import { h, withDirectives, vShow, toRef, ref } from 'vue'
|
||||
import FadeInHeightExpandTransition from '../../_transition/FadeInHeightExpandTransition'
|
||||
import NPopover from '../../popover/src/Popover'
|
||||
import NMenuItemContent from './MenuItemContent'
|
||||
import menuChildMixin from './menu-child-mixin'
|
||||
import { itemRenderer } from './utils'
|
||||
import { useInjectionRef, useMemo } from '../../_utils/composition'
|
||||
|
||||
export default {
|
||||
name: 'Submenu',
|
||||
mixins: [
|
||||
menuChildMixin
|
||||
],
|
||||
provide () {
|
||||
return {
|
||||
NSubmenu: this,
|
||||
NMenuItemGroup: null
|
||||
}
|
||||
},
|
||||
props: {
|
||||
titleExtra: {
|
||||
type: [String, Function],
|
||||
default: null
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
children: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
icon: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
onClick: {
|
||||
type: Function,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
const activePathRef = useInjectionRef('NMenu', 'activePath')
|
||||
const nameRef = toRef(props, 'name')
|
||||
return {
|
||||
selectedInside: useMemo(() => {
|
||||
return activePathRef.value.includes(nameRef.value)
|
||||
}, [
|
||||
activePathRef,
|
||||
nameRef
|
||||
]),
|
||||
popoverBodyStyle: ref({
|
||||
padding: '2px 4px',
|
||||
minWidth: '180px'
|
||||
}),
|
||||
popoverShow: ref(false)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
mergedDisabled () {
|
||||
const {
|
||||
NMenu,
|
||||
NSubmenu,
|
||||
disabled
|
||||
} = this
|
||||
if (NSubmenu && NSubmenu.mergedDisabled) return true
|
||||
if (NMenu.disabled) return true
|
||||
return disabled
|
||||
},
|
||||
collapsed () {
|
||||
if (this.horizontal) return false
|
||||
if (this.insidePopover) return false
|
||||
if (this.menuCollapsed) {
|
||||
return true
|
||||
}
|
||||
return this.NMenu.mergedExpandedNames.includes(this.name)
|
||||
},
|
||||
popoverEnabled () {
|
||||
return !this.mergedDisabled && (this.horizontal || this.menuCollapsed)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClick () {
|
||||
if (!this.mergedDisabled && !this.menuCollapsed) {
|
||||
this.NMenu.toggleExpand(this.name)
|
||||
this.onClick()
|
||||
}
|
||||
},
|
||||
handlePopoverShowChange (value) {
|
||||
this.popoverShow = value
|
||||
}
|
||||
},
|
||||
render () {
|
||||
const createSubmenuItem = () => {
|
||||
const {
|
||||
delayedPaddingLeft,
|
||||
collapsed,
|
||||
mergedDisabled,
|
||||
maxIconSize,
|
||||
activeIconSize,
|
||||
title,
|
||||
titleExtra,
|
||||
horizontal,
|
||||
selectedInside,
|
||||
icon,
|
||||
handleClick,
|
||||
popoverShow,
|
||||
insidePopover
|
||||
} = this
|
||||
return h(NMenuItemContent, {
|
||||
paddingLeft: delayedPaddingLeft,
|
||||
collapsed,
|
||||
disabled: mergedDisabled,
|
||||
maxIconSize,
|
||||
activeIconSize,
|
||||
title,
|
||||
titleExtra,
|
||||
showArrow: !horizontal && !insidePopover,
|
||||
uncollapsable: insidePopover,
|
||||
childSelected: selectedInside,
|
||||
icon,
|
||||
hover: popoverShow,
|
||||
onClick: handleClick
|
||||
})
|
||||
}
|
||||
const createSubmenuChildren = (insidePopover = false) => {
|
||||
return h(FadeInHeightExpandTransition, null, {
|
||||
default: () => {
|
||||
const {
|
||||
children,
|
||||
collapsed
|
||||
} = this
|
||||
return withDirectives(
|
||||
h('div', {
|
||||
class: 'n-submenu-children'
|
||||
}, children.map(item => itemRenderer(item, insidePopover))),
|
||||
[
|
||||
[vShow, insidePopover || !collapsed]
|
||||
]
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
return this.root ? h(NPopover, {
|
||||
trigger: 'hover',
|
||||
disabled: !this.popoverEnabled,
|
||||
bodyStyle: this.popoverBodyStyle,
|
||||
placement: this.popoverPlacement,
|
||||
showArrow: false,
|
||||
'onUpdate:show': this.handlePopoverShowChange
|
||||
}, {
|
||||
trigger: () => h('div', {
|
||||
class: 'n-submenu'
|
||||
}, [
|
||||
createSubmenuItem(),
|
||||
!this.horizontal ? createSubmenuChildren() : null
|
||||
]),
|
||||
default: () => {
|
||||
const theme = this.NMenu.syntheticTheme
|
||||
return h('div', {
|
||||
class: [
|
||||
'n-menu',
|
||||
{
|
||||
[`n-${theme}-theme`]: theme
|
||||
}
|
||||
]
|
||||
}, createSubmenuChildren(true))
|
||||
}
|
||||
}) : h('div', {
|
||||
class: 'n-submenu'
|
||||
}, [
|
||||
createSubmenuItem(),
|
||||
createSubmenuChildren()
|
||||
])
|
||||
}
|
||||
}
|
@ -1,210 +0,0 @@
|
||||
<template>
|
||||
<li
|
||||
class="n-submenu"
|
||||
>
|
||||
<template v-if="renderContentAsPopover">
|
||||
<n-popover
|
||||
trigger="hover"
|
||||
:theme="NMenu.syntheticTheme"
|
||||
:placement="submenuPopoverPlacement"
|
||||
:show-arrow="false"
|
||||
:controller="popoverController"
|
||||
:disabled="(!rootMenuIsHorizontal && !rootMenuCollapsed) || syntheticDisabled"
|
||||
:display-directive="rootMenuIsHorizontal ? 'show' : 'if'"
|
||||
:overlay-style="{
|
||||
width: overlayWidth === null ? null : overlayMinWidth,
|
||||
minWidth: overlayMinWidth,
|
||||
padding: '8px 0'
|
||||
}"
|
||||
@show="handlePopMenuShow"
|
||||
@hide="handlePopMenuHide"
|
||||
>
|
||||
<template v-slot:activator>
|
||||
<n-menu-item-content
|
||||
:padding-left="delayedPaddingLeft"
|
||||
:collapsed="syntheticCollapsed"
|
||||
:disabled="disabled"
|
||||
:max-icon-size="maxIconSize"
|
||||
:active-icon-size="activeIconSize"
|
||||
:title="title"
|
||||
:title-extra="titleExtra"
|
||||
:hover="hover"
|
||||
:show-arrow="!rootMenuIsHorizontal"
|
||||
:child-selected="selectedInside"
|
||||
@click="handleClick"
|
||||
>
|
||||
<template v-if="$slots.icon" v-slot:icon>
|
||||
<slot name="icon" />
|
||||
</template>
|
||||
<template v-slot:header>
|
||||
<slot v-if="$slots.header" name="header" />
|
||||
</template>
|
||||
<template v-if="$slots['header-extra']" v-slot:header-extra>
|
||||
<slot name="header-extra" />
|
||||
</template>
|
||||
</n-menu-item-content>
|
||||
</template>
|
||||
<n-menu
|
||||
:theme="NMenu.syntheticTheme"
|
||||
:root-indent="24"
|
||||
:indent="24"
|
||||
:inside-popover="true"
|
||||
:submenu-collapsable="false"
|
||||
:value="rootMenuValue"
|
||||
@select="handlePopMenuSelect"
|
||||
>
|
||||
<slot />
|
||||
</n-menu>
|
||||
</n-popover>
|
||||
<fade-in-height-expand-transition v-if="!rootMenuIsHorizontal">
|
||||
<ul
|
||||
v-show="!syntheticCollapsed"
|
||||
class="n-submenu-content"
|
||||
>
|
||||
<slot />
|
||||
</ul>
|
||||
</fade-in-height-expand-transition>
|
||||
</template>
|
||||
<template v-else>
|
||||
<n-menu-item-content
|
||||
:padding-left="delayedPaddingLeft"
|
||||
:collapsed="syntheticCollapsed"
|
||||
:disabled="disabled"
|
||||
:max-icon-size="maxIconSize"
|
||||
:active-icon-size="activeIconSize"
|
||||
:title="title"
|
||||
:title-extra="titleExtra"
|
||||
:show-arrow="!rootMenuInsidePopover"
|
||||
:uncollapsable="rootMenuInsidePopover"
|
||||
:child-selected="selectedInside"
|
||||
@click="handleClick"
|
||||
>
|
||||
<template v-if="$slots.icon" v-slot:icon>
|
||||
<slot name="icon" />
|
||||
</template>
|
||||
<template v-if="$slots.header" v-slot:header>
|
||||
<slot name="header" />
|
||||
</template>
|
||||
<template v-if="$slots['header-extra']" v-slot:header-extra>
|
||||
<slot name="header-extra" />
|
||||
</template>
|
||||
</n-menu-item-content>
|
||||
<fade-in-height-expand-transition>
|
||||
<ul
|
||||
v-show="!syntheticCollapsed"
|
||||
class="n-submenu-content"
|
||||
>
|
||||
<slot />
|
||||
</ul>
|
||||
</fade-in-height-expand-transition>
|
||||
</template>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FadeInHeightExpandTransition from '../../_transition/FadeInHeightExpandTransition'
|
||||
import NPopover from '../../popover'
|
||||
import NMenuItemContent from './MenuItemContent'
|
||||
import NMenu from './Menu'
|
||||
import menuContentMixin from './menuContentMixin'
|
||||
import formatLength from '../../_utils/css/formatLength'
|
||||
|
||||
export default {
|
||||
name: 'NSubmenu',
|
||||
components: {
|
||||
NMenuItemContent,
|
||||
FadeInHeightExpandTransition,
|
||||
NPopover,
|
||||
NMenu
|
||||
},
|
||||
mixins: [menuContentMixin],
|
||||
props: {
|
||||
title: {
|
||||
type: [String, Function],
|
||||
default: null
|
||||
},
|
||||
titleExtra: {
|
||||
type: [String, Function],
|
||||
default: null
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: undefined
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
delayedPaddingLeft: null,
|
||||
menuItemNames: [],
|
||||
hover: false,
|
||||
popoverController: {}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
overlayWidth () {
|
||||
return formatLength(this.NMenu.overlayWidth)
|
||||
},
|
||||
overlayMinWidth () {
|
||||
return formatLength(this.NMenu.overlayMinWidth)
|
||||
},
|
||||
selectedInside () {
|
||||
return this.menuItemNames.includes(this.NMenu.value)
|
||||
},
|
||||
renderedContentAsPopover () {
|
||||
return this.rootMenuCollapsed && this.atRoot
|
||||
},
|
||||
syntheticDisabled () {
|
||||
if (this.disabled !== undefined) return this.disabled
|
||||
if (this.PenetratedNSubmenu) return this.PenetratedNSubmenu.syntheticDisabled
|
||||
return this.NMenu && this.NMenu.disabled
|
||||
},
|
||||
collapsedAccrodingToExpandedNames () {
|
||||
return !this.NMenu.syntheticExpandedNames.includes(this.name)
|
||||
},
|
||||
syntheticCollapsed () {
|
||||
if (!this.NMenu.submenuCollapsable) return false
|
||||
else if (this.rootMenuCollapsed) return true
|
||||
return this.collapsedAccrodingToExpandedNames
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
paddingLeft (value) {
|
||||
this.$nextTick().then(() => {
|
||||
this.delayedPaddingLeft = value
|
||||
})
|
||||
}
|
||||
},
|
||||
provide () {
|
||||
return {
|
||||
NSubmenu: this,
|
||||
PenetratedNSubmenu: this,
|
||||
NMenuItemGroup: null
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.delayedPaddingLeft = this.paddingLeft
|
||||
},
|
||||
methods: {
|
||||
handleClick () {
|
||||
if (!this.disabled && !this.NMenu.collapsed) {
|
||||
this.NMenu.toggleOpenName(this.name)
|
||||
this.$emit('click', this)
|
||||
}
|
||||
},
|
||||
handlePopMenuSelect (value) {
|
||||
this.NMenu.handleSelect(value)
|
||||
this.popoverController.hide()
|
||||
},
|
||||
handlePopMenuHide () {
|
||||
this.hover = false
|
||||
},
|
||||
handlePopMenuShow () {
|
||||
this.hover = true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
129
src/menu/src/menu-child-mixin.js
Normal file
129
src/menu/src/menu-child-mixin.js
Normal file
@ -0,0 +1,129 @@
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
export default {
|
||||
inject: {
|
||||
NMenu: {
|
||||
default: null
|
||||
},
|
||||
NSubmenu: {
|
||||
default: null
|
||||
},
|
||||
NMenuItemGroup: {
|
||||
default: null
|
||||
}
|
||||
},
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
root: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
level: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
title: {
|
||||
type: [String, Function],
|
||||
default: null
|
||||
},
|
||||
insidePopover: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
delayedPaddingLeft: null
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.delayedPaddingLeft = this.paddingLeft
|
||||
},
|
||||
watch: {
|
||||
paddingLeft (value) {
|
||||
nextTick(() => {
|
||||
this.delayedPaddingLeft = value
|
||||
})
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
horizontal () {
|
||||
return this.NMenu.mode === 'horizontal'
|
||||
},
|
||||
popoverPlacement () {
|
||||
if (this.horizontal) {
|
||||
return 'bottom'
|
||||
}
|
||||
if ('children' in this) return 'right-start'
|
||||
return 'right'
|
||||
},
|
||||
menuCollapsed () {
|
||||
return this.NMenu.collapsed
|
||||
},
|
||||
maxIconSize () {
|
||||
return Math.max(this.collapsedIconSize, this.iconSize)
|
||||
},
|
||||
activeIconSize () {
|
||||
if (
|
||||
!this.horizontal &&
|
||||
this.root &&
|
||||
this.menuCollapsed
|
||||
) {
|
||||
return this.collapsedIconSize
|
||||
} else {
|
||||
return this.iconSize
|
||||
}
|
||||
},
|
||||
iconSize () {
|
||||
const {
|
||||
NMenu
|
||||
} = this
|
||||
return NMenu.iconSize
|
||||
},
|
||||
collapsedIconSize () {
|
||||
const {
|
||||
NMenu: {
|
||||
iconSize,
|
||||
collapsedIconSize
|
||||
}
|
||||
} = this
|
||||
return collapsedIconSize === null ? iconSize : collapsedIconSize
|
||||
},
|
||||
paddingLeft () {
|
||||
// TODO handle popover
|
||||
const {
|
||||
NMenu: {
|
||||
collapsedWidth,
|
||||
indent,
|
||||
rootIndent
|
||||
},
|
||||
NSubmenu,
|
||||
NMenuItemGroup,
|
||||
root,
|
||||
horizontal,
|
||||
collapsedIconSize,
|
||||
menuCollapsed,
|
||||
insidePopover,
|
||||
level
|
||||
} = this
|
||||
if (insidePopover && level === 1) return 12
|
||||
const mergedRootIndent = rootIndent === null ? indent : rootIndent
|
||||
const menuCollapsedPaddingLeft = collapsedWidth / 2 - collapsedIconSize / 2
|
||||
const menuCollapsedPaddingDiff = menuCollapsed ? mergedRootIndent - menuCollapsedPaddingLeft : 0
|
||||
if (root) {
|
||||
if (horizontal) return null
|
||||
return mergedRootIndent - menuCollapsedPaddingDiff
|
||||
}
|
||||
if (NMenuItemGroup) {
|
||||
return indent / 2 + NMenuItemGroup.paddingLeft
|
||||
}
|
||||
if (NSubmenu) {
|
||||
return indent + NSubmenu.paddingLeft
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
export default {
|
||||
inject: {
|
||||
NMenu: {
|
||||
default: null
|
||||
},
|
||||
NSubmenu: {
|
||||
default: null
|
||||
},
|
||||
NMenuItemGroup: {
|
||||
default: null
|
||||
},
|
||||
PenetratedNSubmenu: {
|
||||
default: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
atRoot () {
|
||||
return !this.NSubmenu && !this.NMenuItemGroup
|
||||
},
|
||||
rootMenuInsidePopover () {
|
||||
return this.NMenu.insidePopover
|
||||
},
|
||||
renderContentAsPopover () {
|
||||
return this.atRoot && !this.rootMenuInsidePopover
|
||||
},
|
||||
rootMenuCollapsed () {
|
||||
return !this.rootMenuIsHorizontal && this.NMenu.collapsed
|
||||
},
|
||||
rootMenuValue () {
|
||||
return this.NMenu.value
|
||||
},
|
||||
rootMenuMode () {
|
||||
return this.NMenu.mode
|
||||
},
|
||||
rootMenuIsHorizontal () {
|
||||
return this.rootMenuMode === 'horizontal'
|
||||
},
|
||||
menuItemPopoverPlacement () {
|
||||
if (this.rootMenuMode === 'horizontal') {
|
||||
return 'bottom'
|
||||
}
|
||||
return 'right'
|
||||
},
|
||||
submenuPopoverPlacement () {
|
||||
if (this.rootMenuMode === 'horizontal') {
|
||||
return 'bottom'
|
||||
}
|
||||
return 'right-start'
|
||||
},
|
||||
maxIconSize () {
|
||||
return Math.max(this.collapsedIconSize, this.iconSize)
|
||||
},
|
||||
activeIconSize () {
|
||||
if (
|
||||
!this.rootMenuInsidePopover &&
|
||||
this.rootMenuCollapsed &&
|
||||
this.atRoot
|
||||
) {
|
||||
return this.collapsedIconSize
|
||||
} else {
|
||||
return this.iconSize
|
||||
}
|
||||
},
|
||||
iconSize () {
|
||||
return this.NMenu && this.NMenu.iconSize
|
||||
},
|
||||
collapsedIconSize () {
|
||||
return this.NMenu.collapsedIconSize === null ? this.NMenu.iconSize : this.NMenu.collapsedIconSize
|
||||
},
|
||||
paddingLeft () {
|
||||
if (this.rootMenuIsHorizontal) return null
|
||||
if (this.atRoot && this.NMenu.collapsedWidth !== null && this.NMenu.collapsed) {
|
||||
return this.NMenu.collapsedWidth / 2 - this.iconSize / 2
|
||||
}
|
||||
if (this.NMenuItemGroup) {
|
||||
return this.NMenu.indent / 2 + this.NMenuItemGroup.paddingLeft
|
||||
} else if (this.NSubmenu) {
|
||||
return this.NMenu.indent + this.NSubmenu.paddingLeft
|
||||
} else {
|
||||
return this.NMenu.rootIndent === null ? this.NMenu.indent : this.NMenu.rootIndent
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -32,7 +32,8 @@ export default c([
|
||||
transition: `background-color .3s ${easeInOutCubicBezier}`,
|
||||
width: '100%',
|
||||
boxSizing: 'border-box',
|
||||
fontSize: '14px'
|
||||
fontSize: '14px',
|
||||
paddingBottom: '6px'
|
||||
}, [
|
||||
cM('transition-disabled', [
|
||||
cB('menu-item-content', [
|
||||
@ -50,80 +51,67 @@ export default c([
|
||||
})
|
||||
])
|
||||
]),
|
||||
cM('horizontal', [
|
||||
cB('menu-list', {
|
||||
display: 'flex'
|
||||
cM('horizontal', {
|
||||
display: 'flex'
|
||||
}, [
|
||||
cB('submenu', {
|
||||
margin: 0
|
||||
}),
|
||||
cB('menu-item', {
|
||||
margin: 0
|
||||
}, [
|
||||
cB('submenu', {
|
||||
margin: 0
|
||||
c('&::after', {
|
||||
backgroundColor: 'transparent !important'
|
||||
}),
|
||||
cB('menu-item', {
|
||||
margin: 0
|
||||
}, [
|
||||
c('&::after', {
|
||||
backgroundColor: 'transparent !important'
|
||||
}),
|
||||
cM('selected', [
|
||||
cB('menu-item-content', {
|
||||
borderBottom: `2px solid ${borderColorHorizontal}`
|
||||
})
|
||||
])
|
||||
]),
|
||||
cB('menu-item-content', {
|
||||
padding: '0 20px',
|
||||
borderBottom: '2px solid transparent'
|
||||
}, [
|
||||
cM('child-selected', {
|
||||
cM('selected', [
|
||||
cB('menu-item-content', {
|
||||
borderBottom: `2px solid ${borderColorHorizontal}`
|
||||
}),
|
||||
cNotM('disabled', [
|
||||
c('&:hover', {
|
||||
borderBottom: `2px solid ${borderColorHorizontal}`
|
||||
}),
|
||||
cM('hover', {
|
||||
borderBottom: `2px solid ${borderColorHorizontal}`
|
||||
})
|
||||
])
|
||||
})
|
||||
])
|
||||
]),
|
||||
cB('menu-item-content', {
|
||||
padding: '0 20px',
|
||||
borderBottom: '2px solid transparent'
|
||||
}, [
|
||||
cM('child-selected', {
|
||||
borderBottom: `2px solid ${borderColorHorizontal}`
|
||||
}),
|
||||
cNotM('disabled', [
|
||||
hoverStyle({
|
||||
borderBottom: `2px solid ${borderColorHorizontal}`
|
||||
})
|
||||
])
|
||||
])
|
||||
]),
|
||||
cM('collapsed', [
|
||||
cB('menu-list', [
|
||||
cB('menu-item', [
|
||||
c('&::after', {
|
||||
backgroundColor: 'transparent !important'
|
||||
cB('menu-item', [
|
||||
c('&::after', {
|
||||
backgroundColor: 'transparent !important'
|
||||
})
|
||||
]),
|
||||
cB('menu-item-content', [
|
||||
cB('menu-item-content-header', {
|
||||
opacity: 0
|
||||
}),
|
||||
cE('arrow', {
|
||||
opacity: 0
|
||||
}),
|
||||
cE('icon', [
|
||||
cB('icon', {
|
||||
fill: itemIconColorCollapsed,
|
||||
stroke: itemIconColorCollapsed
|
||||
})
|
||||
]),
|
||||
cB('menu-item-content', [
|
||||
cB('menu-item-content-header', {
|
||||
opacity: 0
|
||||
}),
|
||||
cE('arrow', {
|
||||
opacity: 0
|
||||
}),
|
||||
cE('icon', [
|
||||
cB('icon', {
|
||||
fill: itemIconColorCollapsed,
|
||||
stroke: itemIconColorCollapsed
|
||||
})
|
||||
])
|
||||
])
|
||||
])
|
||||
]),
|
||||
cB('menu-list', {
|
||||
listStyle: 'none',
|
||||
margin: 0,
|
||||
padding: 0
|
||||
cB('menu-item', {
|
||||
transition: `background-color .3s ${easeInOutCubicBezier}`,
|
||||
height: '42px',
|
||||
marginTop: '6px',
|
||||
position: 'relative'
|
||||
}, [
|
||||
cB('menu-item', {
|
||||
transition: `background-color .3s ${easeInOutCubicBezier}`,
|
||||
listStyle: 'none',
|
||||
height: '42px',
|
||||
marginTop: '6px',
|
||||
position: 'relative'
|
||||
}, [
|
||||
c('&::after', {
|
||||
raw: `
|
||||
c('&::after', {
|
||||
raw: `
|
||||
content: "";
|
||||
background-color: transparent;
|
||||
position: absolute;
|
||||
@ -133,37 +121,37 @@ export default c([
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
`,
|
||||
borderRadius,
|
||||
transition: `background-color .3s ${easeInOutCubicBezier}`
|
||||
borderRadius,
|
||||
transition: `background-color .3s ${easeInOutCubicBezier}`
|
||||
}),
|
||||
cNotM('disabled', [
|
||||
c('&:active::after', {
|
||||
backgroundColor: itemColorMatch
|
||||
})
|
||||
]),
|
||||
cM('selected', [
|
||||
c('&::after', {
|
||||
backgroundColor: itemColorMatch
|
||||
}),
|
||||
cNotM('disabled', [
|
||||
c('&:active::after', {
|
||||
backgroundColor: itemColorMatch
|
||||
})
|
||||
]),
|
||||
cM('selected', [
|
||||
c('&::after', {
|
||||
backgroundColor: itemColorMatch
|
||||
}),
|
||||
cB('menu-item-content', [
|
||||
cE('icon', [
|
||||
cB('icon', {
|
||||
fill: itemIconColorSelected,
|
||||
stroke: itemIconColorSelected
|
||||
})
|
||||
]),
|
||||
cB('menu-item-content-header', {
|
||||
color: itemTextColorSelected
|
||||
}, [
|
||||
cE('extra', {
|
||||
color: itemExtraTextColorSelected
|
||||
})
|
||||
])
|
||||
cB('menu-item-content', [
|
||||
cE('icon', [
|
||||
cB('icon', {
|
||||
fill: itemIconColorSelected,
|
||||
stroke: itemIconColorSelected
|
||||
})
|
||||
]),
|
||||
cB('menu-item-content-header', {
|
||||
color: itemTextColorSelected
|
||||
}, [
|
||||
cE('extra', {
|
||||
color: itemExtraTextColorSelected
|
||||
})
|
||||
])
|
||||
])
|
||||
]),
|
||||
cB('menu-item-content', {
|
||||
raw: `
|
||||
])
|
||||
]),
|
||||
cB('menu-item-content', {
|
||||
raw: `
|
||||
box-sizing: border-box;
|
||||
line-height: 1.75;
|
||||
height: 100%;
|
||||
@ -178,60 +166,60 @@ export default c([
|
||||
padding-left .3s ${easeInOutCubicBezier},
|
||||
border-color .3s ${easeInOutCubicBezier};
|
||||
`
|
||||
}, [
|
||||
cM('disabled', {
|
||||
opacity: '.45',
|
||||
cursor: 'not-allowed'
|
||||
}),
|
||||
cM('collapsed', [
|
||||
cE('arrow', {
|
||||
transition: `
|
||||
}, [
|
||||
cM('disabled', {
|
||||
opacity: '.45',
|
||||
cursor: 'not-allowed'
|
||||
}),
|
||||
cM('collapsed', [
|
||||
cE('arrow', {
|
||||
transition: `
|
||||
transform 0.2s ${easeInOutCubicBezier},
|
||||
opacity 0.2s ${easeInOutCubicBezier},
|
||||
border-color 0.3s ${easeInOutCubicBezier}
|
||||
`,
|
||||
transform: 'rotate(225deg)'
|
||||
transform: 'rotate(225deg)'
|
||||
})
|
||||
]),
|
||||
cM('uncollapsable', {
|
||||
cursor: 'default'
|
||||
}),
|
||||
cM('child-selected', [
|
||||
cB('menu-item-content-header', {
|
||||
color: itemTextColorChildSelected
|
||||
}, [
|
||||
cE('extra', {
|
||||
color: itemExtraTextColorChildSelected
|
||||
})
|
||||
]),
|
||||
cM('uncollapsable', {
|
||||
cursor: 'default'
|
||||
}),
|
||||
cM('child-selected', [
|
||||
cB('menu-item-content-header', {
|
||||
color: itemTextColorChildSelected
|
||||
}, [
|
||||
cE('extra', {
|
||||
color: itemExtraTextColorChildSelected
|
||||
})
|
||||
]),
|
||||
cE('icon', [
|
||||
cB('icon', {
|
||||
fill: itemIconColorChildSelected,
|
||||
stroke: itemIconColorChildSelected
|
||||
})
|
||||
])
|
||||
]),
|
||||
cNotM('disabled', [
|
||||
cNotM('uncollapsable', [
|
||||
c('&:hover', [
|
||||
cE('icon', [
|
||||
cB('icon', {
|
||||
fill: itemIconColorHover,
|
||||
stroke: itemIconColorHover
|
||||
})
|
||||
]),
|
||||
cB('menu-item-content-header', {
|
||||
color: itemTextColorHover
|
||||
}, [
|
||||
cE('extra', {
|
||||
color: itemExtraTextColorHover
|
||||
})
|
||||
])
|
||||
cE('icon', [
|
||||
cB('icon', {
|
||||
fill: itemIconColorChildSelected,
|
||||
stroke: itemIconColorChildSelected
|
||||
})
|
||||
])
|
||||
]),
|
||||
cNotM('disabled', [
|
||||
cNotM('uncollapsable', [
|
||||
hoverStyle(null, [
|
||||
cE('icon', [
|
||||
cB('icon', {
|
||||
fill: itemIconColorHover,
|
||||
stroke: itemIconColorHover
|
||||
})
|
||||
]),
|
||||
cB('menu-item-content-header', {
|
||||
color: itemTextColorHover
|
||||
}, [
|
||||
cE('extra', {
|
||||
color: itemExtraTextColorHover
|
||||
})
|
||||
])
|
||||
])
|
||||
]),
|
||||
cE('icon', {
|
||||
raw: `
|
||||
])
|
||||
]),
|
||||
cE('icon', {
|
||||
raw: `
|
||||
transition:
|
||||
font-size .3s ${easeInOutCubicBezier},
|
||||
padding-right .3s ${easeInOutCubicBezier};
|
||||
@ -242,14 +230,14 @@ export default c([
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`
|
||||
}, [
|
||||
cB('icon', {
|
||||
fill: itemIconColor,
|
||||
stroke: itemIconColor
|
||||
})
|
||||
]),
|
||||
cE('arrow', {
|
||||
raw: `
|
||||
}, [
|
||||
cB('icon', {
|
||||
fill: itemIconColor,
|
||||
stroke: itemIconColor
|
||||
})
|
||||
]),
|
||||
cE('arrow', {
|
||||
raw: `
|
||||
content: '';
|
||||
height: 6px;
|
||||
width: 6px;
|
||||
@ -260,15 +248,15 @@ export default c([
|
||||
transform-origin: 25% 25%;
|
||||
opacity: 1;
|
||||
`,
|
||||
borderLeft: `2px solid ${submenuArrowColor}`,
|
||||
borderTop: `2px solid ${submenuArrowColor}`,
|
||||
transition: `
|
||||
borderLeft: `2px solid ${submenuArrowColor}`,
|
||||
borderTop: `2px solid ${submenuArrowColor}`,
|
||||
transition: `
|
||||
transform 0.2s ${easeInOutCubicBezier},
|
||||
opacity 0.2s ${easeInOutCubicBezier} .1s,
|
||||
border-color 0.3s ${easeInOutCubicBezier}`
|
||||
}),
|
||||
cB('menu-item-content-header', {
|
||||
raw: `
|
||||
}),
|
||||
cB('menu-item-content-header', {
|
||||
raw: `
|
||||
transition:
|
||||
color .3s ${easeInOutCubicBezier},
|
||||
opacity .3s ${easeInOutCubicBezier};
|
||||
@ -279,41 +267,40 @@ export default c([
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`,
|
||||
color: itemTextColor
|
||||
}, [
|
||||
cE('extra', {
|
||||
raw: `
|
||||
color: itemTextColor
|
||||
}, [
|
||||
cE('extra', {
|
||||
raw: `
|
||||
white-space: nowrap;
|
||||
margin-left: 6px;
|
||||
display: inline-block;
|
||||
transition: color 0.3s ${easeInOutCubicBezier};
|
||||
font-size: 13px;
|
||||
`,
|
||||
color: itemExtraTextColor
|
||||
})
|
||||
])
|
||||
]),
|
||||
cB('submenu', {
|
||||
cursor: 'pointer',
|
||||
position: 'relative',
|
||||
marginTop: '6px'
|
||||
color: itemExtraTextColor
|
||||
})
|
||||
])
|
||||
]),
|
||||
cB('submenu', {
|
||||
cursor: 'pointer',
|
||||
position: 'relative',
|
||||
marginTop: '6px'
|
||||
}, [
|
||||
cB('menu-item-content', {
|
||||
height: '42px'
|
||||
}),
|
||||
cB('submenu-children', {
|
||||
overflow: 'hidden',
|
||||
padding: 0
|
||||
}, [
|
||||
cB('menu-item-content', {
|
||||
height: '42px'
|
||||
}),
|
||||
cB('submenu-content', {
|
||||
overflow: 'hidden',
|
||||
padding: 0,
|
||||
listStyle: 'none'
|
||||
}, [
|
||||
fadeInHeightExpandTransition({
|
||||
duration: '.2s'
|
||||
})
|
||||
])
|
||||
]),
|
||||
cB('menu-item-group', [
|
||||
cB('menu-item-group-title', {
|
||||
raw: `
|
||||
fadeInHeightExpandTransition({
|
||||
duration: '.3s'
|
||||
})
|
||||
])
|
||||
]),
|
||||
cB('menu-item-group', [
|
||||
cB('menu-item-group-title', {
|
||||
raw: `
|
||||
margin-top: 6px;
|
||||
color: ${groupTextColor};
|
||||
cursor: default;
|
||||
@ -325,9 +312,15 @@ export default c([
|
||||
padding-left .3s ${easeInOutCubicBezier},
|
||||
color .3s ${easeInOutCubicBezier};
|
||||
`
|
||||
})
|
||||
])
|
||||
})
|
||||
])
|
||||
])
|
||||
}
|
||||
])
|
||||
|
||||
function hoverStyle (props, children) {
|
||||
return [
|
||||
cM('hover', props, children),
|
||||
c('&:hover', props, children)
|
||||
]
|
||||
}
|
||||
|
63
src/menu/src/utils.js
Normal file
63
src/menu/src/utils.js
Normal file
@ -0,0 +1,63 @@
|
||||
import { h } from 'vue'
|
||||
import omit from '../../_utils/vue/omit'
|
||||
import NMenuItemGroup from './MenuItemGroup'
|
||||
import NSubmenu from './Submenu'
|
||||
import NMenuItem from './MenuItem'
|
||||
import { warn } from '../../_utils/naive/warn'
|
||||
|
||||
function getWrappedItem (item, level) {
|
||||
const clonedItem = {
|
||||
...item,
|
||||
level,
|
||||
root: level === 0
|
||||
}
|
||||
const { children } = item
|
||||
if (children) {
|
||||
clonedItem.children = getWrappedItems(children, level + 1)
|
||||
}
|
||||
return clonedItem
|
||||
}
|
||||
|
||||
export function getWrappedItems (items, level = 0) {
|
||||
return items.map(item => getWrappedItem(item, level))
|
||||
}
|
||||
|
||||
export function getActivePath (menuItems, activeName) {
|
||||
const path = []
|
||||
function traverse (items) {
|
||||
for (const item of items) {
|
||||
if (item.children) {
|
||||
path.push(item.name)
|
||||
if (__DEV__ && activeName === item.name) {
|
||||
warn('menu', `Menu can't select a subment name.`)
|
||||
}
|
||||
if (traverse(item.children)) return true
|
||||
path.pop()
|
||||
} else {
|
||||
if (activeName === item.name) {
|
||||
path.push(item.name)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
traverse(menuItems)
|
||||
return path
|
||||
}
|
||||
|
||||
export function itemRenderer (item, insidePopover = false) {
|
||||
const props = {
|
||||
key: item.name,
|
||||
insidePopover,
|
||||
...item
|
||||
}
|
||||
if (item.children) {
|
||||
if (item.group || item.type === 'group') {
|
||||
return h(NMenuItemGroup, omit(props, ['disabled', 'group', 'type']))
|
||||
}
|
||||
return h(NSubmenu, props)
|
||||
} else {
|
||||
return h(NMenuItem, omit(props, ['children']))
|
||||
}
|
||||
}
|
@ -101,7 +101,7 @@ export default {
|
||||
validator (value) {
|
||||
return ['hover', 'click'].includes(value)
|
||||
},
|
||||
default: 'hover'
|
||||
default: null
|
||||
},
|
||||
delay: {
|
||||
type: Number,
|
||||
@ -212,7 +212,7 @@ export default {
|
||||
}
|
||||
},
|
||||
handleMouseEnter (e) {
|
||||
if (this.trigger === 'hover') {
|
||||
if (this.trigger === 'hover' && !this.disabled) {
|
||||
this.clearTimer()
|
||||
if (this.mergedShow) return
|
||||
if (
|
||||
@ -226,7 +226,7 @@ export default {
|
||||
}
|
||||
},
|
||||
handleMouseLeave (e) {
|
||||
if (this.trigger === 'hover') {
|
||||
if (this.trigger === 'hover' && !this.disabled) {
|
||||
this.clearTimer()
|
||||
if (!this.mergedShow) return
|
||||
if (
|
||||
@ -253,7 +253,7 @@ export default {
|
||||
}
|
||||
},
|
||||
handleClick () {
|
||||
if (this.trigger === 'click') {
|
||||
if (this.trigger === 'click' && !this.disabled) {
|
||||
this.clearTimer()
|
||||
const nextShow = !this.mergedShow
|
||||
this.uncontrolledShow = nextShow
|
||||
@ -295,7 +295,6 @@ export default {
|
||||
return [
|
||||
h(NPopoverBody, omit(this.$props, [
|
||||
'defaultShow',
|
||||
'showArrow',
|
||||
'disabled'
|
||||
], {
|
||||
show: this.mergedShow
|
||||
|
@ -32,7 +32,7 @@ export default {
|
||||
type: String,
|
||||
default: undefined
|
||||
},
|
||||
arrow: {
|
||||
showArrow: {
|
||||
type: Boolean,
|
||||
default: undefined
|
||||
},
|
||||
@ -44,18 +44,6 @@ export default {
|
||||
type: Number,
|
||||
default: undefined
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
},
|
||||
minWidth: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
},
|
||||
maxWidth: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
},
|
||||
raw: {
|
||||
type: Boolean,
|
||||
default: undefined
|
||||
@ -92,6 +80,19 @@ export default {
|
||||
containerClass: {
|
||||
type: String,
|
||||
default: undefined
|
||||
},
|
||||
// deprecated
|
||||
width: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
},
|
||||
minWidth: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
},
|
||||
maxWidth: {
|
||||
type: Number,
|
||||
default: undefined
|
||||
}
|
||||
},
|
||||
mixins: [
|
||||
@ -130,26 +131,12 @@ export default {
|
||||
return directives
|
||||
},
|
||||
style () {
|
||||
const style = {}
|
||||
const {
|
||||
width,
|
||||
maxWidth,
|
||||
minWidth,
|
||||
bodyStyle
|
||||
} = this
|
||||
if (width) {
|
||||
style.width = formatLength(width)
|
||||
return {
|
||||
width: formatLength(this.width),
|
||||
maxWidth: formatLength(this.maxWidth),
|
||||
minWidth: formatLength(this.minWidth),
|
||||
...this.bodyStyle
|
||||
}
|
||||
if (maxWidth) {
|
||||
style.maxWidth = formatLength(maxWidth)
|
||||
}
|
||||
if (minWidth) {
|
||||
style.minWidth = formatLength(minWidth)
|
||||
}
|
||||
if (bodyStyle) {
|
||||
Object.assign(style, bodyStyle)
|
||||
}
|
||||
return style
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@ -196,11 +183,13 @@ export default {
|
||||
render () {
|
||||
return withDirectives(
|
||||
h('div', {
|
||||
class: {
|
||||
'n-positioning-container': true,
|
||||
[this.containerClass || 'n-popover']: true,
|
||||
[this.namespace]: this.namespace
|
||||
},
|
||||
class: [
|
||||
'n-positioning-container',
|
||||
{
|
||||
[this.containerClass || 'n-popover']: true,
|
||||
[this.namespace]: this.namespace
|
||||
}
|
||||
],
|
||||
ref: 'container'
|
||||
}, [
|
||||
h('div', {
|
||||
@ -222,12 +211,11 @@ export default {
|
||||
class: [
|
||||
'n-popover-body',
|
||||
{
|
||||
'n-popover-body--no-arrow': !this.arrow,
|
||||
[`n-${this.syntheticTheme}-theme`]: this.syntheticTheme,
|
||||
'n-popover-body--no-arrow': !this.showArrow,
|
||||
'n-popover-body--shadow': this.shadow,
|
||||
[this.bodyClass]: this.bodyClass,
|
||||
'n-popover-body--styled': !this.raw,
|
||||
'n-popover-body--fix-width': this.width !== null || this.maxWidth !== null
|
||||
'n-popover-body--styled': !this.raw
|
||||
}
|
||||
],
|
||||
style: this.style,
|
||||
@ -235,14 +223,14 @@ export default {
|
||||
onMouseLeave: this.handleMouseLeave
|
||||
}, [
|
||||
getDefaultSlot(this),
|
||||
this.arrow
|
||||
this.showArrow
|
||||
? h(
|
||||
'div',
|
||||
{
|
||||
staticClass: 'n-popover-arrow-wrapper'
|
||||
class: 'n-popover-arrow-wrapper'
|
||||
}, [
|
||||
h('div', {
|
||||
staticClass: 'n-popover-arrow',
|
||||
class: 'n-popover-arrow',
|
||||
style: this.arrowStyle
|
||||
})
|
||||
])
|
||||
|
@ -42,13 +42,6 @@ export default c([
|
||||
padding: 8px 14px;
|
||||
`
|
||||
}),
|
||||
cM('fix-width', {
|
||||
raw: `
|
||||
white-space: normal;
|
||||
width: max-content;
|
||||
box-sizing: border-box;
|
||||
`
|
||||
}),
|
||||
cB('popover-arrow-wrapper', {
|
||||
raw: `
|
||||
position: absolute;
|
||||
|
@ -14,11 +14,11 @@ export default function ({
|
||||
leaveToProps = null
|
||||
} = {}) {
|
||||
return [
|
||||
c(`&.${namespace}-fade-in-height-expand-transition-leave, &.${namespace}-fade-in-height-expand-transition-enter-to`, {
|
||||
c(`&.${namespace}-fade-in-height-expand-transition-leave-from, &.${namespace}-fade-in-height-expand-transition-enter-to`, {
|
||||
...enterToProps,
|
||||
opacity: 1
|
||||
}),
|
||||
c(`&.${namespace}-fade-in-height-expand-transition-leave-to, &.${namespace}-fade-in-height-expand-transition-enter`, {
|
||||
c(`&.${namespace}-fade-in-height-expand-transition-leave-to, &.${namespace}-fade-in-height-expand-transition-enter-from`, {
|
||||
...leaveToProps,
|
||||
opacity: 0,
|
||||
marginTop: '0 !important',
|
||||
|
@ -55,7 +55,6 @@ export default {
|
||||
methods: {
|
||||
handleClick () {
|
||||
if (!this.disabled) {
|
||||
console.log(this['onUpdate:modelValue'])
|
||||
this['onUpdate:modelValue'](!this.modelValue)
|
||||
}
|
||||
}
|
||||
|
5
vue3.md
5
vue3.md
@ -4,7 +4,7 @@ zindexable 最好写成 directive
|
||||
placeable 进行了大调整
|
||||
|
||||
|
||||
- [ ] Form
|
||||
- [ ] form
|
||||
- [ ] affix
|
||||
- [x] alert
|
||||
- [ ] anchor
|
||||
@ -43,7 +43,6 @@ placeable 进行了大调整
|
||||
- [ ] layout
|
||||
- [ ] list
|
||||
- [ ] loading-bar
|
||||
- [ ] locale
|
||||
- [ ] log
|
||||
- [ ] menu
|
||||
- [x] message
|
||||
@ -59,7 +58,7 @@ placeable 进行了大调整
|
||||
- remove default hide behavior for preset
|
||||
- deprecate `overlay-style`, use `body-style`
|
||||
- TODO: update docs, scrollbar mouseup
|
||||
- [ ] notification
|
||||
- [x] notification
|
||||
- deprecate `open`, use `create`
|
||||
- deprecate `onHide`, use `onLeave`
|
||||
- deprecate `onAfterShow`, use `onAfterEnter`
|
||||
|
Loading…
x
Reference in New Issue
Block a user