mirror of
https://github.com/tusen-ai/naive-ui.git
synced 2025-01-30 12:52:43 +08:00
feat(select): support group
This commit is contained in:
parent
c9254b58b1
commit
c93a8e3f10
140
demo/documentation/components/select/enUS/group.md
Normal file
140
demo/documentation/components/select/enUS/group.md
Normal file
@ -0,0 +1,140 @@
|
||||
# Group
|
||||
```html
|
||||
<n-select
|
||||
filterable
|
||||
v-model=value
|
||||
:options='options'
|
||||
/>
|
||||
```
|
||||
```js
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
value: null,
|
||||
options: [
|
||||
{
|
||||
type: 'group',
|
||||
name: 'Rubber Soul',
|
||||
children: [
|
||||
{
|
||||
label: 'Everybody\'s Got Something to Hide Except Me and My Monkey',
|
||||
value: 'song0',
|
||||
disabled: true
|
||||
},
|
||||
{
|
||||
label: 'Drive My Car',
|
||||
value: 'song1'
|
||||
},
|
||||
{
|
||||
label: 'Norwegian Wood',
|
||||
value: 'song2'
|
||||
},
|
||||
{
|
||||
label: 'You Won\'t See',
|
||||
value: 'song3',
|
||||
disabled: true
|
||||
},
|
||||
{
|
||||
label: 'Nowhere Man',
|
||||
value: 'song4'
|
||||
},
|
||||
{
|
||||
label: 'Think For Yourself',
|
||||
value: 'song5'
|
||||
},
|
||||
{
|
||||
label: 'The Word',
|
||||
value: 'song6'
|
||||
},
|
||||
{
|
||||
label: 'Michelle',
|
||||
value: 'song7',
|
||||
disabled: true
|
||||
},
|
||||
{
|
||||
label: 'What goes on',
|
||||
value: 'song8'
|
||||
},
|
||||
{
|
||||
label: 'Girl',
|
||||
value: 'song9'
|
||||
},
|
||||
{
|
||||
label: 'I\'m looking through you',
|
||||
value: 'song10'
|
||||
},
|
||||
{
|
||||
label: 'In My Life',
|
||||
value: 'song11'
|
||||
},
|
||||
{
|
||||
label: 'Wait',
|
||||
value: 'song12'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'group',
|
||||
name: 'Let It Be',
|
||||
children: [
|
||||
{
|
||||
label: 'Two Of Us',
|
||||
value: 'Two Of Us'
|
||||
},
|
||||
{
|
||||
label: 'Dig A Pony',
|
||||
value: 'Dig A Pony'
|
||||
},
|
||||
{
|
||||
label: 'Across The Universe',
|
||||
value: 'Across The Universe'
|
||||
},
|
||||
{
|
||||
label: 'I Me Mine',
|
||||
value: 'I Me Mine'
|
||||
},
|
||||
{
|
||||
label: 'Dig It',
|
||||
value: 'Dig It'
|
||||
},
|
||||
{
|
||||
label: 'Let It Be',
|
||||
value: 'Let It Be'
|
||||
},
|
||||
{
|
||||
label: 'Maggie Mae',
|
||||
value: 'Maggie Mae'
|
||||
},
|
||||
{
|
||||
label: 'I\'ve Got A Feeling',
|
||||
value: 'I\'ve Got A Feeling'
|
||||
},
|
||||
{
|
||||
label: 'One After 909',
|
||||
value: 'One After 909'
|
||||
},
|
||||
{
|
||||
label: 'The Long And Winding Road',
|
||||
value: 'The Long And Winding Road'
|
||||
},
|
||||
{
|
||||
label: 'For You Blue',
|
||||
value: 'For You Blue'
|
||||
},
|
||||
{
|
||||
label: 'Get Back',
|
||||
value: 'Get Back'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
```css
|
||||
.n-select {
|
||||
width: 180px;
|
||||
margin: 0 12px 8px 0;
|
||||
}
|
||||
```
|
@ -10,6 +10,7 @@ remote
|
||||
remote-multiple
|
||||
clearable
|
||||
scroll-event
|
||||
group
|
||||
```
|
||||
## API
|
||||
|
||||
|
17
packages/base/SelectMenu/src/SelectGroupHeader.vue
Normal file
17
packages/base/SelectMenu/src/SelectGroupHeader.vue
Normal file
@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<div class="n-base-select-group-header">
|
||||
{{ name }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'NBaseSelectGroupHeader',
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -29,19 +29,25 @@
|
||||
:items="flattenedOptions"
|
||||
:item-size="itemSize"
|
||||
key-field="key"
|
||||
@visible="handleMenuVisible"
|
||||
>
|
||||
<template v-slot:before>
|
||||
<n-base-light-bar ref="lightBar" :item-size="itemSize" :theme="theme" />
|
||||
</template>
|
||||
<template v-slot="{ item: option }">
|
||||
<n-select-option
|
||||
:key="option.data.value"
|
||||
v-if="option.type === OPTION_TYPE.OPTION"
|
||||
:index="option.index"
|
||||
:label="option.data.label"
|
||||
:value="option.data.value"
|
||||
:disabled="option.data.disabled"
|
||||
:grouped="option.grouped"
|
||||
:selected="isOptionSelected({ value: option.data.value })"
|
||||
/>
|
||||
<n-select-group-header
|
||||
v-else-if="option.type === OPTION_TYPE.GROUP_HEADER"
|
||||
:name="option.data.name"
|
||||
/>
|
||||
</template>
|
||||
</recycle-scroller>
|
||||
</template>
|
||||
@ -73,9 +79,11 @@ import NScrollbar from '../../../common/Scrollbar'
|
||||
import {
|
||||
getPrevAvailableIndex,
|
||||
getNextAvailableIndex,
|
||||
flattenedOptions
|
||||
flattenOptions,
|
||||
OPTION_TYPE
|
||||
} from '../../../utils/data/flattenedOptions'
|
||||
import NSelectOption from './SelectOption.vue'
|
||||
import NSelectGroupHeader from './SelectGroupHeader.vue'
|
||||
import NBaseLightBar from '../../LightBar'
|
||||
import debounce from 'lodash-es/debounce'
|
||||
import { RecycleScroller } from 'vue-virtual-scroller'
|
||||
@ -91,6 +99,7 @@ export default {
|
||||
NScrollbar,
|
||||
NBaseLightBar,
|
||||
NSelectOption,
|
||||
NSelectGroupHeader,
|
||||
RecycleScroller
|
||||
},
|
||||
props: {
|
||||
@ -159,7 +168,8 @@ export default {
|
||||
data () {
|
||||
return {
|
||||
active: true,
|
||||
pendingWrappedOption: null
|
||||
pendingWrappedOption: null,
|
||||
OPTION_TYPE
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -169,7 +179,8 @@ export default {
|
||||
return pendingWrappedOption.index
|
||||
},
|
||||
flattenedOptions () {
|
||||
return flattenedOptions(this.options)
|
||||
const flattenedOptions = flattenOptions(this.options)
|
||||
return flattenedOptions
|
||||
},
|
||||
notFound () {
|
||||
return this.filterable && (this.pattern.length && !this.flattenedOptions.length)
|
||||
@ -227,6 +238,9 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleMenuVisible () {
|
||||
this.$emit('menu-visible')
|
||||
},
|
||||
handleMenuScroll (e, scrollContainer, scrollContent) {
|
||||
this.$emit('menu-scroll', e, scrollContainer, scrollContent)
|
||||
},
|
||||
|
@ -32,6 +32,12 @@ export default {
|
||||
},
|
||||
default: false
|
||||
},
|
||||
grouped: {
|
||||
validator (value) {
|
||||
return typeof value === 'boolean'
|
||||
},
|
||||
default: false
|
||||
},
|
||||
index: {
|
||||
validator (value) {
|
||||
return typeof value === 'number'
|
||||
@ -62,7 +68,8 @@ export default {
|
||||
staticClass: 'n-base-select-option',
|
||||
class: {
|
||||
'n-base-select-option--selected': this.selected,
|
||||
'n-base-select-option--disabled': this.disabled
|
||||
'n-base-select-option--disabled': this.disabled,
|
||||
'n-base-select-option--grouped': this.grouped
|
||||
},
|
||||
on: {
|
||||
click: this.handleClick,
|
||||
|
@ -84,13 +84,13 @@ export default {
|
||||
return {
|
||||
memorizedId: null,
|
||||
internalActive: false,
|
||||
show: false
|
||||
keepPlaceableTracingWhenInactive: false
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.memorizedId = this.id
|
||||
popoverManager.registerContent(this.memorizedId, this)
|
||||
if (this.active) this.show = true
|
||||
if (this.active) this.keepPlaceableTracingWhenInactive = true
|
||||
if (this.controller) {
|
||||
this.controller.updatePosition = this.updatePosition
|
||||
}
|
||||
@ -240,10 +240,10 @@ export default {
|
||||
},
|
||||
on: {
|
||||
enter: () => {
|
||||
this.show = true
|
||||
this.keepPlaceableTracingWhenInactive = true
|
||||
},
|
||||
afterLeave: () => {
|
||||
this.show = false
|
||||
this.keepPlaceableTracingWhenInactive = false
|
||||
}
|
||||
}
|
||||
}, [
|
||||
|
@ -58,6 +58,7 @@
|
||||
v-if="active"
|
||||
ref="contentInner"
|
||||
class="n-select-menu"
|
||||
auto-pending-first-option
|
||||
:theme="synthesizedTheme"
|
||||
:pattern="pattern"
|
||||
:options="filteredOptions"
|
||||
@ -71,6 +72,7 @@
|
||||
:mirror="false"
|
||||
@menu-toggle-option="handleToggleOption"
|
||||
@menu-scroll="handleMenuScroll"
|
||||
@menu-visible="handleMenuVisible"
|
||||
/>
|
||||
</transition>
|
||||
</div>
|
||||
@ -86,6 +88,10 @@ import clickoutside from '../../../directives/clickoutside'
|
||||
import {
|
||||
NBaseSelectMenu
|
||||
} from '../../../base/SelectMenu'
|
||||
import {
|
||||
filterOptions,
|
||||
valueToOptionMap
|
||||
} from '../../../utils/data/flattenedOptions'
|
||||
import NBasePicker from '../../../base/Picker'
|
||||
import withapp from '../../../mixins/withapp'
|
||||
import themeable from '../../../mixins/themeable'
|
||||
@ -202,7 +208,8 @@ export default {
|
||||
active: false,
|
||||
scrolling: false,
|
||||
pattern: '',
|
||||
memorizedValueToOptionMap: new Map()
|
||||
memorizedValueToOptionMap: new Map(),
|
||||
disablePlaceableTracingWhenActive: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -220,18 +227,17 @@ export default {
|
||||
return options
|
||||
} else {
|
||||
const trimmedPattern = this.pattern.trim()
|
||||
if (trimmedPattern.length || !this.filterable) {
|
||||
if (!trimmedPattern.length || !this.filterable) {
|
||||
return options
|
||||
} else {
|
||||
const filter = this.filter
|
||||
return options.filter(option => filter(trimmedPattern, option))
|
||||
const filter = option => this.filter(trimmedPattern, option)
|
||||
const filteredOptions = filterOptions(options, filter)
|
||||
return filteredOptions
|
||||
}
|
||||
}
|
||||
},
|
||||
valueToOptionMap () {
|
||||
const valueToOptionMap = new Map()
|
||||
this.adpatedOptions.forEach(option => valueToOptionMap.set(option.value, option))
|
||||
return valueToOptionMap
|
||||
return valueToOptionMap(this.options)
|
||||
},
|
||||
selectedOptions () {
|
||||
if (this.multiple) {
|
||||
@ -267,6 +273,7 @@ export default {
|
||||
methods: {
|
||||
activate () {
|
||||
this.active = true
|
||||
this.disablePlaceableTracingWhenActive = true
|
||||
},
|
||||
deactivate () {
|
||||
this.active = false
|
||||
@ -434,6 +441,10 @@ export default {
|
||||
this.$emit('change', null)
|
||||
}
|
||||
},
|
||||
handleMenuVisible () {
|
||||
this.disablePlaceableTracingWhenActive = false
|
||||
this.updatePosition()
|
||||
},
|
||||
/**
|
||||
* scroll events on menu
|
||||
*/
|
||||
|
@ -190,9 +190,6 @@ export default {
|
||||
this.trackedElement = this.getTrackedElement()
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Need to be fulfilled!
|
||||
*/
|
||||
setOffsetOfTrackingElement (position, transformOrigin) {
|
||||
this.trackingElement.style.position = 'absolute'
|
||||
this.trackingElement.style.top = position.top
|
||||
@ -203,7 +200,12 @@ export default {
|
||||
this.trackingElement.setAttribute('n-suggested-transform-origin', transformOrigin)
|
||||
},
|
||||
updatePosition (el, cb) {
|
||||
if (!this.active && !this.show) return
|
||||
if (!this.active) {
|
||||
if (!this.keepPlaceableTracingWhenInactive) return
|
||||
}
|
||||
if (this.active) {
|
||||
if (this.disablePlaceableTracingWhenActive) return
|
||||
}
|
||||
this._getTrackingElement()
|
||||
this.trackingElement.style.position = 'absolute'
|
||||
if (this.manuallyPositioned) {
|
||||
|
@ -4,35 +4,100 @@
|
||||
*/
|
||||
const OPTION_TYPE = {
|
||||
OPTION: 0,
|
||||
RENDER: 1
|
||||
RENDER: 1,
|
||||
GROUP_HEADER: 2
|
||||
}
|
||||
|
||||
function flattenOptions (optionsToFlatten) {
|
||||
const flattenedOptions = []
|
||||
let index = 0
|
||||
function valueToOptionMap (rawOptions) {
|
||||
const map = new Map()
|
||||
function traverse (options) {
|
||||
if (!Array.isArray(options)) return
|
||||
for (let option of options) {
|
||||
options.forEach(option => {
|
||||
if (typeof option === 'function') {
|
||||
flattenedOptions.push({
|
||||
type: OPTION_TYPE.RENDER,
|
||||
index: index++,
|
||||
key: index,
|
||||
data: option
|
||||
})
|
||||
// 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: index,
|
||||
render: option,
|
||||
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,
|
||||
key: index
|
||||
})
|
||||
index++
|
||||
traverse(option.children, {
|
||||
grouped: true
|
||||
})
|
||||
} else {
|
||||
const wrappedOption = {
|
||||
type: OPTION_TYPE.OPTION,
|
||||
index: index++,
|
||||
data: option,
|
||||
key: option.value
|
||||
})
|
||||
key: option.value,
|
||||
grouped: false
|
||||
}
|
||||
if (context.grouped) {
|
||||
wrappedOption.grouped = true
|
||||
}
|
||||
flattenedOptions.push(wrappedOption)
|
||||
}
|
||||
}
|
||||
}
|
||||
traverse(optionsToFlatten)
|
||||
traverse(optionsToBeFlattened)
|
||||
return flattenedOptions
|
||||
}
|
||||
|
||||
@ -80,13 +145,11 @@ function getAvailableIndex (options, currentIndex, direction) {
|
||||
return null
|
||||
}
|
||||
|
||||
function flattenedOptions (options) {
|
||||
const flattenedOptions = flattenOptions(options)
|
||||
return flattenedOptions
|
||||
}
|
||||
|
||||
export {
|
||||
getPrevAvailableIndex,
|
||||
getNextAvailableIndex,
|
||||
flattenedOptions
|
||||
valueToOptionMap,
|
||||
filterOptions,
|
||||
flattenOptions,
|
||||
OPTION_TYPE
|
||||
}
|
||||
|
@ -22,6 +22,11 @@
|
||||
line-height: map-get($map: $--n-height, $key: $size);
|
||||
font-size: map-get($map: $--n-font-size, $key: $size);
|
||||
}
|
||||
@include b(base-select-group-header) {
|
||||
height: map-get($map: $--n-height, $key: $size);
|
||||
line-height: map-get($map: $--n-height, $key: $size);
|
||||
font-size: map-get($map: $--n-font-size, $key: $size) - 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,15 +51,23 @@
|
||||
}
|
||||
background-color: $--base-select-menu-background-color;
|
||||
box-shadow: $--base-select-menu-box-shadow;
|
||||
@include b(base-select-group-header) {
|
||||
cursor: default;
|
||||
color: $--n-meta-text-color;
|
||||
padding: 0 14px;
|
||||
}
|
||||
@include b(base-select-option) {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
padding: 0px 14px;
|
||||
padding: 0 14px;
|
||||
white-space: nowrap;
|
||||
transition: color .3s $--n-ease-in-out-cubic-bezier;
|
||||
color: map-get($map: $--base-select-menu-option-color, $key: "default");
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
@include m(grouped) {
|
||||
padding: 0 21px;
|
||||
}
|
||||
@include m(selected) {
|
||||
color: map-get($map: $--base-select-menu-option-color, $key: "selected");
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user