refactor(select): use treemate

This commit is contained in:
07akioni 2020-10-28 01:00:55 +08:00
parent 0861ef3bf1
commit 8f41aab269
22 changed files with 326 additions and 557 deletions

View File

@ -30,7 +30,8 @@ const options = [
key: 'daisy buchanan'
},
{
type: 'divider'
type: 'divider',
key: 'd1'
},
{
label: 'Nick Carraway',

View File

@ -32,6 +32,7 @@ For other props, see [Popover Props](n-popover#Props). Note that `arrow`, `raw`
|Property|Type|Description|
|-|-|-|
|type|`'divider'`||
|key|`string \| number`|Should be unique|
### DropdownSubmenu Type
|Property|Type|Description|

View File

@ -25,7 +25,8 @@ const options = [
key: 'daisy buchanan'
},
{
type: 'divider'
type: 'divider',
key: 'd1'
},
{
label: 'Nick Carraway',

View File

@ -51,7 +51,8 @@ const options = [
key: 'daisy buchanan'
},
{
type: 'divider'
type: 'divider',
key: 'd1'
},
{
label: 'Nick Carraway',

View File

@ -30,7 +30,8 @@ const options = [
key: 'daisy buchanan'
},
{
type: 'divider'
type: 'divider',
key: 'd1'
},
{
label: '尼克·卡拉威',

View File

@ -32,6 +32,7 @@ manual-position
|属性|类型|说明|
|-|-|-|
|type|`'divider'`||
|key|`string \| number`|需要唯一|
### DropdownSubmenu Type
|属性|类型|说明|

View File

@ -27,7 +27,8 @@ const options = [
key: 'daisy buchanan'
},
{
type: 'divider'
type: 'divider',
key: 'd1'
},
{
label: '尼克·卡拉威',

View File

@ -50,7 +50,8 @@ const options = [
key: 'daisy buchanan'
},
{
type: 'divider'
type: 'divider',
key: 'd1'
},
{
label: '尼克·卡拉威',

View File

@ -131,7 +131,7 @@
"highlight.js": "^9.18.1",
"lodash-es": "^4.17.15",
"resize-observer-polyfill": "^1.5.1",
"treemate": "^0.1.6",
"treemate": "^0.1.7",
"vooks": "0.0.1-alpha.1",
"vue": "^3.0.2",
"vue-runtime-helpers": "^1.1.2",

View File

@ -3,14 +3,14 @@ import { h } from 'vue'
export default {
name: 'NBaseSelectGroupHeader',
props: {
data: {
tmNode: {
type: Object,
required: true
}
},
render () {
const data = this.data
const children = data.render ? [data.render(h, data)] : [ data.name ]
const { tmNode: { rawNode } } = this
const children = rawNode.render ? [rawNode.render(rawNode)] : [ rawNode.name ]
return h('div', {
class: 'n-base-select-group-header'
}, children)

View File

@ -17,30 +17,28 @@
:scrollable="scrollable"
:container="virtualListContainer"
:content="virtualListContent"
@scroll="handleMenuScroll"
@scroll="doScroll"
>
<virtual-list
v-if="virtualScroll"
ref="virtualListRef"
class="n-virtual-list"
:items="flattenedOptions"
:items="tmNodes"
:item-size="itemSize"
:show-scrollbar="false"
@resize="handleListResize"
@scroll="handleListScroll"
>
<template v-slot="{ item: option }">
<n-select-option
v-if="option.type === OPTION_TYPE.OPTION"
:key="option.key"
:index="option.index"
:wrapped-option="option"
:grouped="option.grouped"
/>
<template v-slot="{ item: tmNode }">
<n-select-group-header
v-else-if="option.type === OPTION_TYPE.GROUP_HEADER"
:key="option.key"
:data="option.data"
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>
@ -48,23 +46,16 @@
v-else
class="n-base-select-menu-option-wrapper"
>
<template v-for="option in flattenedOptions">
<n-select-option
v-if="option.type === OPTION_TYPE.OPTION"
:key="option.key"
:index="option.index"
:wrapped-option="option"
:grouped="option.grouped"
/>
<template v-for="tmNode in tmNodes">
<n-select-group-header
v-else-if="option.type === OPTION_TYPE.GROUP_HEADER"
:key="option.key"
:data="option.data"
v-if="tmNode.rawNode.type === 'group'"
:key="tmNode.key"
:tm-node="tmNode"
/>
<render
v-else-if="option.type === OPTION_TYPE.RENDER"
:key="option.key"
:render="option.render"
<n-select-option
v-else
:key="tmNode.key"
:tm-node="tmNode"
/>
</template>
</div>
@ -84,19 +75,12 @@
</template>
<script>
import { ref, onMounted } from 'vue'
import { ref, onMounted, computed } from 'vue'
import { VirtualList } from 'vueuc'
import NScrollbar from '../../../scrollbar'
import NSelectOption from './SelectOption.js'
import NSelectGroupHeader from './SelectGroupHeader.js'
import NEmpty from '../../../empty'
import { render } from '../../../_utils/vue'
import {
getPrevAvailableIndex,
getNextAvailableIndex,
flattenOptions,
OPTION_TYPE
} from '../../../_utils/component/select'
import { depx, formatLength } from '../../../_utils/css'
import { createKey } from '../../../_utils/cssr'
import { usecssr } from '../../../_mixins'
@ -114,8 +98,7 @@ export default {
NScrollbar,
NSelectOption,
NEmpty,
NSelectGroupHeader,
render
NSelectGroupHeader
},
mixins: [
usecssr(styles, {
@ -126,15 +109,15 @@ export default {
props: {
theme: {
type: String,
default: null
default: undefined
},
scrollable: {
type: Boolean,
default: true
},
options: {
type: Array,
default: null
treeMate: {
type: Object,
required: true
},
multiple: {
type: Boolean,
@ -142,11 +125,11 @@ export default {
},
size: {
type: String,
default: 'default'
default: 'medium'
},
pattern: {
type: String,
default: null
default: undefined
},
value: {
type: [String, Number, Array],
@ -154,7 +137,7 @@ export default {
},
width: {
type: [Number, String],
default: null
default: undefined
},
autoPendingFirstOption: {
type: Boolean,
@ -172,14 +155,9 @@ export default {
onMenuScroll: {
type: Function,
default: undefined
},
// deprecated
emitOption: {
type: Boolean,
default: false
}
},
setup () {
setup (props) {
const virtualListRef = ref(null)
const scrollbarRef = ref(null)
onMounted(() => {
@ -189,6 +167,7 @@ export default {
if (value) value.sync()
})
return {
tmNodes: computed(() => props.treeMate.flattenedNodes),
virtualListRef,
scrollbarRef,
virtualListContainer () {
@ -202,21 +181,12 @@ export default {
value
} = virtualListRef
return value && value.itemsRef
}
}
},
data () {
const flattenedOptions = flattenOptions(this.options)
const firstAvailableOptionIndex = this.autoPendingFirstOption
? getNextAvailableIndex(flattenedOptions, null)
: null
const pendingWrappedOption = firstAvailableOptionIndex === null
? null
: flattenedOptions[firstAvailableOptionIndex]
return {
flattenedOptions,
pendingWrappedOption,
OPTION_TYPE
},
pendingTmNode: ref(
props.autoPendingFirstOption
? props.treeMate.getFirstAvailableNode()
: null
)
}
},
computed: {
@ -227,38 +197,13 @@ export default {
) return new Set(this.value)
return null
},
/**
* scrollbar related
*/
getScrollContainer () {
if (this.virtualScroll) return () => this.$refs.virtualScroller && this.$refs.virtualScroller.$el
return null
},
getScrollContent () {
if (this.virtualScroll) return () => this.$refs.virtualScroller && this.$refs.virtualScroller.$refs.wrapper
return null
},
pendingWrappedOptionIndex () {
const pendingWrappedOption = this.pendingWrappedOption
if (!pendingWrappedOption) return null
return pendingWrappedOption.index
},
empty () {
const flattenedOptions = this.flattenedOptions
return flattenedOptions && flattenedOptions.length === 0
const { tmNodes } = this
return tmNodes && tmNodes.length === 0
},
itemSize () {
return depx(this.cssrProps.$local[createKey('optionHeight', this.size)])
},
pendingOptionValue () {
const pendingWrappedOption = this.pendingWrappedOption
const data = (pendingWrappedOption && pendingWrappedOption.data) || null
return (
data &&
data.value !== undefined &&
data.value
) || null
},
style () {
return {
width: formatLength(this.width)
@ -266,54 +211,46 @@ export default {
}
},
watch: {
options (value) {
this.flattenedOptions = flattenOptions(value)
treeMate (value) {
if (this.autoPendingFirstOption) {
const firstAvailableOptionIndex = getNextAvailableIndex(this.flattenedOptions, null)
this.setPendingWrappedOptionIndex(firstAvailableOptionIndex)
const tmNode = this.treeMate.getFirstAvailableNode()
this.setPendingTmNode(tmNode)
} else {
this.pendingWrappedOption = null
this.setPendingTmNode(null)
}
this.$nextTick(() => {
this.scrollbarRef.sync()
})
}
},
methods: {
doToggleOption (option) {
const {
onMenuToggleOption
} = this
if (onMenuToggleOption) onMenuToggleOption(option)
},
doScroll (e) {
const {
onMenuScroll
} = this
if (onMenuScroll) onMenuScroll(e)
},
// required, scroller sync need to be triggered manually
handleListScroll () {
this.scrollbarRef.sync()
},
handleListResize () {
this.scrollbarRef.sync()
},
handleMenuScroll (e, scrollContainer, scrollContent) {
const {
onMenuScroll
} = this
if (onMenuScroll) onMenuScroll(e, scrollContainer, scrollContent)
getPendingOption () {
const { pendingTmNode } = this
return pendingTmNode && pendingTmNode.rawNode
},
getPendingOptionData () {
const pendingWrappedOption = this.pendingWrappedOption
return pendingWrappedOption && pendingWrappedOption.data
handleOptionMouseEnter (e, tmNode) {
if (tmNode.disabled) return
this.setPendingTmNode(tmNode, false)
},
handleOptionMouseEnter (e, index, wrappedOption) {
const data = wrappedOption.data
if (data.disabled) return
this.setPendingWrappedOptionIndex(index, false)
},
handleOptionClick (e, index, wrappedOption) {
const data = wrappedOption.data
if (data.disabled || wrappedOption.as === 'dropdown-submenu') return
this.toggleOption(data)
},
toggleOption (option) {
const {
onMenuToggleOption
} = this
if (onMenuToggleOption) onMenuToggleOption(option)
},
handleMenuMouseLeave () {
this.pendingWrappedOption = null
handleOptionClick (e, tmNode) {
if (tmNode.disabled) return
this.doToggleOption(tmNode.rawNode)
},
/**
* keyboard related methods
@ -325,36 +262,23 @@ export default {
this.next()
},
next () {
if (
this.pendingWrappedOption === null
) {
this.setPendingWrappedOptionIndex(
getNextAvailableIndex(this.flattenedOptions, null),
true
)
} else {
this.setPendingWrappedOptionIndex(
getNextAvailableIndex(this.flattenedOptions, this.pendingWrappedOptionIndex),
true
)
const {
pendingTmNode
} = this
if (pendingTmNode) {
this.setPendingTmNode(pendingTmNode.getNext(), true)
}
},
prev () {
if (this.pendingWrappedOption) {
this.setPendingWrappedOptionIndex(
getPrevAvailableIndex(this.flattenedOptions, this.pendingWrappedOptionIndex),
true
)
const {
pendingTmNode
} = this
if (pendingTmNode) {
this.setPendingTmNode(pendingTmNode.getPrev(), true)
}
},
setPendingWrappedOptionIndex (index, doScroll = false) {
if (index === null) {
this.pendingWrappedOption = null
}
// TODO: fix scroll logic
// const scrollbar = this.scrollbarRef
this.pendingWrappedOption = this.flattenedOptions[index]
// scrollbar.scrollTo({ y: index * this.itemSize })
setPendingTmNode (tmNode, doScroll = false) {
if (tmNode !== null) this.pendingTmNode = tmNode
}
}
}

View File

@ -1,6 +1,5 @@
import { h, inject, toRef } from 'vue'
import { useMemo } from '../../../_utils/composition'
import { createClassObject } from '../../../data-table/src/utils'
export default {
name: 'NBaseSelectOption',
@ -10,30 +9,25 @@ export default {
}
},
props: {
wrappedOption: {
tmNode: {
type: Object,
required: true
},
grouped: {
validator (value) {
return typeof value === 'boolean'
},
default: false
},
index: {
validator (value) {
return typeof value === 'number'
},
required: true
}
},
setup (props) {
const NBaseSelectMenu = inject('NBaseSelectMenu')
const dataRef = toRef(props.wrappedOption, 'data')
const rawNodeRef = toRef(props.tmNode, 'rawNode')
return {
data: dataRef,
rawNode: rawNodeRef,
isGrouped: useMemo(() => {
const { tmNode } = props
const { parent } = tmNode
return parent && parent.rawNode.type === 'group'
}),
isPending: useMemo(() => {
return props.index === NBaseSelectMenu.pendingWrappedOptionIndex
const { pendingTmNode } = NBaseSelectMenu
if (!pendingTmNode) return false
return props.tmNode.key === pendingTmNode.key
}),
isSelected: useMemo(() => {
const {
@ -41,7 +35,7 @@ export default {
value
} = NBaseSelectMenu
if (value === null) return false
const optionValue = dataRef.value.value
const optionValue = rawNodeRef.value.value
if (multiple) {
const {
valueSet
@ -55,35 +49,45 @@ export default {
},
methods: {
handleClick (e) {
if (this.disabled) return
this.NBaseSelectMenu.handleOptionClick(e, this.index, this.wrappedOption)
const {
tmNode
} = this
if (tmNode.disabled) return
this.NBaseSelectMenu.handleOptionClick(e, tmNode)
},
handleMouseEnter (e) {
if (this.disabled) return
this.NBaseSelectMenu.handleOptionMouseEnter(e, this.index, this.wrappedOption)
const {
tmNode
} = this
if (tmNode.disabled) return
this.NBaseSelectMenu.handleOptionMouseEnter(e, tmNode)
},
handleMouseMove (e) {
if (this.disabled || this.isPending) return
this.NBaseSelectMenu.handleOptionMouseEnter(e, this.index, this.wrappedOption)
const {
tmNode,
isPending
} = this
if (tmNode.disabled || isPending) return
this.NBaseSelectMenu.handleOptionMouseEnter(e, tmNode)
}
},
render () {
const { data } = this
const children = data.render ? data.render(data, this.isSelected) : [ data.label ]
const classObject = createClassObject(data.class)
const { rawNode } = this
const children = rawNode.render ? rawNode.render(rawNode, this.isSelected) : [ rawNode.label ]
return h('div', {
class: [
'n-base-select-option',
{
'n-base-select-option--selected': this.isSelected,
'n-base-select-option--disabled': data.disabled,
'n-base-select-option--grouped': this.grouped,
'n-base-select-option--pending': this.isPending,
...classObject
}
[
rawNode.class,
{
'n-base-select-option--disabled': rawNode.disabled,
'n-base-select-option--selected': this.isSelected,
'n-base-select-option--grouped': this.isGrouped,
'n-base-select-option--pending': this.isPending
}
]
],
'n-option-index': this.index,
style: data.style,
style: rawNode.style,
onClick: this.handleClick,
onMouseEnter: this.handleMouseEnter,
onMouseMove: this.handleMouseMove

View File

@ -1,176 +0,0 @@
/**
* For Select Component to use
*/
const OPTION_TYPE = {
OPTION: 0,
RENDER: 1,
GROUP_HEADER: 2
}
function valueToOptionMap (rawOptions) {
const map = new Map()
function traverse (options) {
if (!Array.isArray(options)) return
options.forEach(option => {
if (typeof option === 'function') {
// do nothing
} else if (option.type === 'group') {
traverse(option.children)
} else {
map.set(option.value, option)
}
})
}
traverse(rawOptions)
return map
}
function filterOptions (optionsToBeFiltered, filter) {
if (!filter) return optionsToBeFiltered
function traverse (options) {
if (!Array.isArray(options)) return []
const filteredOptions = []
for (let option of options) {
if (typeof option === 'function') {
filteredOptions.push(option)
} else if (option.type === 'group') {
const children = traverse(option.children)
if (children.length) {
filteredOptions.push(Object.assign({}, option, {
children
}))
}
} else {
if (filter(option)) {
filteredOptions.push(option)
}
}
}
return filteredOptions
}
return traverse(optionsToBeFiltered)
}
function flattenOptions (optionsToBeFlattened) {
const flattenedOptions = []
let index = 0
function traverse (options, context = {}) {
if (!Array.isArray(options)) return
for (let option of options) {
if (typeof option === 'function') {
const wrappedOption = {
type: OPTION_TYPE.RENDER,
index: index,
key: '__NAIVE_SELECT_RENDER__' + index,
render: option,
grouped: false
}
if (context.grouped) {
wrappedOption.grouped = true
}
flattenedOptions.push(wrappedOption)
index++
} else if (option.type === 'render') {
const wrappedOption = {
type: OPTION_TYPE.RENDER,
index: index,
key: '__NAIVE_SELECT_RENDER__' + index,
data: option.__NAIVE_OPTION_DATA__ || option,
render: option.render,
grouped: false
}
if (context.grouped) {
wrappedOption.grouped = true
}
flattenedOptions.push(wrappedOption)
index++
} else if (option.type === 'group') {
flattenedOptions.push({
type: OPTION_TYPE.GROUP_HEADER,
index: index,
data: option.__NAIVE_OPTION_DATA__ || option,
key: '__NAIVE_SELECT_GROUP_HEADER__' + index
})
index++
traverse(option.children, {
grouped: true
})
} else if (option.type === undefined) {
const wrappedOption = {
type: OPTION_TYPE.OPTION,
as: option.as,
index: index++,
data: option.__NAIVE_OPTION_DATA__ || option,
key: option.value,
grouped: false
}
if (context.grouped) {
wrappedOption.grouped = true
}
flattenedOptions.push(wrappedOption)
} else {
console.error(
'[naive-ui/select-menu]:',
option.type,
'typed option is supported.'
)
}
}
}
traverse(optionsToBeFlattened)
return flattenedOptions
}
function getNextAvailableIndex (options, currentIndex) {
return getAvailableIndex(options, currentIndex, 'next')
}
function getPrevAvailableIndex (options, currentIndex) {
return getAvailableIndex(options, currentIndex, 'prev')
}
function getAvailableIndex (options, currentIndex, direction) {
const length = options.length
let iterationStartsAt = null
let iterationEndsAt = null
if (direction === 'next') {
if (currentIndex !== null) {
iterationStartsAt = currentIndex + 1
iterationEndsAt = currentIndex + length + 1
} else {
iterationStartsAt = 0
iterationEndsAt = length
}
} else {
if (currentIndex !== null) {
iterationStartsAt = currentIndex + length - 1
iterationEndsAt = currentIndex - 1
} else {
iterationStartsAt = length
iterationEndsAt = 0
}
}
for (let i = iterationStartsAt; i !== iterationEndsAt;) {
const option = options[i % length]
if (option.type === OPTION_TYPE.OPTION) {
const data = option.data
if (!data.disabled) return i % length
}
if (direction === 'prev') {
--i
} else {
++i
}
}
return null
}
export {
getPrevAvailableIndex,
getNextAvailableIndex,
valueToOptionMap,
filterOptions,
flattenOptions,
OPTION_TYPE
}

View File

@ -1,14 +0,0 @@
export default function createClassObject (classValue) {
if (typeof classValue === 'string') {
return classValue
.split(' ')
.filter(v => v.length)
.reduce((prevValue, currentValue) => {
prevValue[currentValue] = true
return prevValue
}, {})
} else if (classValue && typeof classValue === 'object') {
return classValue
}
return null
}

View File

@ -57,7 +57,7 @@
class="n-auto-complete-menu"
:theme="mergedTheme"
:pattern="value"
:options="selectOptions"
:tree-mate="treeMate"
:multiple="false"
:size="mergedSize"
@menu-toggle-option="handleToggleOption"
@ -70,6 +70,7 @@
</template>
<script>
import { createTreeMate } from 'treemate'
import {
configurable,
themeable,
@ -182,11 +183,19 @@ export default {
__placeableEnabled () {
return this.active
},
selectOptions () {
return mapAutoCompleteOptionsToSelectOptions(this.options)
},
active () {
return !!this.value && this.canBeActivated && !!this.selectOptions.length
},
selectOptions () {
return mapAutoCompleteOptionsToSelectOptions(this.options)
treeMate () {
return createTreeMate(this.selectOptions, {
getKey (node) {
if (node.type === 'group') return node.name
return node.value
}
})
}
},
methods: {
@ -250,7 +259,7 @@ export default {
},
handleKeyDownEnter (e) {
if (this.$refs.menu && !this.isComposing) {
const pendingOptionData = this.$refs.menu.getPendingOptionData()
const pendingOptionData = this.$refs.menu.getPendingOption()
if (pendingOptionData) {
this.select(pendingOptionData)
e.preventDefault()

View File

@ -21,7 +21,7 @@
>
<n-checkbox
:disabled="disabled"
:checked="checked"
:value="checked"
:indeterminate="indeterminate"
@click.stop="handleCheck"
/>

View File

@ -17,7 +17,7 @@
auto-pending-first-option
:theme="theme"
:pattern="pattern"
:options="filteredSelectOptions"
:tree-mate="selectTreeMate"
:multiple="multiple"
:size="size"
:value="value"
@ -30,6 +30,7 @@
<script>
import { ref, inject, toRef } from 'vue'
import { createTreeMate } from 'treemate'
import NBaseSelectMenu from '../../_base/select-menu'
import { createSelectOptions, getPickerElement } from './utils'
import {
@ -129,6 +130,15 @@ export default {
value: option.value,
label: option.label
}))
},
selectTreeMate () {
return createTreeMate(
this.filteredSelectOptions, {
getKey (node) {
return node.value
}
}
)
}
},
watch: {
@ -195,7 +205,7 @@ export default {
enter () {
const { menuRef } = this
if (menuRef) {
const pendingOptionData = menuRef.getPendingOptionData()
const pendingOptionData = menuRef.getPendingOption()
this.doCheck(pendingOptionData)
return true
}

View File

@ -27,7 +27,7 @@ export function useCascader (props) {
}
const treeMateRef = computed(() => {
return TreeMate(props.options, {
getKey ({ node }) {
getKey (node) {
return node.value
}
})

View File

@ -24,11 +24,11 @@
v-for="(rowData, index) in data"
:key="rowKey === null ? rowData.key : rowKey(rowData)"
class="n-data-table-tr"
:class="
createClassObject(typeof rowClassName === 'function'
? createClassObject(rowClassName(rowData, index))
: rowClassName)
"
:class="[
typeof rowClassName === 'function'
? rowClassName(rowData, index)
: rowClassName
]"
>
<td
v-for="column in columns"
@ -39,15 +39,17 @@
right: NDataTable.currentFixedColumnRight(column)
}"
class="n-data-table-td"
:class="{
'n-data-table-td--ellipsis': column.ellipsis,
[`n-data-table-td--${column.align}-align`]: column.align,
...(column.className && createClassObject(column.className)),
[`n-data-table-td--fixed-${column.fixed}`]: column.width && column.fixed,
'n-data-table-td--shadow-after': NBaseTable.leftActiveFixedColumn[column.key],
'n-data-table-td--shadow-before': NBaseTable.rightActiveFixedColumn[column.key],
'n-data-table-td--selection': column.type === 'selection'
}"
:class="[
column.className,
{
'n-data-table-td--ellipsis': column.ellipsis,
[`n-data-table-td--${column.align}-align`]: column.align,
[`n-data-table-td--fixed-${column.fixed}`]: column.width && column.fixed,
'n-data-table-td--shadow-after': NBaseTable.leftActiveFixedColumn[column.key],
'n-data-table-td--shadow-before': NBaseTable.rightActiveFixedColumn[column.key],
'n-data-table-td--selection': column.type === 'selection'
}
]"
>
<n-checkbox
v-if="column.type === 'selection'"
@ -72,7 +74,7 @@
<script>
import { ref } from 'vue'
import Cell from './Cell.vue'
import { createCustomWidthStyle, setCheckStatusOfRow, createClassObject, createRowKey } from '../utils'
import { createCustomWidthStyle, setCheckStatusOfRow, createRowKey } from '../utils'
import NScrollbar from '../../../scrollbar'
import formatLength from '../../../_utils/css/formatLength'
@ -146,7 +148,6 @@ export default {
}
},
methods: {
createClassObject,
createRowKey,
handleCheckboxInput (row, checked) {
const newCheckedRowKeys = this.checkedRowKeys.map(v => v)

View File

@ -1,16 +1,5 @@
import formatLength from '../../_utils/css/formatLength'
export function createClassObject (classString) {
if (!classString) return {}
if (typeof classString === 'string') {
return classString.split(' ').filter(className => className).reduce((classObject, className) => {
classObject[className] = true
return classObject
}, {})
}
return classString
}
export function createCustomWidthStyle (column, index, placement) {
if (column.width) {
const width = column.width

View File

@ -14,11 +14,7 @@ import { keep, call } from '../../_utils/vue'
import styles from './styles'
const treemateOptions = {
getKey ({ parentKey, index, node }) {
if (node.type === 'divider') {
if (parentKey === null) return `${index}`
return `${parentKey}-${index}`
}
getKey (node) {
return node.key
},
getDisabled ({ node }) {

View File

@ -71,7 +71,7 @@
auto-pending-first-option
:theme="mergedTheme"
:pattern="pattern"
:options="filteredOptions"
:tree-mate="treeMate"
:multiple="multiple"
:size="mergedSize"
:filterable="filterable"
@ -97,7 +97,13 @@
</template>
<script>
import { ref } from 'vue'
import { ref, computed, toRef } from 'vue'
import { createTreeMate } from 'treemate'
import {
useIsMounted,
useMergedState,
useCompitable
} from 'vooks'
import {
configurable,
placeable,
@ -110,13 +116,6 @@ import {
clickoutside,
zindexable
} from '../../_directives'
import {
filterOptions,
valueToOptionMap
} from '../../_utils/component/select'
import {
useIsMounted
} from '../../_utils/composition'
import {
warn
} from '../../_utils/naive'
@ -136,6 +135,28 @@ function patternMatched (pattern, value) {
}
}
function filterOptions (originalOpts, filter) {
if (!filter) return originalOpts
function traverse (options) {
if (!Array.isArray(options)) return []
const filteredOptions = []
for (const option of options) {
if (option.type === 'group') {
const children = traverse(option.children)
if (children.length) {
filteredOptions.push(Object.assign({}, option, {
children
}))
}
} else if (filter(option)) {
filteredOptions.push(option)
}
}
return filteredOptions
}
return traverse(originalOpts)
}
export default {
name: 'Select',
components: {
@ -167,7 +188,7 @@ export default {
},
options: {
type: Array,
default: () => []
required: true
},
value: {
type: [String, Number, Array],
@ -241,6 +262,10 @@ export default {
value
})
},
show: {
type: Boolean,
default: undefined
},
// eslint-disable-next-line vue/prop-name-casing
'onUpdate:value': {
type: [Function, Array],
@ -287,23 +312,45 @@ export default {
default: false
}
},
setup () {
setup (props) {
const patternRef = ref('')
const filteredOptionsRef = computed(() => filterOptions(
props.options,
patternRef.value
))
const treeMateRef = computed(() => createTreeMate(filteredOptionsRef.value, {
getKey (node) {
if (node.type === 'group') return node.name
return node.value
}
}))
const tmNodeMap = computed(() => treeMateRef.value.treeNodeMap)
const uncontrolledShowRef = ref(false)
const mergedShowRef = useMergedState(
toRef(props, 'show'),
uncontrolledShowRef
)
return {
treeMate: treeMateRef,
flattenedNodes: computed(() => {
return treeMateRef.value.flattenedNodes
}),
tmNodeMap,
isMounted: useIsMounted(),
offsetContainerRef: ref(null),
triggerRef: ref(null),
trackingRef: ref(null),
menuRef: ref(null)
}
},
data () {
return {
show: false,
scrolling: false,
pattern: '',
memorizedValueToOptionMap: new Map(),
createdOptions: [],
beingCreatedOptions: []
menuRef: ref(null),
pattern: patternRef,
uncontrolledShow: uncontrolledShowRef,
show: mergedShowRef,
compitableOptions: useCompitable(props, [
'items',
'options'
]),
createdOptions: ref([]),
beingCreatedOptions: ref([]),
memoValOptMap: ref(new Map())
}
},
computed: {
@ -311,67 +358,76 @@ export default {
return this.show
},
localizedPlaceholder () {
if (this.placeholder !== undefined) {
return this.placeholder
}
return this.localeNs.placeholder
},
adpatedOptions () {
/**
* If using deprecated API, make it work at first
*/
const options = this.items || this.options
if (options) return options
return []
return this.placeholder ?? this.localeNs.placeholder
},
localOptions () {
return this.adpatedOptions
return this.compitableOptions
.concat(this.createdOptions)
.concat(this.beingCreatedOptions)
},
filteredOptions () {
if (this.remote) {
return this.adpatedOptions
return this.compitableOptions
} else {
const options = this.localOptions
const trimmedPattern = this.pattern.trim()
if (!trimmedPattern.length || !this.filterable) {
return options
const { localOptions, pattern } = this
if (!pattern.length || !this.filterable) {
return localOptions
} else {
const filter = option => this.filter(trimmedPattern, option)
const filteredOptions = filterOptions(options, filter)
return filteredOptions
const { filter } = this
const mergedFilter = option => filter(pattern, option)
return filterOptions(localOptions, mergedFilter)
}
}
},
valueToOptionMap () {
return valueToOptionMap(this.localOptions)
},
selectedOptions () {
if (this.multiple) {
return this.mapValuesToOptions(this.value)
const { value: values } = this
if (!Array.isArray(values)) return []
const remote = this.remote
const {
tmNodeMap,
memoValOptMap,
wrappedFallbackOption
} = this
const options = []
values.forEach(value => {
if (tmNodeMap.has(value)) {
options.push(tmNodeMap.get(value).rawNode)
} else if (remote && memoValOptMap.has(value)) {
options.push(memoValOptMap.get(value))
} else if (wrappedFallbackOption) {
const option = wrappedFallbackOption(value)
if (option) {
options.push(option)
}
}
})
return options
}
return null
},
selectedOption () {
if (!this.multiple) {
const value = this.value
const selectedOption = this.getOption(value)
const fallbackOption = this.wrappedFallbackOption
const { value, tmNodeMap, wrappedFallbackOption } = this
let selectedOption = null
if (tmNodeMap.has(value)) {
selectedOption = tmNodeMap.get(value).rawNode
} else if (this.remote) {
selectedOption = this.memoValOptMap.get(value)
}
return (
selectedOption ||
(fallbackOption && fallbackOption(value)) ||
(wrappedFallbackOption && wrappedFallbackOption(value)) ||
null
)
}
return null
},
wrappedFallbackOption () {
const fallbackOption = this.fallbackOption
const { fallbackOption } = this
if (!fallbackOption) return false
return value => {
const type = typeof value
if (type === 'string' || type === 'number') {
if (['string', 'number'].includes(typeof value)) {
return Object.assign(fallbackOption(value), { value })
} return null
}
@ -382,9 +438,11 @@ export default {
this.updateMemorizedOptions()
},
filteredOptions () {
if (!this.show) return
this.$nextTick(this.__placeableSyncPosition)
},
value () {
if (!this.show) return
this.$nextTick(this.__placeableSyncPosition)
}
},
@ -444,28 +502,24 @@ export default {
} = this
if (onScroll) call(onScroll, ...args)
},
activate () {
this.show = true
},
deactivate () {
this.show = false
},
/**
* remote related methods
*/
updateMemorizedOptions () {
const remote = this.remote
const multiple = this.multiple
const {
remote,
multiple
} = this
if (remote) {
const memorizedValueToOptionMap = this.memorizedValueToOptionMap
const { memoValOptMap } = this
if (multiple) {
this.selectedOptions.forEach(option => {
memorizedValueToOptionMap.set(option.value, option)
memoValOptMap.set(option.value, option)
})
} else {
const option = this.selectedOption
if (option) {
memorizedValueToOptionMap.set(option.value, option)
memoValOptMap.set(option.value, option)
}
}
}
@ -476,14 +530,14 @@ export default {
openMenu () {
if (!this.disabled) {
this.pattern = ''
this.activate()
this.uncontrolledShow = true
if (this.filterable) {
this.triggerRef.focusPatternInput()
}
}
},
closeMenu () {
this.deactivate()
this.uncontrolledShow = false
},
handleMenuAfterLeave () {
this.pattern = ''
@ -500,9 +554,6 @@ export default {
},
handleTriggerBlur () {
this.doBlur()
if (__DEV__) {
if (this.debug) return
}
this.closeMenu()
},
handleTriggerFocus () {
@ -510,65 +561,35 @@ export default {
},
handleMenuClickOutside (e) {
if (this.show) {
if (!this.triggerRef.$el.contains(e.target) && !this.scrolling) {
if (!this.triggerRef.$el.contains(e.target)) {
this.closeMenu()
}
}
},
/**
* data utils methods
*/
mapValuesToOptions (values) {
if (!Array.isArray(values)) return []
const remote = this.remote
const valueOptionMap = this.valueToOptionMap
const memorizedValueToOptionMap = this.memorizedValueToOptionMap
const options = []
const fallbackOption = this.wrappedFallbackOption
values.forEach(value => {
if (valueOptionMap.has(value)) {
options.push(valueOptionMap.get(value))
} else if (remote && memorizedValueToOptionMap.has(value)) {
options.push(memorizedValueToOptionMap.get(value))
} else if (fallbackOption) {
const option = fallbackOption(value)
if (option) {
options.push(option)
}
}
})
return options
},
getOption (value) {
if (this.valueToOptionMap.has(value)) {
return this.valueToOptionMap.get(value)
} else if (this.remote && this.memorizedValueToOptionMap.has(value)) {
return this.memorizedValueToOptionMap.get(value)
}
},
createClearedMultipleSelectValue (value) {
if (!Array.isArray(value)) return []
if (this.wrappedFallbackOption) {
/** if option has a fallback, I can't help user to clear some unknown value */
// if option has a fallback, I can't help user to clear some unknown value
return Array.from(value)
} else {
/** if there's no option fallback, unappeared options are treated as invalid */
const remote = this.remote
const valueOptionMap = this.valueToOptionMap
// if there's no option fallback, unappeared options are treated as invalid
const {
remote,
tmNodeMap
} = this
if (remote) {
const memorizedValueToOptionMap = this.memorizedValueToOptionMap
return value.filter(v => valueOptionMap.has(v) || memorizedValueToOptionMap.has(v))
const { memoValOptMap } = this
return value.filter(v => tmNodeMap.has(v) || memoValOptMap.has(v))
} else {
return value.filter(v => valueOptionMap.has(v))
return value.filter(v => tmNodeMap.has(v))
}
}
},
handleToggleOption (option) {
if (this.disabled) return
const tag = this.tag
const remote = this.remote
const { tag, remote } = this
if (tag && !remote) {
const beingCreatedOptions = this.beingCreatedOptions
const { beingCreatedOptions } = this
const beingCreatedOption = beingCreatedOptions[0] || null
if (beingCreatedOption) {
this.createdOptions.push(beingCreatedOption)
@ -577,7 +598,7 @@ export default {
}
if (this.multiple) {
if (remote) {
this.memorizedValueToOptionMap.set(option.value, option)
this.memoValOptMap.set(option.value, option)
}
const changedValue = this.createClearedMultipleSelectValue(this.value)
const index = changedValue.findIndex(value => value === option.value)
@ -615,8 +636,8 @@ export default {
if (!this.pattern.length) {
const changedValue = this.createClearedMultipleSelectValue(this.value)
if (Array.isArray(changedValue)) {
const popedValue = changedValue.pop()
const createdOptionIndex = this.getCreatedOptionIndex(popedValue)
const poppedValue = changedValue.pop()
const createdOptionIndex = this.getCreatedOptionIndex(poppedValue)
~createdOptionIndex && this.createdOptions.splice(createdOptionIndex, 1)
this.doUpdateValue(changedValue)
}
@ -631,19 +652,19 @@ export default {
handlePatternInput (e) {
const value = e.target.value
this.pattern = value
const onSearch = this.onSearch
const { onSearch, tag, remote } = this
if (onSearch) {
onSearch(value)
this.doSearch(value)
}
if (this.tag && !this.remote) {
if (tag && !remote) {
if (!value) {
this.beingCreatedOptions = []
return
}
const optionBeingCreated = this.onCreate(value)
if (
this.adpatedOptions.some(
this.compitableOptions.some(
option => option.value === optionBeingCreated.value
) ||
this.createdOptions.some(
@ -658,28 +679,25 @@ export default {
},
handleClear (e) {
e.stopPropagation()
if (!this.multiple && this.filterable) {
const { multiple, doUpdateValue } = this
if (!multiple && this.filterable) {
this.closeMenu()
}
if (this.multiple) {
this.doUpdateValue([])
if (multiple) {
doUpdateValue([])
} else {
this.doUpdateValue(null)
doUpdateValue(null)
}
},
/**
* scroll events on menu
*/
// scroll events on menu
handleMenuScroll (e, scrollContainer, scrollContent) {
this.doScroll(e, scrollContainer, scrollContent)
},
/**
* keyboard events
*/
// keyboard events
handleKeyUpEnter (e) {
if (this.show) {
const menu = this.menuRef
const pendingOptionData = menu && menu.getPendingOptionData()
const pendingOptionData = menu && menu.getPendingOption()
if (pendingOptionData) {
this.handleToggleOption(pendingOptionData)
} else {