mirror of
https://github.com/tusen-ai/naive-ui.git
synced 2025-01-12 12:25:16 +08:00
refactor(base-select-menu): ts
This commit is contained in:
parent
9e095c1a6a
commit
5faf1a8870
@ -1,2 +0,0 @@
|
||||
/* istanbul ignore file */
|
||||
export { default } from './src/SelectMenu.vue'
|
2
src/_base/select-menu/index.ts
Normal file
2
src/_base/select-menu/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
/* istanbul ignore file */
|
||||
export { default } from './src/SelectMenu'
|
@ -1,10 +1,11 @@
|
||||
import { h, defineComponent } from 'vue'
|
||||
import { h, defineComponent, PropType } from 'vue'
|
||||
import { TreeNode } from 'treemate'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NBaseSelectGroupHeader',
|
||||
props: {
|
||||
tmNode: {
|
||||
type: Object,
|
||||
type: Object as PropType<TreeNode>,
|
||||
required: true
|
||||
}
|
||||
},
|
387
src/_base/select-menu/src/SelectMenu.tsx
Normal file
387
src/_base/select-menu/src/SelectMenu.tsx
Normal file
@ -0,0 +1,387 @@
|
||||
import {
|
||||
h,
|
||||
ref,
|
||||
onMounted,
|
||||
computed,
|
||||
defineComponent,
|
||||
PropType,
|
||||
watch,
|
||||
toRef,
|
||||
renderSlot,
|
||||
VNode,
|
||||
provide,
|
||||
reactive
|
||||
} from 'vue'
|
||||
import { RawNode, TreeMate, TreeNode } from 'treemate'
|
||||
import { VirtualList, VirtualListRef } from 'vueuc'
|
||||
import { depx, getPadding } from 'seemly'
|
||||
import { NEmpty } from '../../../empty'
|
||||
import { NScrollbar } from '../../../scrollbar'
|
||||
import type { ScrollbarRef } from '../../../scrollbar'
|
||||
import { formatLength } from '../../../_utils'
|
||||
import { createKey } from '../../../_utils/cssr'
|
||||
import { useTheme } from '../../../_mixins'
|
||||
import type { ThemeProps } from '../../../_mixins'
|
||||
import NSelectOption from './SelectOption'
|
||||
import NSelectGroupHeader from './SelectGroupHeader'
|
||||
import style from './styles/index.cssr'
|
||||
import { baseSelectMenuLight, BaseSelectMenuTheme } from '../styles'
|
||||
|
||||
export interface BaseSelectMenuInjection {
|
||||
handleOptionMouseEnter: (e: MouseEvent, tmNode: TreeNode) => void
|
||||
handleOptionClick: (e: MouseEvent, tmNode: TreeNode) => void
|
||||
valueSet: Set<number | string>
|
||||
pendingTmNode: TreeNode | null
|
||||
multiple: boolean
|
||||
value: string | number | Array<string | number> | null
|
||||
}
|
||||
|
||||
export interface BaseSelectMenuRef {
|
||||
getPendingOption: () => TreeNode | null
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BaseSelectMenu',
|
||||
props: {
|
||||
...(useTheme.props as ThemeProps<BaseSelectMenuTheme>),
|
||||
scrollable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
treeMate: {
|
||||
type: Object as PropType<TreeMate>,
|
||||
required: true
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
size: {
|
||||
type: String as PropType<'small' | 'medium' | 'large'>,
|
||||
default: 'medium'
|
||||
},
|
||||
pattern: {
|
||||
type: String,
|
||||
default: undefined
|
||||
},
|
||||
value: {
|
||||
type: [String, Number, Array] as PropType<
|
||||
string | number | null | Array<string | number>
|
||||
>,
|
||||
default: null
|
||||
},
|
||||
width: {
|
||||
type: [Number, String],
|
||||
default: undefined
|
||||
},
|
||||
autoPending: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
virtualScroll: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
onScroll: {
|
||||
type: Function as PropType<((e: Event) => void) | undefined>,
|
||||
default: undefined
|
||||
},
|
||||
// deprecated
|
||||
onMenuToggleOption: {
|
||||
type: Function,
|
||||
default: undefined
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
const themeRef = useTheme(
|
||||
'BaseSelectMenu',
|
||||
'BaseSelectMenu',
|
||||
style,
|
||||
baseSelectMenuLight,
|
||||
props
|
||||
)
|
||||
const virtualListRef = ref<VirtualListRef | null>(null)
|
||||
const scrollbarRef = ref<ScrollbarRef | null>(null)
|
||||
const { treeMate } = props
|
||||
const pendingNodeRef = ref(
|
||||
props.autoPending
|
||||
? props.value === null
|
||||
? treeMate.getFirstAvailableNode()
|
||||
: props.multiple
|
||||
? treeMate.getNode(
|
||||
((props.value as Array<string | number> | null) || [])[
|
||||
((props.value as Array<string | number> | null) || []).length -
|
||||
1
|
||||
]
|
||||
) || treeMate.getFirstAvailableNode()
|
||||
: treeMate.getNode(props.value as string | number) ||
|
||||
treeMate.getFirstAvailableNode()
|
||||
: null
|
||||
)
|
||||
const itemSizeRef = computed(() => {
|
||||
return depx(themeRef.value.self[createKey('optionHeight', props.size)])
|
||||
})
|
||||
const paddingRef = computed(() => {
|
||||
return getPadding(themeRef.value.self[createKey('padding', props.size)])
|
||||
})
|
||||
const valueSetRef = computed(() => {
|
||||
if (props.multiple && Array.isArray(props.value)) {
|
||||
return new Set(props.value)
|
||||
}
|
||||
return new Set<string | number>()
|
||||
})
|
||||
const emptyRef = computed(() => {
|
||||
const tmNodes = props.treeMate.flattenedNodes
|
||||
return tmNodes && tmNodes.length === 0
|
||||
})
|
||||
const styleRef = computed(() => {
|
||||
return [{ width: formatLength(props.width) }, cssVarsRef.value]
|
||||
})
|
||||
const tmNodesRef = computed(() => {
|
||||
return props.treeMate.treeNodes
|
||||
})
|
||||
watch(toRef(props, 'treeMate'), () => {
|
||||
if (props.autoPending) {
|
||||
const tmNode = props.treeMate.getFirstAvailableNode()
|
||||
setPendingTmNode(tmNode)
|
||||
} else {
|
||||
setPendingTmNode(null)
|
||||
}
|
||||
})
|
||||
function doToggleOption (option: RawNode): void {
|
||||
const { onMenuToggleOption } = props
|
||||
if (onMenuToggleOption) onMenuToggleOption(option)
|
||||
}
|
||||
function doScroll (e: Event): void {
|
||||
const { onScroll } = props
|
||||
if (onScroll) onScroll(e)
|
||||
}
|
||||
// required, scroller sync need to be triggered manually
|
||||
function handleVirtualListScroll (e: Event): void {
|
||||
scrollbarRef.value?.sync()
|
||||
doScroll(e)
|
||||
}
|
||||
function handleVirtualListResize (): void {
|
||||
scrollbarRef.value?.sync()
|
||||
}
|
||||
function getPendingOption (): RawNode | null {
|
||||
const { value: pendingTmNode } = pendingNodeRef
|
||||
if (pendingTmNode) return pendingTmNode.rawNode
|
||||
return null
|
||||
}
|
||||
function handleOptionMouseEnter (e: MouseEvent, tmNode: TreeNode): void {
|
||||
if (tmNode.disabled) return
|
||||
setPendingTmNode(tmNode, false)
|
||||
}
|
||||
function handleOptionClick (e: MouseEvent, tmNode: TreeNode): void {
|
||||
if (tmNode.disabled) return
|
||||
doToggleOption(tmNode.rawNode)
|
||||
}
|
||||
// keyboard related methods
|
||||
function handleKeyUp (e: KeyboardEvent): void {
|
||||
switch (e.code) {
|
||||
case 'ArrowUp':
|
||||
prev()
|
||||
break
|
||||
case 'ArrowDown':
|
||||
next()
|
||||
break
|
||||
}
|
||||
}
|
||||
function handleMouseDown (e: MouseEvent): void {
|
||||
e.preventDefault()
|
||||
}
|
||||
function next (): void {
|
||||
const { value: pendingTmNode } = pendingNodeRef
|
||||
if (pendingTmNode) {
|
||||
setPendingTmNode(pendingTmNode.getNext({ loop: true }), true)
|
||||
}
|
||||
}
|
||||
function prev (): void {
|
||||
const { value: pendingTmNode } = pendingNodeRef
|
||||
if (pendingTmNode) {
|
||||
setPendingTmNode(pendingTmNode.getPrev({ loop: true }), true)
|
||||
}
|
||||
}
|
||||
function setPendingTmNode (tmNode: TreeNode | null, doScroll = false): void {
|
||||
pendingNodeRef.value = tmNode
|
||||
if (doScroll && tmNode) {
|
||||
if (props.virtualScroll) {
|
||||
virtualListRef.value?.scrollTo({ index: tmNode.fIndex })
|
||||
} else {
|
||||
scrollbarRef.value?.scrollTo({
|
||||
index: tmNode.fIndex,
|
||||
elSize: itemSizeRef.value
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
provide<BaseSelectMenuInjection>(
|
||||
'NBaseSelectMenu',
|
||||
reactive({
|
||||
handleOptionMouseEnter,
|
||||
handleOptionClick,
|
||||
valueSet: valueSetRef,
|
||||
multiple: toRef(props, 'multiple'),
|
||||
value: toRef(props, 'value'),
|
||||
pendingTmNode: pendingNodeRef
|
||||
})
|
||||
)
|
||||
onMounted(() => {
|
||||
const { value } = scrollbarRef
|
||||
if (value) value.sync()
|
||||
})
|
||||
const cssVarsRef = computed(() => {
|
||||
const { size } = props
|
||||
const {
|
||||
common: { cubicBezierEaseInOut },
|
||||
self: {
|
||||
borderRadius,
|
||||
color,
|
||||
boxShadow,
|
||||
groupHeaderTextColor,
|
||||
actionDividerColor,
|
||||
optionTextColorPressed,
|
||||
optionTextColor,
|
||||
optionTextColorDisabled,
|
||||
optionTextColorActive,
|
||||
optionOpacityDisabled,
|
||||
optionCheckColor,
|
||||
actionTextColor,
|
||||
optionColorPending,
|
||||
[createKey('optionFontSize', size)]: fontSize,
|
||||
[createKey('optionHeight', size)]: optionHeight
|
||||
}
|
||||
} = themeRef.value
|
||||
return {
|
||||
'--action-divider-color': actionDividerColor,
|
||||
'--action-text-color': actionTextColor,
|
||||
'--bezier': cubicBezierEaseInOut,
|
||||
'--border-radius': borderRadius,
|
||||
'--box-shadow': boxShadow,
|
||||
'--color': color,
|
||||
'--option-font-size': fontSize,
|
||||
'--group-header-text-color': groupHeaderTextColor,
|
||||
'--option-check-color': optionCheckColor,
|
||||
'--option-color-pending': optionColorPending,
|
||||
'--option-height': optionHeight,
|
||||
'--option-opacity-disabled': optionOpacityDisabled,
|
||||
'--option-text-color': optionTextColor,
|
||||
'--option-text-color-active': optionTextColorActive,
|
||||
'--option-text-color-disabled': optionTextColorDisabled,
|
||||
'--option-text-color-pressed': optionTextColorPressed
|
||||
}
|
||||
})
|
||||
return {
|
||||
virtualListRef,
|
||||
scrollbarRef,
|
||||
style: styleRef,
|
||||
defaultScrollIndex: pendingNodeRef.value?.fIndex,
|
||||
itemSize: itemSizeRef,
|
||||
padding: paddingRef,
|
||||
tmNodes: tmNodesRef,
|
||||
empty: emptyRef,
|
||||
next,
|
||||
prev,
|
||||
virtualListContainer () {
|
||||
const { value } = virtualListRef
|
||||
return value?.listRef as HTMLElement
|
||||
},
|
||||
virtualListContent () {
|
||||
const { value } = virtualListRef
|
||||
return value?.itemsRef as HTMLElement
|
||||
},
|
||||
doScroll,
|
||||
handleKeyUp,
|
||||
handleMouseDown,
|
||||
handleVirtualListResize,
|
||||
handleVirtualListScroll,
|
||||
getPendingOption
|
||||
}
|
||||
},
|
||||
render () {
|
||||
const { $slots, virtualScroll } = this
|
||||
return (
|
||||
<div
|
||||
class={[
|
||||
'n-base-select-menu',
|
||||
{
|
||||
'n-base-select-menu--multiple': this.multiple
|
||||
}
|
||||
]}
|
||||
style={this.style as any}
|
||||
onKeyup={this.handleKeyUp}
|
||||
onMousedown={this.handleMouseDown}
|
||||
>
|
||||
{!this.empty ? (
|
||||
<NScrollbar
|
||||
ref="scrollbarRef"
|
||||
scrollable={this.scrollable}
|
||||
container={virtualScroll ? this.virtualListContainer : undefined}
|
||||
content={virtualScroll ? this.virtualListContent : undefined}
|
||||
onScroll={this.doScroll}
|
||||
>
|
||||
{{
|
||||
default: () => {
|
||||
return virtualScroll ? (
|
||||
<VirtualList
|
||||
ref="virtualListRef"
|
||||
class="n-virtual-list"
|
||||
items={this.tmNodes}
|
||||
itemSize={this.itemSize}
|
||||
showScrollbar={false}
|
||||
defaultScrollIndex={this.defaultScrollIndex}
|
||||
paddingTop={this.padding.top}
|
||||
paddingBottom={this.padding.bottom}
|
||||
onResize={this.handleVirtualListResize}
|
||||
onScroll={this.handleVirtualListScroll}
|
||||
>
|
||||
{{
|
||||
default: ({ item: tmNode }: { item: TreeNode }) => {
|
||||
return tmNode.rawNode.type === 'group' ? (
|
||||
<NSelectGroupHeader
|
||||
key={tmNode.key}
|
||||
tmNode={tmNode}
|
||||
/>
|
||||
) : (
|
||||
<NSelectOption key={tmNode.key} tmNode={tmNode} />
|
||||
)
|
||||
}
|
||||
}}
|
||||
</VirtualList>
|
||||
) : (
|
||||
<div
|
||||
class="n-base-select-menu-option-wrapper"
|
||||
style={{
|
||||
paddingTop: this.padding.top,
|
||||
paddingBottom: this.padding.bottom
|
||||
}}
|
||||
>
|
||||
{this.tmNodes.map((tmNode) =>
|
||||
tmNode.rawNode.type === 'group' ? (
|
||||
<NSelectGroupHeader key={tmNode.key} tmNode={tmNode} />
|
||||
) : (
|
||||
<NSelectOption key={tmNode.key} tmNode={tmNode} />
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}}
|
||||
</NScrollbar>
|
||||
) : (
|
||||
<div style="padding: 14px 0; width: 100%">
|
||||
{renderSlot($slots, 'empty', undefined, () => [
|
||||
(<NEmpty />) as VNode
|
||||
])}
|
||||
</div>
|
||||
)}
|
||||
{$slots.action && (
|
||||
<div class="n-base-select-menu__action">
|
||||
{renderSlot($slots, 'action')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
@ -1,327 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="n-base-select-menu"
|
||||
:class="{
|
||||
'n-base-select-menu--multiple': multiple
|
||||
}"
|
||||
:style="style"
|
||||
@keyup.up.stop="handleKeyUpUp"
|
||||
@keyup.down.stop="handleKeyUpDown"
|
||||
@mousedown.prevent
|
||||
>
|
||||
<n-scrollbar
|
||||
v-if="!empty"
|
||||
ref="scrollbarRef"
|
||||
:scrollable="scrollable"
|
||||
:container="virtualScroll ? virtualListContainer : undefined"
|
||||
:content="virtualScroll ? virtualListContent : undefined"
|
||||
@scroll="doScroll"
|
||||
>
|
||||
<virtual-list
|
||||
v-if="virtualScroll"
|
||||
ref="virtualListRef"
|
||||
class="n-virtual-list"
|
||||
:items="tmNodes"
|
||||
:item-size="itemSize"
|
||||
:show-scrollbar="false"
|
||||
:default-scroll-index="defaultScrollIndex"
|
||||
:padding-top="padding.top"
|
||||
:padding-bottom="padding.bottom"
|
||||
@resize="handleVirtualListResize"
|
||||
@scroll="handleVirtualListScroll"
|
||||
>
|
||||
<template #default="{ item: tmNode }">
|
||||
<n-select-group-header
|
||||
v-if="tmNode.rawNode.type === 'group'"
|
||||
:key="tmNode.key"
|
||||
:tm-node="tmNode"
|
||||
/>
|
||||
<n-select-option v-else :key="tmNode.key" :tm-node="tmNode" />
|
||||
</template>
|
||||
</virtual-list>
|
||||
<div
|
||||
v-else
|
||||
class="n-base-select-menu-option-wrapper"
|
||||
:style="{
|
||||
paddingTop: padding.top,
|
||||
paddingBottom: padding.bottom
|
||||
}"
|
||||
>
|
||||
<template v-for="tmNode in tmNodes">
|
||||
<n-select-group-header
|
||||
v-if="tmNode.rawNode.type === 'group'"
|
||||
:key="tmNode.key"
|
||||
:tm-node="tmNode"
|
||||
/>
|
||||
<n-select-option v-else :key="tmNode.key" :tm-node="tmNode" />
|
||||
</template>
|
||||
</div>
|
||||
</n-scrollbar>
|
||||
<div v-else style="padding: 14px 0; width: 100%">
|
||||
<slot name="empty">
|
||||
<n-empty />
|
||||
</slot>
|
||||
</div>
|
||||
<div v-if="$slots.action" class="n-base-select-menu__action">
|
||||
<slot name="action" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted, computed, defineComponent } from 'vue'
|
||||
import { VirtualList } from 'vueuc'
|
||||
import { depx, getPadding } from 'seemly'
|
||||
import { NEmpty } from '../../../empty'
|
||||
import { NScrollbar } from '../../../scrollbar'
|
||||
import { formatLength } from '../../../_utils'
|
||||
import { createKey } from '../../../_utils/cssr'
|
||||
import { useTheme } from '../../../_mixins'
|
||||
import NSelectOption from './SelectOption.js'
|
||||
import NSelectGroupHeader from './SelectGroupHeader.js'
|
||||
import style from './styles/index.cssr.js'
|
||||
import { baseSelectMenuLight } from '../styles'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BaseSelectMenu',
|
||||
components: {
|
||||
VirtualList,
|
||||
NScrollbar,
|
||||
NSelectOption,
|
||||
NEmpty,
|
||||
NSelectGroupHeader
|
||||
},
|
||||
provide () {
|
||||
return {
|
||||
NBaseSelectMenu: this
|
||||
}
|
||||
},
|
||||
props: {
|
||||
...useTheme.props,
|
||||
scrollable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
treeMate: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'medium'
|
||||
},
|
||||
pattern: {
|
||||
type: String,
|
||||
default: undefined
|
||||
},
|
||||
value: {
|
||||
type: [String, Number, Array],
|
||||
default: null
|
||||
},
|
||||
width: {
|
||||
type: [Number, String],
|
||||
default: undefined
|
||||
},
|
||||
autoPending: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
virtualScroll: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
onScroll: {
|
||||
type: Function,
|
||||
default: undefined
|
||||
},
|
||||
// deprecated
|
||||
onMenuToggleOption: {
|
||||
type: Function,
|
||||
default: undefined
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
const themeRef = useTheme(
|
||||
'BaseSelectMenu',
|
||||
'BaseSelectMenu',
|
||||
style,
|
||||
baseSelectMenuLight,
|
||||
props
|
||||
)
|
||||
const virtualListRef = ref(null)
|
||||
const scrollbarRef = ref(null)
|
||||
const { treeMate } = props
|
||||
const pendingNodeRef = ref(
|
||||
props.autoPending
|
||||
? props.value === null
|
||||
? treeMate.getFirstAvailableNode()
|
||||
: props.multiple
|
||||
? treeMate.getNode(
|
||||
(props.value || [])[(props.value || []).length - 1]
|
||||
) || treeMate.getFirstAvailableNode()
|
||||
: treeMate.getNode(props.value) || treeMate.getFirstAvailableNode()
|
||||
: null
|
||||
)
|
||||
const itemSizeRef = computed(() => {
|
||||
return depx(themeRef.value.self[createKey('optionHeight', props.size)])
|
||||
})
|
||||
const paddingRef = computed(() => {
|
||||
return getPadding(themeRef.value.self[createKey('padding', props.size)])
|
||||
})
|
||||
onMounted(() => {
|
||||
const { value } = scrollbarRef
|
||||
if (value) value.sync()
|
||||
})
|
||||
return {
|
||||
tmNodes: computed(() => props.treeMate.flattenedNodes),
|
||||
virtualListRef,
|
||||
scrollbarRef,
|
||||
virtualListContainer () {
|
||||
const { value } = virtualListRef
|
||||
return value && value.listRef
|
||||
},
|
||||
virtualListContent () {
|
||||
const { value } = virtualListRef
|
||||
return value && value.itemsRef
|
||||
},
|
||||
pendingTmNode: pendingNodeRef,
|
||||
defaultScrollIndex: pendingNodeRef.value?.fIndex,
|
||||
itemSize: itemSizeRef,
|
||||
padding: paddingRef,
|
||||
cssVars: computed(() => {
|
||||
const { size } = props
|
||||
const {
|
||||
common: { cubicBezierEaseInOut },
|
||||
self: {
|
||||
borderRadius,
|
||||
color,
|
||||
boxShadow,
|
||||
groupHeaderTextColor,
|
||||
actionDividerColor,
|
||||
optionTextColorPressed,
|
||||
optionTextColor,
|
||||
optionTextColorDisabled,
|
||||
optionTextColorActive,
|
||||
optionOpacityDisabled,
|
||||
optionCheckColor,
|
||||
actionTextColor,
|
||||
optionColorPending,
|
||||
[createKey('optionFontSize', size)]: fontSize,
|
||||
[createKey('optionHeight', size)]: optionHeight
|
||||
}
|
||||
} = themeRef.value
|
||||
return {
|
||||
'--action-divider-color': actionDividerColor,
|
||||
'--action-text-color': actionTextColor,
|
||||
'--bezier': cubicBezierEaseInOut,
|
||||
'--border-radius': borderRadius,
|
||||
'--box-shadow': boxShadow,
|
||||
'--color': color,
|
||||
'--option-font-size': fontSize,
|
||||
'--group-header-text-color': groupHeaderTextColor,
|
||||
'--option-check-color': optionCheckColor,
|
||||
'--option-color-pending': optionColorPending,
|
||||
'--option-height': optionHeight,
|
||||
'--option-opacity-disabled': optionOpacityDisabled,
|
||||
'--option-text-color': optionTextColor,
|
||||
'--option-text-color-active': optionTextColorActive,
|
||||
'--option-text-color-disabled': optionTextColorDisabled,
|
||||
'--option-text-color-pressed': optionTextColorPressed
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
valueSet () {
|
||||
if (this.multiple && Array.isArray(this.value)) return new Set(this.value)
|
||||
return null
|
||||
},
|
||||
empty () {
|
||||
const { tmNodes } = this
|
||||
return tmNodes && tmNodes.length === 0
|
||||
},
|
||||
style () {
|
||||
return {
|
||||
width: formatLength(this.width),
|
||||
...this.cssVars
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
treeMate (value) {
|
||||
if (this.autoPending) {
|
||||
const tmNode = this.treeMate.getFirstAvailableNode()
|
||||
this.setPendingTmNode(tmNode)
|
||||
} else {
|
||||
this.setPendingTmNode(null)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
doToggleOption (option) {
|
||||
const { onMenuToggleOption } = this
|
||||
if (onMenuToggleOption) onMenuToggleOption(option)
|
||||
},
|
||||
doScroll (e) {
|
||||
const { onScroll } = this
|
||||
if (onScroll) onScroll(e)
|
||||
},
|
||||
// required, scroller sync need to be triggered manually
|
||||
handleVirtualListScroll (e) {
|
||||
this.scrollbarRef.sync()
|
||||
this.doScroll(e)
|
||||
},
|
||||
handleVirtualListResize () {
|
||||
this.scrollbarRef.sync()
|
||||
},
|
||||
getPendingOption () {
|
||||
const { pendingTmNode } = this
|
||||
return pendingTmNode && pendingTmNode.rawNode
|
||||
},
|
||||
handleOptionMouseEnter (e, tmNode) {
|
||||
if (tmNode.disabled) return
|
||||
this.setPendingTmNode(tmNode, false)
|
||||
},
|
||||
handleOptionClick (e, tmNode) {
|
||||
if (tmNode.disabled) return
|
||||
this.doToggleOption(tmNode.rawNode)
|
||||
},
|
||||
// keyboard related methods
|
||||
handleKeyUpUp () {
|
||||
this.prev()
|
||||
},
|
||||
handleKeyUpDown () {
|
||||
this.next()
|
||||
},
|
||||
next () {
|
||||
const { pendingTmNode } = this
|
||||
if (pendingTmNode) {
|
||||
this.setPendingTmNode(pendingTmNode.getNext({ loop: true }), true)
|
||||
}
|
||||
},
|
||||
prev () {
|
||||
const { pendingTmNode } = this
|
||||
if (pendingTmNode) {
|
||||
this.setPendingTmNode(pendingTmNode.getPrev({ loop: true }), true)
|
||||
}
|
||||
},
|
||||
setPendingTmNode (tmNode, doScroll = false) {
|
||||
this.pendingTmNode = tmNode
|
||||
if (doScroll) {
|
||||
if (this.virtualScroll) {
|
||||
this.virtualListRef.scrollTo({ index: tmNode.fIndex })
|
||||
} else {
|
||||
this.scrollbarRef.scrollTo({
|
||||
index: tmNode.fIndex,
|
||||
elSize: this.itemSize
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
@ -1,4 +1,14 @@
|
||||
import { h, inject, toRef, defineComponent, Transition } from 'vue'
|
||||
import {
|
||||
h,
|
||||
inject,
|
||||
toRef,
|
||||
defineComponent,
|
||||
Transition,
|
||||
PropType,
|
||||
VNode
|
||||
} from 'vue'
|
||||
import { BaseSelectMenuInjection } from './SelectMenu'
|
||||
import { TreeNode } from 'treemate'
|
||||
import { useMemo } from 'vooks'
|
||||
import { CheckmarkIcon } from '../../icons'
|
||||
import NBaseIcon from '../../icon'
|
||||
@ -7,7 +17,7 @@ const checkMark = h(NBaseIcon, null, {
|
||||
default: () => h(CheckmarkIcon)
|
||||
})
|
||||
|
||||
function renderCheckMark (show) {
|
||||
function renderCheckMark (show: boolean): VNode {
|
||||
return h(
|
||||
Transition,
|
||||
{
|
||||
@ -22,20 +32,38 @@ function renderCheckMark (show) {
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NBaseSelectOption',
|
||||
inject: {
|
||||
NBaseSelectMenu: {
|
||||
default: null
|
||||
}
|
||||
},
|
||||
props: {
|
||||
tmNode: {
|
||||
type: Object,
|
||||
type: Object as PropType<TreeNode>,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
const NBaseSelectMenu = inject('NBaseSelectMenu')
|
||||
const NBaseSelectMenu = inject<BaseSelectMenuInjection>(
|
||||
'NBaseSelectMenu'
|
||||
) as BaseSelectMenuInjection
|
||||
const rawNodeRef = toRef(props.tmNode, 'rawNode')
|
||||
const isPendingRef = useMemo(() => {
|
||||
const { pendingTmNode } = NBaseSelectMenu
|
||||
if (!pendingTmNode) return false
|
||||
return props.tmNode.key === pendingTmNode.key
|
||||
})
|
||||
function handleClick (e: MouseEvent): void {
|
||||
const { tmNode } = props
|
||||
if (tmNode.disabled) return
|
||||
NBaseSelectMenu.handleOptionClick(e, tmNode)
|
||||
}
|
||||
function handleMouseEnter (e: MouseEvent): void {
|
||||
const { tmNode } = props
|
||||
if (tmNode.disabled) return
|
||||
NBaseSelectMenu.handleOptionMouseEnter(e, tmNode)
|
||||
}
|
||||
function handleMouseMove (e: MouseEvent): void {
|
||||
const { tmNode } = props
|
||||
const { value: isPending } = isPendingRef
|
||||
if (tmNode.disabled || isPending) return
|
||||
NBaseSelectMenu.handleOptionMouseEnter(e, tmNode)
|
||||
}
|
||||
return {
|
||||
NBaseSelectMenu,
|
||||
rawNode: rawNodeRef,
|
||||
@ -44,11 +72,7 @@ export default defineComponent({
|
||||
const { parent } = tmNode
|
||||
return parent && parent.rawNode.type === 'group'
|
||||
}),
|
||||
isPending: useMemo(() => {
|
||||
const { pendingTmNode } = NBaseSelectMenu
|
||||
if (!pendingTmNode) return false
|
||||
return props.tmNode.key === pendingTmNode.key
|
||||
}),
|
||||
isPending: isPendingRef,
|
||||
isSelected: useMemo(() => {
|
||||
const { multiple, value } = NBaseSelectMenu
|
||||
if (value === null) return false
|
||||
@ -59,24 +83,10 @@ export default defineComponent({
|
||||
} else {
|
||||
return value === optionValue
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleClick (e) {
|
||||
const { tmNode } = this
|
||||
if (tmNode.disabled) return
|
||||
this.NBaseSelectMenu.handleOptionClick(e, tmNode)
|
||||
},
|
||||
handleMouseEnter (e) {
|
||||
const { tmNode } = this
|
||||
if (tmNode.disabled) return
|
||||
this.NBaseSelectMenu.handleOptionMouseEnter(e, tmNode)
|
||||
},
|
||||
handleMouseMove (e) {
|
||||
const { tmNode, isPending } = this
|
||||
if (tmNode.disabled || isPending) return
|
||||
this.NBaseSelectMenu.handleOptionMouseEnter(e, tmNode)
|
||||
}),
|
||||
handleMouseMove,
|
||||
handleMouseEnter,
|
||||
handleClick
|
||||
}
|
||||
},
|
||||
render () {
|
||||
@ -90,7 +100,7 @@ export default defineComponent({
|
||||
handleMouseMove,
|
||||
NBaseSelectMenu: { multiple }
|
||||
} = this
|
||||
const showCheckMark = multiple & isSelected
|
||||
const showCheckMark = multiple && isSelected
|
||||
const children = rawNode.render
|
||||
? [rawNode.render(rawNode, isSelected), renderCheckMark(showCheckMark)]
|
||||
: [rawNode.label, renderCheckMark(showCheckMark)]
|
@ -96,7 +96,7 @@ export default cB('base-select-menu', `
|
||||
transition: color .3s var(--bezier);
|
||||
`, [
|
||||
fadeInScaleUpTransition({
|
||||
enterScale: 0.5
|
||||
enterScale: '0.5'
|
||||
})
|
||||
])
|
||||
]),
|
@ -2,15 +2,16 @@ import { emptyDark } from '../../../empty/styles'
|
||||
import { scrollbarDark } from '../../../scrollbar/styles'
|
||||
import { commonDark } from '../../../_styles/new-common'
|
||||
import commonVariables from './_common'
|
||||
import type { BaseSelectMenuTheme } from './light'
|
||||
|
||||
export default {
|
||||
const baseSelectMenuDark: BaseSelectMenuTheme = {
|
||||
name: 'BaseSelectMenu',
|
||||
common: commonDark,
|
||||
peers: {
|
||||
Scrollbar: scrollbarDark,
|
||||
Empty: emptyDark
|
||||
},
|
||||
getLocalVars (vars) {
|
||||
self (vars) {
|
||||
const {
|
||||
borderRadius,
|
||||
popoverColor,
|
||||
@ -42,3 +43,5 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default baseSelectMenuDark
|
@ -1,2 +1,3 @@
|
||||
export { default as baseSelectMenuLight } from './light'
|
||||
export { default as baseSelectMenuDark } from './dark'
|
||||
export type { BaseSelectMenuThemeVars, BaseSelectMenuTheme } from './light'
|
@ -1,44 +0,0 @@
|
||||
import { emptyLight } from '../../../empty/styles'
|
||||
import { scrollbarLight } from '../../../scrollbar/styles'
|
||||
import { commonLight } from '../../../_styles/new-common'
|
||||
import commonVariables from './_common'
|
||||
|
||||
export default {
|
||||
name: 'BaseSelectMenu',
|
||||
common: commonLight,
|
||||
peers: {
|
||||
Scrollbar: scrollbarLight,
|
||||
Empty: emptyLight
|
||||
},
|
||||
self (vars) {
|
||||
const {
|
||||
borderRadius,
|
||||
popoverColor,
|
||||
boxShadow2,
|
||||
textColor3,
|
||||
dividerColorOverlay,
|
||||
textColor2,
|
||||
primaryColorPressed,
|
||||
textColorDisabled,
|
||||
primaryColor,
|
||||
opacityDisabled,
|
||||
hoverColorOverlay
|
||||
} = vars
|
||||
return {
|
||||
...commonVariables,
|
||||
borderRadius: borderRadius,
|
||||
color: popoverColor,
|
||||
boxShadow: boxShadow2,
|
||||
groupHeaderTextColor: textColor3,
|
||||
actionDividerColor: dividerColorOverlay,
|
||||
optionTextColor: textColor2,
|
||||
optionTextColorPressed: primaryColorPressed,
|
||||
optionTextColorDisabled: textColorDisabled,
|
||||
optionTextColorActive: primaryColor,
|
||||
optionOpacityDisabled: opacityDisabled,
|
||||
optionCheckColor: primaryColor,
|
||||
optionColorPending: hoverColorOverlay,
|
||||
actionTextColor: textColor2
|
||||
}
|
||||
}
|
||||
}
|
53
src/_base/select-menu/styles/light.ts
Normal file
53
src/_base/select-menu/styles/light.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { emptyLight } from '../../../empty/styles'
|
||||
import { scrollbarLight } from '../../../scrollbar/styles'
|
||||
import { commonLight } from '../../../_styles/new-common'
|
||||
import type { ThemeCommonVars } from '../../../_styles/new-common'
|
||||
import commonVariables from './_common'
|
||||
import { createTheme } from '../../../_mixins'
|
||||
|
||||
const self = (vars: ThemeCommonVars) => {
|
||||
const {
|
||||
borderRadius,
|
||||
popoverColor,
|
||||
boxShadow2,
|
||||
textColor3,
|
||||
dividerColorOverlay,
|
||||
textColor2,
|
||||
primaryColorPressed,
|
||||
textColorDisabled,
|
||||
primaryColor,
|
||||
opacityDisabled,
|
||||
hoverColorOverlay
|
||||
} = vars
|
||||
return {
|
||||
...commonVariables,
|
||||
borderRadius: borderRadius,
|
||||
color: popoverColor,
|
||||
boxShadow: boxShadow2,
|
||||
groupHeaderTextColor: textColor3,
|
||||
actionDividerColor: dividerColorOverlay,
|
||||
optionTextColor: textColor2,
|
||||
optionTextColorPressed: primaryColorPressed,
|
||||
optionTextColorDisabled: textColorDisabled,
|
||||
optionTextColorActive: primaryColor,
|
||||
optionOpacityDisabled: opacityDisabled,
|
||||
optionCheckColor: primaryColor,
|
||||
optionColorPending: hoverColorOverlay,
|
||||
actionTextColor: textColor2
|
||||
}
|
||||
}
|
||||
|
||||
export type BaseSelectMenuThemeVars = ReturnType<typeof self>
|
||||
|
||||
const baseSelectMenuLight = createTheme({
|
||||
name: 'BaseSelectMenu',
|
||||
common: commonLight,
|
||||
peers: {
|
||||
Scrollbar: scrollbarLight,
|
||||
Empty: emptyLight
|
||||
},
|
||||
self
|
||||
})
|
||||
|
||||
export default baseSelectMenuLight
|
||||
export type BaseSelectMenuTheme = typeof baseSelectMenuLight
|
@ -20,7 +20,7 @@ import type { BaseSelectionTheme } from '../styles'
|
||||
import Suffix from './Suffix'
|
||||
import style from './styles/index.cssr'
|
||||
|
||||
interface Option {
|
||||
export interface SelectOption {
|
||||
value: string | number
|
||||
label: string
|
||||
}
|
||||
@ -46,11 +46,11 @@ export default defineComponent({
|
||||
default: undefined
|
||||
},
|
||||
selectedOption: {
|
||||
type: Object as PropType<Option | null>,
|
||||
type: Object as PropType<SelectOption | null>,
|
||||
default: null
|
||||
},
|
||||
selectedOptions: {
|
||||
type: Array as PropType<Option[] | null>,
|
||||
type: Array as PropType<SelectOption[] | null>,
|
||||
default: null
|
||||
},
|
||||
multiple: {
|
||||
@ -184,7 +184,7 @@ export default defineComponent({
|
||||
const { onBlur } = props
|
||||
if (onBlur) onBlur(e)
|
||||
}
|
||||
function doDeleteOption (value: Option): void {
|
||||
function doDeleteOption (value: SelectOption): void {
|
||||
const { onDeleteOption } = props
|
||||
if (onDeleteOption) onDeleteOption(value)
|
||||
}
|
||||
@ -220,7 +220,9 @@ export default defineComponent({
|
||||
// ', this may be a bug of naive-ui.'
|
||||
// )
|
||||
// }
|
||||
if (e.relatedTarget && selfRef.value?.contains(e.relatedTarget as Node)) { return }
|
||||
if (e.relatedTarget && selfRef.value?.contains(e.relatedTarget as Node)) {
|
||||
return
|
||||
}
|
||||
doBlur(e)
|
||||
}
|
||||
function handleClear (e: MouseEvent): void {
|
||||
@ -248,7 +250,7 @@ export default defineComponent({
|
||||
}
|
||||
}
|
||||
}
|
||||
function handleDeleteOption (option: Option): void {
|
||||
function handleDeleteOption (option: SelectOption): void {
|
||||
doDeleteOption(option)
|
||||
}
|
||||
function handlePatternKeyDown (e: KeyboardEvent): void {
|
||||
@ -343,6 +345,7 @@ export default defineComponent({
|
||||
borderActive,
|
||||
arrowColor,
|
||||
arrowColorDisabled,
|
||||
loadingColor,
|
||||
// form warning
|
||||
colorActiveWarning,
|
||||
boxShadowFocusWarning,
|
||||
@ -393,6 +396,7 @@ export default defineComponent({
|
||||
'--text-color-disabled': textColorDisabled,
|
||||
'--arrow-color': arrowColor,
|
||||
'--arrow-color-disabled': arrowColorDisabled,
|
||||
'--loading-color': loadingColor,
|
||||
// form warning
|
||||
'--color-active-warning': colorActiveWarning,
|
||||
'--box-shadow-focus-warning': boxShadowFocusWarning,
|
||||
|
@ -30,6 +30,7 @@ import {
|
||||
// --text-color
|
||||
// --text-color-disabled
|
||||
// --arrow-color
|
||||
// --loading-color
|
||||
// ...clear vars
|
||||
// ...form item vars
|
||||
export default c([
|
||||
@ -46,6 +47,9 @@ export default c([
|
||||
line-height: var(--height);
|
||||
font-size: var(--font-size);
|
||||
`, [
|
||||
cB('base-loading', `
|
||||
color: var(--loading-color);
|
||||
`),
|
||||
cB('base-selection-label', `
|
||||
height: var(--height);
|
||||
line-height: var(--height);
|
||||
|
@ -48,6 +48,7 @@ const baseSelectionDark: BaseSelectionTheme = {
|
||||
caretColor: primaryColor,
|
||||
arrowColor: iconColorOverlay,
|
||||
arrowColorDisabled: iconColorDisabledOverlay,
|
||||
loadingColor: primaryColor,
|
||||
// warning
|
||||
borderWarning: `1px solid ${warningColor}`,
|
||||
borderHoverWarning: `1px solid ${warningColorHover}`,
|
||||
|
@ -2,6 +2,7 @@ import { changeColor, scaleColor } from 'seemly'
|
||||
import { commonLight } from '../../../_styles/new-common'
|
||||
import type { ThemeCommonVars } from '../../../_styles/new-common'
|
||||
import commonVariables from './_common'
|
||||
import { createTheme } from '../../../_mixins'
|
||||
|
||||
const self = (vars: ThemeCommonVars) => {
|
||||
const {
|
||||
@ -46,6 +47,7 @@ const self = (vars: ThemeCommonVars) => {
|
||||
caretColor: primaryColor,
|
||||
arrowColor: iconColor,
|
||||
arrowColorDisabled: iconColorDisabled,
|
||||
loadingColor: primaryColor,
|
||||
// warning
|
||||
borderWarning: `1px solid ${warningColor}`,
|
||||
borderHoverWarning: `1px solid ${warningColorHover}`,
|
||||
@ -82,11 +84,11 @@ const self = (vars: ThemeCommonVars) => {
|
||||
|
||||
export type BaseSelectionThemeVars = ReturnType<typeof self>
|
||||
|
||||
const baseSelectionLight = {
|
||||
const baseSelectionLight = createTheme({
|
||||
name: 'BaseSelection',
|
||||
common: commonLight,
|
||||
self
|
||||
}
|
||||
})
|
||||
|
||||
export default baseSelectionLight
|
||||
export type BaseSelectionTheme = typeof baseSelectionLight
|
||||
|
Loading…
Reference in New Issue
Block a user