Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
describe('Filter Control Multiple Select', () => {
const baseUrl = '/for-tests/extensions/filter-control/'

it('Test multiple select filter control - basic functionality', () => {
cy.visit(`${baseUrl}filter-control-multiple-select.html`)
.get('.table > thead > tr > th > .fht-cell > .filter-control')
.find('select[multiple]')
.should('exist')
.and('have.length.gte', 1)
})

it('Test multiple select - selecting multiple options filters correctly', () => {
cy.visit(`${baseUrl}filter-control-multiple-select.html`)
.wait(1000)
.get('.table > thead > tr > th > .fht-cell > .filter-control')
.find('select[multiple]')
.first()
.select(['Option1', 'Option2'])
.wait(1000)
.get('.table > tbody > tr')
.should('have.length.gte', 1)
.should('have.length.lte', 21) // Should be filtered
})

it('Test multiple select - clearing selection shows all rows', () => {
cy.visit(`${baseUrl}filter-control-multiple-select.html`)
.wait(1000)
.get('.table > thead > tr > th > .fht-cell > .filter-control')
.find('select[multiple]')
.first()
.select(['Option1'])
.wait(1000)
.get('.table > tbody > tr')
.then(($filteredRows) => {
const filteredCount = $filteredRows.length

// Clear selection
cy.get('.table > thead > tr > th > .fht-cell > .filter-control')
.find('select[multiple]')
.first()
.select([])
.wait(1000)
.get('.table > tbody > tr')
.should('have.length.gt', filteredCount) // Should show more rows
})
})

it('Test multiple select with multiple-select.js plugin integration', () => {
cy.visit(`${baseUrl}filter-control-multiple-select-plugin.html`)
.wait(1000)
.get('.ms-parent') // multiple-select plugin creates this wrapper
.should('exist')
.and('have.length.gte', 1)
})

it('Test multiple select default values', () => {
cy.visit(`${baseUrl}filter-control-multiple-select-defaults.html`)
.wait(1000)
.get('.table > thead > tr > th > .fht-cell > .filter-control')
.find('select[multiple] option:selected')
.should('have.length.gte', 1) // Should have pre-selected options
})
})
30 changes: 30 additions & 0 deletions site/docs/extensions/filter-control.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,36 @@ toc: true

- **Default:** `undefined`

### filterControlMultipleSelect

- **Attribute:** `data-filter-control-multiple-select`

- **type:** `Boolean`

- **Detail:**

Set to `true` to enable multiple selection in select filter controls. When enabled, users can select multiple filter options simultaneously. This option only works with `filterControl: 'select'` and requires the [multiple-select.js](https://github.com/wenzhixin/multiple-select) plugin to be loaded for enhanced styling and functionality.

- **Default:** `false`

### filterControlMultipleSelectOptions

- **Attribute:** `data-filter-control-multiple-select-options`

- **type:** `Object`

- **Detail:**

Configuration options passed to the multiple-select.js plugin when `filterControlMultipleSelect` is enabled. Common options include:
- `placeholder`: Text to show when no options are selected
- `selectAll`: Whether to show a "Select All" option
- `selectAllText`: Text for the "Select All" option
- `countSelected`: Format string for showing selected count

Example: `data-filter-control-multiple-select-options='{"placeholder": "Choose categories...", "selectAll": true}'`

- **Default:** `{}`

### filterDatepickerOptions

- **Attribute:** `data-filter-datepicker-options`
Expand Down
95 changes: 73 additions & 22 deletions src/extensions/filter-control/bootstrap-table-filter-control.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,16 @@
},

select (that, column) {
const isMultiple = column.filterControlMultipleSelect
const multipleAttr = isMultiple ? 'multiple' : ''
const multipleClass = isMultiple ? 'multiple-select' : ''

Check failure on line 38 in src/extensions/filter-control/bootstrap-table-filter-control.js

View workflow job for this annotation

GitHub Actions / test

Trailing spaces not allowed
return Utils.sprintf(
'<select class="%s bootstrap-table-filter-control-%s %s" %s style="width: 100%;" dir="%s"></select>',
UtilsFilterControl.getInputClass(that, true),
column.field,
'',
'',
multipleClass,
multipleAttr,
UtilsFilterControl.getDirectionOfSelectOptions(
that.options.alignmentSelectControlOptions
)
Expand Down Expand Up @@ -217,20 +221,31 @@
keys.forEach(key => {
const thisColumn = that.columns[that.fieldsColumnsIndex[key]]
const rawFilterValue = filterPartial[key] || ''
let filterValue = rawFilterValue.toLowerCase()
let filterValue = Array.isArray(rawFilterValue) ? rawFilterValue : rawFilterValue.toLowerCase()
let value = Utils.unescapeHTML(Utils.getItemField(item, key, false))
let tmpItemIsExpected

if (this.options.searchAccentNeutralise) {
if (this.options.searchAccentNeutralise && !Array.isArray(filterValue)) {
filterValue = Utils.normalizeAccent(filterValue)
}

let filterValues = [filterValue]
let filterValues = []

if (
// Handle multiple select values (arrays)
if (Array.isArray(rawFilterValue)) {
filterValues = rawFilterValue.map(val => {
let processedVal = val.toLowerCase()
if (this.options.searchAccentNeutralise) {

Check failure on line 238 in src/extensions/filter-control/bootstrap-table-filter-control.js

View workflow job for this annotation

GitHub Actions / test

Expected blank line before this statement
processedVal = Utils.normalizeAccent(processedVal)
}
return processedVal
})
} else if (
this.options.filterControlMultipleSearch
) {
filterValues = filterValue.split(this.options.filterControlMultipleSearchDelimiter)
} else {
filterValues = [filterValue]
}

filterValues.forEach(filterValue => {
Expand Down Expand Up @@ -304,31 +319,55 @@
value = Utils.normalizeAccent(value)
}

if (
column.filterStrictSearch ||
column.filterControl === 'select' && column.passed.filterStrictSearch !== false
) {
tmpItemIsExpected = value.toString().toLowerCase() === searchValue.toString().toLowerCase()
} else if (column.filterStartsWithSearch) {
tmpItemIsExpected = `${value}`.toLowerCase().indexOf(searchValue) === 0
} else if (column.filterControl === 'datepicker') {
tmpItemIsExpected = new Date(value).getTime() === new Date(searchValue).getTime()
} else if (this.options.regexSearch) {
tmpItemIsExpected = Utils.regexCompare(value, searchValue)
// Handle multiple select values (when searchValue is an array)
if (Array.isArray(searchValue)) {
// For multiple select, use OR logic - match if value equals ANY selected option
tmpItemIsExpected = searchValue.some(singleSearchValue => {
if (
column.filterStrictSearch ||
column.filterControl === 'select' && column.passed.filterStrictSearch !== false
) {
return value.toString().toLowerCase() === singleSearchValue.toString().toLowerCase()
} else if (column.filterStartsWithSearch) {

Check failure on line 331 in src/extensions/filter-control/bootstrap-table-filter-control.js

View workflow job for this annotation

GitHub Actions / test

Unnecessary 'else' after 'return'
return `${value}`.toLowerCase().indexOf(singleSearchValue) === 0
} else if (column.filterControl === 'datepicker') {
return new Date(value).getTime() === new Date(singleSearchValue).getTime()
} else if (this.options.regexSearch) {
return Utils.regexCompare(value, singleSearchValue)
} else {
return `${value}`.toLowerCase().includes(singleSearchValue)
}
})
} else {
tmpItemIsExpected = `${value}`.toLowerCase().includes(searchValue)
// Single value logic (existing behavior)
if (

Check failure on line 343 in src/extensions/filter-control/bootstrap-table-filter-control.js

View workflow job for this annotation

GitHub Actions / test

Unexpected if as the only statement in an else block
column.filterStrictSearch ||
column.filterControl === 'select' && column.passed.filterStrictSearch !== false
) {
tmpItemIsExpected = value.toString().toLowerCase() === searchValue.toString().toLowerCase()
} else if (column.filterStartsWithSearch) {
tmpItemIsExpected = `${value}`.toLowerCase().indexOf(searchValue) === 0
} else if (column.filterControl === 'datepicker') {
tmpItemIsExpected = new Date(value).getTime() === new Date(searchValue).getTime()
} else if (this.options.regexSearch) {
tmpItemIsExpected = Utils.regexCompare(value, searchValue)
} else {
tmpItemIsExpected = `${value}`.toLowerCase().includes(searchValue)
}
}

const largerSmallerEqualsRegex = /(?:(<=|=>|=<|>=|>|<)(?:\s+)?(\d+)?|(\d+)?(\s+)?(<=|=>|=<|>=|>|<))/gm
const matches = largerSmallerEqualsRegex.exec(searchValue)
// Handle numeric comparison operators (only for single values, not arrays)
if (!Array.isArray(searchValue)) {
const largerSmallerEqualsRegex = /(?:(<=|=>|=<|>=|>|<)(?:\s+)?(\d+)?|(\d+)?(\s+)?(<=|=>|=<|>=|>|<))/gm
const matches = largerSmallerEqualsRegex.exec(searchValue)

if (matches) {

Check failure on line 364 in src/extensions/filter-control/bootstrap-table-filter-control.js

View workflow job for this annotation

GitHub Actions / test

Expected indentation of 6 spaces but found 4
const operator = matches[1] || `${matches[5]}l`

Check failure on line 365 in src/extensions/filter-control/bootstrap-table-filter-control.js

View workflow job for this annotation

GitHub Actions / test

Expected indentation of 8 spaces but found 6
const comparisonValue = matches[2] || matches[3]

Check failure on line 366 in src/extensions/filter-control/bootstrap-table-filter-control.js

View workflow job for this annotation

GitHub Actions / test

Expected indentation of 8 spaces but found 6
const int = parseInt(value, 10)

Check failure on line 367 in src/extensions/filter-control/bootstrap-table-filter-control.js

View workflow job for this annotation

GitHub Actions / test

Expected indentation of 8 spaces but found 6
const comparisonInt = parseInt(comparisonValue, 10)

Check failure on line 368 in src/extensions/filter-control/bootstrap-table-filter-control.js

View workflow job for this annotation

GitHub Actions / test

Expected indentation of 8 spaces but found 6

switch (operator) {

Check failure on line 370 in src/extensions/filter-control/bootstrap-table-filter-control.js

View workflow job for this annotation

GitHub Actions / test

Expected indentation of 8 spaces but found 6
case '>':
case '<l':
tmpItemIsExpected = int > comparisonInt
Expand All @@ -353,6 +392,7 @@
break
}
}
}

if (column.filterCustomSearch) {
const customSearchResult = Utils.calculateObjectValue(column, column.filterCustomSearch, [searchValue, value, key, this.options.data], true)
Expand Down Expand Up @@ -504,12 +544,23 @@
controls.forEach(element => {
const $element = $(element)
const elementValue = $element.val()
const text = elementValue ? elementValue.trim() : ''
let text = ''

// Handle multiple select (array) vs single select (string)
if (Array.isArray(elementValue)) {
text = elementValue // Keep as array for multiple select
} else {
text = elementValue ? elementValue.trim() : ''
}

const $field = $element.closest('[data-field]').data('field')

this.trigger('column-search', $field, text)

if (text) {
// Check if we have a valid filter value (string with content or non-empty array)
const hasValue = Array.isArray(text) ? text.length > 0 : (text && text.length > 0)

if (hasValue) {
this.filterColumnsPartial[$field] = text
} else {
delete this.filterColumnsPartial[$field]
Expand Down
49 changes: 49 additions & 0 deletions src/extensions/filter-control/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -567,9 +567,58 @@ export function createControls (that, header) {
header.find('.filter-control, .no-filter-control').hide()
}

// Initialize multiple select controls if needed
initMultipleSelectControls(that, header)

that.trigger('created-controls')
}

export function initMultipleSelectControls (that, header) {
$.each(that.columns, (_, column) => {
if (column.filterControlMultipleSelect && column.filterControl === 'select') {
const selectControl = header.find(`select.bootstrap-table-filter-control-${escapeID(column.field)}`)

if (selectControl.length > 0 && typeof $.fn.multipleSelect === 'function') {
// Set flag that we're using multiple select
that._usingMultipleSelect = true

// Initialize multiple-select plugin with options
const multipleSelectOptions = Object.assign({
placeholder: column.filterControlPlaceholder || 'Choose...',
selectAll: true,
selectAllText: 'Select All',
allSelected: 'All selected',
countSelected: '# of % selected',
noMatchesFound: 'No matches found'
}, column.filterControlMultipleSelectOptions || {})

selectControl.multipleSelect(multipleSelectOptions)

// Handle multiple select change events
selectControl.on('change', function () {
const $this = $(this)
const selectedValues = $this.val() || []

// Store the values for later use
that._valuesFilterControl = that._valuesFilterControl || []
const existingIndex = that._valuesFilterControl.findIndex(item => item.field === column.field)

if (existingIndex >= 0) {
that._valuesFilterControl[existingIndex].value = selectedValues
} else {
that._valuesFilterControl.push({
field: column.field,
value: selectedValues,
position: 0,
hasFocus: false
})
}
})
}
}
})
}

export function getDirectionOfSelectOptions (_alignment) {
const alignment = _alignment === undefined ? 'left' : _alignment.toLowerCase()

Expand Down