bootstrap-table
Version:
An extended table to integration with some of the most widely used CSS frameworks. (Supports Bootstrap, Semantic UI, Bulma, Material Design, Foundation)
348 lines (272 loc) • 9.91 kB
JavaScript
/**
* @author: Yura Knoxville
* @version: v1.1.0
*/
const Utils = $.fn.bootstrapTable.utils
let initBodyCaller
const groupBy = (array, f) => {
const tmpGroups = new Map()
array.forEach(o => {
const groups = f(o)
if (!tmpGroups.has(groups)) {
tmpGroups.set(groups, [])
}
tmpGroups.get(groups).push(o)
})
return Object.fromEntries(tmpGroups)
}
Utils.assignIcons($.fn.bootstrapTable.icons, 'collapseGroup', {
glyphicon: 'glyphicon-chevron-up',
fa: 'fa-angle-up',
bi: 'bi-chevron-up',
'material-icons': 'arrow_drop_down'
})
Utils.assignIcons($.fn.bootstrapTable.icons, 'expandGroup', {
glyphicon: 'glyphicon-chevron-down',
fa: 'fa-angle-down',
bi: 'bi-chevron-down',
'material-icons': 'arrow_drop_up'
})
Object.assign($.fn.bootstrapTable.defaults, {
groupBy: false,
groupByField: '',
groupByFormatter: undefined,
groupByToggle: false,
groupByShowToggleIcon: false,
groupByCollapsedGroups: []
})
const BootstrapTable = $.fn.bootstrapTable.Constructor
const _initSort = BootstrapTable.prototype.initSort
const _initBody = BootstrapTable.prototype.initBody
const _updateSelected = BootstrapTable.prototype.updateSelected
BootstrapTable.prototype.initSort = function (...args) {
// for custom sort
this.enableCustomSort = this.options.groupBy && this.options.groupByField !== ''
this.tableGroups = []
_initSort.apply(this, args)
if (!this.enableCustomSort) {
return
}
// Initialize group expand/collapse state tracking
if (!this._groupCollapsedState) {
this._groupCollapsedState = new Map()
// Pre-populate with default collapsed groups
if (this.options.groupByCollapsedGroups) {
const collapsedGroups = Array.isArray(this.options.groupByCollapsedGroups) ?
this.options.groupByCollapsedGroups : []
collapsedGroups.forEach(group => {
this._groupCollapsedState.set(group, true)
})
}
}
if (this.options.sortName !== this.options.groupByField) {
if (this.options.customSort) {
Utils.calculateObjectValue(this.options, this.options.customSort, [
this.options.sortName,
this.options.sortOrder,
this.data
])
} else {
const groupByFields = this.getGroupByFields()
this.data.sort((a, b) => {
const fieldValuesA = groupByFields.map(field => a[field])
const fieldValuesB = groupByFields.map(field => b[field])
return fieldValuesA.join().localeCompare(fieldValuesB.join(), undefined, { numeric: true })
})
}
}
const groups = groupBy(this.data, item => {
const groupByFields = this.getGroupByFields()
const groupValues = []
for (const field of groupByFields) {
const value_ = Utils.getItemField(item, field, this.options.escape, item.escape)
groupValues.push(value_)
}
return groupValues.join(', ')
})
let index = 0
for (const [key, value] of Object.entries(groups)) {
this.tableGroups.push({
id: index,
name: key,
data: value
})
value.forEach(item => {
// Clone _data to avoid modifying original dataset reference
item._data = { ...item._data || {}, 'parent-index': index }
// Remove existing hidden class from previous render
if (item._class) {
item._class = item._class
.split(/\s+/)
.filter(className => className && className !== 'hidden')
.join(' ')
}
// Add hidden class if collapsed
if (this.isCollapsed(key)) {
item._class = `${item._class || ''} hidden`
}
})
index++
}
}
BootstrapTable.prototype.initBody = function (...args) {
initBodyCaller = true
this.initSort()
_initBody.apply(this, args)
if (this.options.groupBy && this.options.groupByField !== '') {
let checkBox = false
let visibleColumns = 0
this.columns.forEach(column => {
if (column.checkbox && !this.options.singleSelect) {
checkBox = true
} else if (column.visible) {
visibleColumns += 1
}
})
if (this.options.detailView && !this.options.cardView) {
visibleColumns += 1
}
this.tableGroups.forEach(item => {
const html = []
const isCollapsed = this.isCollapsed(item.name)
const groupClass = this.options.groupByToggle ? isCollapsed ? 'collapsed' : 'expanded' : ''
html.push(Utils.sprintf('<tr class="info group-by %s" data-group-index="%s">', groupClass, item.id))
if (this.options.detailView && !this.options.cardView) {
html.push('<td class="detail"></td>')
}
if (checkBox) {
html.push('<td class="bs-checkbox">',
Utils.getCheckboxHtml({
name: 'btSelectGroup',
centered: true,
withLabel: false
}),
'</td>'
)
}
const formattedValue = this.options.groupByFormatter ?
Utils.calculateObjectValue(this.options, this.options.groupByFormatter, [item.name, item.id, item.data]) :
item.name
html.push('<td',
Utils.sprintf(' colspan="%s"', visibleColumns),
'>', formattedValue
)
const icon = isCollapsed ? this.options.icons.expandGroup : this.options.icons.collapseGroup
if (this.options.groupByToggle && this.options.groupByShowToggleIcon) {
html.push(`<span class="float-right ${this.options.iconsPrefix} ${icon}"></span>`)
}
html.push('</td></tr>')
this.$body.find(`tr[data-parent-index=${item.id}]:first`).before($(html.join('')))
})
this.selectGroup = []
for (const el of this.$body.find('[name="btSelectGroup"]')) {
const groupIndex = $(el).closest('tr').data('group-index')
this.selectGroup.push({
group: $(el),
item: this.$selectItem.filter((i, el) => $(el).closest('tr').data('parent-index') === groupIndex)
})
}
if (this.options.groupByToggle) {
this.$container.off('click', '.group-by')
.on('click', '.group-by', event => {
const $this = $(event.currentTarget)
const groupIndex = $this.closest('tr').data('group-index')
const $groupRows = this.$body.find(`tr[data-parent-index=${groupIndex}]`)
$this.toggleClass('expanded collapsed')
$this.find('span').toggleClass(`${this.options.icons.collapseGroup} ${this.options.icons.expandGroup}`)
$groupRows.toggleClass('hidden')
// Store the user's toggle state
const groupItem = this.tableGroups.find(g => g.id === groupIndex)
if (groupItem) {
this._groupCollapsedState.set(groupItem.name, $this.hasClass('collapsed'))
}
for (const element of $groupRows) {
this.collapseRow($(element).data('index'))
}
})
}
this.$container.off('click', '[name="btSelectGroup"]')
.on('click', '[name="btSelectGroup"]', event => {
event.stopImmediatePropagation()
const $this = $(event.currentTarget)
const checked = $this.prop('checked')
this[checked ? 'checkGroup' : 'uncheckGroup']($this.closest('tr').data('group-index'))
})
}
initBodyCaller = false
this.updateSelected()
}
BootstrapTable.prototype.updateSelected = function (...args) {
if (!initBodyCaller) {
_updateSelected.apply(this, args)
if (this.options.groupBy && this.options.groupByField !== '') {
this.selectGroup.forEach(item => {
item.group.prop('checked', item.item.filter(':enabled').length ===
item.item.filter(':enabled').filter(':checked').length)
})
}
}
}
BootstrapTable.prototype.checkGroup = function (index) {
this.checkGroup_(index, true)
}
BootstrapTable.prototype.uncheckGroup = function (index) {
this.checkGroup_(index, false)
}
BootstrapTable.prototype.isCollapsed = function (groupKey) {
// First, respect any explicitly stored collapsed state
if (this._groupCollapsedState && this._groupCollapsedState.has(groupKey)) {
return this._groupCollapsedState.get(groupKey)
}
// Backwards compatibility: support groupByCollapsedGroups as array or function
const collapsedGroups = this.options.groupByCollapsedGroups
if (typeof collapsedGroups === 'function') {
return Utils.calculateObjectValue(this.options, collapsedGroups, [groupKey], false)
}
if (Array.isArray(collapsedGroups)) {
return collapsedGroups.includes(groupKey)
}
return false
}
BootstrapTable.prototype.checkGroup_ = function (index, checked) {
const rowsBefore = this.getSelections()
this.$selectItem
.filter((i, el) => $(el).closest('tr').data('parent-index') === index)
.prop('checked', checked)
this.updateRows()
this.updateSelected()
const rowsAfter = this.getSelections()
if (checked) {
this.trigger('check-all', rowsAfter, rowsBefore)
return
}
this.trigger('uncheck-all', rowsAfter, rowsBefore)
}
BootstrapTable.prototype.getGroupByFields = function () {
return Array.isArray(this.options.groupByField) ? this.options.groupByField : [this.options.groupByField]
}
$.BootstrapTable = class extends $.BootstrapTable {
scrollTo (params) {
if (this.options.groupBy) {
let options = { unit: 'px', value: 0 }
if (typeof params === 'object') {
options = Object.assign(options, params)
}
if (options.unit === 'rows') {
let scrollTo = 0
const rows = this.$body.find(`> tr:not(.group-by):lt(${options.value})`)
for (const row of rows) {
scrollTo += $(row).outerHeight(true)
}
const $targetColumn = this.$body.find(`> tr:not(.group-by):eq(${options.value})`)
const prevGroupRows = $targetColumn.prevAll('.group-by')
for (const row of prevGroupRows) {
scrollTo += $(row).outerHeight(true)
}
this.$tableBody.scrollTop(scrollTo)
return
}
}
super.scrollTo(params)
}
}