Browse Source

Merge branch 'feature/multiselect-filtercontrol' of https://github.com/wenzhixin/bootstrap-table into feature/multiselect-filtercontrol

djhvscf 4 years ago
parent
commit
c7a2ef6a71

+ 9 - 9
.stylelintrc

@@ -1,12 +1,12 @@
 {
-  'extends': 'stylelint-config-standard',
-  'rules': {
-    'indentation': null,
-    'selector-pseudo-element-colon-notation': null,
-    'function-comma-space-after': null,
-    'no-descending-specificity': null,
-    'declaration-bang-space-before': null,
-    'number-leading-zero': null,
-    'rule-empty-line-before': null
+  "extends": "stylelint-config-standard",
+  "rules": {
+    "indentation": null,
+    "selector-pseudo-element-colon-notation": null,
+    "function-comma-space-after": null,
+    "no-descending-specificity": null,
+    "declaration-bang-space-before": null,
+    "number-leading-zero": null,
+    "rule-empty-line-before": null
   }
 }

+ 1 - 0
index.d.ts

@@ -202,6 +202,7 @@ interface BootstrapTableOptions{
     onColumnSwitch?: (field, checked) => boolean;
     searchSelector?: boolean;
     strictSearch?: boolean;
+    regexSearch?: boolean;
     multipleSelectRow?: boolean;
     onLoadError?: (status) => boolean;
     buttonsToolbar?: any;

+ 3 - 2
package.json

@@ -20,6 +20,7 @@
     "headr": "^0.0.4",
     "node-sass": "^6.0.0",
     "npm-run-all": "^4.1.5",
+    "rimraf": "^3.0.2",
     "rollup": "^2.6.1",
     "rollup-plugin-babel": "^4.3.3",
     "rollup-plugin-commonjs": "^10.0.0",
@@ -29,7 +30,7 @@
     "rollup-plugin-node-resolve": "^5.0.4",
     "rollup-plugin-terser": "^7.0.0",
     "rollup-plugin-vue": "5.1.9",
-    "stylelint": "^13.3.3",
+    "stylelint": "^13.13.1",
     "stylelint-config-standard": "^22.0.0",
     "vue-template-compiler": "^2.6.10"
   },
@@ -51,7 +52,7 @@
     "css:build:min": "foreach -g \"dist/**/*.css\" -x \"cleancss #{path} -o #{dir}/#{name}.min.css\"",
     "css:build:banner": "foreach -g \"dist/**/*.min.css\" -x \"headr #{path} -o #{path} --version --homepage --author --license\"",
     "css:build": "run-s css:build:*",
-    "clean": "rm -rf dist",
+    "clean": "rimraf dist",
     "build": "run-s lint clean *:build",
     "pre-commit": "run-s lint docs:check"
   },

+ 20 - 1
site/docs/api/table-options.md

@@ -1074,6 +1074,23 @@ The table options are defined in `jQuery.fn.bootstrapTable.defaults`.
 
 - **Example:** [Query Params Type](https://examples.bootstrap-table.com/#options/query-params-type.html)
 
+## regexSearch
+
+- **Attribute:** `data-regex-search`
+
+- **Type:** `Boolean`
+
+- **Detail:**
+
+  Set `true` to enable the regex search.
+  Is this option enabled, you can search with regex e.g. `[47a]$` for all items which ends with a `4`, `7` or `a`.     
+  The regex can be entered with `/[47a]$/gim` or without `[47a]$` flags, the default flags are `gim`.
+
+
+- **Default:** `false`
+
+- **Example:** [Regex Search](https://examples.bootstrap-table.com/#options/regex-search.html)
+
 ## rememberOrder
 
 - **Attribute:** `data-remember-order`
@@ -1161,7 +1178,9 @@ The table options are defined in `jQuery.fn.bootstrapTable.defaults`.
   - Comparisons (<, >, <=, =<, >=, =>).
     Example: 4 is larger than 3.
 
-  Note: If you want to use a custom search input use the [searchSelector](https://bootstrap-table.com/docs/api/table-options/#searchSelector).
+  Notes:
+  - If you want to use a custom search input use the [searchSelector](https://bootstrap-table.com/docs/api/table-options/#searchSelector).
+  - You can also search via regex using the [regexSearch](https://bootstrap-table.com/docs/api/table-options/#regexSearch) option.
 
 - **Default:** `false`
 

+ 78 - 52
src/bootstrap-table.js

@@ -946,11 +946,12 @@ class BootstrapTable {
         return
       }
 
-      let s = this.searchText && (this.fromHtml ? Utils.escapeHTML(this.searchText) : this.searchText).toLowerCase()
+      const rawSearchText = this.searchText && (this.fromHtml ? Utils.escapeHTML(this.searchText) : this.searchText)
+      let searchText = rawSearchText.toLowerCase()
       const f = Utils.isEmptyObject(this.filterColumns) ? null : this.filterColumns
 
       if (this.options.searchAccentNeutralise) {
-        s = Utils.normalizeAccent(s)
+        searchText = Utils.normalizeAccent(searchText)
       }
 
       // Check filter
@@ -994,7 +995,7 @@ class BootstrapTable {
 
       const visibleFields = this.getVisibleFields()
 
-      this.data = s ? this.data.filter((item, i) => {
+      this.data = searchText ? this.data.filter((item, i) => {
         for (let j = 0; j < this.header.fields.length; j++) {
           if (!this.header.searchables[j] || (this.options.visibleSearch && visibleFields.indexOf(this.header.fields[j]) === -1)) {
             continue
@@ -1028,51 +1029,52 @@ class BootstrapTable {
           }
 
           if (typeof value === 'string' || typeof value === 'number') {
-            if (this.options.strictSearch) {
-              if ((`${value}`).toLowerCase() === s) {
-                return true
-              }
-            } else {
-              const largerSmallerEqualsRegex = /(?:(<=|=>|=<|>=|>|<)(?:\s+)?(-?\d+)?|(-?\d+)?(\s+)?(<=|=>|=<|>=|>|<))/gm
-              const matches = largerSmallerEqualsRegex.exec(this.searchText)
-              let comparisonCheck = false
-
-              if (matches) {
-                const operator = matches[1] || `${matches[5]}l`
-                const comparisonValue = matches[2] || matches[3]
-                const int = parseInt(value, 10)
-                const comparisonInt = parseInt(comparisonValue, 10)
-
-                switch (operator) {
-                  case '>':
-                  case '<l':
-                    comparisonCheck = int > comparisonInt
-                    break
-                  case '<':
-                  case '>l':
-                    comparisonCheck = int < comparisonInt
-                    break
-                  case '<=':
-                  case '=<':
-                  case '>=l':
-                  case '=>l':
-                    comparisonCheck = int <= comparisonInt
-                    break
-                  case '>=':
-                  case '=>':
-                  case '<=l':
-                  case '=<l':
-                    comparisonCheck = int >= comparisonInt
-                    break
-                  default:
-                    break
-                }
-              }
+            if (
+              this.options.strictSearch && (`${value}`).toLowerCase() === searchText ||
+              (this.options.regexSearch && Utils.regexCompare(value, rawSearchText))
+            ) {
+              return true
+            }
 
-              if (comparisonCheck || (`${value}`).toLowerCase().includes(s)) {
-                return true
+            const largerSmallerEqualsRegex = /(?:(<=|=>|=<|>=|>|<)(?:\s+)?(-?\d+)?|(-?\d+)?(\s+)?(<=|=>|=<|>=|>|<))/gm
+            const matches = largerSmallerEqualsRegex.exec(this.searchText)
+            let comparisonCheck = false
+
+            if (matches) {
+              const operator = matches[1] || `${matches[5]}l`
+              const comparisonValue = matches[2] || matches[3]
+              const int = parseInt(value, 10)
+              const comparisonInt = parseInt(comparisonValue, 10)
+
+              switch (operator) {
+                case '>':
+                case '<l':
+                  comparisonCheck = int > comparisonInt
+                  break
+                case '<':
+                case '>l':
+                  comparisonCheck = int < comparisonInt
+                  break
+                case '<=':
+                case '=<':
+                case '>=l':
+                case '=>l':
+                  comparisonCheck = int <= comparisonInt
+                  break
+                case '>=':
+                case '=>':
+                case '<=l':
+                case '=<l':
+                  comparisonCheck = int >= comparisonInt
+                  break
+                default:
+                  break
               }
             }
+
+            if (comparisonCheck || (`${value}`).toLowerCase().includes(searchText)) {
+              return true
+            }
           }
         }
         return false
@@ -1658,7 +1660,7 @@ class BootstrapTable {
     return html.join('')
   }
 
-  initBody (fixedScroll) {
+  initBody (fixedScroll, updatedUid) {
     const data = this.getData()
 
     this.trigger('pre-body', data)
@@ -1677,15 +1679,35 @@ class BootstrapTable {
     const rows = []
     const trFragments = $(document.createDocumentFragment())
     let hasTr = false
+    const toExpand = []
 
     this.autoMergeCells = Utils.checkAutoMergeCells(data.slice(this.pageFrom - 1, this.pageTo))
 
     for (let i = this.pageFrom - 1; i < this.pageTo; i++) {
       const item = data[i]
-      const tr = this.initRow(item, i, data, trFragments)
+      let tr = this.initRow(item, i, data, trFragments)
 
       hasTr = hasTr || !!tr
       if (tr && typeof tr === 'string') {
+
+        const uniqueId = this.options.uniqueId
+
+        if (uniqueId && item.hasOwnProperty(uniqueId)) {
+          const itemUniqueId = item[uniqueId]
+
+          const oldTr = this.$body.find(Utils.sprintf('> tr[data-uniqueid="%s"][data-has-detail-view]', itemUniqueId))
+          const oldTrNext = oldTr.next()
+
+          if (oldTrNext.is('tr.detail-view')) {
+
+            toExpand.push(i)
+
+            if (!updatedUid || itemUniqueId !== updatedUid) {
+              tr += oldTrNext[0].outerHTML
+            }
+          }
+        }
+
         if (!this.options.virtualScroll) {
           trFragments.append(tr)
         } else {
@@ -1718,6 +1740,8 @@ class BootstrapTable {
       })
     }
 
+    toExpand.forEach(index => { this.expandRow(index) })
+
     if (!fixedScroll) {
       this.scrollTo(0)
     }
@@ -2549,6 +2573,7 @@ class BootstrapTable {
 
   updateByUniqueId (params) {
     const allParams = Array.isArray(params) ? params : [params]
+    let updatedUid = null
 
     for (const params of allParams) {
       if (!params.hasOwnProperty('id') || !params.hasOwnProperty('row')) {
@@ -2566,12 +2591,13 @@ class BootstrapTable {
       } else {
         $.extend(this.options.data[rowId], params.row)
       }
+      updatedUid = params.id
     }
 
     this.initSearch()
     this.initPagination()
     this.initSort()
-    this.initBody(true)
+    this.initBody(true, updatedUid)
   }
 
   removeByUniqueId (id) {
@@ -3150,14 +3176,14 @@ class BootstrapTable {
     const row = this.data[index]
     const $tr = this.$body.find(Utils.sprintf('> tr[data-index="%s"][data-has-detail-view]', index))
 
-    if ($tr.next().is('tr.detail-view')) {
-      return
-    }
-
     if (this.options.detailViewIcon) {
       $tr.find('a.detail-icon').html(Utils.sprintf(this.constants.html.icon, this.options.iconsPrefix, this.options.icons.detailClose))
     }
 
+    if ($tr.next().is('tr.detail-view')) {
+      return
+    }
+
     $tr.after(Utils.sprintf('<tr class="detail-view"><td colspan="%s"></td></tr>', $tr.children('td').length))
 
     const $element = $tr.next().find('td')

+ 2 - 1
src/constants/index.js

@@ -160,7 +160,7 @@ const CONSTANTS = {
       pagination: ['<ul class="pagination%s">', '</ul>'],
       paginationItem: '<li class="page-item%s"><a class="page-link" aria-label="%s" href="javascript:void(0)">%s</a></li>',
       icon: '<i class="%s %s"></i>',
-      inputGroup: '<div class="input-group">%s<div class="input-group-append">%s</div></div>',
+      inputGroup: '<div class="input-group">%s%s</div>',
       searchInput: '<input class="%s%s" type="text" placeholder="%s">',
       searchButton: '<button class="%s" type="button" name="search" title="%s">%s %s</button>',
       searchClearButton: '<button class="%s" type="button" name="clearSearch" title="%s">%s %s</button>'
@@ -240,6 +240,7 @@ const DEFAULTS = {
   searchHighlight: false,
   searchOnEnterKey: false,
   strictSearch: false,
+  regexSearch: false,
   searchSelector: false,
   visibleSearch: false,
   showButtonIcons: true,

+ 11 - 9
src/extensions/filter-control/bootstrap-table-filter-control.js

@@ -194,29 +194,29 @@ $.BootstrapTable = class extends $.BootstrapTable {
 
   initSearch () {
     const that = this
-    const fp = $.isEmptyObject(that.filterColumnsPartial) ? null : that.filterColumnsPartial
+    const filterPartial = $.isEmptyObject(that.filterColumnsPartial) ? null : that.filterColumnsPartial
 
     super.initSearch()
 
-    if (this.options.sidePagination === 'server' || fp === null) {
+    if (this.options.sidePagination === 'server' || filterPartial === null) {
       return
     }
 
     // Check partial column filter
-    that.data = fp ?
+    that.data = filterPartial ?
       that.data.filter((item, i) => {
         const itemIsExpected = []
         const keys1 = Object.keys(item)
-        const keys2 = Object.keys(fp)
+        const keys2 = Object.keys(filterPartial)
         const keys = keys1.concat(keys2.filter(item => !keys1.includes(item)))
 
         keys.forEach(key => {
           const thisColumn = that.columns[that.fieldsColumnsIndex[key]]
-          const fval = (fp[key] || '').toLowerCase()
+          const filterValue = (filterPartial[key] || '').toLowerCase()
           let value = Utils.unescapeHTML(Utils.getItemField(item, key, false))
           let tmpItemIsExpected
 
-          if (fval === '') {
+          if (filterValue === '') {
             tmpItemIsExpected = true
           } else {
             // Fix #142: search use formatted data
@@ -241,14 +241,13 @@ $.BootstrapTable = class extends $.BootstrapTable {
                   if (this.options.searchAccentNeutralise) {
                     objectValue = Utils.normalizeAccent(objectValue)
                   }
-
-                  tmpItemIsExpected = that.isValueExpected(fval, objectValue, thisColumn, key)
+                  tmpItemIsExpected = that.isValueExpected(filterValue, objectValue, thisColumn, key)
                 })
               } else if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
                 if (this.options.searchAccentNeutralise) {
                   value = Utils.normalizeAccent(value)
                 }
-                tmpItemIsExpected = that.isValueExpected(fval, value, thisColumn, key)
+                tmpItemIsExpected = that.isValueExpected(filterValue, value, thisColumn, key)
               }
             }
           }
@@ -272,6 +271,8 @@ $.BootstrapTable = class extends $.BootstrapTable {
       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 {
       tmpItemIsExpected = (`${value}`).toLowerCase().includes(searchValue)
     }
@@ -373,6 +374,7 @@ $.BootstrapTable = class extends $.BootstrapTable {
     const table = this.$el.closest('table')
     const cookies = UtilsFilterControl.collectBootstrapTableFilterCookies()
     const controls = UtilsFilterControl.getSearchControls(that)
+    const cookies = UtilsFilterControl.collectBootstrapTableFilterCookies()
     // const search = Utils.getSearchInput(this)
     let hasValues = false
     let timeoutId = 0

+ 34 - 0
src/extensions/filter-control/utils.js

@@ -218,6 +218,40 @@ export function setValues (that) {
   }
 }
 
+export function collectBootstrapTableFilterCookies () {
+  const cookies = []
+  const foundCookies = document.cookie.match(/bs\.table\.(filterControl|searchText)/g)
+  const foundLocalStorage = localStorage
+
+  if (foundCookies) {
+    $.each(foundCookies, (i, _cookie) => {
+      let cookie = _cookie
+
+      if (/./.test(cookie)) {
+        cookie = cookie.split('.').pop()
+      }
+
+      if ($.inArray(cookie, cookies) === -1) {
+        cookies.push(cookie)
+      }
+    })
+  }
+  if (foundLocalStorage) {
+    for (let i = 0; i < foundLocalStorage.length; i++) {
+      let cookie = foundLocalStorage.key(i)
+
+      if (/./.test(cookie)) {
+        cookie = cookie.split('.').pop()
+      }
+
+      if (!cookies.includes(cookie)) {
+        cookies.push(cookie)
+      }
+    }
+  }
+  return cookies
+}
+
 export function escapeID (id) {
   // eslint-disable-next-line no-useless-escape
   return String(id).replace(/([:.\[\],])/g, '\\$1')

+ 1 - 0
src/extensions/fixed-columns/bootstrap-table-fixed-columns.js

@@ -225,6 +225,7 @@ $.BootstrapTable = class extends $.BootstrapTable {
     const initFixedBody = ($fixedColumns, $fixedHeader) => {
       $fixedColumns.find('.fixed-table-body').remove()
       $fixedColumns.append(this.$tableBody.clone(true))
+      $fixedColumns.find('.fixed-table-body table').removeAttr('id')
 
       const $fixedBody = $fixedColumns.find('.fixed-table-body')
 

+ 2 - 0
src/extensions/resizable/bootstrap-table-resizable.js

@@ -43,6 +43,8 @@ $.BootstrapTable = class extends $.BootstrapTable {
       .on('column-switch.bs.table page-change.bs.table', () => {
         reInitResizable(this)
       })
+
+    reInitResizable(this)
   }
 
   toggleView (...args) {

+ 12 - 0
src/utils/index.js

@@ -193,6 +193,18 @@ export default {
     return true
   },
 
+  regexCompare (value, search) {
+    try {
+      const regexpParts = search.match(/^\/(.*?)\/([gim]*)$/)
+
+      if (value.toString().search(regexpParts ? new RegExp(regexpParts[1], regexpParts[2]) : new RegExp(search, 'gim')) !== -1) {
+        return true
+      }
+    } catch (e) {
+      return false
+    }
+  },
+
   escapeHTML (text) {
     if (typeof text === 'string') {
       return text