From e6f507bd59171b52c9c3cc2a31c02acb4b6e469f Mon Sep 17 00:00:00 2001 From: weibangtuo Date: Wed, 31 Jul 2024 18:14:35 +0800 Subject: [PATCH 1/2] Supports column formatter options returns dom element or jquery element --- src/bootstrap-table.js | 313 +++++++----------- src/extensions/print/bootstrap-table-print.js | 2 +- .../toolbar/bootstrap-table-toolbar.js | 4 + src/utils/index.js | 157 ++++++++- 4 files changed, 279 insertions(+), 197 deletions(-) diff --git a/src/bootstrap-table.js b/src/bootstrap-table.js index 89c90e90a3..73c8c3600f 100644 --- a/src/bootstrap-table.js +++ b/src/bootstrap-table.js @@ -1109,6 +1109,10 @@ class BootstrapTable { if (column && column.searchFormatter) { value = Utils.calculateObjectValue(column, this.header.formatters[j], [value, item, i, column.field], value) + if (this.header.formatters[j] && typeof value !== 'number') { + // search innerText + value = $('
').html(value).text() + } } if (typeof value === 'string' || typeof value === 'number') { @@ -1510,33 +1514,13 @@ class BootstrapTable { // eslint-disable-next-line no-unused-vars initRow (item, i, data, trFragments) { - const html = [] - let style = {} - const csses = [] - let data_ = '' - let attributes = {} - const htmlAttributes = [] - if (Utils.findIndex(this.hiddenRows, item) > -1) { return } - - style = Utils.calculateObjectValue(this.options, this.options.rowStyle, [item, i], style) - - if (style && style.css) { - for (const [key, value] of Object.entries(style.css)) { - csses.push(`${key}: ${value}`) - } - } - - attributes = Utils.calculateObjectValue(this.options, - this.options.rowAttributes, [item, i], attributes) - - if (attributes) { - for (const [key, value] of Object.entries(attributes)) { - htmlAttributes.push(`${key}="${Utils.escapeHTML(value)}"`) - } - } + 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)) { @@ -1544,61 +1528,46 @@ class BootstrapTable { if (k === 'index') { return } - data_ += ` data-${k}='${typeof v === 'object' ? JSON.stringify(v) : v}'` + data_[`data-${k}`] = typeof v === 'object' ? JSON.stringify(v) : v } } - - html.push('' - ) - - if (this.options.cardView) { - html.push(`
`) - } - + const tr = Utils.h('tr', { + ...attributes, + 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, + ...data_ + }) + const trChildren = [] let detailViewTemplate = '' if (Utils.hasDetailViewIcon(this.options)) { - detailViewTemplate = '' + detailViewTemplate = Utils.h('td') if (Utils.calculateObjectValue(null, this.options.detailFilter, [i, item])) { - detailViewTemplate += ` - - ${Utils.sprintf(this.constants.html.icon, this.options.iconsPrefix, this.options.icons.detailOpen)} - - ` + detailViewTemplate.append(Utils.h('a', { + class: 'detail-icon', + href: '#', + html: Utils.sprintf(this.constants.html.icon, this.options.iconsPrefix, this.options.icons.detailOpen) + })) } - - detailViewTemplate += '' } if (detailViewTemplate && this.options.detailViewAlign !== 'right') { - html.push(detailViewTemplate) + trChildren.push(detailViewTemplate) } - this.header.fields.forEach((field, j) => { + const tds = this.header.fields.map((field, j) => { const column = this.columns[j] - let text = '' const value_ = Utils.getItemField(item, field, this.options.escape, column.escape) let value = '' - let type = '' - let cellStyle = {} - let id_ = '' - let class_ = this.header.classes[j] - let style_ = '' - let styleToAdd_ = '' - let data_ = '' - let rowspan_ = '' - let colspan_ = '' - let title_ = '' + const attrs = { + style: [] + } if ((this.fromHtml || this.autoMergeCells) && typeof value_ === 'undefined') { if (!column.checkbox && !column.radio) { @@ -1614,47 +1583,21 @@ class BootstrapTable { return } - // Style concat - if (csses.concat([this.header.styles[j]]).length) { - styleToAdd_ += `${csses.concat([this.header.styles[j]]).join('; ')}` - } - if (item[`_${field}_style`]) { - styleToAdd_ += `${item[`_${field}_style`]}` + // handle id and class of td + for (const item of ['id', 'class', 'rowspan', 'colspan', 'title']) { + attrs[item] = item[`_${field}_${item}`] || undefined } - if (styleToAdd_) { - style_ = ` style="${styleToAdd_}"` - } - // Style concat + attrs.style.push(this.header.styles[j], item[`_${field}_style`]) + const cellStyle = Utils.calculateObjectValue(this.header, + this.header.cellStyles[j], [value_, item, i, field], {}) - // handle id and class of td - if (item[`_${field}_id`]) { - id_ = Utils.sprintf(' id="%s"', item[`_${field}_id`]) - } - if (item[`_${field}_class`]) { - class_ = Utils.sprintf(' class="%s"', item[`_${field}_class`]) - } - if (item[`_${field}_rowspan`]) { - rowspan_ = Utils.sprintf(' rowspan="%s"', item[`_${field}_rowspan`]) - } - if (item[`_${field}_colspan`]) { - colspan_ = Utils.sprintf(' colspan="%s"', item[`_${field}_colspan`]) - } - if (item[`_${field}_title`]) { - title_ = Utils.sprintf(' title="%s"', item[`_${field}_title`]) - } - cellStyle = Utils.calculateObjectValue(this.header, - this.header.cellStyles[j], [value_, item, i, field], cellStyle) if (cellStyle.classes) { - class_ = ` class="${cellStyle.classes}"` + attrs.class = attrs.class || [] + attrs.class.push(cellStyle.classes) } if (cellStyle.css) { - const csses_ = [] - - for (const [key, value] of Object.entries(cellStyle.css)) { - csses_.push(`${key}: ${value}`) - } - style_ = ` style="${csses_.concat(this.header.styles[j]).join('; ')}"` + attrs.style.push(cellStyle.css) } value = Utils.calculateObjectValue(column, @@ -1673,7 +1616,7 @@ class BootstrapTable { ) { let searchText = this.searchText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') - if (this.options.searchAccentNeutralise) { + if (this.options.searchAccentNeutralise && typeof value === 'string') { const indexRegex = new RegExp(`${Utils.normalizeAccent(searchText)}`, 'gmi') const match = indexRegex.exec(Utils.normalizeAccent(value)) @@ -1694,64 +1637,80 @@ class BootstrapTable { if (k === 'index') { return } - data_ += ` data-${k}="${v}"` + attrs[`data-${k}`] = v } } if (column.checkbox || column.radio) { - type = column.checkbox ? 'checkbox' : type - type = column.radio ? 'radio' : type - - const c = column['class'] || '' + 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 - - text = [ - this.options.cardView ? - `
` : - ``, - ``, - this.header.formatters[j] && typeof value === 'string' ? value : '', - this.options.cardView ? '
' : '' - ].join('') + 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) - } else if (this.options.cardView) { - const cardTitle = this.options.showHeader ? - `${Utils.getFieldTitle(this.columns, field)}` : '' - text = `
${cardTitle}${value}
` + return Utils.h(this.options.cardView ? 'div' : 'td', { + class: [this.options.cardView ? 'card-view' : '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 === '') { - text = '
' + return Utils.h('div', { class: 'card-view' }) } - } else { - text = `${value}` + + 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: 'card-view' }, [ + cardTitle, + Utils.h('span', { + class: ['card-view-value', cellStyle.classes], + style: attrs.style + }, [...Utils.htmlToNodes(value)]) + ]) } - html.push(text) - }) + return Utils.h('td', attrs, [...Utils.htmlToNodes(value)]) + }).filter(x => x) + + trChildren.push(...tds) if (detailViewTemplate && this.options.detailViewAlign === 'right') { - html.push(detailViewTemplate) + trChildren.push(detailViewTemplate) } if (this.options.cardView) { - html.push('
') + tr.append(Utils.h('td', { + colspan: this.header.fields.length + }, [ + Utils.h('div', { class: 'card-views' }, trChildren) + ])) + } else { + tr.append(...trChildren) } - html.push('') - return html.join('') + return tr } initBody (fixedScroll, updatedUid) { @@ -1779,12 +1738,13 @@ class BootstrapTable { for (let i = this.pageFrom - 1; i < this.pageTo; i++) { const item = data[i] - let tr = this.initRow(item, i, data, trFragments) + const tr = this.initRow(item, i, data, trFragments) hasTr = hasTr || !!tr - if (tr && typeof tr === 'string') { + if (tr && tr instanceof Node) { const uniqueId = this.options.uniqueId + const toAppend = [tr] if (uniqueId && item.hasOwnProperty(uniqueId)) { const itemUniqueId = item[uniqueId] @@ -1797,15 +1757,15 @@ class BootstrapTable { toExpand.push(i) if (!updatedUid || itemUniqueId !== updatedUid) { - tr += oldTrNext[0].outerHTML + toAppend.push(oldTrNext[0]) } } } if (!this.options.virtualScroll) { - trFragments.append(tr) + trFragments.append(toAppend) } else { - rows.push(tr) + rows.push($('
').html(toAppend).html()) } } } @@ -2291,7 +2251,10 @@ class BootstrapTable { let detailTemplate = '' if (Utils.hasDetailViewIcon(this.options)) { - detailTemplate = '
' + detailTemplate = Utils.h('th', { class: 'detail' }, [ + Utils.h('div', { class: 'th-inner' }), + Utils.h('div', { class: 'fht-cell' }) + ]) } if (detailTemplate && this.options.detailViewAlign !== 'right') { @@ -2299,15 +2262,11 @@ class BootstrapTable { } for (const column of this.columns) { - let falign = '' - let valign = '' - const csses = [] - let style = {} - let class_ = Utils.sprintf(' class="%s"', column['class']) + const hasData = this.footerData && this.footerData.length > 0 if ( !column.visible || - this.footerData && this.footerData.length > 0 && !(column.field in this.footerData[0]) + hasData && !(column.field in this.footerData[0]) ) { continue } @@ -2316,46 +2275,28 @@ class BootstrapTable { return } - falign = Utils.sprintf('text-align: %s; ', column.falign ? column.falign : column.align) - valign = Utils.sprintf('vertical-align: %s; ', column.valign) + const style = Utils.calculateObjectValue(null, column.footerStyle || this.options.footerStyle, [column]) + const csses = style && style.css || {} + const colspan = hasData && this.footerData[0][`_${column.field}_colspan`] || 0 + let value = hasData && this.footerData[0][column.field] || '' - style = Utils.calculateObjectValue(null, column.footerStyle || this.options.footerStyle, [column]) - - if (style && style.css) { - for (const [key, value] of Object.entries(style.css)) { - csses.push(`${key}: ${value}`) - } - } - if (style && style.classes) { - class_ = Utils.sprintf(' class="%s"', column['class'] ? - [column['class'], style.classes].join(' ') : style.classes) - } - - html.push(' 0) { - colspan = this.footerData[0][`_${column.field}_colspan`] || 0 - } - if (colspan) { - html.push(` colspan="${colspan}" `) - } - - html.push('>') - html.push('
') - - let value = '' - - if (this.footerData && this.footerData.length > 0) { - value = this.footerData[0][column.field] || '' - } - html.push(Utils.calculateObjectValue(column, column.footerFormatter, - [data, value], value)) + value = Utils.calculateObjectValue(column, column.footerFormatter, + [data, value], value) - html.push('
') - html.push('
') - html.push('
') - html.push('') + html.push(Utils.h('th', { + class: [column['class'], style && style.classes], + style: { + 'text-align': column.falign ? column.falign : column.align, + 'vertical-align': column.valign, + ...csses + }, + colspan: colspan || undefined + }, [ + Utils.h('div', { + class: 'th-inner' + }, [...Utils.htmlToNodes(value)]), + Utils.h('div', { class: 'fht-cell' }) + ])) } if (detailTemplate && this.options.detailViewAlign === 'right') { @@ -2371,7 +2312,7 @@ class BootstrapTable { this.$tableFooter.html('
') } - this.$tableFooter.find('tr').html(html.join('')) + this.$tableFooter.find('tr').html(html) this.trigger('post-footer', this.$tableFooter) } diff --git a/src/extensions/print/bootstrap-table-print.js b/src/extensions/print/bootstrap-table-print.js index ea7c73ae63..646d908b90 100644 --- a/src/extensions/print/bootstrap-table-print.js +++ b/src/extensions/print/bootstrap-table-print.js @@ -148,7 +148,7 @@ $.BootstrapTable = class extends $.BootstrapTable { [value_, row, i], value_) return typeof value === 'undefined' || value === null ? - this.options.undefinedText : value + this.options.undefinedText : $('
').html(value).html() } const buildTable = (data, columnsArray) => { diff --git a/src/extensions/toolbar/bootstrap-table-toolbar.js b/src/extensions/toolbar/bootstrap-table-toolbar.js index 37c78735ab..a5fcd0bfd7 100644 --- a/src/extensions/toolbar/bootstrap-table-toolbar.js +++ b/src/extensions/toolbar/bootstrap-table-toolbar.js @@ -350,6 +350,10 @@ $.BootstrapTable = class extends $.BootstrapTable { value = Utils.calculateObjectValue(this.header, this.header.formatters[index], [value, item, i], value) + if (this.header.formatters[index]) { + // search innerText + value = $('
').html(value).text() + } if ( !(index !== -1 && diff --git a/src/utils/index.js b/src/utils/index.js index c3b4727590..018bfa26f4 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -668,24 +668,161 @@ export default { }, replaceSearchMark (html, searchText) { - const node = document.createElement('div') - const replaceMark = (node, searchText) => { - const regExp = new RegExp(searchText, 'gim') + const isDom = html instanceof Element + const node = isDom ? html : document.createElement('div') + const regExp = new RegExp(searchText, 'gim') + const replaceTextWithDom = (text, regExp) => { + const result = [] + let match + let lastIndex = 0 + + while ((match = regExp.exec(text)) !== null) { + if (lastIndex !== match.index) { + result.push(document.createTextNode(text.substring(lastIndex, match.index))) + } + const mark = document.createElement('mark') + + mark.innerText = match[0] + result.push(mark) + lastIndex = match.index + match[0].length + } + if (!result.length) { + // no match + return + } + if (lastIndex !== text.length) { + result.push(document.createTextNode(text.substring(lastIndex))) + } + return result + } + const replaceMark = node => { + for (let i = 0; i < node.childNodes.length; i++) { + const child = node.childNodes[i] - for (const child of node.childNodes) { if (child.nodeType === document.TEXT_NODE) { - child.data = child.data.replace(regExp, match => `___${match}___`) + const elements = replaceTextWithDom(child.data, regExp) + + if (elements) { + for (const el of elements) { + node.insertBefore(el, child) + } + node.removeChild(child) + i += elements.length - 1 + } } if (child.nodeType === document.ELEMENT_NODE) { - replaceMark(child, searchText) + replaceMark(child) + } + } + } + + if (!isDom) { + node.innerHTML = html + } + replaceMark(node) + return isDom ? node : node.innerHTML + }, + + classToString (class_) { + if (typeof class_ === 'string') { + return class_ + } + if (Array.isArray(class_)) { + return class_.map(x => this.classToString(x)).filter(x => x).join(' ') + } + if (class_ && typeof class_ === 'object') { + return Object.entries(class_).map(([k, v]) => v ? k : '').filter(x => x).join(' ') + } + return '' + }, + + parseStyle (dom, style) { + if (!style) { + return dom + } + if (typeof style === 'string') { + style.split(';').forEach(i => { + const index = i.indexOf(':') + + if (index > 0) { + const k = i.substring(0, index).trim() + const v = i.substring(index + 1).trim() + + dom.style.setProperty(k, v) + } + }) + } else if (Array.isArray(style)) { + for (const item of style) { + this.parseStyle(item) + } + } else if (typeof style === 'object') { + for (const [k, v] of Object.entries(style)) { + dom.style.setProperty(k, v) + } + } + return dom + }, + + h (element, attrs, children) { + const el = element instanceof HTMLElement ? element : document.createElement(element) + const _attrs = attrs || {} + const _children = children || [] + + // default attributes + if (el.tagName === 'A') { + el.href = 'javascript:' + } + + for (const [k, v] of Object.entries(_attrs)) { + if (v === undefined) { + continue + } + if (['text', 'innerText'].includes(k)) { + el.innerText = v + } else if (['html', 'innerHTML'].includes(k)) { + el.innerHTML = v + } else if (k === 'children') { + _children.push(...v) + } else if (k === 'class') { + el.setAttribute('class', this.classToString(v)) + } else if (k === 'style') { + if (typeof v === 'string') { + el.setAttribute('style', v) + } else { + this.parseStyle(el, v) } + } else if (k.startsWith('@') || k.startsWith('on')) { + // event handlers + const event = k.startsWith('@') ? k.substring(1) : k.substring(2).toLowerCase() + const args = Array.isArray(v) ? v : [v] + + el.addEventListener(event, ...args) + } else if (k.startsWith('.')) { + // set property + el[k.substring(1)] = v + } else { + el.setAttribute(k, v) } } + if (_children.length) { + el.append(..._children) + } + return el + }, - node.innerHTML = html - replaceMark(node, searchText) + htmlToNodes (html) { + if (html instanceof $) { + return html.get() + } + if (html instanceof Node) { + return [html] + } + if (typeof html !== 'string') { + html = new String(html).toString() + } + const d = document.createElement('div') - return node.innerHTML.replace(new RegExp(`___${searchText}___`, 'gim'), - match => `${match.slice(3, -3)}`) + d.innerHTML = html + return d.childNodes } } From cc913a37ae4ee0bd3e5ed112534cc39aae147604 Mon Sep 17 00:00:00 2001 From: weibangtuo Date: Fri, 2 Aug 2024 11:50:48 +0800 Subject: [PATCH 2/2] Update doc for column options data-formatter and data-footer-formatter --- CHANGELOG.md | 6 ++++++ site/docs/api/column-options.md | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dc1642394..a05f908ab0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ ChangeLog --------- +### 1.23.3 + +### Core + +- **New:** Added support for column options `formatter` and `footerFormatter` methods returning type `jQuery`, `HTMLElement`. + ### 1.23.2 ### Core diff --git a/site/docs/api/column-options.md b/site/docs/api/column-options.md index 9787866761..1d4ded0d5b 100644 --- a/site/docs/api/column-options.md +++ b/site/docs/api/column-options.md @@ -230,7 +230,7 @@ The column options is defined in `jQuery.fn.bootstrapTable.columnDefaults`. * `data`: Array of all the data rows. * `value`: If footer data is set, the value of the footer column. - The function should return a string with the text to show in the footer cell. + The expected return data type is `jQuery`, `String` or `HTMLElement`. Other types will be forced to the `String` type. If you fetch data from a server and set the footer value from the server response, please use the `footerField` Option. @@ -282,6 +282,8 @@ The column options is defined in `jQuery.fn.bootstrapTable.columnDefaults`. * `index`: the row index. * `field`: the row field. + The expected return data type is `jQuery`, `String` or `HTMLElement`. Other types will be forced to the `String` type. + - **Default:** `undefined` - **Example:** [Column Formatter](https://examples.bootstrap-table.com/#column-options/formatter.html)