浏览代码

Supports column formatter options returns dom element or jquery element

weibangtuo 1 年之前
父节点
当前提交
e6f507bd59
共有 4 个文件被更改,包括 279 次插入197 次删除
  1. 127 186
      src/bootstrap-table.js
  2. 1 1
      src/extensions/print/bootstrap-table-print.js
  3. 4 0
      src/extensions/toolbar/bootstrap-table-toolbar.js
  4. 147 10
      src/utils/index.js

+ 127 - 186
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 = $('<div>').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('<tr',
-      Utils.sprintf(' %s', htmlAttributes.length ? htmlAttributes.join(' ') : undefined),
-      Utils.sprintf(' id="%s"', Array.isArray(item) ? undefined : item._id),
-      Utils.sprintf(' class="%s"', style.classes || (Array.isArray(item) ? undefined : item._class)),
-      Utils.sprintf(' style="%s"', Array.isArray(item) ? undefined : item._style),
-      ` data-index="${i}"`,
-      Utils.sprintf(' data-uniqueid="%s"', Utils.getItemField(item, this.options.uniqueId, false)),
-      Utils.sprintf(' data-has-detail-view="%s"', this.options.detailView && Utils.calculateObjectValue(null, this.options.detailFilter, [i, item]) ? 'true' : undefined),
-      Utils.sprintf('%s', data_),
-      '>'
-    )
-
-    if (this.options.cardView) {
-      html.push(`<td colspan="${this.header.fields.length}"><div class="card-views">`)
-    }
-
+    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 = '<td>'
+      detailViewTemplate = Utils.h('td')
 
       if (Utils.calculateObjectValue(null, this.options.detailFilter, [i, item])) {
-        detailViewTemplate += `
-          <a class="detail-icon" href="#">
-          ${Utils.sprintf(this.constants.html.icon, this.options.iconsPrefix, this.options.icons.detailOpen)}
-          </a>
-        `
+        detailViewTemplate.append(Utils.h('a', {
+          class: 'detail-icon',
+          href: '#',
+          html: Utils.sprintf(this.constants.html.icon, this.options.iconsPrefix, this.options.icons.detailOpen)
+        }))
       }
-
-      detailViewTemplate += '</td>'
     }
 
     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 ?
-            `<div class="card-view ${c}">` :
-            `<td class="bs-checkbox ${c}"${class_}${style_}>`,
-          `<label>
-            <input
-            data-index="${i}"
-            name="${this.options.selectItemName}"
-            type="${type}"
-            ${Utils.sprintf('value="%s"', item[this.options.idField])}
-            ${Utils.sprintf('checked="%s"', isChecked ? 'checked' : undefined)}
-            ${Utils.sprintf('disabled="%s"', isDisabled ? 'disabled' : undefined)} />
-            <span></span>
-            </label>`,
-          this.header.formatters[j] && typeof value === 'string' ? value : '',
-          this.options.cardView ? '</div>' : '</td>'
-        ].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 ?
-          `<span class="card-view-title ${cellStyle.classes || ''}"${style_}>${Utils.getFieldTitle(this.columns, field)}</span>` : ''
 
-        text = `<div class="card-view">${cardTitle}<span class="card-view-value ${cellStyle.classes || ''}"${style_}>${value}</span></div>`
+        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 = '<div class="card-view"></div>'
+          return Utils.h('div', { class: 'card-view' })
         }
-      } else {
-        text = `<td${id_}${class_}${style_}${data_}${rowspan_}${colspan_}${title_}>${value}</td>`
+
+        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('</div></td>')
+      tr.append(Utils.h('td', {
+        colspan: this.header.fields.length
+      }, [
+        Utils.h('div', { class: 'card-views' }, trChildren)
+      ]))
+    } else {
+      tr.append(...trChildren)
     }
-    html.push('</tr>')
 
-    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($('<div>').html(toAppend).html())
         }
       }
     }
@@ -2291,7 +2251,10 @@ class BootstrapTable {
     let detailTemplate = ''
 
     if (Utils.hasDetailViewIcon(this.options)) {
-      detailTemplate = '<th class="detail"><div class="th-inner"></div><div class="fht-cell"></div></th>'
+      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('<th', class_, Utils.sprintf(' style="%s"', falign + valign + csses.concat().join('; ') || undefined))
-      let colspan = 0
-
-      if (this.footerData && this.footerData.length > 0) {
-        colspan = this.footerData[0][`_${column.field}_colspan`] || 0
-      }
-      if (colspan) {
-        html.push(` colspan="${colspan}" `)
-      }
-
-      html.push('>')
-      html.push('<div class="th-inner">')
-
-      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('</div>')
-      html.push('<div class="fht-cell"></div>')
-      html.push('</div>')
-      html.push('</th>')
+      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('<table><thead><tr></tr></thead></table>')
     }
 
-    this.$tableFooter.find('tr').html(html.join(''))
+    this.$tableFooter.find('tr').html(html)
 
     this.trigger('post-footer', this.$tableFooter)
   }

+ 1 - 1
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 : $('<div>').html(value).html()
     }
 
     const buildTable = (data, columnsArray) => {

+ 4 - 0
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 = $('<div>').html(value).text()
+        }
 
         if (
           !(index !== -1 &&

+ 147 - 10
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 => `<mark>${match.slice(3, -3)}</mark>`)
+    d.innerHTML = html
+    return d.childNodes
   }
 }