feat(select): remote search on multiple select

This commit is contained in:
07akioni 2019-08-12 16:46:10 +08:00
parent be3221d7bf
commit b44b17e8be
6 changed files with 234 additions and 45 deletions

View File

@ -17,6 +17,7 @@
<change-event /> <change-event />
<change-event-emit-item /> <change-event-emit-item />
<search /> <search />
<remote-search />
</div> </div>
</div> </div>
</template> </template>
@ -28,6 +29,7 @@ import multipleSelect from './multipleSelect.demo.vue'
import changeEvent from './changeEvent.demo' import changeEvent from './changeEvent.demo'
import changeEventEmitItem from './changeEventEmitItem.demo' import changeEventEmitItem from './changeEventEmitItem.demo'
import search from './search.demo.vue' import search from './search.demo.vue'
import remoteSearch from './remoteSearch.demo.vue'
export default { export default {
components: { components: {
@ -36,7 +38,8 @@ export default {
changeEvent, changeEvent,
changeEventEmitItem, changeEventEmitItem,
search, search,
disabled disabled,
remoteSearch
}, },
data () { data () {
return { return {

View File

@ -0,0 +1,107 @@
<template>
<div class="n-doc-section">
<div class="n-doc-section__header">
Remote Search
</div>
<div
class="n-doc-section__view"
style="flex-wrap: nowrap;"
>
<!--EXAMPLE_START-->
<n-select
v-model="selectedValues"
multiple
filterable
placeholder="Search Songs"
:items="items"
:on-search="handleSearch"
remote
:no-data-content="noDataContent"
:loading="loading"
style="flex-grow: 1; margin-right: 12px; width: 300px;"
/>
<!--EXAMPLE_END-->
</div>
<pre class="n-doc-section__inspect">v-model(multiple): {{ JSON.stringify(selectedValues) }}</pre>
<n-doc-source-block>
<!--SOURCE-->
</n-doc-source-block>
</div>
</template>
<script>
const items = [
{
label: 'Drive My Car',
value: 'song1'
},
{
label: 'Norwegian Wood',
value: 'song2'
},
{
label: 'You Won\'t See',
value: 'song3'
},
{
label: 'Nowhere Man',
value: 'song4'
},
{
label: 'Think For Yourself',
value: 'song5'
},
{
label: 'The Word',
value: 'song6'
},
{
label: 'Michelle',
value: 'song7'
},
{
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'
}
]
export default {
data () {
return {
selectedValues: null,
loading: false,
items: [],
noDataContent: 'please search',
handleSearch: (query) => {
if (!query.length) {
this.items = []
this.noDataContent = 'please search'
return
}
this.loading = true
window.setTimeout(() => {
this.items = items.filter(item => ~item.label.search(query))
if (!this.items.length) this.noDataContent = 'no result found'
this.loading = false
}, 1000)
}
}
}
}
</script>

View File

@ -4,9 +4,9 @@
class="n-select" class="n-select"
:class="{ :class="{
[`n-select--${size}-size`]: true, [`n-select--${size}-size`]: true,
[`n-select--remote`]: remote,
'n-select--disabled': disabled 'n-select--disabled': disabled
}" }"
:style="{'cursor':cursor}"
@click="handleActivatorClick" @click="handleActivatorClick"
@keyup.up.prevent="handleActivatorKeyUpUp" @keyup.up.prevent="handleActivatorKeyUpUp"
@keyup.down.prevent="handleActivatorKeyUpDown" @keyup.down.prevent="handleActivatorKeyUpDown"
@ -63,9 +63,6 @@
</div> </div>
<div <div
class="n-select-link__placeholder" class="n-select-link__placeholder"
:class="{
'n-select-link__placeholder--verbose-transition': verboseTransition
}"
> >
{{ placeholder }} {{ placeholder }}
</div> </div>
@ -103,24 +100,26 @@
:style="{ top: `${lightBarTop}px` }" :style="{ top: `${lightBarTop}px` }"
/> />
</transition> </transition>
<template v-if="!loading">
<div
v-for="(item, index) in filteredItems"
ref="menuItems"
:key="item.value"
:data-index="index"
class="n-select-menu__item"
:class="{
'n-select-menu__item--selected':
isSelected(item)
}"
@click="toggleItem(item)"
@mousemove="showLightBarTop($event, item, index)"
>
{{ item.label }}
</div>
</template>
<div <div
v-for="(item, index) in filteredItems" v-if="loading"
ref="menuItems" class="n-select-menu__item n-select-menu__item--loading"
:key="item.value"
:data-index="index"
class="n-select-menu__item"
:class="{
'n-select-menu__item--selected':
isSelected(item)
}"
@click="toggleItem(item)"
@mousemove="showLightBarTop($event, item, index)"
>
{{ item.label }}
</div>
<div
v-if="pattern.length && !filteredItems.length"
class="n-select-menu__item n-select-menu__item--not-found"
> >
{{ {{
/** /**
@ -129,7 +128,25 @@
*/ */
hideLightBar() hideLightBar()
}} }}
none result matched loading
</div>
<div
v-else-if="items && items.length === 0"
class="n-select-menu__item n-select-menu__item--no-data"
>
{{
hideLightBar()
}}
{{ noDataContent }}
</div>
<div
v-else-if="pattern.length && !filteredItems.length"
class="n-select-menu__item n-select-menu__item--not-found"
>
{{
hideLightBar()
}}
{{ notFoundContent }}
</div> </div>
</div> </div>
</scrollbar> </scrollbar>
@ -203,9 +220,25 @@ export default {
type: Boolean, type: Boolean,
default: false default: false
}, },
cursor: { remote: {
type: String, type: Boolean,
default: 'inherit' default: false
},
onSearch: {
type: Function,
default: null
},
loading: {
type: Boolean,
default: false
},
noDataContent: {
type: [String, Function],
default: 'no data'
},
notFoundContent: {
type: [String, Function],
default: 'none result matched'
} }
}, },
data () { data () {
@ -215,21 +248,19 @@ export default {
scrolling: false, scrolling: false,
pattern: '', pattern: '',
pendingItem: null, pendingItem: null,
pendingItemIndex: null pendingItemIndex: null,
memorizedValueItemMap: new Map()
} }
}, },
computed: { computed: {
filteredItems () { filteredItems () {
if (!this.filterable || !this.pattern.trim().length) return this.items if (this.remote) {
return this.items
} else if (!this.filterable || !this.pattern.trim().length) return this.items
return this.items.filter(item => this.patternMatched(item.label)) return this.items.filter(item => this.patternMatched(item.label))
}, },
selected () { selected () {
if (Array.isArray(this.value)) { return this.selectedItems.length
const itemValues = new Set(this.items.map(item => item.value))
return this.value.filter(value => itemValues.has(value)).length
} else {
return false
}
}, },
valueItemMap () { valueItemMap () {
const valueToItem = new Map() const valueToItem = new Map()
@ -238,7 +269,13 @@ export default {
}, },
selectedItems () { selectedItems () {
if (!Array.isArray(this.value)) return [] if (!Array.isArray(this.value)) return []
return this.value.filter(value => this.valueItemMap.has(value)).map(value => this.valueItemMap.get(value)) if (this.remote) {
return this.value
.filter(value => this.valueItemMap.has(value) || this.memorizedValueItemMap.has(value))
.map(value => this.valueItemMap.has(value) ? this.valueItemMap.get(value) : this.memorizedValueItemMap.get(value))
} else {
return this.value.filter(value => this.valueItemMap.has(value)).map(value => this.valueItemMap.get(value))
}
}, },
clearedPattern () { clearedPattern () {
return this.pattern.toLowerCase().trim() return this.pattern.toLowerCase().trim()
@ -332,10 +369,13 @@ export default {
}, },
toggleItem (item) { toggleItem (item) {
if (this.disabled) return if (this.disabled) return
if (this.remote) {
this.memorizedValueItemMap.set(item.value, item)
}
let newValue = [] let newValue = []
if (Array.isArray(this.value)) { if (Array.isArray(this.value)) {
const itemValues = new Set(this.items.map(item => item.value)) const itemValues = new Set(this.items.map(item => item.value))
newValue = this.value.filter(value => itemValues.has(value)) newValue = this.value.filter(value => itemValues.has(value) || this.memorizedValueItemMap.has(value))
} }
const index = newValue.findIndex(value => value === item.value) const index = newValue.findIndex(value => value === item.value)
if (~index) { if (~index) {
@ -365,6 +405,9 @@ export default {
this.$nextTick().then(() => { this.$nextTick().then(() => {
const textWidth = this.$refs.inputTagMirror.getBoundingClientRect().width const textWidth = this.$refs.inputTagMirror.getBoundingClientRect().width
this.$refs.inputTagInput.style.width = textWidth + 'px' this.$refs.inputTagInput.style.width = textWidth + 'px'
if (this.onSearch) {
this.onSearch(this.pattern)
}
}) })
}, },
handlePatternInputDelete (e) { handlePatternInputDelete (e) {

View File

@ -6,7 +6,6 @@
[`n-select--${size}-size`]: true, [`n-select--${size}-size`]: true,
'n-select--disabled': disabled 'n-select--disabled': disabled
}" }"
:style="{'cursor':cursor}"
@click="toggleMenu" @click="toggleMenu"
> >
<div <div
@ -154,11 +153,18 @@ export default {
type: Boolean, type: Boolean,
default: false default: false
}, },
cursor: { remote: {
type: String, type: Boolean,
default: 'inherit' default: false
},
onSearch: {
type: Function,
default: null
},
loading: {
type: Boolean,
default: false
} }
}, },
data () { data () {
return { return {

View File

@ -16,8 +16,11 @@ export default {
type: Array, type: Array,
required: true required: true
}, },
// eslint-disable-next-line vue/require-prop-types
value: { value: {
validator () {
return true
},
required: false,
default: null default: null
}, },
placeholder: { placeholder: {
@ -48,9 +51,21 @@ export default {
type: Boolean, type: Boolean,
default: false default: false
}, },
cursor: { remote: {
type: String, type: Boolean,
default: 'inherit' default: false
},
onSearch: {
type: Function,
default: null
},
loading: {
type: Boolean,
default: false
},
noDataContent: {
type: [String, Function],
default: 'no data'
} }
}, },
data () { data () {

View File

@ -10,6 +10,13 @@
font-family: $default-font-family; font-family: $default-font-family;
text-align: start; text-align: start;
cursor: pointer; cursor: pointer;
&.n-select--remote {
.n-select-link {
&::after {
display: none;
}
}
}
&.n-select--disabled { &.n-select--disabled {
cursor: not-allowed; cursor: not-allowed;
.n-select-link { .n-select-link {
@ -295,10 +302,18 @@
&.n-select-menu__item--selected { &.n-select-menu__item--selected {
color: #63E2B7FF; color: #63E2B7FF;
} }
&.n-select-menu__item--no-data {
color: rgba(255, 255, 255, .5);
text-align: center;
}
&.n-select-menu__item--not-found { &.n-select-menu__item--not-found {
color: rgba(255, 255, 255, .5); color: rgba(255, 255, 255, .5);
text-align: center; text-align: center;
} }
&.n-select-menu__item--loading {
color: rgba(255, 255, 255, .5);
text-align: center;
}
} }
.n-select-menu__light-bar { .n-select-menu__light-bar {
position: absolute; position: absolute;