Browse Source

Merge pull request #4268 from wenzhixin/feature/performance-improve

Feature/performance improve
文翼 6 years ago
parent
commit
4ded825e46
4 changed files with 3018 additions and 2880 deletions
  1. 2234 2880
      src/bootstrap-table.js
  2. 415 0
      src/constants/index.js
  3. 249 0
      src/utils/index.js
  4. 120 0
      src/virtual-scroll/index.js

File diff suppressed because it is too large
+ 2234 - 2880
src/bootstrap-table.js


+ 415 - 0
src/constants/index.js

@@ -0,0 +1,415 @@
+let bootstrapVersion = 4
+try {
+  const rawVersion = $.fn.dropdown.Constructor.VERSION
+
+  // Only try to parse VERSION if is is defined.
+  // It is undefined in older versions of Bootstrap (tested with 3.1.1).
+  if (rawVersion !== undefined) {
+    bootstrapVersion = parseInt(rawVersion, 10)
+  }
+} catch (e) {
+  // ignore
+}
+
+const CONSTANTS = {
+  3: {
+    theme: 'bootstrap3',
+    iconsPrefix: 'glyphicon',
+    icons: {
+      paginationSwitchDown: 'glyphicon-collapse-down icon-chevron-down',
+      paginationSwitchUp: 'glyphicon-collapse-up icon-chevron-up',
+      refresh: 'glyphicon-refresh icon-refresh',
+      toggleOff: 'glyphicon-list-alt icon-list-alt',
+      toggleOn: 'glyphicon-list-alt icon-list-alt',
+      columns: 'glyphicon-th icon-th',
+      detailOpen: 'glyphicon-plus icon-plus',
+      detailClose: 'glyphicon-minus icon-minus',
+      fullscreen: 'glyphicon-fullscreen'
+    },
+    classes: {
+      buttonsPrefix: 'btn',
+      buttons: 'default',
+      buttonsGroup: 'btn-group',
+      buttonsDropdown: 'btn-group',
+      pull: 'pull',
+      inputGroup: '',
+      input: 'form-control',
+      paginationDropdown: 'btn-group dropdown',
+      dropup: 'dropup',
+      dropdownActive: 'active',
+      paginationActive: 'active'
+    },
+    html: {
+      toobarDropdow: ['<ul class="dropdown-menu" role="menu">', '</ul>'],
+      toobarDropdowItem: '<li role="menuitem"><label>%s</label></li>',
+      pageDropdown: ['<ul class="dropdown-menu" role="menu">', '</ul>'],
+      pageDropdownItem: '<li role="menuitem" class="%s"><a href="#">%s</a></li>',
+      dropdownCaret: '<span class="caret"></span>',
+      pagination: ['<ul class="pagination%s">', '</ul>'],
+      paginationItem: '<li class="page-item%s"><a class="page-link" href="#">%s</a></li>',
+      icon: '<i class="%s %s"></i>'
+    }
+  },
+  4: {
+    theme: 'bootstrap4',
+    iconsPrefix: 'fa',
+    icons: {
+      paginationSwitchDown: 'fa-caret-square-down',
+      paginationSwitchUp: 'fa-caret-square-up',
+      refresh: 'fa-sync',
+      toggleOff: 'fa-toggle-off',
+      toggleOn: 'fa-toggle-on',
+      columns: 'fa-th-list',
+      fullscreen: 'fa-arrows-alt',
+      detailOpen: 'fa-plus',
+      detailClose: 'fa-minus'
+    },
+    classes: {
+      buttonsPrefix: 'btn',
+      buttons: 'secondary',
+      buttonsGroup: 'btn-group',
+      buttonsDropdown: 'btn-group',
+      pull: 'float',
+      inputGroup: '',
+      input: 'form-control',
+      paginationDropdown: 'btn-group dropdown',
+      dropup: 'dropup',
+      dropdownActive: 'active',
+      paginationActive: 'active'
+    },
+    html: {
+      toobarDropdow: ['<div class="dropdown-menu dropdown-menu-right">', '</div>'],
+      toobarDropdowItem: '<label class="dropdown-item">%s</label>',
+      pageDropdown: ['<div class="dropdown-menu">', '</div>'],
+      pageDropdownItem: '<a class="dropdown-item %s" href="#">%s</a>',
+      dropdownCaret: '<span class="caret"></span>',
+      pagination: ['<ul class="pagination%s">', '</ul>'],
+      paginationItem: '<li class="page-item%s"><a class="page-link" href="#">%s</a></li>',
+      icon: '<i class="%s %s"></i>'
+    }
+  }
+}[bootstrapVersion]
+
+const DEFAULTS = {
+  height: undefined,
+  classes: 'table table-bordered table-hover',
+  theadClasses: '',
+  rowStyle (row, index) {
+    return {}
+  },
+  rowAttributes (row, index) {
+    return {}
+  },
+  undefinedText: '-',
+  locale: undefined,
+  sortable: true,
+  sortClass: undefined,
+  silentSort: true,
+  sortName: undefined,
+  sortOrder: 'asc',
+  sortStable: false,
+  rememberOrder: false,
+  customSort: undefined,
+  columns: [
+    []
+  ],
+  data: [],
+  url: undefined,
+  method: 'get',
+  cache: true,
+  contentType: 'application/json',
+  dataType: 'json',
+  ajax: undefined,
+  ajaxOptions: {},
+  queryParams (params) {
+    return params
+  },
+  queryParamsType: 'limit', // 'limit', undefined
+  responseHandler (res) {
+    return res
+  },
+  totalField: 'total',
+  totalNotFilteredField: 'totalNotFiltered',
+  dataField: 'rows',
+  pagination: false,
+  onlyInfoPagination: false,
+  showExtendedPagination: false,
+  paginationLoop: true,
+  sidePagination: 'client', // client or server
+  totalRows: 0,
+  totalNotFiltered: 0,
+  pageNumber: 1,
+  pageSize: 10,
+  pageList: [10, 25, 50, 100],
+  paginationHAlign: 'right', // right, left
+  paginationVAlign: 'bottom', // bottom, top, both
+  paginationDetailHAlign: 'left', // right, left
+  paginationPreText: '&lsaquo;',
+  paginationNextText: '&rsaquo;',
+  paginationSuccessivelySize: 5, // Maximum successively number of pages in a row
+  paginationPagesBySide: 1, // Number of pages on each side (right, left) of the current page.
+  paginationUseIntermediate: false, // Calculate intermediate pages for quick access
+  search: false,
+  searchOnEnterKey: false,
+  strictSearch: false,
+  trimOnSearch: true,
+  searchAlign: 'right',
+  searchTimeOut: 500,
+  searchText: '',
+  customSearch: undefined,
+  showHeader: true,
+  showFooter: false,
+  footerStyle (row, index) {
+    return {}
+  },
+  showColumns: false,
+  minimumCountColumns: 1,
+  showPaginationSwitch: false,
+  showRefresh: false,
+  showToggle: false,
+  showFullscreen: false,
+  smartDisplay: true,
+  escape: false,
+  filterOptions: {
+    'filterAlgorithm': 'and' // and means all given filter must match, or means one of the given filter must match
+  },
+  idField: undefined,
+  selectItemName: 'btSelectItem',
+  clickToSelect: false,
+  ignoreClickToSelectOn ({tagName}) {
+    return ['A', 'BUTTON'].includes(tagName)
+  },
+  singleSelect: false,
+  checkboxHeader: true,
+  maintainSelected: false,
+  multipleSelectRow: false,
+  uniqueId: undefined,
+  cardView: false,
+  detailView: false,
+  detailViewIcon: true,
+  detailViewByClick: false,
+  detailFormatter (index, row) {
+    return ''
+  },
+  detailFilter (index, row) {
+    return true
+  },
+  toolbar: undefined,
+  toolbarAlign: 'left',
+  buttonsToolbar: undefined,
+  buttonsAlign: 'right',
+  buttonsPrefix: CONSTANTS.classes.buttonsPrefix,
+  buttonsClass: CONSTANTS.classes.buttons,
+  icons: CONSTANTS.icons,
+  iconSize: undefined,
+  iconsPrefix: CONSTANTS.iconsPrefix, // glyphicon or fa(font-awesome)
+  onAll (name, args) {
+    return false
+  },
+  onClickCell (field, value, row, $element) {
+    return false
+  },
+  onDblClickCell (field, value, row, $element) {
+    return false
+  },
+  onClickRow (item, $element) {
+    return false
+  },
+  onDblClickRow (item, $element) {
+    return false
+  },
+  onSort (name, order) {
+    return false
+  },
+  onCheck (row) {
+    return false
+  },
+  onUncheck (row) {
+    return false
+  },
+  onCheckAll (rows) {
+    return false
+  },
+  onUncheckAll (rows) {
+    return false
+  },
+  onCheckSome (rows) {
+    return false
+  },
+  onUncheckSome (rows) {
+    return false
+  },
+  onLoadSuccess (data) {
+    return false
+  },
+  onLoadError (status) {
+    return false
+  },
+  onColumnSwitch (field, checked) {
+    return false
+  },
+  onPageChange (number, size) {
+    return false
+  },
+  onSearch (text) {
+    return false
+  },
+  onToggle (cardView) {
+    return false
+  },
+  onPreBody (data) {
+    return false
+  },
+  onPostBody () {
+    return false
+  },
+  onPostHeader () {
+    return false
+  },
+  onPostFooter () {
+    return false
+  },
+  onExpandRow (index, row, $detail) {
+    return false
+  },
+  onCollapseRow (index, row) {
+    return false
+  },
+  onRefreshOptions (options) {
+    return false
+  },
+  onRefresh (params) {
+    return false
+  },
+  onResetView () {
+    return false
+  },
+  onScrollBody () {
+    return false
+  }
+}
+
+const EN = {
+  formatLoadingMessage () {
+    return 'Loading, please wait'
+  },
+  formatRecordsPerPage (pageNumber) {
+    return `${pageNumber} rows per page`
+  },
+  formatShowingRows (pageFrom, pageTo, totalRows, totalNotFiltered) {
+    if (totalNotFiltered !== undefined && totalNotFiltered > 0 && totalNotFiltered < totalRows) {
+      return `Showing ${pageFrom} to ${pageTo} of ${totalRows} rows (filtered from ${totalNotFiltered} total entries)`
+    }
+
+    return `Showing ${pageFrom} to ${pageTo} of ${totalRows} rows`
+  },
+  formatDetailPagination (totalRows) {
+    return `Showing ${totalRows} rows`
+  },
+  formatSearch () {
+    return 'Search'
+  },
+  formatNoMatches () {
+    return 'No matching records found'
+  },
+  formatPaginationSwitch () {
+    return 'Hide/Show pagination'
+  },
+  formatRefresh () {
+    return 'Refresh'
+  },
+  formatToggle () {
+    return 'Toggle'
+  },
+  formatColumns () {
+    return 'Columns'
+  },
+  formatFullscreen () {
+    return 'Fullscreen'
+  },
+  formatAllRows () {
+    return 'All'
+  }
+}
+
+const COLUMN_DEFAULTS = {
+  field: undefined,
+  title: undefined,
+  titleTooltip: undefined,
+  'class': undefined,
+  width: undefined,
+  rowspan: undefined,
+  colspan: undefined,
+  align: undefined, // left, right, center
+  halign: undefined, // left, right, center
+  falign: undefined, // left, right, center
+  valign: undefined, // top, middle, bottom
+  cellStyle: undefined,
+  radio: false,
+  checkbox: false,
+  checkboxEnabled: true,
+  clickToSelect: true,
+  showSelectTitle: false,
+  sortable: false,
+  sortName: undefined,
+  order: 'asc', // asc, desc
+  sorter: undefined,
+  visible: true,
+  switchable: true,
+  cardVisible: true,
+  searchable: true,
+  formatter: undefined,
+  footerFormatter: undefined,
+  detailFormatter: undefined,
+  searchFormatter: true,
+  escape: false,
+  events: undefined
+}
+
+const EVENTS = {
+  'all.bs.table': 'onAll',
+  'click-cell.bs.table': 'onClickCell',
+  'dbl-click-cell.bs.table': 'onDblClickCell',
+  'click-row.bs.table': 'onClickRow',
+  'dbl-click-row.bs.table': 'onDblClickRow',
+  'sort.bs.table': 'onSort',
+  'check.bs.table': 'onCheck',
+  'uncheck.bs.table': 'onUncheck',
+  'check-all.bs.table': 'onCheckAll',
+  'uncheck-all.bs.table': 'onUncheckAll',
+  'check-some.bs.table': 'onCheckSome',
+  'uncheck-some.bs.table': 'onUncheckSome',
+  'load-success.bs.table': 'onLoadSuccess',
+  'load-error.bs.table': 'onLoadError',
+  'column-switch.bs.table': 'onColumnSwitch',
+  'page-change.bs.table': 'onPageChange',
+  'search.bs.table': 'onSearch',
+  'toggle.bs.table': 'onToggle',
+  'pre-body.bs.table': 'onPreBody',
+  'post-body.bs.table': 'onPostBody',
+  'post-header.bs.table': 'onPostHeader',
+  'post-footer.bs.table': 'onPostFooter',
+  'expand-row.bs.table': 'onExpandRow',
+  'collapse-row.bs.table': 'onCollapseRow',
+  'refresh-options.bs.table': 'onRefreshOptions',
+  'reset-view.bs.table': 'onResetView',
+  'refresh.bs.table': 'onRefresh',
+  'scroll-body.bs.table': 'onScrollBody'
+}
+
+Object.assign(DEFAULTS, EN)
+
+export default {
+  CONSTANTS,
+
+  DEFAULTS,
+
+  COLUMN_DEFAULTS,
+
+  EVENTS,
+
+  LOCALES: {
+    en: EN,
+    'en-US': EN
+  }
+}

+ 249 - 0
src/utils/index.js

@@ -0,0 +1,249 @@
+export default {
+  // it only does '%s', and return '' when arguments are undefined
+  sprintf (_str, ...args) {
+    let flag = true
+    let i = 0
+
+    const str = _str.replace(/%s/g, () => {
+      const arg = args[i++]
+
+      if (typeof arg === 'undefined') {
+        flag = false
+        return ''
+      }
+      return arg
+    })
+    return flag ? str : ''
+  },
+
+  isEmptyObject (obj = {}) {
+    return Object.entries(obj).length === 0 && obj.constructor === Object
+  },
+
+  isNumeric (n) {
+    return !isNaN(parseFloat(n)) && isFinite(n)
+  },
+
+  getFieldTitle (list, value) {
+    for (const item of list) {
+      if (item.field === value) {
+        return item.title
+      }
+    }
+    return ''
+  },
+
+  setFieldIndex (columns) {
+    let totalCol = 0
+    const flag = []
+
+    for (const column of columns[0]) {
+      totalCol += column.colspan || 1
+    }
+
+    for (let i = 0; i < columns.length; i++) {
+      flag[i] = []
+      for (let j = 0; j < totalCol; j++) {
+        flag[i][j] = false
+      }
+    }
+
+    for (let i = 0; i < columns.length; i++) {
+      for (const r of columns[i]) {
+        const rowspan = r.rowspan || 1
+        const colspan = r.colspan || 1
+        const index = flag[i].indexOf(false)
+
+        if (colspan === 1) {
+          r.fieldIndex = index
+          // when field is undefined, use index instead
+          if (typeof r.field === 'undefined') {
+            r.field = index
+          }
+        }
+
+        for (let k = 0; k < rowspan; k++) {
+          flag[i + k][index] = true
+        }
+        for (let k = 0; k < colspan; k++) {
+          flag[i][index + k] = true
+        }
+      }
+    }
+  },
+
+  getScrollBarWidth () {
+    if (this.cachedWidth === undefined) {
+      const $inner = $('<div/>').addClass('fixed-table-scroll-inner')
+      const $outer = $('<div/>').addClass('fixed-table-scroll-outer')
+
+      $outer.append($inner)
+      $('body').append($outer)
+
+      const w1 = $inner[0].offsetWidth
+      $outer.css('overflow', 'scroll')
+      let w2 = $inner[0].offsetWidth
+
+      if (w1 === w2) {
+        w2 = $outer[0].clientWidth
+      }
+
+      $outer.remove()
+      this.cachedWidth = w1 - w2
+    }
+    return this.cachedWidth
+  },
+
+  calculateObjectValue (self, name, args, defaultValue) {
+    let func = name
+
+    if (typeof name === 'string') {
+      // support obj.func1.func2
+      const names = name.split('.')
+
+      if (names.length > 1) {
+        func = window
+        for (const f of names) {
+          func = func[f]
+        }
+      } else {
+        func = window[name]
+      }
+    }
+
+    if (func !== null && typeof func === 'object') {
+      return func
+    }
+
+    if (typeof func === 'function') {
+      return func.apply(self, args || [])
+    }
+
+    if (
+      !func &&
+      typeof name === 'string' &&
+      this.sprintf(name, ...args)
+    ) {
+      return this.sprintf(name, ...args)
+    }
+
+    return defaultValue
+  },
+
+  compareObjects (objectA, objectB, compareLength) {
+    const aKeys = Object.keys(objectA)
+    const bKeys = Object.keys(objectB)
+
+    if (compareLength && aKeys.length !== bKeys.length) {
+      return false
+    }
+
+    for (const key of aKeys) {
+      if (bKeys.includes(key) && objectA[key] !== objectB[key]) {
+        return false
+      }
+    }
+
+    return true
+  },
+
+  escapeHTML (text) {
+    if (typeof text === 'string') {
+      return text
+        .replace(/&/g, '&amp;')
+        .replace(/</g, '&lt;')
+        .replace(/>/g, '&gt;')
+        .replace(/"/g, '&quot;')
+        .replace(/'/g, '&#039;')
+        .replace(/`/g, '&#x60;')
+    }
+    return text
+  },
+
+  getRealDataAttr (dataAttr) {
+    for (const [attr, value] of Object.entries(dataAttr)) {
+      const auxAttr = attr.split(/(?=[A-Z])/).join('-').toLowerCase()
+      if (auxAttr !== attr) {
+        dataAttr[auxAttr] = value
+        delete dataAttr[attr]
+      }
+    }
+    return dataAttr
+  },
+
+  getItemField (item, field, escape) {
+    let value = item
+
+    if (typeof field !== 'string' || item.hasOwnProperty(field)) {
+      return escape ? this.escapeHTML(item[field]) : item[field]
+    }
+
+    const props = field.split('.')
+    for (const p of props) {
+      value = value && value[p]
+    }
+    return escape ? this.escapeHTML(value) : value
+  },
+
+  isIEBrowser () {
+    return navigator.userAgent.includes('MSIE ') ||
+      /Trident.*rv:11\./.test(navigator.userAgent)
+  },
+
+  findIndex (items, item) {
+    for (const it of items) {
+      if (JSON.stringify(it) === JSON.stringify(item)) {
+        return items.indexOf(it)
+      }
+    }
+    return -1
+  },
+
+  trToData (columns, $els) {
+    const data = []
+    const m = []
+
+    $els.each((y, el) => {
+      const row = {}
+
+      // save tr's id, class and data-* attributes
+      row._id = $(el).attr('id')
+      row._class = $(el).attr('class')
+      row._data = this.getRealDataAttr($(el).data())
+
+      $(el).find('>td,>th').each((_x, el) => {
+        const cspan = +$(el).attr('colspan') || 1
+        const rspan = +$(el).attr('rowspan') || 1
+        let x = _x
+
+        // skip already occupied cells in current row
+        for (; m[y] && m[y][x]; x++) {
+          // ignore
+        }
+
+        // mark matrix elements occupied by current cell with true
+        for (let tx = x; tx < x + cspan; tx++) {
+          for (let ty = y; ty < y + rspan; ty++) {
+            if (!m[ty]) { // fill missing rows
+              m[ty] = []
+            }
+            m[ty][tx] = true
+          }
+        }
+
+        const field = columns[x].field
+
+        row[field] = $(el).html().trim()
+        // save td's id, class and data-* attributes
+        row[`_${field}_id`] = $(el).attr('id')
+        row[`_${field}_class`] = $(el).attr('class')
+        row[`_${field}_rowspan`] = $(el).attr('rowspan')
+        row[`_${field}_colspan`] = $(el).attr('colspan')
+        row[`_${field}_title`] = $(el).attr('title')
+        row[`_${field}_data`] = this.getRealDataAttr($(el).data())
+      })
+      data.push(row)
+    })
+    return data
+  }
+}

+ 120 - 0
src/virtual-scroll/index.js

@@ -0,0 +1,120 @@
+const BLOCK_ROWS = 50
+const CLUSTER_BLOCKS = 4
+
+class VirtualScroll {
+
+  constructor (options) {
+    this.rows = options.rows
+    this.scrollEl = options.scrollEl
+    this.contentEl = options.contentEl
+    this.callback = options.callback
+
+    this.cache = {}
+    this.scrollTop = this.scrollEl.scrollTop
+
+    this.initDOM(this.rows)
+
+    this.scrollEl.scrollTop = this.scrollTop
+    this.lastCluster = 0
+
+    const onScroll = () => {
+      if (this.lastCluster !== (this.lastCluster = this.getNum())) {
+        this.initDOM(this.rows)
+        this.callback()
+      }
+    }
+
+    this.scrollEl.addEventListener('scroll', onScroll, false)
+    this.destroy = () => {
+      this.contentEl.innerHtml = ''
+      this.scrollEl.removeEventListener('scroll', onScroll, false)
+    }
+  }
+
+  initDOM (rows) {
+    if (!this.clusterHeight) {
+      this.cache.data = this.contentEl.innerHTML = rows[0] + rows[0] + rows[0]
+      this.getRowsHeight(rows)
+    }
+
+    const data = this.initData(rows, this.getNum())
+    const thisRows = data.rows.join('')
+    const dataChanged = this.checkChanges('data', thisRows)
+    const topOffsetChanged = this.checkChanges('top', data.topOffset)
+    const bottomOffsetChanged = this.checkChanges('bottom', data.bottomOffset)
+    const html = []
+
+    if (dataChanged && topOffsetChanged) {
+      if (data.topOffset) {
+        html.push(this.getExtra('top', data.topOffset))
+      }
+      html.push(thisRows)
+      if (data.bottomOffset) {
+        html.push(this.getExtra('bottom', data.bottomOffset))
+      }
+      this.contentEl.innerHTML = html.join('')
+    } else if (bottomOffsetChanged) {
+      this.contentEl.lastChild.style.height = `${data.bottomOffset}px`
+    }
+  }
+
+  getRowsHeight () {
+    const nodes = this.contentEl.children
+    const node = nodes[Math.floor(nodes.length / 2)]
+    this.itemHeight = node.offsetHeight
+    this.blockHeight = this.itemHeight * BLOCK_ROWS
+    this.clusterRows = BLOCK_ROWS * CLUSTER_BLOCKS
+    this.clusterHeight = this.blockHeight * CLUSTER_BLOCKS
+  }
+
+  getNum () {
+    this.scrollTop = this.scrollEl.scrollTop
+    return Math.floor(this.scrollTop / (this.clusterHeight - this.blockHeight)) || 0
+  }
+
+  initData (rows, num) {
+    if (rows.length < BLOCK_ROWS) {
+      return {
+        topOffset: 0,
+        bottomOffset: 0,
+        rowsAbove: 0,
+        rows
+      }
+    }
+    const start = Math.max((this.clusterRows - BLOCK_ROWS) * num, 0)
+    const end = start + this.clusterRows
+    const topOffset = Math.max(start * this.itemHeight, 0)
+    const bottomOffset = Math.max((rows.length - end) * this.itemHeight, 0)
+    const thisRows = []
+    let rowsAbove = start
+    if (topOffset < 1) {
+      rowsAbove++
+    }
+    for (let i = start; i < end; i++) {
+      rows[i] && thisRows.push(rows[i])
+    }
+    return {
+      topOffset,
+      bottomOffset,
+      rowsAbove,
+      rows: thisRows
+    }
+  }
+
+  checkChanges (type, value) {
+    const changed = value !== this.cache[type]
+    this.cache[type] = value
+    return changed
+  }
+
+  getExtra (className, height) {
+    const tag = document.createElement('tr')
+    tag.className = `virtual-scroll-${className}`
+    if (height) {
+      tag.style.height = `${height}px`
+    }
+    return tag.outerHTML
+  }
+}
+
+export default VirtualScroll