refactor(menu): support vue3

This commit is contained in:
07akioni 2020-09-17 01:02:59 +08:00
parent 88d142d64b
commit 1684139614
41 changed files with 1018 additions and 1128 deletions

View File

@ -27,6 +27,12 @@ module.exports = {
describe: 'readonly',
it: 'readonly'
}
},
{
files: '*',
globals: {
__DEV__: 'readonly'
}
}
]
}

View File

@ -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')
})
]

View File

@ -51,7 +51,8 @@ const webpackConfig = {
filename: '[name].css',
chunkFilename: '[id].css',
ignoreOrder: false
})
}),
...config.plugins
]
}

View File

@ -48,7 +48,8 @@ const webpackConfig = {
preserveWhitespace: false
}
}
})
}),
...config.plugins
]
}

View File

@ -48,7 +48,8 @@ const webpackConfig = {
preserveWhitespace: false
}
}
})
}),
...config.plugins
]
}

View File

@ -44,7 +44,8 @@ const webpackConfig = {
filename: '[name].css',
chunkFilename: '[id].css',
ignoreOrder: false
})
}),
...config.plugins
]
}

View File

@ -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

View File

@ -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"
>

View File

@ -1,6 +1,6 @@
# 基础用法
```html
<n-popover>
<n-popover trigger="hover">
<template v-slot:trigger>
<n-button>
悬浮

View File

@ -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

View File

@ -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

View File

@ -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` 的像素值||

View File

@ -1,6 +1,9 @@
# 不要箭头
```html
<n-popover :show-arrow="false">
<n-popover
trigger="hover"
:show-arrow="false"
>
<template v-slot:trigger>
<n-button>
悬浮

View File

@ -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;">
悬浮

View File

@ -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

View File

@ -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>

View File

@ -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',

View File

@ -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

View File

@ -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
View 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}`)
}

View File

@ -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') {

View File

@ -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
View 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)))
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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)
}
}
}

View File

@ -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()
}
}
}

View 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)))
])
}
}

View File

@ -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
View 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()
])
}
}

View File

@ -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>

View 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
}
}
}

View File

@ -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
}
}
}
}

View File

@ -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
View 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']))
}
}

View File

@ -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

View File

@ -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
})
])

View File

@ -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;

View File

@ -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',

View File

@ -55,7 +55,6 @@ export default {
methods: {
handleClick () {
if (!this.disabled) {
console.log(this['onUpdate:modelValue'])
this['onUpdate:modelValue'](!this.modelValue)
}
}

View File

@ -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`