refactor(menu): ts

This commit is contained in:
07akioni 2021-01-15 17:15:59 +08:00
parent 1a2df922ff
commit 73c9d3f9c1
32 changed files with 1247 additions and 919 deletions

View File

@ -1,5 +1,6 @@
import { rgba, composite } from 'seemly'
import commonVariables from './_common.js'
import type { ThemeCommonVars } from './light'
import commonVariables from './_common'
const base = {
neutralBase: '#000',
@ -72,15 +73,18 @@ const baseBackgroundRgb = rgba(base.neutralBase)
const baseInvertBackgroundRgb = rgba(base.neutralInvertBase)
const overlayPrefix =
'rgba(' + baseInvertBackgroundRgb.slice(0, 3).join(', ') + ', '
function overlay (alpha) {
function overlay (alpha: number | string) {
return overlayPrefix + String(alpha) + ')'
}
function neutral (alpha) {
function neutral (alpha: number | string) {
const overlayRgba = Array.from(baseInvertBackgroundRgb)
overlayRgba[3] = Number(alpha)
return composite(baseBackgroundRgb, overlayRgba as any)
return composite(
baseBackgroundRgb,
overlayRgba as [number, number, number, number]
)
}
const derived = {
const derived: ThemeCommonVars = {
...commonVariables,
baseColor: base.neutralBase,

View File

@ -1,2 +1,3 @@
export { default as commonDark } from './dark'
export { default as commonLight } from './light'
export type { ThemeCommonVars } from './light'

View File

@ -1,5 +1,5 @@
import { rgba, composite } from 'seemly'
import commonVariables from './_common.js'
import commonVariables from './_common'
const base = {
neutralBase: '#FFF',
@ -74,13 +74,16 @@ const baseBackgroundRgb = rgba(base.neutralBase)
const baseInvertBackgroundRgb = rgba(base.neutralInvertBase)
const overlayPrefix =
'rgba(' + baseInvertBackgroundRgb.slice(0, 3).join(', ') + ', '
function overlay (alpha) {
function overlay (alpha: string | number) {
return overlayPrefix + String(alpha) + ')'
}
function neutral (alpha) {
function neutral (alpha: string | number) {
const overlayRgba = Array.from(baseInvertBackgroundRgb)
overlayRgba[3] = Number(alpha)
return composite(baseBackgroundRgb, overlayRgba as any)
return composite(
baseBackgroundRgb,
overlayRgba as [number, number, number, number]
)
}
const derived = {
...commonVariables,
@ -203,3 +206,4 @@ const derived = {
}
export default derived
export type ThemeCommonVars = typeof derived

View File

@ -1,19 +1,22 @@
import { useMemo } from 'vooks'
import { inject } from 'vue'
import { ComputedRef, inject } from 'vue'
interface UseAdjustedToProps {
to?: string
[key: string]: unknown
}
interface ModalInjection {
bodyRef: Element
bodyRef: HTMLElement
}
interface DrawerInjection {
bodyRef: Element
bodyRef: HTMLElement
}
export function useAdjustedTo (props: UseAdjustedToProps) {
export function useAdjustedTo (
props: UseAdjustedToProps
): ComputedRef<HTMLElement | string> {
const modal = inject<ModalInjection | null>('NModalBody', null)
const drawer = inject<DrawerInjection | null>('NDrawerBody', null)
return useMemo(() => {

View File

@ -0,0 +1,98 @@
type characterMap = {
1: '1'
2: '2'
3: '3'
4: '4'
5: '5'
6: '6'
7: '7'
8: '8'
9: '9'
0: '0'
q: 'Q'
w: 'W'
e: 'E'
r: 'R'
t: 'T'
y: 'Y'
u: 'U'
i: 'I'
o: 'O'
p: 'P'
a: 'A'
s: 'S'
d: 'D'
f: 'F'
g: 'G'
h: 'H'
j: 'J'
k: 'K'
l: 'L'
z: 'Z'
x: 'X'
c: 'C'
v: 'V'
b: 'B'
n: 'N'
m: 'M'
[key: string]: string
}
type char =
| '1'
| '2'
| '3'
| '4'
| '5'
| '6'
| '7'
| '8'
| '9'
| '0'
| 'q'
| 'w'
| 'e'
| 'r'
| 't'
| 'y'
| 'u'
| 'i'
| 'o'
| 'p'
| 'a'
| 's'
| 'd'
| 'f'
| 'g'
| 'h'
| 'j'
| 'k'
| 'l'
| 'z'
| 'x'
| 'c'
| 'v'
| 'b'
| 'n'
| 'm'
type RestChars<T> = T extends `${char}${infer P}` ? P : ''
type UpperFirst<T> = T extends `${infer P}${string}`
? `${characterMap[P]}${RestChars<T>}`
: T
export function createKey<P extends string, S extends string> (
prefix: P,
suffix: S
): S extends 'default' ? P : `${P}${UpperFirst<S>}` {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (prefix +
(suffix === 'default'
? ''
: suffix.replace(/^[a-z]/, (startChar) =>
startChar.toUpperCase()
))) as any
}
createKey('abc', 'def')

View File

@ -31,18 +31,6 @@ function insideModal (style: CNode) {
return c(`${prefix}modal, ${prefix}drawer`, [style])
}
function createKey (keyPrefix: string, ...suffixs: string[]) {
return (
keyPrefix +
suffixs
.map((suffix) => {
if (suffix === 'default') return ''
return suffix.replace(/^[a-z]/, (startChar) => startChar.toUpperCase())
})
.join('')
)
}
function cRB (selector: string, ...rest: any[]): CNode {
return (c as any)(`${prefix}${selector}`, ...rest)
}
@ -63,6 +51,7 @@ export {
withPrefix,
prefix,
namespace,
createKey,
find
}
export { createKey } from './create-key'

View File

@ -5,6 +5,7 @@ export {
flatten,
getSlot,
getVNodeChildren,
keysOf,
render
} from './vue'
export { warn, warnOnce } from './naive'

View File

@ -4,4 +4,5 @@ export { keep } from './keep'
export { omit } from './omit'
export { flatten } from './flatten'
export { call } from './call'
export { keysOf } from './keysOf'
export { render } from './render'

View File

@ -1,7 +1,11 @@
export function keep<T, K extends keyof T, R> (object: T, keys: K[] = [], rest: R): Pick<T, K> & R {
export function keep<T, K, R> (
object: T,
keys: K[] = [],
rest?: R
): Pick<T, K & keyof T> & R {
const keepedObject: any = {}
keys.forEach((key) => {
keepedObject[key] = object[key]
keepedObject[key] = (object as any)[key]
})
return Object.assign(keepedObject, rest)
}

3
src/_utils/vue/keysOf.ts Normal file
View File

@ -0,0 +1,3 @@
export function keysOf<T> (obj: T): (keyof T)[] {
return Object.keys(obj) as any
}

View File

@ -9,7 +9,7 @@ import {
provide,
reactive
} from 'vue'
import { RawNode, TreeMate } from 'treemate'
import { RawNode, TreeMate, Key } from 'treemate'
import { useMergedState, useKeyboard, useMemo } from 'vooks'
import { useTheme } from '../../_mixins'
import { NPopover, popoverProps } from '../../popover'
@ -19,7 +19,6 @@ import type { DropdownThemeVars } from '../styles'
import NDropdownMenu from './DropdownMenu'
import style from './styles/index.cssr'
type Key = string | number
type OnSelect = (key: Key, rawNode: RawNode) => void
const treemateOptions = {
@ -79,7 +78,7 @@ const dropdownProps = {
},
// for menu
value: {
type: String,
type: [String, Number] as PropType<Key>,
default: undefined
}
} as const
@ -92,7 +91,8 @@ export default defineComponent({
name: 'Dropdown',
props: {
...popoverProps,
...dropdownProps
...dropdownProps,
...useTheme.createProps<DropdownThemeVars>()
},
setup (props) {
const uncontrolledShowRef = ref(false)
@ -162,7 +162,7 @@ export default defineComponent({
keyboardEnabledRef
)
const themeRef = useTheme<DropdownThemeVars>(
const themeRef = useTheme(
'Dropdown',
'Dropdown',
style,
@ -332,7 +332,9 @@ export default defineComponent({
'onUpdate:show': this.doUpdateShow,
showArrow: false,
raw: true,
shadow: false
shadow: false,
// TODO: using peers
unstableTheme: undefined
}),
{
trigger: this.$slots.default,

View File

@ -1 +0,0 @@
export { default as NMenu } from './src/Menu.js'

1
src/menu/index.ts Normal file
View File

@ -0,0 +1 @@
export { default as NMenu } from './src/Menu'

View File

@ -1,23 +1,29 @@
import { h, ref, toRef, computed, defineComponent } from 'vue'
import { createTreeMate } from 'treemate'
import {
h,
ref,
toRef,
computed,
defineComponent,
provide,
reactive,
PropType
} from 'vue'
import { createTreeMate, Key, RawNode } from 'treemate'
import { useCompitable, useMergedState } from 'vooks'
import { useTheme } from '../../_mixins'
import { call, warn } from '../../_utils'
import { itemRenderer } from './utils'
import { menuLight } from '../styles'
import style from './styles/index.cssr.js'
import type { MenuThemeVars } from '../styles'
import { MenuInjection } from './use-menu-child'
import style from './styles/index.cssr'
export default defineComponent({
name: 'Menu',
provide () {
return {
NMenu: this,
NSubmenu: null
}
},
props: {
...useTheme.props,
...useTheme.createProps<MenuThemeVars>(),
items: {
type: Array,
type: Array as PropType<RawNode[]>,
required: true
},
collapsed: {
@ -49,25 +55,23 @@ export default defineComponent({
default: false
},
defaultExpandedKeys: {
type: Array,
type: Array as PropType<Key[]>,
default: () => []
},
expandedKeys: {
type: Array,
type: Array as PropType<Key[]>,
default: undefined
},
value: {
type: String,
type: [String, Number] as PropType<Key>,
default: undefined
},
defaultValue: {
type: String,
type: [String, Number] as PropType<Key>,
default: null
},
mode: {
validator (value) {
return ['vertical', 'horizontal'].includes(value)
},
type: String as PropType<'vertical' | 'horizontal'>,
default: 'vertical'
},
disabled: {
@ -76,38 +80,74 @@ export default defineComponent({
},
// eslint-disable-next-line vue/prop-name-casing
'onUpdate:expandedKeys': {
type: Function,
default: () => {}
type: Function as PropType<(value: Key[]) => void>,
default: undefined
},
// eslint-disable-next-line vue/prop-name-casing
'onUpdate:value': {
type: Function,
default: () => {}
type: Function as PropType<(value: Key) => void>,
default: undefined
},
// deprecated
onOpenNamesChange: {
type: Function,
default: () => {}
type: Function as PropType<(value: Key[]) => void>,
validator: () => {
warn(
'menu',
'`on-open-names-change` is deprecated, please use `on-update:expanded-keys` instead.'
)
return true
},
default: undefined
},
onSelect: {
type: Function,
default: () => {}
type: Function as PropType<(value: Key) => void>,
validator: () => {
warn(
'menu',
'`on-select` is deprecated, please use `on-update:value` instead.'
)
return true
},
default: undefined
},
onExpandedNamesChange: {
type: Function,
default: () => {}
type: Function as PropType<(value: Key[]) => void>,
validator: () => {
warn(
'menu',
'`on-expanded-names-change` is deprecated, please use `on-update:expanded-keys` instead.'
)
return true
},
default: undefined
},
expandedNames: {
type: Array,
type: Array as PropType<Key[]>,
validator: () => {
warn(
'menu',
'`expanded-names` is deprecated, please use `expanded-keys` instead.'
)
return true
},
default: undefined
},
defaultExpandedNames: {
type: Array,
type: Array as PropType<Key[]>,
validator: () => {
warn(
'menu',
'`default-expanded-names` is deprecated, please use `default-expanded-keys` instead.'
)
return true
},
default: undefined
}
},
setup (props) {
const themeRef = useTheme('Menu', 'Menu', style, menuLight, props)
const treeMateRef = computed(() =>
createTreeMate(props.items, {
getKey (node) {
@ -120,11 +160,10 @@ export default defineComponent({
? treeMateRef.value.getNonLeafKeys()
: props.defaultExpandedNames || props.defaultExpandedKeys
)
const controlledExpandedKeysRef = useCompitable(
props,
const controlledExpandedKeysRef = useCompitable(props, [
'expandedNames',
'expandedKeys'
)
])
const mergedExpandedKeysRef = useMergedState(
controlledExpandedKeysRef,
uncontrolledExpandedKeysRef
@ -139,6 +178,64 @@ export default defineComponent({
const activePathRef = computed(() => {
return treeMateRef.value.getPath(mergedValueRef.value).keyPath
})
provide<MenuInjection>(
'NMenu',
reactive({
mergedTheme: themeRef,
mode: toRef(props, 'mode'),
collapsed: toRef(props, 'collapsed'),
iconSize: toRef(props, 'iconSize'),
indent: toRef(props, 'indent'),
rootIndent: toRef(props, 'rootIndent'),
collapsedWidth: toRef(props, 'collapsedWidth'),
disabled: toRef(props, 'disabled'),
mergedValue: mergedValueRef,
mergedExpandedKeys: mergedExpandedKeysRef,
activePath: activePathRef,
doSelect,
toggleExpand
})
)
function doSelect (value: Key, item: RawNode) {
const { 'onUpdate:value': onUpdateValue, onSelect } = props
if (onUpdateValue) {
call(onUpdateValue, value, item)
}
if (onSelect) {
call(onSelect, value, item)
}
uncontrolledValueRef.value = value
}
function doUpdateExpandedKeys (value: Key[]) {
const {
'onUpdate:expandedKeys': onUpdateExpandedKeys,
onExpandedNamesChange,
onOpenNamesChange
} = props
if (onUpdateExpandedKeys) {
call(onUpdateExpandedKeys, value)
}
// deprecated
if (onExpandedNamesChange) {
call(onExpandedNamesChange, value)
}
if (onOpenNamesChange) {
call(onOpenNamesChange, value)
}
uncontrolledExpandedKeysRef.value = value
}
function toggleExpand (key: Key) {
const currentExpandedKeys = Array.from(mergedExpandedKeysRef.value)
const index = currentExpandedKeys.findIndex(
(expanededKey) => expanededKey === key
)
if (~index) {
currentExpandedKeys.splice(index, 1)
} else {
currentExpandedKeys.push(key)
}
doUpdateExpandedKeys(currentExpandedKeys)
}
return {
controlledExpandedKeys: controlledExpandedKeysRef,
uncontrolledExpanededKeys: uncontrolledExpandedKeysRef,
@ -190,32 +287,6 @@ export default defineComponent({
})
}
},
methods: {
doSelect (value, item) {
this['onUpdate:value'](value, item)
// deprecated
this.onSelect(value, item)
this.uncontrolledValue = value
},
toggleExpand (key) {
const currentExpandedKeys = Array.from(this.mergedExpandedKeys)
const index = currentExpandedKeys.findIndex(
(expanededKey) => expanededKey === key
)
if (~index) {
currentExpandedKeys.splice(index, 1)
} else {
currentExpandedKeys.push(key)
}
if (this.controlledExpandedKeys === undefined) {
this.uncontrolledExpanededKeys = currentExpandedKeys
}
this['onUpdate:expandedKeys'](currentExpandedKeys)
// deprecated
this.onExpandedNamesChange(currentExpandedKeys)
this.onOpenNamesChange(currentExpandedKeys)
}
},
render () {
return h(
'div',

118
src/menu/src/MenuItem.ts Normal file
View File

@ -0,0 +1,118 @@
import { h, computed, defineComponent, toRef, PropType } from 'vue'
import { useMemo } from 'vooks'
import { NTooltip } from '../../tooltip'
import NMenuItemContent from './MenuItemContent'
import { useMenuChild } from './use-menu-child'
import { TreeNode } from 'treemate'
export const menuItemProps = {
...useMenuChild.props,
tmNode: {
type: Object as PropType<TreeNode>,
required: true
},
disabled: {
type: Boolean,
default: false
},
icon: {
type: Function,
default: undefined
},
onClick: {
type: Function,
default: undefined
}
} as const
export default defineComponent({
name: 'MenuItem',
components: {
NMenuItemContent,
NTooltip
},
props: menuItemProps,
setup (props) {
const MenuChild = useMenuChild(props)
const { NSubmenu, NMenu } = MenuChild
const submenuDisabledRef = NSubmenu
? toRef(NSubmenu, 'mergedDisabled')
: { value: false }
const mergedDisabledRef = computed(() => {
return submenuDisabledRef.value || props.disabled
})
function doClick (e: MouseEvent) {
const { onClick } = props
if (onClick) onClick(e)
}
function handleClick (e: MouseEvent): void {
if (!mergedDisabledRef.value) {
NMenu.doSelect(props.internalKey, props.tmNode.rawNode)
doClick(e)
}
}
return {
dropdownPlacement: MenuChild.dropdownPlacement,
paddingLeft: MenuChild.paddingLeft,
iconMarginRight: MenuChild.iconMarginRight,
maxIconSize: MenuChild.maxIconSize,
activeIconSize: MenuChild.activeIconSize,
mergedTheme: NMenu.mergedTheme,
dropdownEnabled: useMemo(() => {
return (
props.root &&
NMenu.collapsed &&
NMenu.mode === 'horizontal' &&
!mergedDisabledRef.value
)
}),
selected: useMemo(() => {
if (NMenu.mergedValue === props.internalKey) return true
return false
}),
mergedDisabled: mergedDisabledRef,
handleClick
}
},
render () {
return h(
'div',
{
class: [
'n-menu-item',
{
'n-menu-item--selected': this.selected,
'n-menu-item--disabled': this.mergedDisabled
}
]
},
[
h(
NTooltip,
{
unstableTheme: this.mergedTheme.peers.Tooltip,
unstableThemeOverrides: this.mergedTheme.overrides.Tooltip,
trigger: 'hover',
placement: this.dropdownPlacement,
disabled: !this.dropdownEnabled
},
{
default: () => this.title,
trigger: () => {
return h(NMenuItemContent, {
paddingLeft: this.paddingLeft,
iconMarginRight: this.iconMarginRight,
maxIconSize: this.maxIconSize,
activeIconSize: this.activeIconSize,
title: this.title,
disabled: this.mergedDisabled,
icon: this.icon,
onClick: this.handleClick
})
}
}
)
]
)
}
})

View File

@ -1,107 +0,0 @@
<template>
<div
class="n-menu-item"
:class="{
'n-menu-item--selected': selected,
'n-menu-item--disabled': mergedDisabled
}"
>
<n-tooltip
:unstable-theme="NMenu.mergedTheme.peers.Tooltip"
:unstable-theme-overrides="NMenu.mergedTheme.overrides.Tooltip"
trigger="hover"
:placement="dropdownPlacement"
:disabled="!dropdownEnabled"
>
<template #trigger>
<n-menu-item-content
:padding-left="paddingLeft"
:icon-margin-right="iconMarginRight"
:max-icon-size="maxIconSize"
:active-icon-size="activeIconSize"
:title="title"
:disabled="mergedDisabled"
:icon="icon"
@click="handleClick"
/>
</template>
{{ title }}
</n-tooltip>
</div>
</template>
<script>
import { computed, defineComponent } from 'vue'
import { useMemo } from 'vooks'
import { useInjectionRef } from '../../_utils/composable'
import { NTooltip } from '../../tooltip'
import NMenuItemContent from './MenuItemContent.vue'
import menuChildMixin from './menu-child-mixin'
export default defineComponent({
name: 'MenuItem',
components: {
NMenuItemContent,
NTooltip
},
mixins: [menuChildMixin],
props: {
tmNode: {
type: Object,
required: true
},
disabled: {
type: Boolean,
default: false
},
icon: {
type: Function,
default: undefined
},
onClick: {
type: Function,
default: undefined
}
},
setup (props) {
const rootMenuValueRef = useInjectionRef('NMenu', 'mergedValue')
const submenuDisabledRef = useInjectionRef(
'NSubmenu',
'mergedDisabled',
false
)
const mergedDisabledRef = computed(() => {
return submenuDisabledRef.value || props.disabled
})
return {
selected: useMemo(() => {
if (rootMenuValueRef.value === props.internalKey) return true
return false
}),
mergedDisabled: mergedDisabledRef
}
},
computed: {
dropdownEnabled () {
return (
this.root &&
this.menuCollapsed &&
!this.horizontal &&
!this.mergedDisabled
)
}
},
methods: {
doClick (e) {
const { onClick } = this
if (onClick) onClick(e)
},
handleClick (e) {
if (!this.mergedDisabled) {
this.NMenu.doSelect(this.internalKey, this.tmNode.rawNode)
this.doClick(e)
}
}
}
})
</script>

View File

@ -0,0 +1,123 @@
import { computed, defineComponent, h } from 'vue'
import { ChevronDownFilledIcon } from '../../_base/icons'
import { render } from '../../_utils'
import { NBaseIcon } from '../../_base'
export default defineComponent({
name: 'MenuItemContent',
props: {
collapsed: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
},
paddingLeft: {
type: Number,
default: undefined
},
maxIconSize: {
type: Number,
default: undefined
},
activeIconSize: {
type: Number,
default: undefined
},
title: {
type: [String, Function],
default: undefined
},
icon: {
type: [String, Function],
default: undefined
},
showArrow: {
type: Boolean,
default: false
},
childActive: {
type: Boolean,
default: false
},
hover: {
type: Boolean,
default: false
},
iconMarginRight: {
type: Number,
required: true
}
},
setup (props) {
return {
style: computed(() => {
const { paddingLeft } = props
return { paddingLeft: paddingLeft && paddingLeft + 'px' }
}),
iconStyle: computed(() => {
const { maxIconSize, activeIconSize, iconMarginRight } = props
return {
width: maxIconSize + 'px',
height: maxIconSize + 'px',
fontSize: activeIconSize + 'px',
marginRight: iconMarginRight + 'px'
}
})
}
},
render () {
return h(
'div',
{
class: [
'n-menu-item-content',
{
'n-menu-item-content--collapsed': this.collapsed,
'n-menu-item-content--child-active': this.childActive,
'n-menu-item-content--disabled': this.disabled,
'n-menu-item-content--hover': this.hover
}
],
style: this.style
},
[
this.icon &&
h(
'div',
{
class: 'n-menu-item-content__icon',
style: this.iconStyle
},
[
h(render, {
render: this.icon
})
]
),
h(
'div',
{
class: 'n-menu-item-content-header'
},
[
h(render, {
render: this.title
})
]
),
this.showArrow
? h(
NBaseIcon,
{
class: 'n-menu-item-content__arrow'
},
[h(ChevronDownFilledIcon)]
)
: null
]
)
}
})

View File

@ -1,109 +0,0 @@
<template>
<div
class="n-menu-item-content"
:style="style"
:class="{
'n-menu-item-content--collapsed': collapsed,
'n-menu-item-content--child-active': childActive,
'n-menu-item-content--disabled': disabled,
'n-menu-item-content--hover': hover
}"
@click="handleClick"
>
<div v-if="icon" class="n-menu-item-content__icon" :style="iconStyle">
<render :render="icon" />
</div>
<div class="n-menu-item-content-header">
<render :render="title" />
</div>
<n-base-icon v-if="showArrow" class="n-menu-item-content__arrow">
<chevron-down-filled-icon />
</n-base-icon>
</div>
</template>
<script>
import { defineComponent } from 'vue'
import { ChevronDownFilledIcon } from '../../_base/icons'
import { render } from '../../_utils'
import { NBaseIcon } from '../../_base'
export default defineComponent({
name: 'MenuItemContent',
components: {
render,
NBaseIcon,
ChevronDownFilledIcon
},
props: {
collapsed: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
},
paddingLeft: {
type: Number,
default: undefined
},
maxIconSize: {
type: Number,
default: undefined
},
activeIconSize: {
type: Number,
default: undefined
},
title: {
type: [String, Function],
default: undefined
},
icon: {
type: [String, Function],
default: undefined
},
showArrow: {
type: Boolean,
default: false
},
childActive: {
type: Boolean,
default: false
},
hover: {
type: Boolean,
default: false
},
iconMarginRight: {
type: Number,
required: true
},
onClick: {
type: Function,
default: () => {}
}
},
computed: {
style () {
const { paddingLeft } = this
return { paddingLeft: paddingLeft && paddingLeft + 'px' }
},
iconStyle () {
const { maxIconSize, activeIconSize, iconMarginRight } = this
return {
width: maxIconSize + 'px',
height: maxIconSize + 'px',
fontSize: activeIconSize + 'px',
marginRight: iconMarginRight + 'px'
}
}
},
methods: {
handleClick () {
this.onClick()
}
}
})
</script>

View File

@ -1,47 +0,0 @@
import { h, defineComponent } from 'vue'
import { render } from '../../_utils'
import { itemRenderer } from './utils'
import menuChildMixin from './menu-child-mixin'
export default defineComponent({
name: 'MenuItemGroup',
mixins: [menuChildMixin],
provide () {
return {
NMenuItemGroup: this,
NSubmenu: null
}
},
props: {
tmNodes: {
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.paddingLeft}px;`
},
[
h(render, {
render: this.title
})
]
),
h(
'div',
this.tmNodes.map((item) => itemRenderer(item))
)
]
)
}
})

View File

@ -0,0 +1,54 @@
import { h, defineComponent, provide, PropType, reactive } from 'vue'
import { render } from '../../_utils'
import { useMenuChild } from './use-menu-child'
import type { MenuItemGroupInjection } from './use-menu-child'
import { itemRenderer } from './utils'
import { TreeNode } from 'treemate'
export const menuItemGroupProps = {
...useMenuChild.props,
tmNodes: {
type: Array as PropType<TreeNode[]>,
required: true
}
} as const
export default defineComponent({
name: 'MenuItemGroup',
props: menuItemGroupProps,
setup (props) {
provide('NSubmenu', null)
const MenuChild = useMenuChild(props)
provide<MenuItemGroupInjection>(
'NMenuItemGroup',
reactive({ paddingLeft: MenuChild.paddingLeft })
)
return function () {
const paddingLeft = MenuChild.paddingLeft.value
return h(
'div',
{
class: 'n-menu-item-group'
},
[
h(
'span',
{
class: 'n-menu-item-group-title',
style: paddingLeft && `padding-left: ${paddingLeft}px;`
},
[
h(render, {
render: props.title
})
]
),
h(
'div',
props.tmNodes.map((tmNode) => itemRenderer(tmNode))
)
]
)
}
}
})

View File

@ -1,171 +0,0 @@
import { h, ref, defineComponent } from 'vue'
import { NFadeInExpandTransition } from '../../_base'
import { NDropdown } from '../../dropdown'
import NMenuItemContent from './MenuItemContent.vue'
import menuChildMixin from './menu-child-mixin'
import { itemRenderer } from './utils'
import { useMemo } from 'vooks'
import { useInjectionRef } from '../../_utils/composable'
export default defineComponent({
name: 'Submenu',
mixins: [menuChildMixin],
provide () {
return {
NSubmenu: this,
NMenuItemGroup: null
}
},
props: {
rawNodes: {
type: Array,
required: true
},
tmNodes: {
type: Array,
required: true
},
disabled: {
type: Boolean,
default: false
},
icon: {
type: Function,
default: undefined
},
onClick: {
type: Function,
default: undefined
}
},
setup (props) {
const activePathRef = useInjectionRef('NMenu', 'activePath')
return {
childActive: useMemo(() => {
return activePathRef.value.includes(props.internalKey)
}),
dropdownShow: 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.menuCollapsed) {
return true
}
return !this.NMenu.mergedExpandedKeys.includes(this.internalKey)
},
dropdownEnabled () {
return !this.mergedDisabled && (this.horizontal || this.menuCollapsed)
}
},
methods: {
doClick () {
const { onClick } = this
if (onClick) onClick()
},
handleClick () {
if (!this.mergedDisabled) {
if (!this.menuCollapsed) {
this.NMenu.toggleExpand(this.internalKey)
}
this.doClick()
}
},
handlePopoverShowChange (value) {
this.dropdownShow = value
}
},
render () {
const createSubmenuItem = () => {
const {
paddingLeft,
collapsed,
mergedDisabled,
maxIconSize,
activeIconSize,
title,
horizontal,
childActive,
icon,
handleClick,
dropdownShow,
iconMarginRight
} = this
return h(NMenuItemContent, {
paddingLeft,
collapsed,
disabled: mergedDisabled,
iconMarginRight,
maxIconSize,
activeIconSize,
title,
showArrow: !horizontal,
childActive: childActive,
icon,
hover: dropdownShow,
onClick: handleClick
})
}
const createSubmenuChildren = () => {
return h(NFadeInExpandTransition, null, {
default: () => {
const { tmNodes, collapsed } = this
return !collapsed
? h(
'div',
{
class: 'n-submenu-children'
},
tmNodes.map((item) => itemRenderer(item))
)
: null
}
})
}
return this.root
? h(
NDropdown,
{
builtinThemeOverrides: {
fontSizeLarge: '14px',
optionIconSizeLarge: '18px'
},
value: this.NMenu.mergedValue,
size: 'large',
trigger: 'hover',
disabled: !this.dropdownEnabled,
placement: this.dropdownPlacement,
'onUpdate:show': this.handlePopoverShowChange,
options: this.rawNodes,
onSelect: this.NMenu.doSelect
},
{
default: () =>
h(
'div',
{
class: 'n-submenu'
},
[
createSubmenuItem(),
this.horizontal ? null : createSubmenuChildren()
]
)
}
)
: h(
'div',
{
class: 'n-submenu'
},
[createSubmenuItem(), createSubmenuChildren()]
)
}
})

196
src/menu/src/Submenu.ts Normal file
View File

@ -0,0 +1,196 @@
import {
h,
ref,
defineComponent,
PropType,
provide,
computed,
reactive
} from 'vue'
import { useMemo } from 'vooks'
import { NFadeInExpandTransition } from '../../_base'
import { NDropdown } from '../../dropdown'
import NMenuItemContent from './MenuItemContent'
import { itemRenderer } from './utils'
import { useMenuChild } from './use-menu-child'
import type { SubmenuInjection } from './use-menu-child'
import { TreeNode, RawNode } from 'treemate'
export const submenuProps = {
...useMenuChild.props,
rawNodes: {
type: Array as PropType<RawNode[]>,
required: true
},
tmNodes: {
type: Array as PropType<TreeNode[]>,
required: true
},
disabled: {
type: Boolean,
default: false
},
icon: {
type: Function,
default: undefined
},
onClick: {
type: Function,
default: undefined
}
} as const
export default defineComponent({
name: 'Submenu',
props: submenuProps,
setup (props) {
const MenuChild = useMenuChild(props)
const { NMenu, NSubmenu } = MenuChild
const mergedDisabledRef = computed(() => {
const { disabled } = props
if (NSubmenu && NSubmenu.mergedDisabled) return true
if (NMenu.disabled) return true
return disabled
})
const dropdownShowRef = ref(false)
provide<SubmenuInjection>(
'NSubmenu',
reactive({
paddingLeft: MenuChild.paddingLeft,
mergedDisabled: mergedDisabledRef
})
)
provide('NMenuItemGroup', null)
function doClick () {
const { onClick } = props
if (onClick) onClick()
}
function handleClick () {
if (!mergedDisabledRef.value) {
if (!NMenu.collapsed) {
NMenu.toggleExpand(props.internalKey)
}
doClick()
}
}
function handlePopoverShowChange (value: boolean) {
dropdownShowRef.value = value
}
return {
NMenu,
maxIconSize: MenuChild.maxIconSize,
activeIconSize: MenuChild.activeIconSize,
iconMarginRight: MenuChild.iconMarginRight,
dropdownPlacement: MenuChild.dropdownPlacement,
dropdownShow: dropdownShowRef,
paddingLeft: MenuChild.paddingLeft,
mergedDisabled: mergedDisabledRef,
childActive: useMemo(() => {
return NMenu.activePath.includes(props.internalKey)
}),
collapsed: computed(() => {
if (NMenu.mode === 'horizontal') return false
if (NMenu.collapsed) {
return true
}
return !NMenu.mergedExpandedKeys.includes(props.internalKey)
}),
dropdownEnabled: computed(() => {
return (
!mergedDisabledRef.value &&
(NMenu.mode === 'horizontal' || NMenu.collapsed)
)
}),
handlePopoverShowChange,
handleClick
}
},
render () {
const createSubmenuItem = () => {
const {
NMenu,
paddingLeft,
collapsed,
mergedDisabled,
maxIconSize,
activeIconSize,
title,
childActive,
icon,
handleClick,
dropdownShow,
iconMarginRight
} = this
return h(NMenuItemContent, {
paddingLeft,
collapsed,
disabled: mergedDisabled,
iconMarginRight,
maxIconSize,
activeIconSize,
title,
showArrow: !(NMenu.mode === 'horizontal'),
childActive: childActive,
icon,
hover: dropdownShow,
onClick: handleClick
})
}
const createSubmenuChildren = () => {
return h(NFadeInExpandTransition, null, {
default: () => {
const { tmNodes, collapsed } = this
return !collapsed
? h(
'div',
{
class: 'n-submenu-children'
},
tmNodes.map((item) => itemRenderer(item))
)
: null
}
})
}
return this.root
? h(
NDropdown,
{
builtinThemeOverrides: {
fontSizeLarge: '14px',
optionIconSizeLarge: '18px'
},
value: this.NMenu.mergedValue,
size: 'large',
trigger: 'hover',
disabled: !this.dropdownEnabled,
placement: this.dropdownPlacement,
'onUpdate:show': this.handlePopoverShowChange,
options: this.rawNodes,
onSelect: this.NMenu.doSelect
},
{
default: () =>
h(
'div',
{
class: 'n-submenu'
},
[
createSubmenuItem(),
this.NMenu.mode === 'horizontal'
? null
: createSubmenuChildren()
]
)
}
)
: h(
'div',
{
class: 'n-submenu'
},
[createSubmenuItem(), createSubmenuChildren()]
)
}
})

View File

@ -1,106 +0,0 @@
export default {
inject: {
NMenu: {
default: null
},
NSubmenu: {
default: null
},
NMenuItemGroup: {
default: null
}
},
props: {
internalKey: {
type: String,
required: true
},
root: {
type: Boolean,
default: false
},
level: {
type: Number,
required: true
},
title: {
type: [String, Function],
default: undefined
}
},
computed: {
horizontal () {
return this.NMenu.mode === 'horizontal'
},
dropdownPlacement () {
if (this.horizontal) {
return 'bottom'
}
if ('tmNodes' 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 === undefined ? iconSize : collapsedIconSize
},
paddingLeft () {
const {
NMenu: { collapsedWidth, indent, rootIndent },
NSubmenu,
NMenuItemGroup,
root,
horizontal,
maxIconSize,
menuCollapsed
} = this
const mergedRootIndent = rootIndent === undefined ? indent : rootIndent
if (root) {
if (horizontal) return undefined
if (menuCollapsed) return collapsedWidth / 2 - maxIconSize / 2
return mergedRootIndent
}
if (NMenuItemGroup) {
return indent / 2 + NMenuItemGroup.paddingLeft
}
if (NSubmenu) {
return indent + NSubmenu.paddingLeft
}
return undefined
},
iconMarginRight () {
const {
NMenu: { collapsedWidth, indent, rootIndent },
root,
maxIconSize,
horizontal,
menuCollapsed
} = this
if (horizontal) return 8
if (!root) return 8
if (!menuCollapsed) return 8
const mergedRootIndent = rootIndent === undefined ? indent : rootIndent
return (
mergedRootIndent + maxIconSize + 8 - (collapsedWidth + maxIconSize) / 2
)
}
}
}

View File

@ -1,234 +0,0 @@
import { c, cB, cE, cM, cNotM } from '../../../_utils/cssr'
import fadeInHeightExpandTransition from '../../../_styles/transitions/fade-in-height-expand'
// vars:
// --group-text-color
// --bezier
// --font-size
// --border-color-horizontal
// --border-radius
// --arrow-color
// --item-color-active
// --item-text-color
// --item-icon-color
// --item-text-color-hover
// --item-icon-color-hover
// --item-text-color-active
// --item-icon-color-active
// --item-icon-color-collapsed
// --item-text-color-child-active
// --item-icon-color-child-active
export default cB('menu', {
color: 'var(--item-text-color)',
overflow: 'hidden',
transition: 'background-color .3s var(--bezier)',
width: '100%',
boxSizing: 'border-box',
fontSize: 'var(--font-size)',
paddingBottom: '6px'
}, [
cM('horizontal', {
display: 'flex',
paddingBottom: 0
}, [
cB('submenu', {
margin: 0
}),
cB('menu-item', {
margin: 0
}, [
c('&::after', {
backgroundColor: 'transparent !important'
}),
cM('selected', [
cB('menu-item-content', {
borderBottom: '2px solid var(--border-color-horizontal)'
})
])
]),
cB('menu-item-content', {
padding: '0 20px',
borderBottom: '2px solid transparent'
}, [
cM('child-active', {
borderBottom: '2px solid var(--border-color-horizontal)'
}),
cNotM('disabled', [
hoverStyle({
borderBottom: '2px solid var(--border-color-horizontal)'
})
])
])
]),
cM('collapsed', [
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', {
color: 'var(--item-icon-color-collapsed)'
})
])
]),
cB('menu-item', {
transition: 'background-color .3s var(--bezier)',
height: '42px',
marginTop: '6px',
position: 'relative'
}, [
c('&::after', `
content: "";
background-color: transparent;
position: absolute;
left: 8px;
right: 8px;
top: 0;
bottom: 0;
pointer-events: none;
border-radius: var(--border-radius);
transition: background-color .3s var(--bezier);
`),
cNotM('disabled', [
c('&:active::after', {
backgroundColor: 'var(--item-color-active)'
})
]),
cM('selected', [
c('&::after', {
backgroundColor: 'var(--item-color-active)'
}),
cB('menu-item-content', [
cE('icon', {
color: 'var(--item-icon-color-active)'
}),
cB('menu-item-content-header', {
color: 'var(--item-text-color-active)'
})
])
])
]),
cB('menu-item-content', `
box-sizing: border-box;
line-height: 1.75;
height: 100%;
display: grid;
grid-template-areas: "icon content arrow";
grid-template-columns: auto 1fr auto;
align-items: center;
cursor: pointer;
position: relative;
z-index: auto;
padding-right: 18px;
transition:
background-color .3s var(--bezier),
padding-left .3s var(--bezier),
border-color .3s var(--bezier);
`, [
cM('disabled', {
opacity: '.45',
cursor: 'not-allowed'
}),
cM('collapsed', [
cE('arrow', `
transform: rotate(0);
`)
]),
cM('child-active', [
cB('menu-item-content-header', {
color: 'var(--item-text-color-child-active)'
}),
cE('icon', {
color: 'var(--item-icon-color-child-active)'
})
]),
cNotM('disabled', [
hoverStyle(null, [
cE('icon', {
color: 'var(--item-icon-color-hover)'
}),
cB('menu-item-content-header', {
color: 'var(--item-text-color-hover)'
})
])
]),
cE('icon', `
grid-area: icon;
color: var(--item-icon-color);
transition:
color .3s var(--bezier),
font-size .3s var(--bezier),
margin-right .3s var(--bezier);
box-sizing: content-box;
display: inline-flex;
align-items: center;
justify-content: center;
`),
cE('arrow', `
grid-area: arrow;
font-size: 16px;
color: var(--arrow-color);
transform: rotate(180deg);
opacity: 1;
transition:
transform 0.2s var(--bezier),
opacity 0.2s var(--bezier);
`),
cB('menu-item-content-header', `
grid-area: content;
transition:
color .3s var(--bezier),
opacity .3s var(--bezier);
opacity: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: var(--item-text-color);
`)
]),
cB('submenu', {
cursor: 'pointer',
position: 'relative',
marginTop: '6px'
}, [
cB('menu-item-content', {
height: '42px'
}),
cB('submenu-children', {
overflow: 'hidden',
padding: 0
}, [
fadeInHeightExpandTransition({
duration: '.2s'
})
])
]),
cB('menu-item-group', [
cB('menu-item-group-title', `
margin-top: 6px;
color: var(--group-text-color);
cursor: default;
font-size: 13px;
height: 36px;
display: flex;
align-items: center;
transition:
padding-left .3s var(--bezier),
color .3s var(--bezier);
`)
])
])
function hoverStyle (props, children) {
return [
cM('hover', props, children),
c('&:hover', props, children)
]
}

View File

@ -0,0 +1,285 @@
import { CNodeChildren, CProperties } from 'css-render'
import { c, cB, cE, cM, cNotM } from '../../../_utils/cssr'
import fadeInHeightExpandTransition from '../../../_styles/transitions/fade-in-height-expand'
// vars:
// --group-text-color
// --bezier
// --font-size
// --border-color-horizontal
// --border-radius
// --arrow-color
// --item-color-active
// --item-text-color
// --item-icon-color
// --item-text-color-hover
// --item-icon-color-hover
// --item-text-color-active
// --item-icon-color-active
// --item-icon-color-collapsed
// --item-text-color-child-active
// --item-icon-color-child-active
export default cB(
'menu',
{
color: 'var(--item-text-color)',
overflow: 'hidden',
transition: 'background-color .3s var(--bezier)',
width: '100%',
boxSizing: 'border-box',
fontSize: 'var(--font-size)',
paddingBottom: '6px'
},
[
cM(
'horizontal',
{
display: 'flex',
paddingBottom: 0
},
[
cB('submenu', {
margin: 0
}),
cB(
'menu-item',
{
margin: 0
},
[
c('&::after', {
backgroundColor: 'transparent !important'
}),
cM('selected', [
cB('menu-item-content', {
borderBottom: '2px solid var(--border-color-horizontal)'
})
])
]
),
cB(
'menu-item-content',
{
padding: '0 20px',
borderBottom: '2px solid transparent'
},
[
cM('child-active', {
borderBottom: '2px solid var(--border-color-horizontal)'
}),
cNotM('disabled', [
hoverStyle(
{
borderBottom: '2px solid var(--border-color-horizontal)'
},
null
)
])
]
)
]
),
cM('collapsed', [
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', {
color: 'var(--item-icon-color-collapsed)'
})
])
]),
cB(
'menu-item',
{
transition: 'background-color .3s var(--bezier)',
height: '42px',
marginTop: '6px',
position: 'relative'
},
[
c(
'&::after',
`
content: "";
background-color: transparent;
position: absolute;
left: 8px;
right: 8px;
top: 0;
bottom: 0;
pointer-events: none;
border-radius: var(--border-radius);
transition: background-color .3s var(--bezier);
`
),
cNotM('disabled', [
c('&:active::after', {
backgroundColor: 'var(--item-color-active)'
})
]),
cM('selected', [
c('&::after', {
backgroundColor: 'var(--item-color-active)'
}),
cB('menu-item-content', [
cE('icon', {
color: 'var(--item-icon-color-active)'
}),
cB('menu-item-content-header', {
color: 'var(--item-text-color-active)'
})
])
])
]
),
cB(
'menu-item-content',
`
box-sizing: border-box;
line-height: 1.75;
height: 100%;
display: grid;
grid-template-areas: "icon content arrow";
grid-template-columns: auto 1fr auto;
align-items: center;
cursor: pointer;
position: relative;
z-index: auto;
padding-right: 18px;
transition:
background-color .3s var(--bezier),
padding-left .3s var(--bezier),
border-color .3s var(--bezier);
`,
[
cM('disabled', {
opacity: '.45',
cursor: 'not-allowed'
}),
cM('collapsed', [
cE(
'arrow',
`
transform: rotate(0);
`
)
]),
cM('child-active', [
cB('menu-item-content-header', {
color: 'var(--item-text-color-child-active)'
}),
cE('icon', {
color: 'var(--item-icon-color-child-active)'
})
]),
cNotM('disabled', [
hoverStyle(null, [
cE('icon', {
color: 'var(--item-icon-color-hover)'
}),
cB('menu-item-content-header', {
color: 'var(--item-text-color-hover)'
})
])
]),
cE(
'icon',
`
grid-area: icon;
color: var(--item-icon-color);
transition:
color .3s var(--bezier),
font-size .3s var(--bezier),
margin-right .3s var(--bezier);
box-sizing: content-box;
display: inline-flex;
align-items: center;
justify-content: center;
`
),
cE(
'arrow',
`
grid-area: arrow;
font-size: 16px;
color: var(--arrow-color);
transform: rotate(180deg);
opacity: 1;
transition:
transform 0.2s var(--bezier),
opacity 0.2s var(--bezier);
`
),
cB(
'menu-item-content-header',
`
grid-area: content;
transition:
color .3s var(--bezier),
opacity .3s var(--bezier);
opacity: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: var(--item-text-color);
`
)
]
),
cB(
'submenu',
{
cursor: 'pointer',
position: 'relative',
marginTop: '6px'
},
[
cB('menu-item-content', {
height: '42px'
}),
cB(
'submenu-children',
{
overflow: 'hidden',
padding: 0
},
[
fadeInHeightExpandTransition({
duration: '.2s'
})
]
)
]
),
cB('menu-item-group', [
cB(
'menu-item-group-title',
`
margin-top: 6px;
color: var(--group-text-color);
cursor: default;
font-size: 13px;
height: 36px;
display: flex;
align-items: center;
transition:
padding-left .3s var(--bezier),
color .3s var(--bezier);
`
)
])
]
)
function hoverStyle (props: CProperties, children: CNodeChildren) {
return [cM('hover', props, children), c('&:hover', props, children)]
}

View File

@ -0,0 +1,139 @@
import { RawNode, Key } from 'treemate'
import { inject, computed, ComputedRef, PropType } from 'vue'
import { MergedTheme } from '../../_mixins/use-theme'
import { MenuThemeVars } from '../styles'
const ICON_MARGIN_RIGHT = 8
export interface MenuInjection {
mergedValue: Key
mode: 'vertical' | 'horizontal'
collapsed: boolean
iconSize: number
collapsedIconSize?: number
indent: number
rootIndent: number
collapsedWidth: number
disabled: boolean
mergedExpandedKeys: Key[]
activePath: Key[]
mergedTheme: MergedTheme<MenuThemeVars>
doSelect: (key: Key, node: RawNode) => void
toggleExpand: (key: Key) => void
}
export interface SubmenuInjection {
paddingLeft: number | undefined
mergedDisabled: boolean
}
export interface MenuItemGroupInjection {
paddingLeft: number | undefined
}
export interface UseMenuChildProps {
root: boolean
}
export interface UseMenuChild {
dropdownPlacement: ComputedRef<'bottom' | 'right' | 'right-start'>
activeIconSize: ComputedRef<number>
maxIconSize: ComputedRef<number>
paddingLeft: ComputedRef<number | undefined>
iconMarginRight: ComputedRef<number>
NMenu: MenuInjection
NSubmenu: SubmenuInjection | null
}
function useMenuChild (props: UseMenuChildProps): UseMenuChild {
const NMenu = inject<MenuInjection>('NMenu') as MenuInjection
const NSubmenu = inject<SubmenuInjection | null>('NSubmenu', null)
const NMenuItemGroup = inject<MenuItemGroupInjection | null>(
'NMenuItemGroup',
null
)
const horizontalRef = computed(() => {
return NMenu.mode === 'horizontal'
})
const dropdownPlacementRef = computed(() => {
if (horizontalRef.value) {
return 'bottom'
}
if ('tmNodes' in props) return 'right-start'
return 'right'
})
const maxIconSizeRef = computed(() => {
return Math.max(NMenu.collapsedIconSize ?? NMenu.iconSize, NMenu.iconSize)
})
const activeIconSizeRef = computed(() => {
if (!horizontalRef.value && props.root && NMenu.collapsed) {
return NMenu.collapsedIconSize ?? NMenu.iconSize
} else {
return NMenu.iconSize
}
})
const paddingLeftRef = computed(() => {
if (horizontalRef.value) return undefined
const { collapsedWidth, indent, rootIndent } = NMenu
const { root } = props
const mergedRootIndent = rootIndent === undefined ? indent : rootIndent
if (root) {
if (NMenu.collapsed) return collapsedWidth / 2 - maxIconSizeRef.value / 2
return mergedRootIndent
}
if (NMenuItemGroup) {
return indent / 2 + (NMenuItemGroup.paddingLeft as number)
}
if (NSubmenu) {
return indent + (NSubmenu.paddingLeft as number)
}
return undefined as never
})
const iconMarginRightRef = computed(() => {
const { collapsedWidth, indent, rootIndent } = NMenu
const { value: maxIconSize } = maxIconSizeRef
const { root } = props
if (horizontalRef.value) return ICON_MARGIN_RIGHT
if (!root) return ICON_MARGIN_RIGHT
if (!NMenu.collapsed) return ICON_MARGIN_RIGHT
const mergedRootIndent = rootIndent === undefined ? indent : rootIndent
return (
mergedRootIndent +
maxIconSize +
ICON_MARGIN_RIGHT -
(collapsedWidth + maxIconSize) / 2
)
})
return {
dropdownPlacement: dropdownPlacementRef,
activeIconSize: activeIconSizeRef,
maxIconSize: maxIconSizeRef,
paddingLeft: paddingLeftRef,
iconMarginRight: iconMarginRightRef,
NMenu,
NSubmenu
}
}
const menuChildProps = {
internalKey: {
type: [String, Number] as PropType<Key>,
required: true
} as const,
root: {
type: Boolean,
default: false
},
level: {
type: Number,
required: true
},
title: {
type: [String, Function],
default: undefined
}
}
useMenuChild.props = menuChildProps
export { useMenuChild }

View File

@ -1,46 +0,0 @@
import { h } from 'vue'
import { keep } from '../../_utils'
import NMenuItemGroup from './MenuItemGroup'
import NSubmenu from './Submenu'
import NMenuItem from './MenuItem.vue'
import menuChildMixin from './menu-child-mixin'
const menuChildProps = Object.keys(menuChildMixin.props)
const menuItemProps = Object.keys(NMenuItem.props).concat(menuChildProps)
const submenuProps = Object.keys(NSubmenu.props).concat(menuChildProps)
const menuItemGroupProps = Object.keys(NMenuItemGroup.props).concat(
menuChildProps
)
export function itemRenderer (tmNode) {
const { rawNode, key, level } = tmNode
const props = {
...rawNode,
key,
internalKey: key, // since key can't be used as a prop
level,
root: level === 0
}
if (tmNode.children) {
if (tmNode.isGroup) {
return h(
NMenuItemGroup,
keep(props, menuItemGroupProps, { tmNodes: tmNode.children })
)
}
return h(
NSubmenu,
keep(props, submenuProps, {
rawNodes: tmNode.rawNode.children,
tmNodes: tmNode.children
})
)
} else {
return h(
NMenuItem,
keep(props, menuItemProps, {
tmNode
})
)
}
}

44
src/menu/src/utils.ts Normal file
View File

@ -0,0 +1,44 @@
import { h, VNode } from 'vue'
import type { TreeNode, RawNode } from 'treemate'
import { keep, keysOf } from '../../_utils'
import NMenuItemGroup, { menuItemGroupProps } from './MenuItemGroup'
import NSubmenu, { submenuProps } from './Submenu'
import NMenuItem, { menuItemProps } from './MenuItem'
const groupPropKeys = keysOf(menuItemGroupProps)
const itemPropKeys = keysOf(menuItemProps)
const submenuPropKeys = keysOf(submenuProps)
export function itemRenderer (tmNode: TreeNode): VNode {
const { rawNode, key, level } = tmNode
const props = {
...rawNode,
key,
internalKey: key, // since key can't be used as a prop
level,
root: level === 0
}
if (tmNode.children) {
if (tmNode.isGroup) {
return h(
NMenuItemGroup,
keep(props, groupPropKeys, { tmNodes: tmNode.children })
)
}
return h(
NSubmenu,
keep(props, submenuPropKeys, {
rawNodes: tmNode.rawNode.children as RawNode[],
tmNodes: tmNode.children
})
)
} else {
return h(
NMenuItem,
keep(props, itemPropKeys, {
tmNode
})
)
}
}

View File

@ -1,6 +1,8 @@
import { changeColor } from 'seemly'
import { tooltipDark } from '../../tooltip/styles'
import { commonDark } from '../../_styles/new-common'
import type { ThemeCommonVars } from '../../_styles/new-common'
import type { MenuThemeVars } from './light'
export default {
name: 'Menu',
@ -8,7 +10,7 @@ export default {
peers: {
Tooltip: tooltipDark
},
self (vars) {
self (vars: ThemeCommonVars): MenuThemeVars {
const {
borderRadius,
textColor3Overlay,

View File

@ -1,2 +0,0 @@
export { default as menuDark } from './dark.js'
export { default as menuLight } from './light.js'

3
src/menu/styles/index.ts Normal file
View File

@ -0,0 +1,3 @@
export { default as menuDark } from './dark'
export { default as menuLight } from './light'
export type { MenuThemeVars } from './light'

View File

@ -1,14 +1,15 @@
import { changeColor } from 'seemly'
import { tooltipLight } from '../../tooltip/styles'
import { commonLight } from '../../_styles/new-common'
import type { ThemeCommonVars } from '../../_styles/new-common'
export default {
const menuLight = {
name: 'Menu',
common: commonLight,
peers: {
Tooltip: tooltipLight
},
self (vars) {
self (vars: ThemeCommonVars) {
const {
borderRadius,
textColor3,
@ -37,3 +38,7 @@ export default {
}
}
}
export default menuLight
export type MenuThemeVars = ReturnType<typeof menuLight.self>