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)

786 lines (639 loc) • 23.6 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' || Utils.isDomNode(value)) ? Utils.htmlToNodes(value) : [] item[this.header.stateField] = value === true || (!!value_ || value && value.checked) const inputAttrs = { 'data-index': i, name: this.options.selectItemName, type, value: item[this.options.idField], checked: isChecked ? 'checked' : undefined, disabled: isDisabled ? 'disabled' : undefined } const config = Utils.getCheckboxVdomConfig({ inputAttrs, formCheckClass: this.constants.classes.formCheck, formCheckInputClass: this.constants.classes.formCheckInput }) const wrapperChildNodes = [Utils.h('input', config.inputAttrs)] if (config.hasSpan) { wrapperChildNodes.push(Utils.h('span')) } const children = [ Utils.h(config.wrapperTag, config.wrapperAttrs, wrapperChildNodes), ...valueNodes ] return Utils.h(this.options.cardView ? 'div' : 'td', { class: [this.options.cardView ? cardViewClass : 'bs-checkbox', column.class], style: this.options.cardView ? undefined : attrs.style }, children) } if (this.options.cardView) { if (this.options.smartDisplay && value === '') { return Utils.h('div', { class: cardViewClass }) } const fieldTitle = Utils.getFieldTitle(this.columns, field) const cardTitle = this.options.showHeader ? Utils.h('span', { class: ['card-view-title', cellStyle.classes], style: attrs.style, html: this.options.escape && this.options.escapeTitle ? Utils.escapeHTML(fieldTitle) : fieldTitle }) : '' 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()) } } } // 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>`) } 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 && this.$el.is(':hidden')) { if (!this._resizeObserver) { this._setDelayTimeout('resetView', () => this.resetView(), 100) } return } 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` } const $loadingWrap = this.$tableLoading.find('.loading-wrap') if ($loadingWrap.length) { $loadingWrap.css('font-size', fontSize) } else { this.$tableLoading.css('font-size', fontSize) } 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') { let rowCount = options.value let $rows = this.$body.find('> tr') scrollTo = 0 if (this.options.virtualScroll && this.virtualScroll) { // Rows outside the rendered block are replaced by spacer rows, // so count them with the item height instead of the spacer height const { itemHeight, startIndex = 0 } = this.virtualScroll const rowsAbove = Math.min(rowCount, startIndex) $rows = $rows.not('.virtual-scroll-top, .virtual-scroll-bottom') rowCount -= rowsAbove const rowsBelow = Math.max(rowCount - $rows.length, 0) rowCount -= rowsBelow scrollTo += (rowsAbove + rowsBelow) * itemHeight } $rows.slice(0, rowCount).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) { this._toggleColumns(Array.isArray(field) ? field : [field], true, true) }, hideColumn (field) { this._toggleColumns(Array.isArray(field) ? field : [field], false, true) }, _toggleColumnVisibility (index, checked) { if (index === undefined || this.columns[index].visible === checked) { return false } this.columns[index].visible = checked return true }, _updateAfterColumnToggle (changedIndices, checked, needUpdate) { 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) { for (const index of changedIndices) { $items.filter(Utils.sprintf('[value="%s"]', index)).prop('checked', checked) } } if ($items.filter(':checked').length <= this.options.minimumCountColumns) { $items.filter(':checked').prop('disabled', true) } } }, _toggleColumns (fields, checked, needUpdate) { if (!fields.length) { return } const changedIndices = [] for (const field of fields) { const index = this.fieldsColumnsIndex[field] if (this._toggleColumnVisibility(index, checked)) { changedIndices.push(index) } } if (changedIndices.length) { this._updateAfterColumnToggle(changedIndices, checked, needUpdate) } }, 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) } }