mirror of
https://github.com/tusen-ai/naive-ui.git
synced 2025-03-19 14:00:50 +08:00
refactor(menu): ts
This commit is contained in:
parent
1a2df922ff
commit
73c9d3f9c1
@ -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,
|
||||
|
@ -1,2 +1,3 @@
|
||||
export { default as commonDark } from './dark'
|
||||
export { default as commonLight } from './light'
|
||||
export type { ThemeCommonVars } from './light'
|
||||
|
@ -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
|
||||
|
@ -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(() => {
|
||||
|
98
src/_utils/cssr/create-key.ts
Normal file
98
src/_utils/cssr/create-key.ts
Normal 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')
|
@ -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'
|
||||
|
@ -5,6 +5,7 @@ export {
|
||||
flatten,
|
||||
getSlot,
|
||||
getVNodeChildren,
|
||||
keysOf,
|
||||
render
|
||||
} from './vue'
|
||||
export { warn, warnOnce } from './naive'
|
||||
|
@ -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'
|
||||
|
@ -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
3
src/_utils/vue/keysOf.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export function keysOf<T> (obj: T): (keyof T)[] {
|
||||
return Object.keys(obj) as any
|
||||
}
|
@ -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,
|
||||
|
@ -1 +0,0 @@
|
||||
export { default as NMenu } from './src/Menu.js'
|
1
src/menu/index.ts
Normal file
1
src/menu/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as NMenu } from './src/Menu'
|
@ -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
118
src/menu/src/MenuItem.ts
Normal 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
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
})
|
@ -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>
|
123
src/menu/src/MenuItemContent.ts
Normal file
123
src/menu/src/MenuItemContent.ts
Normal 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
|
||||
]
|
||||
)
|
||||
}
|
||||
})
|
@ -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>
|
@ -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))
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
})
|
54
src/menu/src/MenuItemGroup.ts
Normal file
54
src/menu/src/MenuItemGroup.ts
Normal 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))
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
@ -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
196
src/menu/src/Submenu.ts
Normal 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()]
|
||||
)
|
||||
}
|
||||
})
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
]
|
||||
}
|
285
src/menu/src/styles/index.cssr.ts
Normal file
285
src/menu/src/styles/index.cssr.ts
Normal 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)]
|
||||
}
|
139
src/menu/src/use-menu-child.ts
Normal file
139
src/menu/src/use-menu-child.ts
Normal 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 }
|
@ -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
44
src/menu/src/utils.ts
Normal 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
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
@ -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,
|
@ -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
3
src/menu/styles/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { default as menuDark } from './dark'
|
||||
export { default as menuLight } from './light'
|
||||
export type { MenuThemeVars } from './light'
|
@ -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>
|
Loading…
x
Reference in New Issue
Block a user