feat(select): support group

This commit is contained in:
07akioni 2020-01-21 18:21:19 +08:00
parent c9254b58b1
commit c93a8e3f10
10 changed files with 309 additions and 41 deletions

View 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;
}
```

View File

@ -10,6 +10,7 @@ remote
remote-multiple
clearable
scroll-event
group
```
## API

View 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>

View File

@ -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)
},

View File

@ -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,

View File

@ -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
}
}
}, [

View File

@ -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
*/

View File

@ -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) {

View File

@ -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
}

View File

@ -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");
}