Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions documentation/src/components/Props.vue
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,18 @@
<td class="table__td">false</td>
<td class="table__td">Disabled the search input focusing when the multiselect opens</td>
</tr>
<tr class="table__tr">
<td class="table__td"><strong>filteringSortFunc</strong></td>
<td class="table__td">Function => Int</td>
<td class="table__td"></td>
<td class="table__td">Allows a custom sorting function when the user searching.
This function will be the <i>compareFn</i> argument for
<i><a target="_blank"
href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort">
Array.sort()
</a></i>
function, so will require two arguments. <br/><b>Added in v3.2.0</b></td>
</tr>
<tr class="table__tr">
<td class="table__td utils--center" colspan="4"><strong>Multiselect.vue</strong></td>
</tr>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
object properties look at the <a href="#sub-asynchronous-select">ajax search example</a>.</p>
<p><code>custom-label</code> accepts a function with the <code>option</code> object as the first param. It should
return a string which is then used to display a custom label.</p>
<p>When sorting a filtered list, <code>filteringSortFunc</code> accepts a function for use in <code>Array.sort()</code>.
By default, it orders by the ascending length of each option.</p>
<CodeDemoAndExample demo-name="SingleSelectSearch"/>
</div>
</template>
Expand Down
5 changes: 5 additions & 0 deletions src/Multiselect.vue
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,11 @@ export default {
type: Number,
default: 0
},
/**
* Adds Required attribute to the input element when there is no value selected
* @default false
* @type {Boolean}
*/
required: {
type: Boolean,
default: false
Expand Down
87 changes: 57 additions & 30 deletions src/multiselectMixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,6 @@ function includes (str, query) {
return text.indexOf(query.trim()) !== -1
}

function filterOptions (options, search, label, customLabel) {
return search
? options
.filter((option) => includes(customLabel(option, label), search))
.sort((a, b) => customLabel(a, label).length - customLabel(b, label).length)
: options
}

function stripGroups (options) {
return options.filter((option) => !option.$isLabel)
}
Expand All @@ -44,25 +36,6 @@ function flattenOptions (values, label) {
}, [])
}

function filterGroups (search, label, values, groupLabel, customLabel) {
return (groups) =>
groups.map((group) => {
/* istanbul ignore else */
if (!group[values]) {
console.warn('Options passed to vue-multiselect do not contain groups, despite the config.')
return []
}
const groupOptions = filterOptions(group[values], search, label, customLabel)

return groupOptions.length
? {
[groupLabel]: group[groupLabel],
[values]: groupOptions
}
: []
})
}

const flow = (...fns) => (x) => fns.reduce((v, f) => f(v), x)

export default {
Expand Down Expand Up @@ -314,10 +287,19 @@ export default {
* Prevent autofocus
* @default false
* @type {Boolean}
*/
*/
preventAutofocus: {
type: Boolean,
default: false
},
/**
* Allows a custom function for sorting search/filtered results.
* @default null
* @type {Function}
*/
filteringSortFunc: {
type: Function,
default: null
}
},
mounted () {
Expand Down Expand Up @@ -349,7 +331,7 @@ export default {
if (this.internalSearch) {
options = this.groupValues
? this.filterAndFlat(options, normalizedSearch, this.label)
: filterOptions(options, normalizedSearch, this.label, this.customLabel)
: this.filterOptions(options, normalizedSearch, this.label, this.customLabel)
} else {
options = this.groupValues ? flattenOptions(this.groupValues, this.groupLabel)(options) : options
}
Expand Down Expand Up @@ -423,7 +405,7 @@ export default {
*/
filterAndFlat (options, search, label) {
return flow(
filterGroups(search, label, this.groupValues, this.groupLabel, this.customLabel),
this.filterGroups(search, label, this.groupValues, this.groupLabel, this.customLabel),
flattenOptions(this.groupValues, this.groupLabel)
)(options)
},
Expand Down Expand Up @@ -723,6 +705,51 @@ export default {
this.preferredOpenDirection = 'above'
this.optimizedHeight = Math.min(spaceAbove - 40, this.maxHeight)
}
},
/**
* Filters and sorts the options ready for selection
* @param {Array} options
* @param {String} search
* @param {String} label
* @param {Function} customLabel
* @returns {Array}
*/
filterOptions (options, search, label, customLabel) {
return search
? options
.filter((option) => includes(customLabel(option, label), search))
.sort((a, b) => {
if (typeof this.filteringSortFunc === 'function') {
return this.filteringSortFunc(a, b)
}
return customLabel(a, label).length - customLabel(b, label).length
})
: options
},
/**
*
* @param {String} search
* @param {String} label
* @param {String} values
* @param {String} groupLabel
* @param {function} customLabel
* @returns {function(*): *}
*/
filterGroups (search, label, values, groupLabel, customLabel) {
return (groups) => groups.map((group) => {
/* istanbul ignore else */
if (!group[values]) {
console.warn('Options passed to vue-multiselect do not contain groups, despite the config.')
return []
}
const groupOptions = this.filterOptions(group[values], search, label, customLabel)

return groupOptions.length
? {
[groupLabel]: group[groupLabel], [values]: groupOptions
}
: []
})
}
}
}
55 changes: 55 additions & 0 deletions tests/unit/Multiselect.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -1824,4 +1824,59 @@ describe('Multiselect.vue', () => {
expect(wrapper.get('.multiselect__input').attributes('required')).toBeUndefined()
})
})
describe('filteringSortFunc prop', () => {
const options = [
{ name: 'Vue.js', language: 'JavaScript' },
{ name: 'Laravel', language: 'PHP' },
{ name: 'Rails', language: 'Ruby' },
{ name: 'Sinatra', language: 'Ruby' },
{ name: 'Phoenix', language: 'Elixir' }
]

const customLabel = ({ name, language }) => {
return `${name} — [${language}]`
}

test('should use default sorting when no function is provided', async () => {
const wrapper = shallowMount(Multiselect, {
props: {
modelValue: [],
options,
customLabel,
label: 'name',
trackBy: 'name'
},
data () {
return {
search: 'a'
}
}
})

expect(wrapper.vm.filteredOptions[0].name).toBe('Rails')
expect(wrapper.vm.filteredOptions[3].name).toBe('Vue.js')
})
test('should use custom sorting when function is provided', async () => {
const wrapper = shallowMount(Multiselect, {
props: {
modelValue: [],
options,
customLabel,
label: 'name',
trackBy: 'name',
filteringSortFunc: (a, b) => {
return a.name.length - b.name.length
}
},
data () {
return {
search: 'a'
}
}
})
expect(wrapper.vm.filteredOptions[0].name).toBe('Rails')
expect(wrapper.vm.filteredOptions[1].name).toBe('Vue.js')
expect(wrapper.vm.filteredOptions[2].name).toBe('Laravel')
})
})
})