Browse Source

Merge pull request #5583 from wenzhixin/feature/multiselect-filtercontrol

Improving filter control
文翼 4 years ago
parent
commit
55a1a6fd80

+ 44 - 0
cypress/extensions/filter-control/options.js

@@ -0,0 +1,44 @@
+module.exports = (theme = '') => {
+    const baseUrl = require('../../common/utils')(theme, 'for-tests/extensions/filter-control')
+  
+    describe('Test basic filter control', () => {
+      it('Test basic filter control', () => {
+        cy.visit(`${baseUrl}filter-control.html`)
+          .get('.table > thead > tr > th > .fht-cell > .filter-control')
+          .its('length')
+          .should('be.gte', 1)
+      })
+
+      it('Test if filter control visible is set to false, controls shouldnt be visible.', () => {
+        cy.visit(`${baseUrl}filter-control-filterControlVisible.html`)
+          .get('.table > thead > tr > th > .fht-cell > .filter-control')
+          .invoke('attr', 'style')
+          .should('eq', 'display: none;')
+      })
+
+      it('Test if filter control searchOnEnteyKey is set to true. Type "cypress" and validate table should not perform any action.', () => {
+        cy.visit(`${baseUrl}filter-control-searchOnEnterKey.html`)
+          .wait(1000)
+          .get('.table > thead > tr > th > .fht-cell > .filter-control')
+          .find('input')
+          .type('cypress')
+          .get('.table > tbody > tr')
+          .its('length')
+          .should('eq', 21)
+      })
+
+      it('Test if filter control searchOnEnteyKey is set to true. Type "Item 0", hit enter and validate table should perform search action.', () => {
+        cy.visit(`${baseUrl}filter-control-searchOnEnterKey.html`)
+          .wait(1000)
+          .get('.table > thead > tr > th > .fht-cell > .filter-control')
+          .find('input')
+          .type('Item 0')
+          .type('{enter}')
+          .wait(1000)
+          .get('.table > tbody > tr')
+          .its('length')
+          .should('eq', 1)
+      })
+    })
+  }
+  

+ 1 - 0
cypress/integration/extensions/filter-control/options/bootstrap3.js

@@ -0,0 +1 @@
+require('../../../../extensions/filter-control/options')('bootstrap3')

+ 1 - 0
cypress/integration/extensions/filter-control/options/bootstrap5.js

@@ -0,0 +1 @@
+require('../../../../extensions/filter-control/options')('bootstrap5')

+ 1 - 0
cypress/integration/extensions/filter-control/options/bulma.js

@@ -0,0 +1 @@
+require('../../../../extensions/filter-control/options')('bulma')

+ 1 - 0
cypress/integration/extensions/filter-control/options/foundation.js

@@ -0,0 +1 @@
+require('../../../../extensions/filter-control/options')('foundation')

+ 1 - 0
cypress/integration/extensions/filter-control/options/index.js

@@ -0,0 +1 @@
+require('../../../../extensions/filter-control/options')()

+ 1 - 0
cypress/integration/extensions/filter-control/options/materialize.js

@@ -0,0 +1 @@
+require('../../../../extensions/filter-control/options')('materialize')

+ 1 - 0
cypress/integration/extensions/filter-control/options/semantic.js

@@ -0,0 +1 @@
+require('../../../../extensions/filter-control/options')('semantic')

+ 11 - 27
site/docs/extensions/filter-control.md

@@ -6,8 +6,6 @@ group: extensions
 toc: true
 ---
 
-Dependence if you use the datepicker option: [bootstrap-datepicker](https://github.com/eternicode/bootstrap-datepicker) v1.4.0
-
 ## Usage
 
 {% highlight html %}
@@ -57,18 +55,6 @@ Dependence if you use the datepicker option: [bootstrap-datepicker](https://gith
 
 - **Default:** `undefined`
 
-### disableControlWhenSearch
-
-- **Attribute:** `data-disable-control-when-search`
-
-- **type:** `Boolean`
-
-- **Detail:**
-
-   Set to true if you want to disable the control while the server is responding the data. This options will work if the sidePagination is 'server'.
-
-- **Default:** `false`
-
 ### filterControlContainer
 
 - **Attribute:** `data-filter-control-container`
@@ -94,39 +80,39 @@ Dependence if you use the datepicker option: [bootstrap-datepicker](https://gith
 
 - **Default:** `undefined`
 
-### hideUnusedSelectOptions
+### searchOnEnterKey
 
-- **Attribute:** `data-hide-unused-select-options`
+- **Attribute:** `data-search-on-enter-key`
 
 - **type:** `Boolean`
 
 - **Detail:**
 
-   Set to true in order to hide the options that are not in the table. This option does not work on server-side pagination.
+   Set to true to fire the search action when the user press the enter key.
 
 - **Default:** `false`
 
-### searchOnEnterKey
+### showFilterControlSwitch
 
-- **Attribute:** `data-search-on-enter-key`
+- **Attribute:** `data-show-filter-control-switch`
 
 - **type:** `Boolean`
 
 - **Detail:**
 
-   Set to true to fire the search action when the user press the enter key.
+   Set to `true` to show the filter control switch button.
 
 - **Default:** `false`
 
-### showFilterControlSwitch
+### sortSelectOptions
 
-- **Attribute:** `data-show-filter-control-switch`
+- **Attribute:** `data-sort-select-options`
 
 - **type:** `Boolean`
 
 - **Detail:**
 
-   Set to `true` to show the filter control switch button.
+   Set to `true` to sort the option elements of the select control.
 
 - **Default:** `false`
 
@@ -140,7 +126,7 @@ Dependence if you use the datepicker option: [bootstrap-datepicker](https://gith
 
 - **Detail:**
 
-   Set `input`: show an input control, `select`: show a select control, `datepicker`: show a datepicker control.
+   Set `input`: show an input control, `select`: show a select control, `datepicker`: show a html5 datepicker control.
 
 - **Default:** `undefined`
 
@@ -202,7 +188,7 @@ Dependence if you use the datepicker option: [bootstrap-datepicker](https://gith
 
 - **Detail:**
 
-   If the datepicker option is set use this option to configure the datepicker with the native options. Use this way: `data-filter-datepicker-options='{"autoclose":true, "clearBtn": true, "todayHighlight": true}'`.
+   If the datepicker option is set use this option to configure the datepicker with the native options. Use this way: `data-filter-datepicker-options='{"max":value1, "min": value2, "step": value3}'`. For more information visit this [documentation](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date)
 
 - **Default:** `undefined`
 
@@ -215,8 +201,6 @@ Dependence if you use the datepicker option: [bootstrap-datepicker](https://gith
 - **Detail:**
 
    Set the default value of the filter.
-   
-   If you use the datepicker, make sure your date format match the [format](https://bootstrap-datepicker.readthedocs.io/en/stable/options.html?highlight=format#format) of the datepicker. You can change the datepicker format using [filterDatepickerOptions](https://bootstrap-table.com/docs/extensions/filter-control/#filterdatepickeroptions).
 
 - **Default:** `undefined`
 

+ 6 - 3
src/bootstrap-table.js

@@ -940,7 +940,7 @@ class BootstrapTable {
       }
     }
 
-    if (!firedByInitSearchText) {
+    if (!firedByInitSearchText && !this.options.cookie) {
       this.options.pageNumber = 1
     }
     this.initSearch()
@@ -3126,8 +3126,11 @@ class BootstrapTable {
   resetSearch (text) {
     const $search = Utils.getSearchInput(this)
 
-    $search.val(text || '')
-    this.onSearch({ currentTarget: $search })
+    const textToUse = text || ''
+
+    $search.val(textToUse)
+    this.searchText = textToUse
+    this.onSearch({ currentTarget: $search }, false)
   }
 
   filterBy (columns, options) {

+ 208 - 188
src/extensions/cookie/bootstrap-table-cookie.js

@@ -19,152 +19,39 @@ const UtilsCookie = {
     filterBy: 'bs.table.filterBy'
   },
   getCurrentHeader (that) {
-    let header = that.$header
-
-    if (that.options.height) {
-      header = that.$tableHeader
-    }
-
-    return header
+    return that.options.height ? that.$tableHeader : that.$header
   },
   getCurrentSearchControls (that) {
-    let searchControls = 'select, input'
-
-    if (that.options.height) {
-      searchControls = 'table select, table input'
-    }
-
-    return searchControls
+    return that.options.height ? 'table select, table input' : 'select, input'
   },
-  cookieEnabled () {
-    return !!(navigator.cookieEnabled)
+  isCookieSupportedByBrowser () {
+    return navigator.cookieEnabled
   },
-  inArrayCookiesEnabled (cookieName, cookiesEnabled) {
-    let index = -1
-
-    for (let i = 0; i < cookiesEnabled.length; i++) {
-      if (cookieName.toLowerCase() === cookiesEnabled[i].toLowerCase()) {
-        index = i
-        break
-      }
-    }
-
-    return index
+  isCookieEnabled (that, cookieName) {
+    return that.options.cookiesEnabled.includes(cookieName)
   },
   setCookie (that, cookieName, cookieValue) {
-    if ((!that.options.cookie) || (!UtilsCookie.cookieEnabled()) || (that.options.cookieIdTable === '')) {
-      return
-    }
-
-    if (UtilsCookie.inArrayCookiesEnabled(cookieName, that.options.cookiesEnabled) === -1) {
+    if (
+      !that.options.cookie ||
+      !UtilsCookie.isCookieEnabled(that, cookieName)
+    ) {
       return
     }
 
-    cookieName = `${that.options.cookieIdTable}.${cookieName}`
-
-    switch (that.options.cookieStorage) {
-      case 'cookieStorage':
-        document.cookie = [
-          cookieName, '=', encodeURIComponent(cookieValue),
-          `; expires=${UtilsCookie.calculateExpiration(that.options.cookieExpire)}`,
-          that.options.cookiePath ? `; path=${that.options.cookiePath}` : '',
-          that.options.cookieDomain ? `; domain=${that.options.cookieDomain}` : '',
-          that.options.cookieSecure ? '; secure' : '',
-          `;SameSite=${ that.options.cookieSameSite}`
-        ].join('')
-        break
-      case 'localStorage':
-        localStorage.setItem(cookieName, cookieValue)
-        break
-      case 'sessionStorage':
-        sessionStorage.setItem(cookieName, cookieValue)
-        break
-      case 'customStorage':
-        if (
-          !that.options.cookieCustomStorageSet ||
-          !that.options.cookieCustomStorageGet ||
-          !that.options.cookieCustomStorageDelete
-        ) {
-          throw new Error('The following options must be set while using the customStorage: cookieCustomStorageSet, cookieCustomStorageGet and cookieCustomStorageDelete')
-        }
-
-        Utils.calculateObjectValue(that.options, that.options.cookieCustomStorageSet, [cookieName, cookieValue], '')
-        break
-      default:
-        return false
-    }
-
-    return true
+    return that._storage.setItem(`${that.options.cookieIdTable}.${cookieName}`, cookieValue)
   },
-  getCookie (that, tableName, cookieName) {
-    if (!cookieName) {
-      return null
-    }
-
-    if (UtilsCookie.inArrayCookiesEnabled(cookieName, that.options.cookiesEnabled) === -1) {
+  getCookie (that, cookieName) {
+    if (
+      !cookieName ||
+      !UtilsCookie.isCookieEnabled(that, cookieName)
+    ) {
       return null
     }
 
-    cookieName = `${tableName}.${cookieName}`
-
-    switch (that.options.cookieStorage) {
-      case 'cookieStorage':
-        const value = `; ${document.cookie}`
-        const parts = value.split(`; ${cookieName}=`)
-
-        return parts.length === 2 ? decodeURIComponent(parts.pop().split(';').shift()) : null
-      case 'localStorage':
-        return localStorage.getItem(cookieName)
-      case 'sessionStorage':
-        return sessionStorage.getItem(cookieName)
-      case 'customStorage':
-        if (
-          !that.options.cookieCustomStorageSet ||
-          !that.options.cookieCustomStorageGet ||
-          !that.options.cookieCustomStorageDelete
-        ) {
-          throw new Error('The following options must be set while using the customStorage: cookieCustomStorageSet, cookieCustomStorageGet and cookieCustomStorageDelete')
-        }
-
-        return Utils.calculateObjectValue(that.options, that.options.cookieCustomStorageGet, [cookieName], '')
-      default:
-        return null
-    }
+    return that._storage.getItem(`${that.options.cookieIdTable}.${cookieName}`)
   },
-  deleteCookie (that, tableName, cookieName) {
-    cookieName = `${tableName}.${cookieName}`
-
-    switch (that.options.cookieStorage) {
-      case 'cookieStorage':
-        document.cookie = [
-          encodeURIComponent(cookieName), '=',
-          '; expires=Thu, 01 Jan 1970 00:00:00 GMT',
-          that.options.cookiePath ? `; path=${that.options.cookiePath}` : '',
-          that.options.cookieDomain ? `; domain=${that.options.cookieDomain}` : '',
-          `;SameSite=${ that.options.cookieSameSite}`
-        ].join('')
-        break
-      case 'localStorage':
-        localStorage.removeItem(cookieName)
-        break
-      case 'sessionStorage':
-        sessionStorage.removeItem(cookieName)
-        break
-      case 'customStorage':
-        if (
-          !that.options.cookieCustomStorageSet ||
-          !that.options.cookieCustomStorageGet ||
-          !that.options.cookieCustomStorageDelete
-        ) {
-          throw new Error('The following options must be set while using the customStorage: cookieCustomStorageSet, cookieCustomStorageGet and cookieCustomStorageDelete')
-        }
-
-        Utils.calculateObjectValue(that.options, that.options.cookieCustomStorageDelete, [cookieName], '')
-        break
-      default:
-        return false
-    }
-    return true
+  deleteCookie (that, cookieName) {
+    return that._storage.removeItem(`${that.options.cookieIdTable}.${cookieName}`)
   },
   calculateExpiration (cookieExpire) {
     const time = cookieExpire.replace(/[0-9]*/, '') // s,mi,h,d,m,y
@@ -204,45 +91,57 @@ const UtilsCookie = {
   },
   initCookieFilters (bootstrapTable) {
     setTimeout(() => {
-      const parsedCookieFilters = JSON.parse(UtilsCookie.getCookie(bootstrapTable, bootstrapTable.options.cookieIdTable, UtilsCookie.cookieIds.filterControl))
-
-      if (!bootstrapTable.options.filterControlValuesLoaded && parsedCookieFilters) {
+      const parsedCookieFilters = JSON.parse(
+        UtilsCookie.getCookie(bootstrapTable, UtilsCookie.cookieIds.filterControl))
 
+      if (!bootstrapTable._filterControlValuesLoaded && parsedCookieFilters) {
         const cachedFilters = {}
         const header = UtilsCookie.getCurrentHeader(bootstrapTable)
         const searchControls = UtilsCookie.getCurrentSearchControls(bootstrapTable)
 
         const applyCookieFilters = (element, filteredCookies) => {
           filteredCookies.forEach(cookie => {
-            if (cookie.text === '' || (element.type === 'radio' && element.value.toString() !== cookie.text.toString())) {
+            const value = element.value.toString()
+            const text = cookie.text
+
+            if (
+              text === '' ||
+              element.type === 'radio' &&
+              value !== text
+            ) {
               return
             }
 
-            if (element.tagName === 'INPUT' && element.type === 'radio' && element.value.toString() === cookie.text.toString()) {
+            if (
+              element.tagName === 'INPUT' &&
+              element.type === 'radio' &&
+              value === text
+            ) {
               element.checked = true
-              cachedFilters[cookie.field] = cookie.text
+              cachedFilters[cookie.field] = text
             } else if (element.tagName === 'INPUT') {
-              element.value = cookie.text
-              cachedFilters[cookie.field] = cookie.text
-            } else if (element.tagName === 'SELECT' && bootstrapTable.options.filterControlContainer) {
-              element.value = cookie.text
-              cachedFilters[cookie.field] = cookie.text
-            } else if (cookie.text !== '' && element.tagName === 'SELECT') {
-              for (let i = 0; i < element.length; i++) {
-                const currentElement = element[i]
-
-                if (currentElement.value === cookie.text) {
+              element.value = text
+              cachedFilters[cookie.field] = text
+            } else if (
+              element.tagName === 'SELECT' &&
+              bootstrapTable.options.filterControlContainer
+            ) {
+              element.value = text
+              cachedFilters[cookie.field] = text
+            } else if (text !== '' && element.tagName === 'SELECT') {
+              cachedFilters[cookie.field] = text
+              for (const currentElement of element) {
+                if (currentElement.value === text) {
                   currentElement.selected = true
                   return
                 }
               }
               const option = document.createElement('option')
 
-              option.value = cookie.text
-              option.text = cookie.text
+              option.value = text
+              option.text = text
               element.add(option, element[1])
               element.selectedIndex = 1
-              cachedFilters[cookie.field] = cookie.text
             }
           })
         }
@@ -261,7 +160,7 @@ const UtilsCookie = {
         })
 
         bootstrapTable.initColumnSearch(cachedFilters)
-        bootstrapTable.options.filterControlValuesLoaded = true
+        bootstrapTable._filterControlValuesLoaded = true
         bootstrapTable.initServer()
       }
     }, 250)
@@ -288,8 +187,13 @@ $.extend($.fn.bootstrapTable.defaults, {
   cookieCustomStorageSet: null,
   cookieCustomStorageDelete: null,
   // internal variable
-  filterControls: [],
-  filterControlValuesLoaded: false
+  _filterControls: [],
+  _filterControlValuesLoaded: false,
+  _storage: {
+    setItem: undefined,
+    getItem: undefined,
+    removeItem: undefined
+  }
 })
 
 $.fn.bootstrapTable.methods.push('getCookies')
@@ -303,8 +207,14 @@ $.extend($.fn.bootstrapTable.utils, {
 $.BootstrapTable = class extends $.BootstrapTable {
   init () {
     if (this.options.cookie) {
+      if (this.options.cookieStorage === 'cookieStorage' && !UtilsCookie.isCookieSupportedByBrowser()) {
+        throw new Error('Cookies are not enabled in this browser.')
+      }
+
+      this.configureStorage()
+
       // FilterBy logic
-      const filterByCookieValue = UtilsCookie.getCookie(this, this.options.cookieIdTable, UtilsCookie.cookieIds.filterBy)
+      const filterByCookieValue = UtilsCookie.getCookie(this, UtilsCookie.cookieIds.filterBy)
 
       if (typeof filterByCookieValue === 'boolean' && !filterByCookieValue) {
         throw new Error('The cookie value of filterBy must be a json!')
@@ -320,8 +230,8 @@ $.BootstrapTable = class extends $.BootstrapTable {
       this.filterColumns = filterByCookie ? filterByCookie : {}
 
       // FilterControl logic
-      this.options.filterControls = []
-      this.options.filterControlValuesLoaded = false
+      this._filterControls = []
+      this._filterControlValuesLoaded = false
 
       this.options.cookiesEnabled = typeof this.options.cookiesEnabled === 'string' ?
         this.options.cookiesEnabled.replace('[', '').replace(']', '')
@@ -334,21 +244,21 @@ $.BootstrapTable = class extends $.BootstrapTable {
         this.$el.on('column-search.bs.table', (e, field, text) => {
           let isNewField = true
 
-          for (let i = 0; i < that.options.filterControls.length; i++) {
-            if (that.options.filterControls[i].field === field) {
-              that.options.filterControls[i].text = text
+          for (let i = 0; i < that._filterControls.length; i++) {
+            if (that._filterControls[i].field === field) {
+              that._filterControls[i].text = text
               isNewField = false
               break
             }
           }
           if (isNewField) {
-            that.options.filterControls.push({
+            that._filterControls.push({
               field,
               text
             })
           }
 
-          UtilsCookie.setCookie(that, UtilsCookie.cookieIds.filterControl, JSON.stringify(that.options.filterControls))
+          UtilsCookie.setCookie(that, UtilsCookie.cookieIds.filterControl, JSON.stringify(that._filterControls))
         }).on('created-controls.bs.table', UtilsCookie.initCookieFilters(that))
       }
     }
@@ -359,9 +269,9 @@ $.BootstrapTable = class extends $.BootstrapTable {
     if (
       this.options.cookie &&
       this.options.filterControl &&
-      !this.options.filterControlValuesLoaded
+      !this._filterControlValuesLoaded
     ) {
-      const cookie = JSON.parse(UtilsCookie.getCookie(this, this.options.cookieIdTable, UtilsCookie.cookieIds.filterControl))
+      const cookie = JSON.parse(UtilsCookie.getCookie(this, UtilsCookie.cookieIds.filterControl))
 
       if (cookie) {
         return
@@ -378,12 +288,16 @@ $.BootstrapTable = class extends $.BootstrapTable {
   onSort (...args) {
     super.onSort(...args)
 
+    if (!this.options.cookie) {
+      return
+    }
+
     if (this.options.sortName === undefined || this.options.sortOrder === undefined) {
-      UtilsCookie.deleteCookie(this, this.options.cookieIdTable, UtilsCookie.cookieIds.sortName)
-      UtilsCookie.deleteCookie(this, this.options.cookieIdTable, UtilsCookie.cookieIds.sortOrder)
+      UtilsCookie.deleteCookie(this, UtilsCookie.cookieIds.sortName)
+      UtilsCookie.deleteCookie(this, UtilsCookie.cookieIds.sortOrder)
     } else {
       this.options.sortPriority = null
-      UtilsCookie.deleteCookie(this, this.options.cookieIdTable, UtilsCookie.cookieIds.sortPriority)
+      UtilsCookie.deleteCookie(this, UtilsCookie.cookieIds.sortPriority)
 
       UtilsCookie.setCookie(this, UtilsCookie.cookieIds.sortOrder, this.options.sortOrder)
       UtilsCookie.setCookie(this, UtilsCookie.cookieIds.sortName, this.options.sortName)
@@ -394,12 +308,12 @@ $.BootstrapTable = class extends $.BootstrapTable {
     super.onMultipleSort(...args)
 
     if (this.options.sortPriority === undefined) {
-      UtilsCookie.deleteCookie(this, this.options.cookieIdTable, UtilsCookie.cookieIds.sortPriority)
+      UtilsCookie.deleteCookie(this, UtilsCookie.cookieIds.sortPriority)
     } else {
       this.options.sortName = undefined
       this.options.sortOrder = undefined
-      UtilsCookie.deleteCookie(this, this.options.cookieIdTable, UtilsCookie.cookieIds.sortName)
-      UtilsCookie.deleteCookie(this, this.options.cookieIdTable, UtilsCookie.cookieIds.sortOrder)
+      UtilsCookie.deleteCookie(this, UtilsCookie.cookieIds.sortName)
+      UtilsCookie.deleteCookie(this, UtilsCookie.cookieIds.sortOrder)
 
       UtilsCookie.setCookie(this, UtilsCookie.cookieIds.sortPriority, JSON.stringify(this.options.sortPriority))
     }
@@ -407,33 +321,50 @@ $.BootstrapTable = class extends $.BootstrapTable {
 
   onPageNumber (...args) {
     super.onPageNumber(...args)
+    if (!this.options.cookie) {
+      return
+    }
     UtilsCookie.setCookie(this, UtilsCookie.cookieIds.pageNumber, this.options.pageNumber)
   }
 
   onPageListChange (...args) {
     super.onPageListChange(...args)
+    if (!this.options.cookie) {
+      return
+    }
     UtilsCookie.setCookie(this, UtilsCookie.cookieIds.pageList, this.options.pageSize)
     UtilsCookie.setCookie(this, UtilsCookie.cookieIds.pageNumber, this.options.pageNumber)
   }
 
   onPagePre (...args) {
     super.onPagePre(...args)
+    if (!this.options.cookie) {
+      return
+    }
     UtilsCookie.setCookie(this, UtilsCookie.cookieIds.pageNumber, this.options.pageNumber)
   }
 
   onPageNext (...args) {
     super.onPageNext(...args)
+    if (!this.options.cookie) {
+      return
+    }
     UtilsCookie.setCookie(this, UtilsCookie.cookieIds.pageNumber, this.options.pageNumber)
   }
 
   _toggleColumn (...args) {
     super._toggleColumn(...args)
+    if (!this.options.cookie) {
+      return
+    }
     UtilsCookie.setCookie(this, UtilsCookie.cookieIds.columns, JSON.stringify(this.getVisibleColumns().map(column => column.field)))
   }
 
   _toggleAllColumns (...args) {
     super._toggleAllColumns(...args)
-
+    if (!this.options.cookie) {
+      return
+    }
     UtilsCookie.setCookie(this, UtilsCookie.cookieIds.columns, JSON.stringify(this.getVisibleColumns().map(column => column.field)))
   }
 
@@ -444,12 +375,17 @@ $.BootstrapTable = class extends $.BootstrapTable {
 
   selectPage (page) {
     super.selectPage(page)
+    if (!this.options.cookie) {
+      return
+    }
     UtilsCookie.setCookie(this, UtilsCookie.cookieIds.pageNumber, page)
   }
 
   onSearch (event) {
-    super.onSearch(event)
-
+    super.onSearch(event, arguments.length > 1 ? arguments[1] : true)
+    if (!this.options.cookie) {
+      return
+    }
     if (this.options.search) {
       UtilsCookie.setCookie(this, UtilsCookie.cookieIds.searchText, this.searchText)
     }
@@ -457,8 +393,8 @@ $.BootstrapTable = class extends $.BootstrapTable {
   }
 
   initHeader (...args) {
-    if (this.options.reorderableColumns) {
-      this.columnsSortOrder = JSON.parse(UtilsCookie.getCookie(this, this.options.cookieIdTable, UtilsCookie.cookieIds.reorderColumns))
+    if (this.options.reorderableColumns && this.options.cookie) {
+      this.columnsSortOrder = JSON.parse(UtilsCookie.getCookie(this, UtilsCookie.cookieIds.reorderColumns))
     }
     super.initHeader(...args)
   }
@@ -469,6 +405,9 @@ $.BootstrapTable = class extends $.BootstrapTable {
 
   filterBy (...args) {
     super.filterBy(...args)
+    if (!this.options.cookie) {
+      return
+    }
     UtilsCookie.setCookie(this, UtilsCookie.cookieIds.filterBy, JSON.stringify(this.filterColumns))
   }
 
@@ -477,21 +416,21 @@ $.BootstrapTable = class extends $.BootstrapTable {
       return
     }
 
-    if ((this.options.cookieIdTable === '') || (this.options.cookieExpire === '') || (!UtilsCookie.cookieEnabled())) {
+    if ((this.options.cookieIdTable === '') || (this.options.cookieExpire === '')) {
       console.error('Configuration error. Please review the cookieIdTable and the cookieExpire property. If the properties are correct, then this browser does not support cookies.')
       this.options.cookie = false // Make sure that the cookie extension is disabled
       return
     }
 
-    const sortOrderCookie = UtilsCookie.getCookie(this, this.options.cookieIdTable, UtilsCookie.cookieIds.sortOrder)
-    const sortOrderNameCookie = UtilsCookie.getCookie(this, this.options.cookieIdTable, UtilsCookie.cookieIds.sortName)
-    let sortPriorityCookie = UtilsCookie.getCookie(this, this.options.cookieIdTable, UtilsCookie.cookieIds.sortPriority)
-    const pageNumberCookie = UtilsCookie.getCookie(this, this.options.cookieIdTable, UtilsCookie.cookieIds.pageNumber)
-    const pageListCookie = UtilsCookie.getCookie(this, this.options.cookieIdTable, UtilsCookie.cookieIds.pageList)
-    const searchTextCookie = UtilsCookie.getCookie(this, this.options.cookieIdTable, UtilsCookie.cookieIds.searchText)
-    const cardViewCookie = UtilsCookie.getCookie(this, this.options.cookieIdTable, UtilsCookie.cookieIds.cardView)
+    const sortOrderCookie = UtilsCookie.getCookie(this, UtilsCookie.cookieIds.sortOrder)
+    const sortOrderNameCookie = UtilsCookie.getCookie(this, UtilsCookie.cookieIds.sortName)
+    let sortPriorityCookie = UtilsCookie.getCookie(this, UtilsCookie.cookieIds.sortPriority)
+    const pageNumberCookie = UtilsCookie.getCookie(this, UtilsCookie.cookieIds.pageNumber)
+    const pageListCookie = UtilsCookie.getCookie(this, UtilsCookie.cookieIds.pageList)
+    const searchTextCookie = UtilsCookie.getCookie(this, UtilsCookie.cookieIds.searchText)
+    const cardViewCookie = UtilsCookie.getCookie(this, UtilsCookie.cookieIds.cardView)
 
-    const columnsCookieValue = UtilsCookie.getCookie(this, this.options.cookieIdTable, UtilsCookie.cookieIds.columns)
+    const columnsCookieValue = UtilsCookie.getCookie(this, UtilsCookie.cookieIds.columns)
 
     if (typeof columnsCookieValue === 'boolean' && !columnsCookieValue) {
       throw new Error('The cookie value of filterBy must be a json!')
@@ -534,7 +473,9 @@ $.BootstrapTable = class extends $.BootstrapTable {
     // pageSize
     this.options.pageSize = pageListCookie ? pageListCookie === this.options.formatAllRows() ? pageListCookie : +pageListCookie : this.options.pageSize
     // searchText
-    this.options.searchText = searchTextCookie ? searchTextCookie : ''
+    if (UtilsCookie.isCookieEnabled(this, 'bs.table.searchText') && this.options.searchText === '') {
+      this.options.searchText = searchTextCookie ? searchTextCookie : ''
+    }
     // cardView
     this.options.cardView = cardViewCookie === 'true' ? cardViewCookie : false
 
@@ -566,7 +507,7 @@ $.BootstrapTable = class extends $.BootstrapTable {
     const cookies = {}
 
     $.each(UtilsCookie.cookieIds, (key, value) => {
-      cookies[key] = UtilsCookie.getCookie(bootstrapTable, bootstrapTable.options.cookieIdTable, value)
+      cookies[key] = UtilsCookie.getCookie(bootstrapTable, value)
       if (key === 'columns') {
         cookies[key] = JSON.parse(cookies[key])
       }
@@ -575,10 +516,89 @@ $.BootstrapTable = class extends $.BootstrapTable {
   }
 
   deleteCookie (cookieName) {
-    if ((cookieName === '') || (!UtilsCookie.cookieEnabled())) {
+    if (!cookieName) {
       return
     }
 
-    UtilsCookie.deleteCookie(this, this.options.cookieIdTable, UtilsCookie.cookieIds[cookieName])
+    UtilsCookie.deleteCookie(this, UtilsCookie.cookieIds[cookieName])
+  }
+
+  configureStorage () {
+    const that = this
+
+    this._storage = {}
+    switch (this.options.cookieStorage) {
+      case 'cookieStorage':
+        this._storage.setItem = function (cookieName, cookieValue) {
+          document.cookie = [
+            cookieName, '=', encodeURIComponent(cookieValue),
+            `; expires=${UtilsCookie.calculateExpiration(that.options.cookieExpire)}`,
+            that.options.cookiePath ? `; path=${that.options.cookiePath}` : '',
+            that.options.cookieDomain ? `; domain=${that.options.cookieDomain}` : '',
+            that.options.cookieSecure ? '; secure' : '',
+            `;SameSite=${ that.options.cookieSameSite}`
+          ].join('')
+        }
+        this._storage.getItem = function (cookieName) {
+          const value = `; ${document.cookie}`
+          const parts = value.split(`; ${cookieName}=`)
+
+          return parts.length === 2 ? decodeURIComponent(parts.pop().split(';').shift()) : null
+        }
+        this._storage.removeItem = function (cookieName) {
+          document.cookie = [
+            encodeURIComponent(cookieName), '=',
+            '; expires=Thu, 01 Jan 1970 00:00:00 GMT',
+            that.options.cookiePath ? `; path=${that.options.cookiePath}` : '',
+            that.options.cookieDomain ? `; domain=${that.options.cookieDomain}` : '',
+            `;SameSite=${ that.options.cookieSameSite}`
+          ].join('')
+        }
+        break
+      case 'localStorage':
+        this._storage.setItem = function (cookieName, cookieValue) {
+          localStorage.setItem(cookieName, cookieValue)
+        }
+        this._storage.getItem = function (cookieName) {
+          return localStorage.getItem(cookieName)
+        }
+        this._storage.removeItem = function (cookieName) {
+          localStorage.removeItem(cookieName)
+        }
+        break
+      case 'sessionStorage':
+        this._storage.setItem = function (cookieName, cookieValue) {
+          sessionStorage.setItem(cookieName, cookieValue)
+        }
+        this._storage.getItem = function (cookieName) {
+          return sessionStorage.getItem(cookieName)
+        }
+        this._storage.removeItem = function (cookieName) {
+          sessionStorage.removeItem(cookieName)
+        }
+        break
+      case 'customStorage':
+        if (
+          !this.options.cookieCustomStorageSet ||
+          !this.options.cookieCustomStorageGet ||
+          !this.options.cookieCustomStorageDelete
+        ) {
+          throw new Error('The following options must be set while using the customStorage: cookieCustomStorageSet, cookieCustomStorageGet and cookieCustomStorageDelete')
+        }
+
+        this._storage.setItem = function (cookieName, cookieValue) {
+          Utils.calculateObjectValue(that.options, that.options.cookieCustomStorageSet, [cookieName, cookieValue], '')
+        }
+        this._storage.getItem = function (cookieName) {
+          return Utils.calculateObjectValue(that.options, that.options.cookieCustomStorageGet, [cookieName], '')
+        }
+        this._storage.removeItem = function (cookieName) {
+          Utils.calculateObjectValue(that.options, that.options.cookieCustomStorageDelete, [cookieName], '')
+        }
+
+        break
+      default:
+        throw new Error('Storage method not supported.')
+    }
   }
 }

+ 200 - 155
src/extensions/filter-control/bootstrap-table-filter-control.js

@@ -1,7 +1,7 @@
 /**
  * @author: Dennis Hernández
  * @webSite: http://djhvscf.github.io/Blog
- * @version: v2.3.0
+ * @version: v3.0.0
  */
 
 import * as UtilsFilterControl from './utils.js'
@@ -19,41 +19,52 @@ $.extend($.fn.bootstrapTable.defaults, {
   },
   alignmentSelectControlOptions: undefined,
   filterTemplate: {
-    input (that, field, placeholder, value) {
+    input (that, column, placeholder, value) {
       return Utils.sprintf(
-        '<input type="search" class="form-control bootstrap-table-filter-control-%s search-input" style="width: 100%;" placeholder="%s" value="%s">',
-        field,
+        '<input type="search" class="%s bootstrap-table-filter-control-%s search-input" style="width: 100%;" placeholder="%s" value="%s">',
+        UtilsFilterControl.getFormControlClass(that.options),
+        column.field,
         'undefined' === typeof placeholder ? '' : placeholder,
         'undefined' === typeof value ? '' : value
       )
     },
 
-    select ({ options }, field) {
+    select ({ options }, column) {
       return Utils.sprintf(
-        '<select class="form-control bootstrap-table-filter-control-%s" style="width: 100%;" dir="%s"></select>',
-        field,
+        '<select class="%s bootstrap-table-filter-control-%s %s" %s style="width: 100%;" dir="%s"></select>',
+        UtilsFilterControl.getFormControlClass(options),
+        column.field,
+        '', // column.filterControlMultipleSelect ? 'fc-multipleselect' : '',
+        '', // column.filterControlMultipleSelect ? 'multiple="multiple"' : '',
         UtilsFilterControl.getDirectionOfSelectOptions(
           options.alignmentSelectControlOptions
         )
       )
     },
-    datepicker (that, field, value) {
+
+    datepicker (that, column, value) {
       return Utils.sprintf(
-        '<input type="text" class="form-control date-filter-control bootstrap-table-filter-control-%s" style="width: 100%;" value="%s">',
-        field,
+        '<input type="date" class="%s date-filter-control bootstrap-table-filter-control-%s" style="width: 100%;" value="%s">',
+        UtilsFilterControl.getFormControlClass(that.options),
+        column.field,
         'undefined' === typeof value ? '' : value
       )
     }
   },
-  disableControlWhenSearch: false,
   searchOnEnterKey: false,
   showFilterControlSwitch: false,
+  sortSelectOptions: false,
   // internal variables
-  valuesFilterControl: []
+  _valuesFilterControl: [],
+  _initialized: false,
+  _isRendering: false,
+  _usingMultipleSelect: false
 })
 
 $.extend($.fn.bootstrapTable.columnDefaults, {
   filterControl: undefined, // input, select, datepicker
+  filterControlMultipleSelect: false,
+  filterControlMultipleSelectOptions: {},
   filterDataCollector: undefined,
   filterData: undefined,
   filterDatepickerOptions: {},
@@ -61,7 +72,8 @@ $.extend($.fn.bootstrapTable.columnDefaults, {
   filterStartsWithSearch: false,
   filterControlPlaceholder: '',
   filterDefault: '',
-  filterOrderBy: 'asc' // asc || desc
+  filterOrderBy: 'asc', // asc || desc
+  filterCustomSearch: undefined
 })
 
 $.extend($.fn.bootstrapTable.Constructor.EVENTS, {
@@ -110,50 +122,59 @@ $.BootstrapTable = class extends $.BootstrapTable {
     // Make sure that the filterControl option is set
     if (this.options.filterControl) {
       // Make sure that the internal variables are set correctly
-      this.options.valuesFilterControl = []
+      this._valuesFilterControl = []
+      this._initialized = false
+      this._usingMultipleSelect = false
+      this._isRendering = false
 
       this.$el
-        .on('reset-view.bs.table', () => {
-          // Create controls on $tableHeader if the height is set
-          if (!this.options.height) {
-            return
-          }
-
-          // Avoid recreate the controls
-          const $controlContainer = UtilsFilterControl.getControlContainer(this)
-
-          if (
-            ($controlContainer.find('select').length > 0 || $controlContainer.find('input:not([type="checkbox"]):not([type="radio"])').length > 0) &&
-            !this.options.filterControlContainer
-          ) {
-            return
+        .on('reset-view.bs.table', Utils.debounce(() => {
+          UtilsFilterControl.initFilterSelectControls(this)
+          UtilsFilterControl.setValues(this)
+        }, 3))
+        .on('toggle.bs.table', Utils.debounce((_, cardView) => {
+          this._initialized = false
+          if (!cardView) {
+            UtilsFilterControl.initFilterSelectControls(this)
+            UtilsFilterControl.setValues(this)
+            this._initialized = true
           }
-
-          UtilsFilterControl.createControls(this, $controlContainer)
-        })
-        .on('post-header.bs.table', () => {
+        }, 1))
+        .on('post-header.bs.table', Utils.debounce(() => {
+          UtilsFilterControl.initFilterSelectControls(this)
           UtilsFilterControl.setValues(this)
-        })
-        .on('post-body.bs.table', () => {
-          if (this.options.height && !this.options.filterControlContainer) {
+        }, 3))
+        .on('column-switch.bs.table', Utils.debounce(() => {
+          UtilsFilterControl.setValues(this)
+          if (this.options.height) {
+            this.fitHeader()
+          }
+        }, 1))
+        .on('post-body.bs.table', Utils.debounce(() => {
+          if (this.options.height && !this.options.filterControlContainer && this.options.filterControlVisible) {
             UtilsFilterControl.fixHeaderCSS(this)
           }
           this.$tableLoading.css('top', this.$header.outerHeight() + 1)
-        })
-        .on('column-switch.bs.table', () => {
-          UtilsFilterControl.setValues(this)
-        })
-        .on('load-success.bs.table', () => {
-          this.enableControls(true)
-        })
-        .on('load-error.bs.table', () => {
-          this.enableControls(true)
+        }, 1))
+        .on('all.bs.table', () => {
+          UtilsFilterControl.syncHeaders(this)
         })
     }
 
     super.init()
   }
 
+  initBody () {
+    super.initBody()
+    if (!this.options.filterControl) {
+      return
+    }
+    setTimeout(() => {
+      UtilsFilterControl.initFilterSelectControls(this)
+      UtilsFilterControl.setValues(this)
+    }, 3)
+  }
+
   load (data) {
     super.load(data)
 
@@ -162,22 +183,17 @@ $.BootstrapTable = class extends $.BootstrapTable {
     }
 
     UtilsFilterControl.createControls(this, UtilsFilterControl.getControlContainer(this))
+    UtilsFilterControl.setValues(this)
   }
 
   initHeader () {
     super.initHeader()
-
-    if (!this.options.filterControl || this.options.height) {
+    if (!this.options.filterControl) {
       return
     }
 
     UtilsFilterControl.createControls(this, UtilsFilterControl.getControlContainer(this))
-  }
-
-  initBody () {
-    super.initBody()
-    UtilsFilterControl.syncControls(this)
-    UtilsFilterControl.initFilterSelectControls(this)
+    this._initialized = true
   }
 
   initSearch () {
@@ -208,13 +224,15 @@ $.BootstrapTable = class extends $.BootstrapTable {
             tmpItemIsExpected = true
           } else {
             // Fix #142: search use formatted data
-            if (thisColumn && thisColumn.searchFormatter) {
-              value = $.fn.bootstrapTable.utils.calculateObjectValue(
-                that.header,
-                that.header.formatters[$.inArray(key, that.header.fields)],
-                [value, item, i],
-                value
-              )
+            if (thisColumn) {
+              if (thisColumn.searchFormatter || thisColumn._forceFormatter) {
+                value = $.fn.bootstrapTable.utils.calculateObjectValue(
+                  that.header,
+                  that.header.formatters[$.inArray(key, that.header.fields)],
+                  [value, item, i],
+                  value
+                )
+              }
             }
 
             if ($.inArray(key, that.header.fields) !== -1) {
@@ -257,6 +275,8 @@ $.BootstrapTable = class extends $.BootstrapTable {
       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) === new Date(searchValue)
     } else if (this.options.regexSearch) {
       tmpItemIsExpected = Utils.regexCompare(value, searchValue)
     } else {
@@ -310,7 +330,7 @@ $.BootstrapTable = class extends $.BootstrapTable {
   }
 
   initColumnSearch (filterColumnsDefaults) {
-    UtilsFilterControl.copyValues(this)
+    UtilsFilterControl.cacheValues(this)
 
     if (filterColumnsDefaults) {
       this.filterColumnsPartial = filterColumnsDefaults
@@ -323,31 +343,6 @@ $.BootstrapTable = class extends $.BootstrapTable {
     }
   }
 
-  onColumnSearch ({ currentTarget, keyCode }) {
-    if ($.inArray(keyCode, [37, 38, 39, 40]) > -1) {
-      return
-    }
-
-    UtilsFilterControl.copyValues(this)
-    const text = $.trim($(currentTarget).val())
-    const $field = $(currentTarget).closest('[data-field]').data('field')
-
-    this.trigger('column-search', $field, text)
-
-    if ($.isEmptyObject(this.filterColumnsPartial)) {
-      this.filterColumnsPartial = {}
-    }
-    if (text) {
-      this.filterColumnsPartial[$field] = text
-    } else {
-      delete this.filterColumnsPartial[$field]
-    }
-
-    this.options.pageNumber = 1
-    this.enableControls(false)
-    this.onSearch({ currentTarget }, false)
-  }
-
   initToolbar () {
     this.showToolbar = this.showToolbar || this.options.showFilterControlSwitch
     this.showSearchClearButton = this.options.filterControl && this.options.showSearchClearButton
@@ -377,99 +372,120 @@ $.BootstrapTable = class extends $.BootstrapTable {
   }
 
   clearFilterControl () {
-    if (this.options.filterControl) {
-      const that = this
-      const cookies = UtilsFilterControl.collectBootstrapTableFilterCookies()
-      const table = this.$el.closest('table')
-      const controls = UtilsFilterControl.getSearchControls(that)
-      const search = Utils.getSearchInput(this)
-      let hasValues = false
-      let timeoutId = 0
-
-      $.each(that.options.valuesFilterControl, (i, item) => {
-        hasValues = hasValues ? true : item.value !== ''
-        item.value = ''
-      })
+    if (!this.options.filterControl) {
+      return
+    }
 
-      $.each(that.options.filterControls, (i, item) => {
-        item.text = ''
-      })
+    const that = this
+    const table = this.$el.closest('table')
+    const cookies = UtilsFilterControl.collectBootstrapTableFilterCookies()
+    const controls = UtilsFilterControl.getSearchControls(that)
+    // const search = Utils.getSearchInput(this)
+    let hasValues = false
+    let timeoutId = 0
+
+    // Clear cache values
+    $.each(that._valuesFilterControl, (i, item) => {
+      hasValues = hasValues ? true : item.value !== ''
+      item.value = ''
+    })
 
-      UtilsFilterControl.setValues(that)
+    // Clear controls in UI
+    $.each(controls, (i, item) => {
+      item.value = ''
+    })
 
-      // clear cookies once the filters are clean
-      clearTimeout(timeoutId)
-      timeoutId = setTimeout(() => {
-        if (cookies && cookies.length > 0) {
-          $.each(cookies, (i, item) => {
-            if (that.deleteCookie !== undefined) {
-              that.deleteCookie(item)
-            }
-          })
-        }
-      }, that.options.searchTimeOut)
+    // Cache controls again
+    UtilsFilterControl.setValues(that)
 
-      // If there is not any value in the controls exit this method
-      if (!hasValues) {
-        return
+    // clear cookies once the filters are clean
+    clearTimeout(timeoutId)
+    timeoutId = setTimeout(() => {
+      if (cookies && cookies.length > 0) {
+        $.each(cookies, (i, item) => {
+          if (that.deleteCookie !== undefined) {
+            that.deleteCookie(item)
+          }
+        })
       }
+    }, that.options.searchTimeOut)
 
-      // Clear each type of filter if it exists.
-      // Requires the body to reload each time a type of filter is found because we never know
-      // which ones are going to be present.
-      if (controls.length > 0) {
-        this.filterColumnsPartial = {}
-        $(controls[0]).trigger(
-          controls[0].tagName === 'INPUT' ? 'keyup' : 'change', { keyCode: 13 }
-        )
-      } else {
-        return
-      }
+    // If there is not any value in the controls exit this method
+    if (!hasValues) {
+      return
+    }
 
-      if (search.length > 0) {
-        that.resetSearch()
-      }
+    // Clear each type of filter if it exists.
+    // Requires the body to reload each time a type of filter is found because we never know
+    // which ones are going to be present.
+    if (controls.length > 0) {
+      this.filterColumnsPartial = {}
+      controls.eq(0).trigger(this.tagName === 'INPUT' ? 'keyup' : 'change', { keyCode: 13 })
+      /* controls.each(function () {
+        $(this).trigger(this.tagName === 'INPUT' ? 'keyup' : 'change', { keyCode: 13 })
+      })*/
+    } else {
+      return
+    }
 
-      // use the default sort order if it exists. do nothing if it does not
-      if (that.options.sortName !== table.data('sortName') || that.options.sortOrder !== table.data('sortOrder')) {
-        const sorter = this.$header.find(Utils.sprintf('[data-field="%s"]', $(controls[0]).closest('table').data('sortName')))
+    /* if (search.length > 0) {
+      that.resetSearch('fc')
+    }*/
 
-        if (sorter.length > 0) {
-          that.onSort({ type: 'keypress', currentTarget: sorter })
-          $(sorter).find('.sortable').trigger('click')
-        }
+    // use the default sort order if it exists. do nothing if it does not
+    if (that.options.sortName !== table.data('sortName') || that.options.sortOrder !== table.data('sortOrder')) {
+      const sorter = this.$header.find(Utils.sprintf('[data-field="%s"]', $(controls[0]).closest('table').data('sortName')))
+
+      if (sorter.length > 0) {
+        that.onSort({ type: 'keypress', currentTarget: sorter })
+        $(sorter).find('.sortable').trigger('click')
       }
     }
   }
 
-  triggerSearch () {
-    const searchControls = UtilsFilterControl.getSearchControls(this)
+  // EVENTS
+  onColumnSearch ({ currentTarget, keyCode }) {
+    if (UtilsFilterControl.isKeyAllowed(keyCode)) {
+      return
+    }
+    UtilsFilterControl.cacheValues(this)
 
-    searchControls.each(function () {
-      const el = $(this)
+    // Cookie extension support
+    if (!this.options.cookie) {
+      this.options.pageNumber = 1
+    } else {
+      // Force call the initServer method in Cookie extension
+      this._filterControlValuesLoaded = true
+    }
 
-      if (el.is('select')) {
-        el.change()
-      } else {
-        el.keyup()
-      }
-    })
-  }
+    if ($.isEmptyObject(this.filterColumnsPartial)) {
+      this.filterColumnsPartial = {}
+    }
 
-  enableControls (enable) {
-    if (this.options.disableControlWhenSearch && this.options.sidePagination === 'server') {
-      const searchControls = UtilsFilterControl.getSearchControls(this)
+    // If searchOnEnterKey is set to true, then we need to iterate over all controls and grab their values.
+    const controls = this.options.searchOnEnterKey ? UtilsFilterControl.getSearchControls(this).toArray() : [currentTarget]
 
-      if (!enable) {
-        searchControls.prop('disabled', 'disabled')
+    controls.forEach(element => {
+      const $element = $(element)
+      const elementValue = $element.val()
+      const text = elementValue ? elementValue.trim() : ''
+      const $field = $element.closest('[data-field]').data('field')
+
+      this.trigger('column-search', $field, text)
+
+      if (text) {
+        this.filterColumnsPartial[$field] = text
       } else {
-        searchControls.removeProp('disabled')
+        delete this.filterColumnsPartial[$field]
       }
-    }
+    })
+
+    this.onSearch({ currentTarget }, false)
   }
 
   toggleFilterControl () {
     this.options.filterControlVisible = !this.options.filterControlVisible
+    // Controls in original header or container.
     const $filterControls = UtilsFilterControl.getControlContainer(this).find('.filter-control, .no-filter-control')
 
     if (this.options.filterControlVisible) {
@@ -478,10 +494,39 @@ $.BootstrapTable = class extends $.BootstrapTable {
       $filterControls.hide()
       this.clearFilterControl()
     }
+
+    // Controls in fixed header
+    if (this.options.height) {
+      const $fixedControls = $('.fixed-table-header table thead').find('.filter-control, .no-filter-control')
+
+      $fixedControls.toggle(this.options.filterControlVisible)
+      UtilsFilterControl.fixHeaderCSS(this)
+    }
+
     const icon = this.options.showButtonIcons ? this.options.filterControlVisible ? this.options.icons.filterControlSwitchHide : this.options.icons.filterControlSwitchShow : ''
     const text = this.options.showButtonText ? this.options.filterControlVisible ? this.options.formatFilterControlSwitchHide() : this.options.formatFilterControlSwitchShow() : ''
 
     this.$toolbar.find('>.columns').find('.filter-control-switch')
       .html(`${Utils.sprintf(this.constants.html.icon, this.options.iconsPrefix, icon) } ${ text}`)
   }
+
+  triggerSearch () {
+    const searchControls = UtilsFilterControl.getSearchControls(this)
+
+    searchControls.each(function () {
+      const $element = $(this)
+
+      if ($element.is('select')) {
+        $element.trigger('change')
+      } else {
+        $element.trigger('keyup')
+      }
+    })
+  }
+
+  _toggleColumn (index, checked, needUpdate) {
+    this._initialized = false
+    super._toggleColumn(index, checked, needUpdate)
+    UtilsFilterControl.syncHeaders(this)
+  }
 }

+ 9 - 1
src/extensions/filter-control/bootstrap-table-filter-control.scss

@@ -5,9 +5,17 @@
  */
 
 .no-filter-control {
-  height: 34px;
+  height: 40px;
 }
 
 .filter-control {
   margin: 0 2px 2px 2px;
 }
+
+.ms-choice {
+  border: 0;
+}
+
+.ms-parent > button:focus {
+  outline: 0;
+}

+ 235 - 149
src/extensions/filter-control/utils.js

@@ -1,17 +1,31 @@
+/* eslint-disable no-use-before-define */
 const Utils = $.fn.bootstrapTable.utils
 const searchControls = 'select, input:not([type="checkbox"]):not([type="radio"])'
 
+export function getFormControlClass (options) {
+  return options.iconSize ? Utils.sprintf('form-control-%s', options.iconSize) : 'form-control'
+}
+
 export function getOptionsFromSelectControl (selectControl) {
-  return selectControl.get(selectControl.length - 1).options
+  return selectControl[0].options
 }
 
 export function getControlContainer (that) {
   if (that.options.filterControlContainer) {
     return $(`${that.options.filterControlContainer}`)
   }
+
+  if (that.options.height && that._initialized) {
+    return $('.fixed-table-header table thead')
+  }
+
   return that.$header
 }
 
+export function isKeyAllowed (keyCode) {
+  return $.inArray(keyCode, [37, 38, 39, 40]) > -1
+}
+
 export function getSearchControls (that) {
   return getControlContainer(that).find(searchControls)
 }
@@ -44,37 +58,55 @@ export function existOptionInSelectControl (selectControl, value) {
   return false
 }
 
-export function addOptionToSelectControl (selectControl, _value, text, selected) {
-  const value = (_value === undefined || _value === null) ? '' : _value.toString().trim()
-  const $selectControl = $(selectControl.get(selectControl.length - 1))
-
-  if (!existOptionInSelectControl(selectControl, value)) {
-    const option = $(`<option value="${value}">${text}</option>`)
+export function addOptionToSelectControl (selectControl, _value, text, selected, shouldCompareText) {
+  let value = (_value === undefined || _value === null) ? '' : _value.toString().trim()
 
-    if (value === selected) {
-      option.attr('selected', true)
-    }
+  value = Utils.removeHTML(value)
+  text = Utils.removeHTML(text)
 
-    $selectControl.append(option)
+  if (existOptionInSelectControl(selectControl, value)) {
+    return
   }
+
+  const isSelected = shouldCompareText ? (value === selected || text === selected) : value === selected
+
+  const option = new Option(text, value, false, isSelected)
+
+  selectControl.get(0).add(option)
 }
 
 export function sortSelectControl (selectControl, orderBy) {
-  const $selectControl = $(selectControl.get(selectControl.length - 1))
-  const $opts = $selectControl.find('option:gt(0)')
+  const $selectControl = selectControl.get(0)
 
-  if (orderBy !== 'server') {
-    $opts.sort((a, b) => {
-      return Utils.sort(a.textContent, b.textContent, orderBy === 'desc' ? -1 : 1)
-    })
+  if (orderBy === 'server') {
+    return
+  }
+
+  const tmpAry = new Array()
+
+  for (let i = 0; i < $selectControl.options.length; i++) {
+    tmpAry[i] = new Array()
+    tmpAry[i][0] = $selectControl.options[i].text
+    tmpAry[i][1] = $selectControl.options[i].value
+    tmpAry[i][2] = $selectControl.options[i].selected
   }
 
-  $selectControl.find('option:gt(0)').remove()
-  $selectControl.append($opts)
+  tmpAry.sort((a, b) => {
+    return Utils.sort(a[0], b[0], orderBy === 'desc' ? -1 : 1)
+  })
+  while ($selectControl.options.length > 0) {
+    $selectControl.options[0] = null
+  }
+
+  for (let i = 0; i < tmpAry.length; i++) {
+    const op = new Option(tmpAry[i][0], tmpAry[i][1], false, tmpAry[i][2])
+
+    $selectControl.add(op)
+  }
 }
 
 export function fixHeaderCSS ({ $tableHeader }) {
-  $tableHeader.css('height', '89px')
+  $tableHeader.css('height', $tableHeader.find('table').outerHeight(true))
 }
 
 export function getElementClass ($element) {
@@ -82,47 +114,42 @@ export function getElementClass ($element) {
 }
 
 export function getCursorPosition (el) {
-  if (Utils.isIEBrowser()) {
-    if ($(el).is('input[type=text]')) {
-      let pos = 0
-
-      if ('selectionStart' in el) {
-        pos = el.selectionStart
-      } else if ('selection' in document) {
-        el.focus()
-        const Sel = document.selection.createRange()
-        const SelLength = document.selection.createRange().text.length
-
-        Sel.moveStart('character', -el.value.length)
-        pos = Sel.text.length - SelLength
-      }
-      return pos
+  if ($(el).is('input[type=search]')) {
+    let pos = 0
+
+    if ('selectionStart' in el) {
+      pos = el.selectionStart
+    } else if ('selection' in document) {
+      el.focus()
+      const Sel = document.selection.createRange()
+      const SelLength = document.selection.createRange().text.length
+
+      Sel.moveStart('character', -el.value.length)
+      pos = Sel.text.length - SelLength
     }
-    return -1
-
+    return pos
   }
   return -1
 }
 
-export function setCursorPosition (el) {
-  $(el).val(el.value)
-}
-
-export function copyValues (that) {
+export function cacheValues (that) {
   const searchControls = getSearchControls(that)
 
-  that.options.valuesFilterControl = []
+  that._valuesFilterControl = []
 
   searchControls.each(function () {
     let $field = $(this)
+    const fieldClass = getElementClass($field)
 
-    if (that.options.height) {
-      const fieldClass = getElementClass($field)
-
+    if (that.options.height && !that.options.filterControlContainer) {
       $field = $(`.fixed-table-header .${fieldClass}`)
+    } else if (that.options.filterControlContainer) {
+      $field = $(`${that.options.filterControlContainer} .${fieldClass}`)
+    } else {
+      $field = $(`.${fieldClass}`)
     }
 
-    that.options.valuesFilterControl.push({
+    that._valuesFilterControl.push({
       field: $field.closest('[data-field]').data('field'),
       value: $field.val(),
       position: getCursorPosition($field.get(0)),
@@ -131,45 +158,71 @@ export function copyValues (that) {
   })
 }
 
+export function setCaretPosition (elem, caretPos) {
+  try {
+    if (elem) {
+      if (elem.createTextRange) {
+        const range = elem.createTextRange()
+
+        range.move('character', caretPos)
+        range.select()
+      } else {
+        elem.setSelectionRange(caretPos, caretPos)
+      }
+    }
+  }
+  catch (ex) {
+    // ignored
+  }
+}
+
 export function setValues (that) {
   let field = null
   let result = []
   const searchControls = getSearchControls(that)
 
-  if (that.options.valuesFilterControl.length > 0) {
+  if (that._valuesFilterControl.length > 0) {
     //  Callback to apply after settings fields values
-    let fieldToFocusCallback = null
+    const callbacks = []
 
     searchControls.each((i, el) => {
       const $this = $(el)
 
       field = $this.closest('[data-field]').data('field')
-      result = that.options.valuesFilterControl.filter(valueObj => valueObj.field === field)
+      result = that._valuesFilterControl.filter(valueObj => valueObj.field === field)
 
       if (result.length > 0) {
-        if ($this.is('[type=radio]')) {
-          return
-        }
-
-        $this.val(result[0].value)
-        if (result[0].hasFocus && result[0].value !== '') {
-          // set callback if the field had the focus.
-          fieldToFocusCallback = ((fieldToFocus, carretPosition) => {
-            // Closure here to capture the field and cursor position
+        if (result[0].hasFocus || result[0].value) {
+          const fieldToFocusCallback = ((element, cacheElementInfo) => {
+            // Closure here to capture the field information
             const closedCallback = () => {
-              fieldToFocus.focus()
-              setCursorPosition(fieldToFocus, carretPosition)
+              if (cacheElementInfo.hasFocus) {
+                element.focus()
+              }
+
+              if (Array.isArray(cacheElementInfo.value)) {
+                const $element = $(element)
+
+                $.each(cacheElementInfo.value, function (i, e) {
+                  $element.find(Utils.sprintf('option[value=\'%s\']', e)).prop('selected', true)
+                })
+              } else {
+                element.value = cacheElementInfo.value
+              }
+              setCaretPosition(element, cacheElementInfo.position)
             }
 
             return closedCallback
-          })($this.get(0), result[0].position)
+          })($this.get(0), result[0])
+
+          callbacks.push(fieldToFocusCallback)
         }
       }
     })
 
     // Callback call.
-    if (fieldToFocusCallback !== null) {
-      fieldToFocusCallback()
+    if (callbacks.length > 0) {
+      callbacks.forEach(callback => callback())
     }
   }
 }
@@ -227,31 +280,31 @@ export function hasSelectControlElement (selectControl) {
 }
 
 export function initFilterSelectControls (that) {
-  const data = that.data
-  const z = that.options.pagination ?
-    that.options.sidePagination === 'server' ?
-      that.pageTo :
-      that.options.totalRows :
-    that.pageTo
+  const data = that.options.data
 
   $.each(that.header.fields, (j, field) => {
     const column = that.columns[that.fieldsColumnsIndex[field]]
     const selectControl = getControlContainer(that).find(`select.bootstrap-table-filter-control-${escapeID(column.field)}`)
 
     if (isColumnSearchableViaSelect(column) && isFilterDataNotGiven(column) && hasSelectControlElement(selectControl)) {
-      if (selectControl.get(selectControl.length - 1).options.length === 0) {
+      if (!selectControl[0].multiple && selectControl.get(selectControl.length - 1).options.length === 0) {
         // Added the default option, must use a non-breaking space(&nbsp;) to pass the W3C validator
-        addOptionToSelectControl(selectControl, '', column.filterControlPlaceholder || '&nbsp;', column.filterDefault)
+        addOptionToSelectControl(selectControl, '', column.filterControlPlaceholder || ' ', column.filterDefault)
       }
 
       const uniqueValues = {}
 
-      for (let i = 0; i < z; i++) {
+      for (let i = 0; i < data.length; i++) {
         // Added a new value
         let fieldValue = Utils.getItemField(data[i], field, false)
         const formatter = that.options.editable && column.editable ? column._formatter : that.header.formatters[j]
         let formattedValue = Utils.calculateObjectValue(that.header, formatter, [fieldValue, data[i], i], fieldValue)
 
+        if (!fieldValue) {
+          fieldValue = formattedValue
+          column._forceFormatter = true
+        }
+
         if (column.filterDataCollector) {
           formattedValue = Utils.calculateObjectValue(that.header, column.filterDataCollector, [fieldValue, data[i], formattedValue], formattedValue)
         }
@@ -267,16 +320,11 @@ export function initFilterSelectControls (that) {
           })
           continue
         }
-
-        // eslint-disable-next-line guard-for-in
-        for (const key in uniqueValues) {
-          addOptionToSelectControl(selectControl, uniqueValues[key], key, column.filterDefault)
-        }
       }
 
-      sortSelectControl(selectControl, column.filterOrderBy)
-      if (that.options.hideUnusedSelectOptions) {
-        hideUnusedSelectOptions(selectControl, uniqueValues)
+      // eslint-disable-next-line guard-for-in
+      for (const key in uniqueValues) {
+        addOptionToSelectControl(selectControl, uniqueValues[key], key, column.filterDefault)
       }
     }
   })
@@ -307,13 +355,14 @@ export function createControls (that, header) {
     if (!column.filterControl && !that.options.filterControlContainer) {
       html.push('<div class="no-filter-control"></div>')
     } else if (that.options.filterControlContainer) {
+      // Use a filter control container instead of th
       const $filterControls = $(`.bootstrap-table-filter-control-${column.field}`)
 
       $.each($filterControls, (_, filterControl) => {
         const $filterControl = $(filterControl)
 
         if (!$filterControl.is('[type=radio]')) {
-          const placeholder = column.filterControlPlaceholder ? column.filterControlPlaceholder : ''
+          const placeholder = column.filterControlPlaceholder || ''
 
           $filterControl.attr('placeholder', placeholder).val(column.filterDefault)
         }
@@ -323,16 +372,16 @@ export function createControls (that, header) {
 
       addedFilterControl = true
     } else {
+      // Create the control based on the html defined in the filterTemplate array.
       const nameControl = column.filterControl.toLowerCase()
 
       html.push('<div class="filter-control">')
       addedFilterControl = true
-
       if (column.searchable && that.options.filterTemplate[nameControl]) {
         html.push(
           that.options.filterTemplate[nameControl](
             that,
-            column.field,
+            column,
             column.filterControlPlaceholder ?
               column.filterControlPlaceholder :
               '',
@@ -342,7 +391,8 @@ export function createControls (that, header) {
       }
     }
 
-    if (!column.filterControl && '' !== column.filterDefault && 'undefined' !== typeof column.filterDefault) {
+    // Filtering by default when it is set.
+    if (column.filterControl && '' !== column.filterDefault && 'undefined' !== typeof column.filterDefault) {
       if ($.isEmptyObject(that.filterColumnsPartial)) {
         that.filterColumnsPartial = {}
       }
@@ -350,7 +400,7 @@ export function createControls (that, header) {
       that.filterColumnsPartial[column.field] = column.filterDefault
     }
 
-    $.each(header.find('th'), (i, th) => {
+    $.each(header.find('th'), (_, th) => {
       const $th = $(th)
 
       if ($th.data('field') === column.field) {
@@ -361,23 +411,16 @@ export function createControls (that, header) {
     })
 
     if (column.filterData && column.filterData.toLowerCase() !== 'column') {
-      const filterDataType = getFilterDataMethod(
-        /* eslint-disable no-use-before-define */
-        filterDataMethods,
-        column.filterData.substring(0, column.filterData.indexOf(':'))
-      )
+      const filterDataType = getFilterDataMethod(filterDataMethods, column.filterData.substring(0, column.filterData.indexOf(':')))
       let filterDataSource
       let selectControl
 
       if (filterDataType) {
-        filterDataSource = column.filterData.substring(
-          column.filterData.indexOf(':') + 1,
-          column.filterData.length
-        )
+        filterDataSource = column.filterData.substring(column.filterData.indexOf(':') + 1, column.filterData.length)
         selectControl = header.find(`.bootstrap-table-filter-control-${escapeID(column.field)}`)
 
-        addOptionToSelectControl(selectControl, '', column.filterControlPlaceholder, column.filterDefault)
-        filterDataType(filterDataSource, selectControl, that.options.filterOrderBy, column.filterDefault)
+        addOptionToSelectControl(selectControl, '', column.filterControlPlaceholder, column.filterDefault, true)
+        filterDataType(that, filterDataSource, selectControl, that.options.filterOrderBy, column.filterDefault)
       } else {
         throw new SyntaxError(
           'Error. You should use any of these allowed filter data methods: var, obj, json, url, func.' +
@@ -389,15 +432,13 @@ export function createControls (that, header) {
 
   if (addedFilterControl) {
     header.off('keyup', 'input').on('keyup', 'input', ({ currentTarget, keyCode }, obj) => {
-      syncControls(that)
-      // Simulate enter key action from clear button
       keyCode = obj ? obj.keyCode : keyCode
 
       if (that.options.searchOnEnterKey && keyCode !== 13) {
         return
       }
 
-      if ($.inArray(keyCode, [37, 38, 39, 40]) > -1) {
+      if (isKeyAllowed(keyCode)) {
         return
       }
 
@@ -413,16 +454,22 @@ export function createControls (that, header) {
       }, that.options.searchTimeOut)
     })
 
-    header.off('change', 'select:not(".ms-offscreen")').on('change', 'select:not(".ms-offscreen")', ({ currentTarget, keyCode }) => {
-      syncControls(that)
-      const $select = $(currentTarget)
-      const value = $select.val()
+    header.off('change', 'select', '.fc-multipleselect').on('change', 'select', '.fc-multipleselect', ({ currentTarget, keyCode }) => {
+      const $selectControl = $(currentTarget)
+      const value = $selectControl.val()
 
-      if (value && value.length > 0 && value.trim()) {
-        $select.find('option[selected]').removeAttr('selected')
-        $select.find(`option[value="${ value }"]`).attr('selected', true)
+      if (Array.isArray(value)) {
+        for (let i = 0; i < value.length; i++) {
+          if (value[i] && value[i].length > 0 && value[i].trim()) {
+            $selectControl.find(`option[value="${ value[i] }"]`).attr('selected', true)
+          }
+        }
+      }
+      else if (value && value.length > 0 && value.trim()) {
+        $selectControl.find('option[selected]').removeAttr('selected')
+        $selectControl.find(`option[value="${ value }"]`).attr('selected', true)
       } else {
-        $select.find('option[selected]').removeAttr('selected')
+        $selectControl.find('option[selected]').removeAttr('selected')
       }
 
       clearTimeout(currentTarget.timeoutId || 0)
@@ -440,7 +487,6 @@ export function createControls (that, header) {
       }
 
       setTimeout(() => {
-        syncControls(that)
         const newValue = $input.val()
 
         if (newValue === '') {
@@ -455,34 +501,47 @@ export function createControls (that, header) {
     header.off('change', 'input[type=radio]').on('change', 'input[type=radio]', ({ currentTarget, keyCode }) => {
       clearTimeout(currentTarget.timeoutId || 0)
       currentTarget.timeoutId = setTimeout(() => {
-        syncControls(that)
         that.onColumnSearch({ currentTarget, keyCode })
       }, that.options.searchTimeOut)
     })
 
+    // See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/date
     if (header.find('.date-filter-control').length > 0) {
       $.each(that.columns, (i, { filterDefault, filterControl, field, filterDatepickerOptions }) => {
         if (filterControl !== undefined && filterControl.toLowerCase() === 'datepicker') {
           const $datepicker = header.find(`.date-filter-control.bootstrap-table-filter-control-${field}`)
 
-          $datepicker.datepicker(filterDatepickerOptions)
-
           if (filterDefault) {
-            $datepicker.datepicker('setDate', filterDefault)
+            $datepicker.value(filterDefault)
+          }
+
+          if (filterDatepickerOptions.min) {
+            $datepicker.attr('min', filterDatepickerOptions.min)
+          }
+
+          if (filterDatepickerOptions.max) {
+            $datepicker.attr('max', filterDatepickerOptions.max)
+          }
+
+          if (filterDatepickerOptions.step) {
+            $datepicker.attr('step', filterDatepickerOptions.step)
           }
 
-          $datepicker.on('changeDate', ({ currentTarget, keyCode }) => {
+          if (filterDatepickerOptions.pattern) {
+            $datepicker.attr('pattern', filterDatepickerOptions.pattern)
+          }
+
+          $datepicker.on('change', ({ currentTarget }) => {
             clearTimeout(currentTarget.timeoutId || 0)
             currentTarget.timeoutId = setTimeout(() => {
-              syncControls(that)
-              that.onColumnSearch({ currentTarget, keyCode })
+              that.onColumnSearch({ currentTarget })
             }, that.options.searchTimeOut)
           })
         }
       })
     }
 
-    if (that.options.sidePagination !== 'server' && !that.options.height) {
+    if (that.options.sidePagination !== 'server') {
       that.triggerSearch()
     }
 
@@ -511,43 +570,50 @@ export function getDirectionOfSelectOptions (_alignment) {
   }
 }
 
-export function syncControls (that) {
-  if (that.options.height) {
-    const controlsTableHeader = that.$tableHeader.find(searchControls)
+export function syncHeaders (that) {
+  if (!that.options.height) {
+    return
+  }
+  const fixedHeader = $('.fixed-table-header table thead')
 
-    that.$header.find(searchControls).each((_, control) => {
-      const $control = $(control)
-      const controlClass = getElementClass($control)
-      const foundControl = controlsTableHeader.filter((_, ele) => {
-        const eleClass = getElementClass($(ele))
+  if (fixedHeader.length === 0) {
+    return
+  }
 
-        return controlClass === eleClass
-      })
+  that.$header.children().find('th[data-field]').each((_, element) => {
+    if (element.classList[0] !== 'bs-checkbox') {
+      const $element = $(element)
+      const $field = $element.data('field')
+      const $fixedField = $(`th[data-field='${$field}']`).not($element)
 
-      if (foundControl.length === 0) {
-        return
-      }
-      if ($control.is('select')) {
-        $control.find('option:selected').removeAttr('selected')
-        $control.find(`option[value='${foundControl.val()}']`).attr('selected', true)
-      } else {
-        $control.val(foundControl.val())
+      const input = $element.find('input')
+      const fixedInput = $fixedField.find('input')
+
+      if (input.length > 0 && fixedInput.length > 0) {
+        if (input.val() !== fixedInput.val()) {
+          input.val(fixedInput.val())
+        }
       }
-    })
-  }
+    }
+  })
 }
 
 const filterDataMethods = {
-  func (filterDataSource, selectControl, filterOrderBy, selected) {
+  func (that, filterDataSource, selectControl, filterOrderBy, selected) {
     const variableValues = window[filterDataSource].apply()
 
     // eslint-disable-next-line guard-for-in
     for (const key in variableValues) {
       addOptionToSelectControl(selectControl, key, variableValues[key], selected)
     }
-    sortSelectControl(selectControl, filterOrderBy)
+
+    if (that.options.sortSelectOptions) {
+      sortSelectControl(selectControl, filterOrderBy)
+    }
+
+    setValues(that)
   },
-  obj (filterDataSource, selectControl, filterOrderBy, selected) {
+  obj (that, filterDataSource, selectControl, filterOrderBy, selected) {
     const objectKeys = filterDataSource.split('.')
     const variableName = objectKeys.shift()
     let variableValues = window[variableName]
@@ -562,22 +628,32 @@ const filterDataMethods = {
     for (const key in variableValues) {
       addOptionToSelectControl(selectControl, key, variableValues[key], selected)
     }
-    sortSelectControl(selectControl, filterOrderBy)
+
+    if (that.options.sortSelectOptions) {
+      sortSelectControl(selectControl, filterOrderBy)
+    }
+
+    setValues(that)
   },
-  var (filterDataSource, selectControl, filterOrderBy, selected) {
+  var (that, filterDataSource, selectControl, filterOrderBy, selected) {
     const variableValues = window[filterDataSource]
     const isArray = Array.isArray(variableValues)
 
     for (const key in variableValues) {
       if (isArray) {
-        addOptionToSelectControl(selectControl, variableValues[key], variableValues[key], selected)
+        addOptionToSelectControl(selectControl, variableValues[key], variableValues[key], selected, true)
       } else {
-        addOptionToSelectControl(selectControl, key, variableValues[key], selected)
+        addOptionToSelectControl(selectControl, key, variableValues[key], selected, true)
       }
     }
-    sortSelectControl(selectControl, filterOrderBy)
+
+    if (that.options.sortSelectOptions) {
+      sortSelectControl(selectControl, filterOrderBy)
+    }
+
+    setValues(that)
   },
-  url (filterDataSource, selectControl, filterOrderBy, selected) {
+  url (that, filterDataSource, selectControl, filterOrderBy, selected) {
     $.ajax({
       url: filterDataSource,
       dataType: 'json',
@@ -586,17 +662,27 @@ const filterDataMethods = {
         for (const key in data) {
           addOptionToSelectControl(selectControl, key, data[key], selected)
         }
-        sortSelectControl(selectControl, filterOrderBy)
+
+        if (that.options.sortSelectOptions) {
+          sortSelectControl(selectControl, filterOrderBy)
+        }
+
+        setValues(that)
       }
     })
   },
-  json (filterDataSource, selectControl, filterOrderBy, selected) {
+  json (that, filterDataSource, selectControl, filterOrderBy, selected) {
     const variableValues = JSON.parse(filterDataSource)
 
     // eslint-disable-next-line guard-for-in
     for (const key in variableValues) {
       addOptionToSelectControl(selectControl, key, variableValues[key], selected)
     }
-    sortSelectControl(selectControl, filterOrderBy)
+
+    if (that.options.sortSelectOptions) {
+      sortSelectControl(selectControl, filterOrderBy)
+    }
+
+    setValues(that)
   }
 }

+ 0 - 21
src/extensions/pipeline/LICENSE

@@ -1,21 +0,0 @@
-(The MIT License)
-
-Copyright (c) 2019 doug-the-guy <badlydrawnsun@yahoo.com>
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.

+ 0 - 92
src/extensions/pipeline/README.md

@@ -1,92 +0,0 @@
-# Bootstrap Table Pipelining
-
-Use Plugin: [bootstrap-table-pipeline]
-
-This plugin enables client side data caching for server side requests which will
-eliminate the need to issue a new request every page change. This will allow
-for a performance balance for a large data set between returning all data at once
-(client side paging) and a new server side request (server side paging).
-
-There are two new options:
-- usePipeline: enables this feature
-- pipelineSize: the size of each cache window
-
-The size of the pipeline must be evenly divisible by the current page size. This is
-assured by rounding up to the nearest evenly divisible value. For example, if
-the pipeline size is 4990 and the current page size is 25, then pipeline size will
-be dynamically set to 5000.
-
-The cache windows are computed based on the pipeline size and the total number of rows
-returned by the server side query. For example, with pipeline size 500 and total rows
-1300, the cache windows will be:
-
-[{'lower': 0, 'upper': 499}, {'lower': 500, 'upper': 999}, {'lower': 1000, 'upper': 1499}]
-
-Using the limit (i.e. the pipelineSize) and offset parameters, the server side request
-**MUST** return only the data in the requested cache window **AND** the total number of rows.
-To wit, the server side code must use the offset and limit parameters to prepare the response
-data.
-
-On a page change, the new offset is checked if it is within the current cache window. If so,
-the requested page data is returned from the cached data set. Otherwise, a new server side
-request will be issued for the new cache window.
-
-The current cached data is only invalidated on these events:
- - sorting
- - searching
- - page size change
- - page change moves into a new cache window
-
-There are two new events:
-- cached-data-hit.bs.table: issued when cached data is used on a page change
-- cached-data-reset.bs.table: issued when the cached data is invalidated and new server side request is issued
-
-## Features
-
-* Created with Bootstrap 4 
-
-## Usage
-
-```
-# assumed import of bootstrap and bootstrap-table assets
-<script src="/path/to/bootstrap-table-pipeline.js"></script>
-...
-<table id="pipeline_table" 
-    class="table table-striped"
-    data-method='post'
-    data-use-pipeline="true"
-    data-pipeline-size="5000"
-    data-pagination="true"
-    data-side-pagination="server"
-    data-page-size="50">
-    <thead><tr>
-        <th data-field="type" data-sortable="true">Type</th>
-        <th data-field="value" data-sortable="true">Value</th>
-        <th data-field="date" data-sortable="true">Date</th>
-    </tr></thead>
-</table>
-```
-
-## Options
-
-### usePipeline
-
-* type: Boolean
-* description: Set true to enable pipelining
-* default: `false`
-
-## pipelineSize
-
-* type: Integer
-* description: Size of each cache window. Must be greater than 0
-* default: `1000`    
-
-## Events
-
-### onCachedDataHit(cached-data-hit.bs.table)
-
-* Fires when paging was able to use the locally cached data.
-
-### onCachedDataReset(cached-data-reset.bs.table)
-
-* Fires when the locally cached data needed to be reset (i.e. on sorting, searching, page size change or paged out of current cache window)

+ 14 - 15
src/extensions/pipeline/extension.json

@@ -1,18 +1,17 @@
 {
-	"name": "Pipeline",
-	"version": "1.0.0",
-	"description": "Plugin to support a hybrid approach to server/client side paging.",
-	"url": "",
-	"example": "#",
-	
-	"plugins": [{
-		"name": "bootstrap-table-pipeline",
-		"url": ""
-	}],
+    "name": "Pipeline",
+    "version": "1.0.0",
+    "description": "Plugin to support a hybrid approach to server/client side paging.",
+    "url": "",
+    "example": "#",
 
-	"author": {
-		"name": "doug-the-guy",
-		"image": ""
-	}
-}
+    "plugins": [{
+        "name": "bootstrap-table-pipeline",
+        "url": ""
+    }],
 
+    "author": {
+        "name": "doug-the-guy",
+        "image": ""
+    }
+}

+ 32 - 0
src/utils/index.js

@@ -341,6 +341,16 @@ export default {
       .replace(/&#39;/g, '\'')
   },
 
+  removeHTML (text) {
+    if (!text) {
+      return text
+    }
+    return text.toString()
+      .replace(/(<([^>]+)>)/ig, '')
+      .replace(/&[#A-Za-z0-9]+;/gi, '')
+      .trim()
+  },
+
   getRealDataAttr (dataAttr) {
     for (const [attr, value] of Object.entries(dataAttr)) {
       const auxAttr = attr.split(/(?=[A-Z])/).join('-').toLowerCase()
@@ -506,5 +516,27 @@ export default {
       return arg
     }
     return $.extend(true, Array.isArray(arg) ? [] : {}, arg)
+  },
+
+  debounce (func, wait, immediate) {
+    let timeout
+
+    return function executedFunction () {
+      const context = this
+      const args = arguments
+
+      const later = function () {
+        timeout = null
+        if (!immediate) func.apply(context, args)
+      }
+
+      const callNow = immediate && !timeout
+
+      clearTimeout(timeout)
+
+      timeout = setTimeout(later, wait)
+
+      if (callNow) func.apply(context, args)
+    }
   }
 }