UNPKG

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)

727 lines (593 loc) 21.8 kB
import Utils from '../utils/index.js' import VirtualScroll from '../virtual-scroll/index.js' export default { initBodyEvent () { // click to select by column this.$body.find('> tr[data-index] > td').off('click dblclick').on('click dblclick', e => { const $td = $(e.currentTarget) if ( $td.find('.detail-icon').length || $td.index() - Utils.getDetailViewIndexOffset(this.options) < 0 ) { return } const $tr = $td.parent() const $cardViewArr = $(e.target).parents('.card-views').children() const $cardViewTarget = $(e.target).parents('.card-view') const rowIndex = $tr.data('index') const item = this.data[rowIndex] const index = this.options.cardView ? $cardViewArr.index($cardViewTarget) : $td[0].cellIndex const fields = this.getVisibleFields() const field = fields[index - Utils.getDetailViewIndexOffset(this.options)] const column = this.columns[this.fieldsColumnsIndex[field]] const value = Utils.getItemField(item, field, this.options.escape, column.escape) this.trigger(e.type === 'click' ? 'click-cell' : 'dbl-click-cell', field, value, item, $td) this.trigger(e.type === 'click' ? 'click-row' : 'dbl-click-row', item, $tr, field) // if click to select - then trigger the checkbox/radio click if ( e.type === 'click' && this.options.clickToSelect && column.clickToSelect && !Utils.calculateObjectValue(this.options, this.options.ignoreClickToSelectOn, [e.target]) ) { const $selectItem = $tr.find(Utils.sprintf('[name="%s"]', this.options.selectItemName)) if ($selectItem.length) { $selectItem[0].click() } } if (e.type === 'click' && this.options.detailViewByClick) { this.toggleDetailView(rowIndex, this.header.detailFormatters[this.fieldsColumnsIndex[field]]) } }).off('mousedown').on('mousedown', e => { // https://github.com/jquery/jquery/issues/1741 this.multipleSelectRowCtrlKey = e.ctrlKey || e.metaKey this.multipleSelectRowShiftKey = e.shiftKey }) this.$body.find('> tr[data-index] > td > .detail-icon').off('click').on('click', e => { e.preventDefault() this.toggleDetailView($(e.currentTarget).parent().parent().data('index')) return false }) this.$selectItem = this.$body.find(Utils.sprintf('[name="%s"]', this.options.selectItemName)) this.$selectItem.off('click').on('click', e => { e.stopImmediatePropagation() const $this = $(e.currentTarget) this._toggleCheck($this.prop('checked'), $this.data('index')) }) this.header.events.forEach((_events, i) => { let events = _events if (!events) { return } // fix bug, if events is defined with namespace if (typeof events === 'string') { events = Utils.calculateObjectValue(null, events) } if (!events) { throw new Error(`Unknown event in the scope: ${_events}`) } const field = this.header.fields[i] let fieldIndex = this.getVisibleFields().indexOf(field) if (fieldIndex === -1) { return } fieldIndex += Utils.getDetailViewIndexOffset(this.options) for (const key in events) { if (!events.hasOwnProperty(key)) { continue } const event = events[key] this.$body.find('>tr:not(.no-records-found)').each((i, tr) => { const $tr = $(tr) const $td = $tr.find(this.options.cardView ? '.card-views>.card-view' : '>td').eq(fieldIndex) const index = key.indexOf(' ') const name = key.substring(0, index) const el = key.substring(index + 1) $td.find(el).off(name).on(name, e => { const index = $tr.data('index') const row = this.data[index] const value = row[field] event.apply(this, [e, value, row, index]) }) }) } }) }, initHiddenRows () { this.hiddenRows = [] }, // eslint-disable-next-line no-unused-vars initRow (item, i, data, trFragments) { if (Utils.findIndex(this.hiddenRows, item) > -1) { return } const style = Utils.calculateObjectValue(this.options, this.options.rowStyle, [item, i], {}) const attributes = Utils.calculateObjectValue(this.options, this.options.rowAttributes, [item, i], {}) const data_ = {} if (item._data && !Utils.isEmptyObject(item._data)) { for (const [k, v] of Object.entries(item._data)) { // ignore data-index if (k === 'index') { return } data_[`data-${k}`] = typeof v === 'object' ? JSON.stringify(v) : v } } const tr = Utils.h('tr', { id: Array.isArray(item) ? undefined : item._id, class: style && style.classes || (Array.isArray(item) ? undefined : item._class), style: style && style.css || (Array.isArray(item) ? undefined : item._style), 'data-index': i, 'data-uniqueid': Utils.getItemField(item, this.options.uniqueId, false), 'data-has-detail-view': this.options.detailView && Utils.calculateObjectValue(null, this.options.detailFilter, [i, item]) ? 'true' : undefined, ...attributes, ...data_ }) const trChildren = [] let detailViewTemplate = '' if (Utils.hasDetailViewIcon(this.options)) { detailViewTemplate = Utils.h('td') if (Utils.calculateObjectValue(null, this.options.detailFilter, [i, item])) { detailViewTemplate.append(Utils.h('a', { class: 'detail-icon', href: '#', html: Utils.sprintf(this.constants.html.icon, this.options.iconsPrefix, this.options.icons.detailOpen) })) } } if (detailViewTemplate && this.options.detailViewAlign !== 'right') { trChildren.push(detailViewTemplate) } const tds = this.header.fields.map((field, j) => { const column = this.columns[j] const value_ = Utils.getItemField(item, field, this.options.escape, column.escape) let value = '' const attrs = { class: this.header.classes[j] ? [this.header.classes[j]] : [], style: this.header.styles[j] ? [this.header.styles[j]] : [] } const cardViewClass = `card-view card-view-field-${field}` if ((this.fromHtml || this.autoMergeCells) && typeof value_ === 'undefined') { if (!column.checkbox && !column.radio) { return } } if (!column.visible) { return } if (this.options.cardView && !column.cardVisible) { return } // handle class, style, id, rowspan, colspan and title of td for (const attr of ['class', 'style', 'id', 'rowspan', 'colspan', 'title']) { const value = item[`_${field}_${attr}`] if (!value) { continue } if (attrs[attr]) { attrs[attr].push(value) } else { attrs[attr] = value } } const cellStyle = Utils.calculateObjectValue(this.header, this.header.cellStyles[j], [value_, item, i, field], {}) if (cellStyle.classes) { attrs.class.push(cellStyle.classes) } if (cellStyle.css) { attrs.style.push(cellStyle.css) } value = Utils.calculateObjectValue(column, this.header.formatters[j], [value_, item, i, field], value_) if (!(column.checkbox || column.radio)) { value = typeof value === 'undefined' || value === null ? this.options.undefinedText : value } if ( column.searchable && this.searchText && this.options.searchHighlight && !(column.checkbox || column.radio) ) { let searchText = this.searchText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') if (this.options.searchAccentNeutralise && typeof value === 'string') { const indexRegex = new RegExp(`${Utils.normalizeAccent(searchText)}`, 'gmi') const match = indexRegex.exec(Utils.normalizeAccent(value)) if (match) { searchText = value.substring(match.index, match.index + searchText.length) } } const defValue = Utils.replaceSearchMark(value, searchText) value = Utils.calculateObjectValue(column, column.searchHighlightFormatter, [value, this.searchText], defValue) } if (item[`_${field}_data`] && !Utils.isEmptyObject(item[`_${field}_data`])) { for (const [k, v] of Object.entries(item[`_${field}_data`])) { // ignore data-index if (k === 'index') { return } attrs[`data-${k}`] = v } } if (column.checkbox || column.radio) { const type = column.checkbox ? 'checkbox' : 'radio' const isChecked = Utils.isObject(value) && value.hasOwnProperty('checked') ? value.checked : (value === true || value_) && value !== false const isDisabled = !column.checkboxEnabled || value && value.disabled const valueNodes = this.header.formatters[j] && ( typeof value === 'string' || value instanceof Node || value instanceof $) ? Utils.htmlToNodes(value) : [] item[this.header.stateField] = value === true || (!!value_ || value && value.checked) return Utils.h(this.options.cardView ? 'div' : 'td', { class: [this.options.cardView ? cardViewClass : 'bs-checkbox', column.class], style: this.options.cardView ? undefined : attrs.style }, [ Utils.h('label', {}, [ Utils.h('input', { 'data-index': i, name: this.options.selectItemName, type, value: item[this.options.idField], checked: isChecked ? 'checked' : undefined, disabled: isDisabled ? 'disabled' : undefined }), Utils.h('span') ]), ...valueNodes ]) } if (this.options.cardView) { if (this.options.smartDisplay && value === '') { return Utils.h('div', { class: cardViewClass }) } const cardTitle = this.options.showHeader ? Utils.h('span', { class: ['card-view-title', cellStyle.classes], style: attrs.style, html: Utils.getFieldTitle(this.columns, field) }) : '' return Utils.h('div', { class: cardViewClass }, [ cardTitle, Utils.h('span', { class: ['card-view-value', cellStyle.classes], style: attrs.style }, [...Utils.htmlToNodes(value)]) ]) } return Utils.h('td', attrs, [...Utils.htmlToNodes(value)]) }).filter(x => x) trChildren.push(...tds) if (detailViewTemplate && this.options.detailViewAlign === 'right') { trChildren.push(detailViewTemplate) } if (this.options.cardView) { tr.append(Utils.h('td', { colspan: this.header.fields.length }, [ Utils.h('div', { class: 'card-views' }, trChildren) ])) } else { tr.append(...trChildren) } return tr }, initBody (fixedScroll, updatedUid) { const data = this.getData() this.trigger('pre-body', data) this.$body = this.$el.find('>tbody') if (!this.$body.length) { this.$body = $('<tbody></tbody>').appendTo(this.$el) } // Fix #389 Bootstrap-table-flatJSON is not working if (!this.options.pagination || this.options.sidePagination === 'server') { this.pageFrom = 1 this.pageTo = data.length } 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) hasTr = hasTr || !!tr if (tr && tr instanceof Node) { const uniqueId = this.options.uniqueId const toAppend = [tr] 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) { toAppend.push(oldTrNext[0]) } } } if (!this.options.virtualScroll) { trFragments.append(toAppend) } else { rows.push($('<div>').html(toAppend).html()) } } } this.$el.removeAttr('role') // show no records if (!hasTr) { this.$body.html(`<tr class="no-records-found">${Utils.sprintf('<td colspan="%s">%s</td>', this.getVisibleFields().length + Utils.getDetailViewIndexOffset(this.options), this.options.formatNoMatches())}</tr>`) this.$el.attr('role', 'presentation') } else if (!this.options.virtualScroll) { this.$body.html(trFragments) } else { if (this.virtualScroll) { this.virtualScroll.destroy() } this.virtualScroll = new VirtualScroll({ rows, fixedScroll, scrollEl: this.$tableBody[0], contentEl: this.$body[0], itemHeight: this.options.virtualScrollItemHeight, callback: (startIndex, endIndex) => { this.fitHeader() this.initBodyEvent() this.trigger('virtual-scroll', startIndex, endIndex) } }) } toExpand.forEach(index => { this.expandRow(index) }) if (!fixedScroll) { this.scrollTo(0) } this.initBodyEvent() this.initFooter() this.resetView() this.updateSelected() if (this.options.sidePagination !== 'server') { this.options.totalRows = data.length } this.trigger('post-body', data) }, resetView (params) { let padding = 0 if (params && params.height) { this.options.height = params.height } this.$tableContainer.toggleClass('has-card-view', this.options.cardView) if (this.options.height) { const fixedBody = this.$tableBody.get(0) this.hasScrollBar = fixedBody.scrollWidth > fixedBody.clientWidth } if (!this.options.cardView && this.options.showHeader && this.options.height) { this.$tableHeader.show() this.resetHeader() padding += this.$header.outerHeight(true) + 1 } else { this.$tableHeader.hide() this.trigger('post-header') } if (!this.options.cardView && this.options.showFooter) { this.$tableFooter.show() this.fitFooter() if (this.options.height) { padding += this.$tableFooter.outerHeight(true) } } if (this.$container.hasClass('fullscreen')) { this.$tableContainer.css('height', '') this.$tableContainer.css('width', '') } else if (this.options.height) { if (this.$tableBorder) { this.$tableBorder.css('width', '') this.$tableBorder.css('height', '') } const toolbarHeight = this.$toolbar.outerHeight(true) const paginationHeight = this.$pagination.outerHeight(true) const height = this.options.height - toolbarHeight - paginationHeight const $bodyTable = this.$tableBody.find('>table') const tableHeight = $bodyTable.outerHeight() this.$tableContainer.css('height', `${height}px`) if (this.$tableBorder && $bodyTable.is(':visible')) { let tableBorderHeight = height - tableHeight - 2 if (this.hasScrollBar) { tableBorderHeight -= Utils.getScrollBarWidth() } this.$tableBorder.css('width', `${$bodyTable.outerWidth()}px`) this.$tableBorder.css('height', `${tableBorderHeight}px`) } } if (this.options.cardView) { // remove the element css this.$el.css('margin-top', '0') this.$tableContainer.css('padding-bottom', '0') this.$tableFooter.hide() } else { // Assign the correct sortable arrow this.resetCaret() this.$tableContainer.css('padding-bottom', `${padding}px`) } this.trigger('reset-view') }, showLoading () { this.$tableLoading.toggleClass('open', true) let fontSize = this.options.loadingFontSize if (this.options.loadingFontSize === 'auto') { fontSize = this.$tableLoading.width() * 0.04 fontSize = Math.max(12, fontSize) fontSize = Math.min(32, fontSize) fontSize = `${fontSize}px` } this.$tableLoading.find('.loading-text').css('font-size', fontSize) }, hideLoading () { this.$tableLoading.toggleClass('open', false) }, scrollTo (params) { let options = { unit: 'px', value: 0 } if (typeof params === 'object') { options = Object.assign(options, params) } else if (typeof params === 'string' && params === 'bottom') { options.value = this.$tableBody[0].scrollHeight } else if (typeof params === 'string' || typeof params === 'number') { options.value = params } let scrollTo = options.value if (options.unit === 'rows') { scrollTo = 0 this.$body.find(`> tr:lt(${options.value})`).each((i, el) => { scrollTo += $(el).outerHeight(true) }) } this.$tableBody.scrollTop(scrollTo) }, getScrollPosition () { return this.$tableBody.scrollTop() }, showRow (params) { this._toggleRow(params, true) }, hideRow (params) { this._toggleRow(params, false) }, _toggleRow (params, visible) { let row if (params.hasOwnProperty('index')) { row = this.getData()[params.index] } else if (params.hasOwnProperty('uniqueId')) { row = this.getRowByUniqueId(params.uniqueId) } if (!row) { return } const index = Utils.findIndex(this.hiddenRows, row) if (!visible && index === -1) { this.hiddenRows.push(row) } else if (visible && index > -1) { this.hiddenRows.splice(index, 1) } this.initBody(true) this.initPagination() }, getHiddenRows (show) { if (show) { this.initHiddenRows() this.initBody(true) this.initPagination() return } const data = this.getData() const rows = [] for (const row of data) { if (this.hiddenRows.includes(row)) { rows.push(row) } } this.hiddenRows = rows return rows }, showColumn (field) { const fields = Array.isArray(field) ? field : [field] fields.forEach(field => { this._toggleColumn(this.fieldsColumnsIndex[field], true, true) }) }, hideColumn (field) { const fields = Array.isArray(field) ? field : [field] fields.forEach(field => { this._toggleColumn(this.fieldsColumnsIndex[field], false, true) }) }, _toggleColumn (index, checked, needUpdate) { if (index === undefined || this.columns[index].visible === checked) { return } this.columns[index].visible = checked this.initHeader() this.initSearch() this.initPagination() this.initBody() if (this.options.showColumns) { const $items = this.$toolbar.find('.keep-open input:not(".toggle-all")').prop('disabled', false) if (needUpdate) { $items.filter(Utils.sprintf('[value="%s"]', index)).prop('checked', checked) } if ($items.filter(':checked').length <= this.options.minimumCountColumns) { $items.filter(':checked').prop('disabled', true) } } }, showAllColumns () { this._toggleAllColumns(true) }, hideAllColumns () { this._toggleAllColumns(false) }, _toggleAllColumns (visible) { for (const column of this.columns.slice().reverse()) { if (column.switchable) { if ( !visible && this.options.showColumns && this.getVisibleColumns().filter(it => it.switchable).length === this.options.minimumCountColumns ) { continue } column.visible = visible } } this.initHeader() this.initSearch() this.initPagination() this.initBody() if (this.options.showColumns) { const $items = this.$toolbar.find('.keep-open input[type="checkbox"]:not(".toggle-all")').prop('disabled', false) if (visible) { $items.prop('checked', visible) } else { $items.get().reverse().forEach(item => { if ($items.filter(':checked').length > this.options.minimumCountColumns) { $(item).prop('checked', visible) } }) } if ($items.filter(':checked').length <= this.options.minimumCountColumns) { $items.filter(':checked').prop('disabled', true) } } }, mergeCells (options) { const row = options.index let col = this.getVisibleFields().indexOf(options.field) const rowspan = +options.rowspan || 1 const colspan = +options.colspan || 1 let i let j const $tr = this.$body.find('>tr[data-index]') col += Utils.getDetailViewIndexOffset(this.options) const $td = $tr.eq(row).find('>td').eq(col) if (row < 0 || col < 0 || row >= this.data.length) { return } for (i = row; i < row + rowspan; i++) { for (j = col; j < col + colspan; j++) { $tr.eq(i).find('>td').eq(j).hide() } } $td.attr('rowspan', rowspan).attr('colspan', colspan).show() }, getVisibleColumns () { return this.columns.filter(column => column.visible && !this.isSelectionColumn(column)) }, getHiddenColumns () { return this.columns.filter(({ visible }) => !visible) } }