Browse Source

Merge branch 'develop' of https://github.com/wenzhixin/bootstrap-table into add-custom-sorter-functionality-to-multiple-sort

jbarrineau06 5 years ago
parent
commit
b5a7776f8a

+ 69 - 21
CHANGELOG.md

@@ -1,32 +1,80 @@
 ChangeLog
 ---------
 
-### 1.15.6
+### 1.16.0
 
-- **New:** Added `filterControlContainer` option for filter-control extension.
-- **New:** Added `printFormatter` data-attribute supported for print extension.
-- **New:** Added `customSort` option supported for group-by extension.
-- **New:** Added `orderColumns` method for reorder-columns extension.
-- **Remove:** Removed natural-sorting extension.
-- **Update:** Updated event name to lowercase hyphen format for vue component.
+#### Core
+
+- **New:** Added `buttonsOrder` option.
+- **New:** Added `headerStyle` option.
+- **New:** Added `showColumnsSearch` option.
+- **New:** Added `serverSort` option.
+- **New:** Added `unfiltered` parameter for `getData` method.
+- **Update:** Updated `event` name to lowercase hyphen format for vue component.
+- **Update:** Updated `es-AR` locale.
+- **Update:** Updated the default classes of semantic theme.
 - **Update:** Improved the `resize` problem with multiple tables.
-- **Update:** Improved `number` type supported for group-by extension.
-- **Update:** Improved to use `undefinedText` option for print extension.
-- **Update:** Fixed `triggerSearch` not work bug.
+- **Update:** Fixed `checkAll` event bug with sortable checkbox field.
+- **Update:** Fixed `checkbox` and not-found td style errors.
+- **Update:** Fixed `customSearch` return empty array bug.
+- **Update:** Fixed column checkboxes not being disabled when using `toggleAll`.
+- **Update:** Fixed `flat` not polyfilled error in vue cli3.
 - **Update:** Fixed `height` and `border` not aligned bug.
-- **Update:** Fixed `sortStable` not work bug.
-- **Update:** Fixed `pageSize` set to all bug with filter.
 - **Update:** Fixed `jqXHR` `undefined` error using custom ajax.
+- **Update:** Fixed `pageSize` set to all bug with filter.
 - **Update:** Fixed `refreshOptions` bug with radio and checkbox.
-- **Update:** Fixed `checkAll` event bug with sortable checkbox field.
-- **Update:** Fixed checkbox and not-found td style errors.
-- **Update:** Fixed `hideUnusedSelectOptions` not work bug for filter-control extension.
-- **Update:** Fixed filter not work bug with `undefined` for filter-control extension.
-- **Update:** Fixed `virtualScroll` option bug with treegrid extension.
-- **Update:** Fixed input keyboard bug for mobile extension.
-- **Update:** Fixed detail view column reorder bug for reorder-columns extension.
-- **Update:** Fixed `flat` not polyfilled error in vue cli3.
-- **Update:** Removed `resetWidth` method and use `resetView` instead.
+- **Update:** Fixed `removeAll` bug in the last page when sidePagination is server.
+- **Update:** Fixed `search` not always trigger in IE11 bug.
+- **Update:** Fixed `search` width `escape` bug.
+- **Update:** Fixed `showColumns` cannot work of foundation theme.
+- **Update:** Fixed `showFullscreen` bug when setting height.
+- **Update:** Fixed `sort` cannot work after searching.
+- **Update:** Fixed `sortable` style error when using `table-sm`.
+- **Update:** Fixed `sortStable` not work bug.
+- **Update:** Fixed `triggerSearch` not work bug.
+- **Update:** Supported build cross all platforms.
+- **Remove:** Removed `resetWidth` method and use `resetView` instead.
+
+#### Extensions
+
+- **New(cookie):** Added new options to get/set/delete the values by a custom function.
+- **New(cookie):** Added save re-order and resize support.
+- **New(filter-control):** Added `filterControlContainer` option.
+- **New(filter-control):** Added `filterCustomSearch` option.
+- **New(filter-control):** Added object and function support in `filterData` column option.
+- **New(filter-control):** Added support for using sticky-header extension.
+- **New(filter-control):** Added support comparisons search(<, >, <=, =<, >=, =>).
+- **New(fixed-columns):** Added all themes support.
+- **New(fixed-columns):** Added `fixedRightNumber` option.
+- **New(group-by):** Added `customSort` option supported.
+- **New(multiple-sort):** Added `multiSortStrictSort` option.
+- **New(multiple-sort):** Added `multiSort` method.
+- **New(print):** Added `printFormatter` data-attribute supported.
+- **New(reorder-columns):** Added `orderColumns` method.
+- **New(reorder-rows):** Added `search` and `cardView` supported.
+- **Update(sticky-header):** Added support for all themes.
+- **Update(cookie):** Fixed cookie localeStorage not work bug with filter-control.
+- **Update(cookie):** Fixed `minimumCountColumns` not working bug.
+- **Update(cookie):** Improved `cookiesEnabled` to support ' in `data-attribute`.
+- **Update(filter-control):** Fixed `hideUnusedSelectOptions` not work bug.
+- **Update(filter-control):** Fixed filter not work bug with `undefined`.
+- **Update(filter-control):** Fixed missing parameter of `resetSearch` and `filterDataType`.
+- **Update(filter-control):** Fixed `search` with filter-control `search` bug.
+- **Update(fixed-columns):** Fixed checkbox bug with fixed columns.
+- **Update(fixed-columns):** Updated default value to `0` of `fixedNumber` option.
+- **Update(group-by):** Improved `number` type supported.
+- **Update(group-by):** Fixed new table using modal bug.
+- **Update(mobile):** Fixed input keyboard bug.
+- **Update(multiple-sort):** Fixed not destroy bug.
+- **Update(multiple-sort):** Fixed sort not work with `boolean` bug.
+- **Update(print):** Improved to use `undefinedText` option.
+- **Update(print):** Fixed IE11 not work bug.
+- **Update(reorder-columns):** Fixed detail view column reorder bug.
+- **Update(resizable):** Fixed columns resizing not work bug.
+- **Update(resizable):** Fixed not work via JavaScript.
+- **Update(sticky-header):** Fixed not work bug with fullscreen.
+- **Update(treegrid):** Fixed `virtualScroll` option bug.
+- **Remove:** Removed natural-sorting extension.
 
 ### 1.15.5
 

+ 3 - 3
site/docs/api/events.md

@@ -191,8 +191,8 @@ $('#table').on('event-name.bs.table', function (e, arg1, arg2, ...) {
   Fires when remote data is loaded successfully, the parameters contain:
 
   * `data`: the remote data.
-  * `status`: the status code of `jqXHR` (from 1.15.5).
-  * `jqXHR`: jqXHR object, which is a superset of the XMLHTTPRequest object. For more information, see the [jqXHR Type](http://api.jquery.com/Types/#jqXHR) (from 1.15.5).
+  * `status`: the status code of `jqXHR`.
+  * `jqXHR`: jqXHR object, which is a superset of the XMLHTTPRequest object. For more information, see the [jqXHR Type](http://api.jquery.com/Types/#jqXHR).
 
 ## onLoadError
 
@@ -369,7 +369,7 @@ $('#table').on('event-name.bs.table', function (e, arg1, arg2, ...) {
 
 - **jQuery Event:** `scroll-body.bs.table`
 
-- **Parameter:**: `$tableBody` (from 1.15.5)
+- **Parameter:**: `$tableBody`
 
 - **Detail:**
 

+ 3 - 3
site/docs/api/table-options.md

@@ -510,7 +510,7 @@ The table options are defined in `jQuery.fn.bootstrapTable.defaults`.
   Before load remote data, handler the response data format, the parameters object contains:
 
   * `res`: the response data.
-  * `jqXHR`: jqXHR object, which is a superset of the XMLHTTPRequest object. For more information, see the [jqXHR Type](http://api.jquery.com/Types/#jqXHR) (from 1.15.5).
+  * `jqXHR`: jqXHR object, which is a superset of the XMLHTTPRequest object. For more information, see the [jqXHR Type](http://api.jquery.com/Types/#jqXHR).
 
 - **Default:** `function(res) { return res }`
 
@@ -999,11 +999,11 @@ The table options are defined in `jQuery.fn.bootstrapTable.defaults`.
 
 - **Detail:**
 
-  The custom search function is executed instead of built-in search function, takes two parameters:
+  The custom search function is executed instead of built-in search function, takes three parameters:
 
   * `data`: the table data.
   * `text`: the search text.
-  * `filter`: the filter object from `filterBy` method (from 1.15.4).
+  * `filter`: the filter object from `filterBy` method.
 
   Example usage:
 

+ 26 - 5
site/docs/extensions/filter-control.md

@@ -128,11 +128,11 @@ Dependence if you use the datepicker option: [bootstrap-datepicker](https://gith
 
 - **Detail:**
 
-   Set custom select filter values, use   
-   `var:variable` to load from a variable   
-   `obj:variable.key` to load from a object   
-   `url:http://www.example.com/data.json` to load from a remote json file   
-   `json:{key:data}` to load from a json string.   
+   Set custom select filter values, use
+   `var:variable` to load from a variable
+   `obj:variable.key` to load from a object
+   `url:http://www.example.com/data.json` to load from a remote json file
+   `json:{key:data}` to load from a json string.
    `func:functionName` to load from a function.
 
 - **Default:** `undefined`
@@ -209,6 +209,27 @@ Dependence if you use the datepicker option: [bootstrap-datepicker](https://gith
 
 - **Default:** `'asc'`
 
+### filterCustomSearch
+
+- **Attribute:** `data-filter-custom-search`
+
+- **type:** `function`
+
+- **Detail:**
+
+   The custom search function is executed instead of built-in search function, takes four parameters:
+
+     * `text`: the search text.
+     * `value`: the the value of the column to compare.
+     * `field`: the column field name.
+     * `data`: the table data.
+
+   Return `false` to filter out the current column/row.
+   Return `true` to not filter out the current column/row.
+   Return `null` to skip the custom search for the current value.
+
+- **Default:** `undefined`
+
 ### Icons
 
 * clear: 'glyphicon-trash icon-clear'

+ 18 - 3
site/docs/extensions/fixed-columns.md

@@ -27,7 +27,7 @@ toc: true
 
 - **Detail:**
 
-  set `true` to enable fixed columns.
+  Set `true` to enable fixed columns.
 
 - **Default:** `false`
 
@@ -39,6 +39,21 @@ toc: true
 
 - **Detail:**
 
-  the number of fixed columns.
+  The number of the left fixed columns.
 
-- **Default:** `1`
+- **Default:** `0`
+
+### fixedRightNumber
+
+- **type:** Number
+
+- **Detail:**
+
+  The number of the right fixed columns.
+
+- **Default:** `0`
+
+## Note
+
+* This extension does not support `detailView` option.
+* This extension does not support `cardView` option.

+ 6 - 4
src/bootstrap-table.js

@@ -1986,16 +1986,18 @@ class BootstrapTable {
   horizontalScroll () {
     // horizontal scroll event
     // TODO: it's probably better improving the layout than binding to scroll event
-    this.$tableBody.off('scroll').on('scroll', ({currentTarget}) => {
+    this.$tableBody.off('scroll').on('scroll', () => {
+      const scrollLeft = this.$tableBody.scrollLeft()
+
       if (this.options.showHeader && this.options.height) {
-        this.$tableHeader.scrollLeft($(currentTarget).scrollLeft())
+        this.$tableHeader.scrollLeft(scrollLeft)
       }
 
       if (this.options.showFooter && !this.options.cardView) {
-        this.$tableFooter.scrollLeft($(currentTarget).scrollLeft())
+        this.$tableFooter.scrollLeft(scrollLeft)
       }
 
-      this.trigger('scroll-body', $(currentTarget))
+      this.trigger('scroll-body', this.$tableBody)
     })
   }
 

+ 14 - 0
src/extensions/editable/bootstrap-table-editable.js

@@ -36,6 +36,7 @@ $.BootstrapTable = class extends $.BootstrapTable {
       return
     }
 
+    this.editedCells = []
     $.each(this.columns, (i, column) => {
       if (!column.editable) {
         return
@@ -59,6 +60,12 @@ $.BootstrapTable = class extends $.BootstrapTable {
       column.formatter = (value, row, index) => {
         let result = Utils.calculateObjectValue(column, column._formatter, [value, row, index], value)
         result = typeof result === 'undefined' || result === null ? this.options.undefinedText : result
+        if (this.options.uniqueId !== undefined) {
+          const uniqueId = Utils.getItemField(row, this.options.uniqueId, false)
+          if ($.inArray(column.field + uniqueId, this.editedCells) !== -1) {
+            result = value
+          }
+        }
 
         $.each(column, processDataOptions)
 
@@ -117,6 +124,13 @@ $.BootstrapTable = class extends $.BootstrapTable {
         const row = data[rowIndex]
         const oldValue = row[column.field]
 
+        if (this.options.uniqueId !== undefined) {
+          const uniqueId = Utils.getItemField(row, this.options.uniqueId, false)
+          if ($.inArray(column.field + uniqueId, this.editedCells) === -1) {
+            this.editedCells.push(column.field + uniqueId)
+          }
+        }
+
         $this.data('value', submitValue)
         row[column.field] = submitValue
         this.trigger('editable-save', column.field, row, rowIndex, oldValue, $this)

+ 50 - 5
src/extensions/filter-control/bootstrap-table-filter-control.js

@@ -707,9 +707,10 @@ $.BootstrapTable = class extends $.BootstrapTable {
           const thisColumn = that.columns[that.fieldsColumnsIndex[key]]
           const fval = (fp[key] || '').toLowerCase()
           let value = Utils.getItemField(item, key, false)
+          let tmpItemIsExpected
 
           if (fval === '') {
-            itemIsExpected.push(true)
+            tmpItemIsExpected = true
           } else {
             // Fix #142: search use formatted data
             if (thisColumn && thisColumn.searchFormatter) {
@@ -723,18 +724,62 @@ $.BootstrapTable = class extends $.BootstrapTable {
 
             if ($.inArray(key, that.header.fields) !== -1) {
               if (value === undefined || value === null) {
-                itemIsExpected.push(false)
+                tmpItemIsExpected = false
               } else if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
                 if (thisColumn.filterStrictSearch) {
-                  itemIsExpected.push(value.toString().toLowerCase() === fval.toString().toLowerCase())
+                  tmpItemIsExpected = value.toString().toLowerCase() === fval.toString().toLowerCase()
                 } else if (thisColumn.filterStartsWithSearch) {
-                  itemIsExpected.push((`${value}`).toLowerCase().indexOf(fval) === 0)
+                  tmpItemIsExpected = (`${value}`).toLowerCase().indexOf(fval) === 0
                 } else {
-                  itemIsExpected.push((`${value}`).toLowerCase().includes(fval))
+                  tmpItemIsExpected = (`${value}`).toLowerCase().includes(fval)
+                }
+
+                const largerSmallerEqualsRegex = /(?:(<=|=>|=<|>=|>|<)(?:\s+)?(\d+)?|(\d+)?(\s+)?(<=|=>|=<|>=|>|<))/gm
+                const matches = largerSmallerEqualsRegex.exec(fval)
+
+                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':
+                      tmpItemIsExpected = int > comparisonInt
+                      break
+                    case '<':
+                    case '>l':
+                      tmpItemIsExpected = int < comparisonInt
+                      break
+                    case '<=':
+                    case '=<':
+                    case '>=l':
+                    case '=>l':
+                      tmpItemIsExpected = int <= comparisonInt
+                      break
+                    case '>=':
+                    case '=>':
+                    case '<=l':
+                    case '=<l':
+                      tmpItemIsExpected = int >= comparisonInt
+                      break
+                    default:
+                      break
+                  }
+                }
+
+                if (thisColumn.filterCustomSearch) {
+                  const customSearchResult = Utils.calculateObjectValue(that, thisColumn.filterCustomSearch, [fval, value, key, that.options.data], true)
+                  if (customSearchResult !== null) {
+                    tmpItemIsExpected = customSearchResult
+                  }
                 }
               }
             }
           }
+
+          itemIsExpected.push(tmpItemIsExpected)
         })
 
         return !itemIsExpected.includes(false)

+ 278 - 60
src/extensions/fixed-columns/bootstrap-table-fixed-columns.js

@@ -2,112 +2,330 @@
  * @author zhixin wen <wenzhixin2010@gmail.com>
  */
 
+const Utils = $.fn.bootstrapTable.utils
+
+// Reasonable defaults
+const PIXEL_STEP = 10
+const LINE_HEIGHT = 40
+const PAGE_HEIGHT = 800
+
+function normalizeWheel (event) {
+  let sX = 0 // spinX
+  let sY = 0 // spinY
+  let pX = 0 // pixelX
+  let pY = 0 // pixelY
+
+  // Legacy
+  if ('detail' in event) { sY = event.detail }
+  if ('wheelDelta' in event) { sY = -event.wheelDelta / 120 }
+  if ('wheelDeltaY' in event) { sY = -event.wheelDeltaY / 120 }
+  if ('wheelDeltaX' in event) { sX = -event.wheelDeltaX / 120 }
+
+  // side scrolling on FF with DOMMouseScroll
+  if ( 'axis' in event && event.axis === event.HORIZONTAL_AXIS ) {
+    sX = sY
+    sY = 0
+  }
+
+  pX = sX * PIXEL_STEP
+  pY = sY * PIXEL_STEP
+
+  if ('deltaY' in event) { pY = event.deltaY }
+  if ('deltaX' in event) { pX = event.deltaX }
+
+  if ((pX || pY) && event.deltaMode) {
+    if (event.deltaMode === 1) { // delta in LINE units
+      pX *= LINE_HEIGHT
+      pY *= LINE_HEIGHT
+    } else { // delta in PAGE units
+      pX *= PAGE_HEIGHT
+      pY *= PAGE_HEIGHT
+    }
+  }
+
+  // Fall-back if spin cannot be determined
+  if (pX && !sX) { sX = (pX < 1) ? -1 : 1 }
+  if (pY && !sY) { sY = (pY < 1) ? -1 : 1 }
+
+  return {
+    spinX: sX,
+    spinY: sY,
+    pixelX: pX,
+    pixelY: pY
+  }
+}
+
 $.extend($.fn.bootstrapTable.defaults, {
   fixedColumns: false,
-  fixedNumber: 1
+  fixedNumber: 0,
+  fixedRightNumber: 0
 })
 
 $.BootstrapTable = class extends $.BootstrapTable {
 
-  fitHeader (...args) {
-    super.fitHeader(...args)
+  fixedColumnsSupported () {
+    return this.options.fixedColumns &&
+      !this.options.detailView &&
+      !this.options.cardView
+  }
 
-    if (!this.options.fixedColumns) {
-      return
-    }
+  initContainer () {
+    super.initContainer()
 
-    if (this.$el.is(':hidden')) {
+    if (!this.fixedColumnsSupported()) {
       return
     }
 
-    this.$container.find('.fixed-table-header-columns').remove()
-    this.$fixedHeader = $('<div class="fixed-table-header-columns"></div>')
-    this.$fixedHeader.append(this.$tableHeader.find('>table').clone(true))
-    this.$tableHeader.after(this.$fixedHeader)
+    if (this.options.fixedNumber) {
+      this.$tableContainer.append('<div class="fixed-columns"></div>')
+      this.$fixedColumns = this.$tableContainer.find('.fixed-columns')
+    }
 
-    const width = this.getFixedColumnsWidth()
+    if (this.options.fixedRightNumber) {
+      this.$tableContainer.append('<div class="fixed-columns-right"></div>')
+      this.$fixedColumnsRight = this.$tableContainer.find('.fixed-columns-right')
+    }
+  }
 
-    this.$fixedHeader.css({
-      top: 0,
-      width,
-      height: this.$tableHeader.outerHeight(true)
-    })
+  initBody (...args) {
+    super.initBody(...args)
 
-    this.initFixedColumnsBody()
+    if (!this.fixedColumnsSupported()) {
+      return
+    }
 
-    this.$fixedBody.css({
-      top: this.$tableHeader.outerHeight(true),
-      width,
-      height: this.$tableBody.outerHeight(true) - 1
-    })
+    if (this.options.showHeader && this.options.height) {
+      return
+    }
 
+    this.initFixedColumnsBody()
     this.initFixedColumnsEvents()
   }
 
-  initBody (...args) {
-    super.initBody(...args)
+  trigger (...args) {
+    super.trigger(...args)
 
-    if (!this.options.fixedColumns) {
+    if (!this.fixedColumnsSupported()) {
       return
     }
 
-    if (this.options.showHeader && this.options.height) {
+    if (args[0] === 'post-header') {
+      this.initFixedColumnsHeader()
+    } else if (args[0] === 'scroll-body') {
+      if (this.needFixedColumns && this.options.fixedNumber) {
+        this.$fixedBody.scrollTop(this.$tableBody.scrollTop())
+      }
+
+      if (this.needFixedColumns && this.options.fixedRightNumber) {
+        this.$fixedBodyRight.scrollTop(this.$tableBody.scrollTop())
+      }
+    }
+  }
+
+  updateSelected () {
+    super.updateSelected()
+
+    if (!this.fixedColumnsSupported()) {
       return
     }
 
-    this.initFixedColumnsBody()
+    this.$tableBody.find('tr').each((i, el) => {
+      const $el = $(el)
+      const index = $el.data('index')
+      const classes = $el.attr('class')
+
+      const inputSelector = `[name="${this.options.selectItemName}"]`
+      const $input = $el.find(inputSelector)
+
+      if (typeof index === undefined) {
+        return
+      }
 
-    this.$fixedBody.css({
-      top: 0,
-      width: this.getFixedColumnsWidth(),
-      height: this.$tableHeader.outerHeight(true) + this.$tableBody.outerHeight(true)
+      const updateFixedBody = ($fixedHeader, $fixedBody) => {
+        const $tr = $fixedBody.find(`tr[data-index="${index}"]`)
+        $tr.attr('class', classes)
+
+        if ($input.length) {
+          $tr.find(inputSelector).prop('checked', $input.prop('checked'))
+        }
+
+        if (this.$selectAll.length) {
+          $fixedHeader.add($fixedBody)
+            .find('[name="btSelectAll"]')
+            .prop('checked', this.$selectAll.prop('checked'))
+        }
+      }
+
+      if (this.$fixedBody && this.options.fixedNumber) {
+        updateFixedBody(this.$fixedHeader, this.$fixedBody)
+      }
+
+      if (this.$fixedBodyRight && this.options.fixedRightNumber) {
+        updateFixedBody(this.$fixedHeaderRight, this.$fixedBodyRight)
+      }
     })
+  }
+
+  initFixedColumnsHeader () {
+    if (this.options.height) {
+      this.needFixedColumns = this.$tableHeader.outerWidth(true) < this.$tableHeader.find('table').outerWidth(true)
+    } else {
+      this.needFixedColumns = this.$tableBody.outerWidth(true) < this.$tableBody.find('table').outerWidth(true)
+    }
+
+    const initFixedHeader = ($fixedColumns, isRight) => {
+      $fixedColumns.find('.fixed-table-header').remove()
+      $fixedColumns.append(this.$tableHeader.clone(true))
+
+      $fixedColumns.css({
+        width: this.getFixedColumnsWidth(isRight)
+      })
+      return $fixedColumns.find('.fixed-table-header')
+    }
+
+    if (this.needFixedColumns && this.options.fixedNumber) {
+      this.$fixedHeader = initFixedHeader(this.$fixedColumns)
+      this.$fixedHeader.css('margin-right', '')
+    } else if (this.$fixedColumns) {
+      this.$fixedColumns.html('').css('width', '')
+    }
+
+    if (this.needFixedColumns && this.options.fixedRightNumber) {
+      this.$fixedHeaderRight = initFixedHeader(this.$fixedColumnsRight, true)
+      this.$fixedHeaderRight.scrollLeft(this.$fixedHeaderRight.find('table').width())
+    } else if (this.$fixedColumnsRight) {
+      this.$fixedColumnsRight.html('').css('width', '')
+    }
 
+    this.initFixedColumnsBody()
     this.initFixedColumnsEvents()
   }
 
   initFixedColumnsBody () {
-    this.$container.find('.fixed-table-body-columns').remove()
-    this.$fixedBody = $('<div class="fixed-table-body-columns"></div>')
-    this.$fixedBody.append(this.$tableBody.find('>table').clone(true))
-    this.$tableBody.after(this.$fixedBody)
+    const initFixedBody = ($fixedColumns, $fixedHeader) => {
+      $fixedColumns.find('.fixed-table-body').remove()
+      $fixedColumns.append(this.$tableBody.clone(true))
+
+      const $fixedBody = $fixedColumns.find('.fixed-table-body')
+
+      const tableBody = this.$tableBody.get(0)
+      const scrollHeight = tableBody.scrollWidth > tableBody.clientWidth
+        ? Utils.getScrollBarWidth() : 0
+      const height = this.$tableContainer.outerHeight(true) - scrollHeight - 1
+
+      $fixedColumns.css({
+        height
+      })
+
+      $fixedBody.css({
+        height: height - $fixedHeader.height()
+      })
+
+      return $fixedBody
+    }
+
+    if (this.needFixedColumns && this.options.fixedNumber) {
+      this.$fixedBody = initFixedBody(this.$fixedColumns, this.$fixedHeader)
+    }
+
+    if (this.needFixedColumns && this.options.fixedRightNumber) {
+      this.$fixedBodyRight = initFixedBody(this.$fixedColumnsRight, this.$fixedHeaderRight)
+      this.$fixedBodyRight.scrollLeft(this.$fixedBodyRight.find('table').width())
+      this.$fixedBodyRight.css('overflow-y', this.options.height ? 'auto' : 'hidden')
+    }
   }
 
-  getFixedColumnsWidth () {
-    const visibleFields = this.getVisibleFields()
+  getFixedColumnsWidth (isRight) {
+    let visibleFields = this.getVisibleFields()
     let width = 0
+    let fixedNumber = this.options.fixedNumber
+    let marginRight = 0
 
-    for (let i = 0; i < this.options.fixedNumber; i++) {
+    if (isRight) {
+      visibleFields = visibleFields.reverse()
+      fixedNumber = this.options.fixedRightNumber
+      marginRight = parseInt(this.$tableHeader.css('margin-right'), 10)
+    }
+
+    for (let i = 0; i < fixedNumber; i++) {
       width += this.$header.find(`th[data-field="${visibleFields[i]}"]`).outerWidth(true)
     }
 
-    return width + 1
+    return width + marginRight + 1
   }
 
   initFixedColumnsEvents () {
-    // events
-    this.$tableBody.off('scroll.fixed-columns').on('scroll.fixed-columns', e => {
-      this.$fixedBody.find('table').css('top', -$(e.currentTarget).scrollTop())
-    })
+    const toggleHover = (e, toggle) => {
+      const tr = `tr[data-index="${$(e.currentTarget).data('index')}"]`
+      let $trs = this.$tableBody.find(tr)
 
-    this.$body.find('> tr[data-index]').off('hover').hover(e => {
-      const index = $(e.currentTarget).data('index')
-      this.$fixedBody.find(`tr[data-index="${index}"]`)
-        .css('background-color', $(e.currentTarget).css('background-color'))
-    }, e => {
-      const index = $(e.currentTarget).data('index')
-      const $tr = this.$fixedBody.find(`tr[data-index="${index}"]`)
-      $tr.attr('style', $tr.attr('style').replace(/background-color:.*;/, ''))
-    })
+      if (this.$fixedBody) {
+        $trs = $trs.add(this.$fixedBody.find(tr))
+      }
+      if (this.$fixedBodyRight) {
+        $trs = $trs.add(this.$fixedBodyRight.find(tr))
+      }
 
-    this.$fixedBody.find('tr[data-index]').off('hover').hover(e => {
-      const index = $(e.currentTarget).data('index')
-      this.$body.find(`tr[data-index="${index}"]`)
-        .css('background-color', $(e.currentTarget).css('background-color'))
+      $trs.css('background-color', toggle ? $(e.currentTarget).css('background-color') : '')
+    }
+
+    this.$tableBody.find('tr').hover(e => {
+      toggleHover(e, true)
     }, e => {
-      const index = $(e.currentTarget).data('index')
-      const $tr = this.$body.find(`> tr[data-index="${index}"]`)
-      $tr.attr('style', $tr.attr('style').replace(/background-color:.*;/, ''))
+      toggleHover(e, false)
     })
+
+    const isFirefox = typeof navigator !== 'undefined' &&
+      navigator.userAgent.toLowerCase().indexOf('firefox') > -1
+    const mousewheel = isFirefox ? 'DOMMouseScroll' : 'mousewheel'
+    const updateScroll = (e, fixedBody) => {
+      const normalized = normalizeWheel(e)
+      const deltaY = Math.ceil(normalized.pixelY)
+      const top = this.$tableBody.scrollTop() + deltaY
+
+      if (
+        deltaY < 0 && top > 0 ||
+        deltaY > 0 && top < fixedBody.scrollHeight - fixedBody.clientHeight
+      ) {
+        e.preventDefault()
+      }
+
+      this.$tableBody.scrollTop(top)
+      if (this.$fixedBody) {
+        this.$fixedBody.scrollTop(top)
+      }
+      if (this.$fixedBodyRight) {
+        this.$fixedBodyRight.scrollTop(top)
+      }
+    }
+
+    if (this.needFixedColumns && this.options.fixedNumber) {
+      this.$fixedBody.find('tr').hover(e => {
+        toggleHover(e, true)
+      }, e => {
+        toggleHover(e, false)
+      })
+
+      this.$fixedBody[0].addEventListener(mousewheel, e => {
+        updateScroll(e, this.$fixedBody[0])
+      })
+    }
+
+    if (this.needFixedColumns && this.options.fixedRightNumber) {
+      this.$fixedBodyRight.find('tr').hover(e => {
+        toggleHover(e, true)
+      }, e => {
+        toggleHover(e, false)
+      })
+
+      this.$fixedBodyRight.off('scroll').on('scroll', () => {
+        const top = this.$fixedBodyRight.scrollTop()
+
+        this.$tableBody.scrollTop(top)
+        if (this.$fixedBody) {
+          this.$fixedBody.scrollTop(top)
+        }
+      })
+    }
   }
 }

+ 14 - 16
src/extensions/fixed-columns/bootstrap-table-fixed-columns.scss

@@ -1,27 +1,25 @@
-.fixed-table-header-columns,
-.fixed-table-body-columns {
+.fixed-columns,
+.fixed-columns-right {
   position: absolute;
+  top: 0;
+  height: 100%;
   background-color: #fff;
   box-sizing: border-box;
-  overflow: hidden;
   z-index: 1;
 }
 
-.fixed-table-header-columns {
-  z-index: 2;
-}
+.fixed-columns {
+  left: 0;
 
-.fixed-table-header-columns .table,
-.fixed-table-body-columns .table {
-  border-right: 1px solid #ddd;
+  .fixed-table-body {
+    overflow: hidden!important;
+  }
 }
 
-.fixed-table-header-columns .table.table-no-bordered,
-.fixed-table-body-columns .table.table-no-bordered {
-  border-right: 1px solid transparent;
-}
+.fixed-columns-right {
+  right: 0;
 
-.fixed-table-body-columns table {
-  position: absolute;
-  animation: none;
+  .fixed-table-body {
+    overflow-x: hidden!important;
+  }
 }

+ 2 - 6
src/extensions/sticky-header/bootstrap-table-sticky-header.js

@@ -13,10 +13,6 @@ $.extend($.fn.bootstrapTable.defaults, {
   stickyHeaderOffsetRight: 0
 })
 
-const hiddenClass = {
-  bootstrap3: 'hidden'
-}[$.fn.bootstrapTable.theme] || 'd-none'
-
 $.BootstrapTable = class extends $.BootstrapTable {
   initHeader (...args) {
     super.initHeader(...args)
@@ -92,7 +88,7 @@ $.BootstrapTable = class extends $.BootstrapTable {
         $(el).css('min-width', this.$header.find('tr:eq(0)').find('th').eq(index).css('width'))
       })
       // match bootstrap table style
-      this.$stickyContainer.removeClass(hiddenClass).addClass('fix-sticky fixed-table-container')
+      this.$stickyContainer.show().addClass('fix-sticky fixed-table-container')
       // stick it in position
       let stickyHeaderOffsetLeft = this.options.stickyHeaderOffsetLeft
       let stickyHeaderOffsetRight = this.options.stickyHeaderOffsetRight
@@ -111,7 +107,7 @@ $.BootstrapTable = class extends $.BootstrapTable {
       // match clone and source header positions when left-right scroll
       this.matchPositionX()
     } else {
-      this.$stickyContainer.removeClass('fix-sticky').addClass(hiddenClass)
+      this.$stickyContainer.removeClass('fix-sticky').hide()
     }
   }
 

+ 5 - 1
src/themes/foundation/bootstrap-table-foundation.js

@@ -76,8 +76,12 @@ $.BootstrapTable = class extends $.BootstrapTable {
     $dropdowns.off('click').on('click', e => {
       const $this = $(e.currentTarget)
       e.stopPropagation()
-      $dropdowns.not($this).next().foundation('close')
+
       $this.next().foundation('toggle')
+
+      if ($dropdowns.not($this).length) {
+        $dropdowns.not($this).next().foundation('close')
+      }
     })
 
     $(document).off('click.bs.dropdown.foundation').on('click.bs.dropdown.foundation', () => {

+ 11 - 13
src/themes/foundation/bootstrap-table-foundation.scss

@@ -21,22 +21,20 @@
       height: 2.5293rem;
     }
 
-    .keep-open {
-      .dropdown-container {
-        .button {
-          &:hover .menu {
-            background: #fff;
-          }
+    .keep-open.dropdown-container {
+      .button {
+        &:hover .menu {
+          background: #fff;
         }
+      }
 
-        .menu {
-          li {
-            padding: 5px 0;
+      .menu {
+        li {
+          padding: 5px 0;
 
-            label {
-              white-space: nowrap;
-              text-align: left;
-            }
+          label {
+            white-space: nowrap;
+            text-align: left;
           }
         }
       }

+ 1 - 1
src/themes/semantic/bootstrap-table-semantic.js

@@ -5,7 +5,7 @@
  */
 
 $.extend($.fn.bootstrapTable.defaults, {
-  classes: 'ui selectable celled table',
+  classes: 'ui selectable celled table unstackable',
   buttonsPrefix: '',
   buttonsClass: 'ui button'
 })