187 lines
6.3 KiB
JavaScript
187 lines
6.3 KiB
JavaScript
/**
|
||
* 富文本表格操作与行内样式/属性注入工具
|
||
* 注入 border="1"、cellpadding="8"、cellspacing="0" 以及 style 属性
|
||
* 保证在 H5、App-plus 原生 editor、小程序以及 rich-text 中均能拥有清晰的表格边框与样式
|
||
*/
|
||
|
||
const TABLE_STYLE = 'border-collapse: collapse; width: 100%; border: 1px solid #dcdfe6; margin: 12px 0; table-layout: auto;'
|
||
const TR_STYLE = 'border-bottom: 1px solid #ebeef5;'
|
||
const TH_STYLE = 'border: 1px solid #dcdfe6; background-color: #f5f7fa; color: #303133; font-weight: 600; padding: 8px 12px; font-size: 14px; text-align: left; min-width: 44px; word-break: break-word;'
|
||
const TD_STYLE = 'border: 1px solid #dcdfe6; background-color: #ffffff; color: #303133; padding: 8px 12px; font-size: 14px; vertical-align: top; word-break: break-word; min-width: 44px;'
|
||
|
||
/**
|
||
* 给 HTML 字符串中的所有 table/tr/th/td 注入高优先级的行内样式与原生 HTML 边框属性
|
||
* (双重保障,即使 Quill 或原生组件重置了部分 CSS,border="1" 仍能强制渲染出边框)
|
||
*/
|
||
export function injectTableInlineStyles(html) {
|
||
if (!html || typeof html !== 'string') return ''
|
||
let res = html
|
||
|
||
// 注入 table 属性与样式
|
||
res = res.replace(/<table(\s+[^>]*)?>/gi, (match, attrs = '') => {
|
||
let cleanAttrs = attrs
|
||
cleanAttrs = cleanAttrs.replace(/\s+(border|cellspacing|cellpadding)\s*=\s*["'][^"']*["']/gi, '')
|
||
if (/style\s*=\s*["']/i.test(cleanAttrs)) {
|
||
cleanAttrs = cleanAttrs.replace(/style\s*=\s*["']([^"']*)["']/i, (m, s) => `style="${s}; ${TABLE_STYLE}"`)
|
||
} else {
|
||
cleanAttrs = ` style="${TABLE_STYLE}"` + cleanAttrs
|
||
}
|
||
return `<table border="1" cellpadding="8" cellspacing="0"${cleanAttrs}>`
|
||
})
|
||
|
||
// 注入 tr 样式
|
||
res = res.replace(/<tr(\s+[^>]*)?>/gi, (match, attrs = '') => {
|
||
if (/style\s*=\s*["']/i.test(attrs)) {
|
||
return `<tr${attrs.replace(/style\s*=\s*["']([^"']*)["']/i, (m, s) => `style="${s}; ${TR_STYLE}"`)}>`
|
||
}
|
||
return `<tr style="${TR_STYLE}"${attrs}>`
|
||
})
|
||
|
||
// 注入 th 属性与样式
|
||
res = res.replace(/<th(\s+[^>]*)?>/gi, (match, attrs = '') => {
|
||
let cleanAttrs = attrs.replace(/\s+border\s*=\s*["'][^"']*["']/gi, '')
|
||
if (/style\s*=\s*["']/i.test(cleanAttrs)) {
|
||
cleanAttrs = cleanAttrs.replace(/style\s*=\s*["']([^"']*)["']/i, (m, s) => `style="${s}; ${TH_STYLE}"`)
|
||
} else {
|
||
cleanAttrs = ` style="${TH_STYLE}"` + cleanAttrs
|
||
}
|
||
return `<th border="1"${cleanAttrs}>`
|
||
})
|
||
|
||
// 注入 td 属性与样式
|
||
res = res.replace(/<td(\s+[^>]*)?>/gi, (match, attrs = '') => {
|
||
let cleanAttrs = attrs.replace(/\s+border\s*=\s*["'][^"']*["']/gi, '')
|
||
if (/style\s*=\s*["']/i.test(cleanAttrs)) {
|
||
cleanAttrs = cleanAttrs.replace(/style\s*=\s*["']([^"']*)["']/i, (m, s) => `style="${s}; ${TD_STYLE}"`)
|
||
} else {
|
||
cleanAttrs = ` style="${TD_STYLE}"` + cleanAttrs
|
||
}
|
||
return `<td border="1"${cleanAttrs}>`
|
||
})
|
||
|
||
return res
|
||
}
|
||
|
||
export function createEmptyCell(tagName = 'td') {
|
||
const cell = document.createElement(tagName)
|
||
cell.innerHTML = '<p><br></p>'
|
||
cell.setAttribute('border', '1')
|
||
if (tagName === 'th') {
|
||
cell.style.cssText = TH_STYLE
|
||
} else {
|
||
cell.style.cssText = TD_STYLE
|
||
}
|
||
return cell
|
||
}
|
||
|
||
export function buildTableHtml(rows = 3, cols = 3) {
|
||
let html = `<table border="1" cellpadding="8" cellspacing="0" style="${TABLE_STYLE}"><tbody>`
|
||
for (let r = 0; r < rows; r += 1) {
|
||
html += `<tr style="${TR_STYLE}">`
|
||
for (let c = 0; c < cols; c += 1) {
|
||
if (r === 0) {
|
||
html += `<th border="1" style="${TH_STYLE}"><p><br></p></th>`
|
||
} else {
|
||
html += `<td border="1" style="${TD_STYLE}"><p><br></p></td>`
|
||
}
|
||
}
|
||
html += '</tr>'
|
||
}
|
||
html += '</tbody></table><p><br></p>'
|
||
return html
|
||
}
|
||
|
||
export function getCurrentCell(editorEl) {
|
||
const sel = window.getSelection()
|
||
if (!sel || sel.rangeCount === 0) return null
|
||
let node = sel.anchorNode
|
||
if (node?.nodeType === Node.TEXT_NODE) node = node.parentElement
|
||
const cell = node?.closest?.('td, th')
|
||
if (!cell || !editorEl?.contains(cell)) return null
|
||
return cell
|
||
}
|
||
|
||
export function addRowBefore(cell) {
|
||
const row = cell.closest('tr')
|
||
const newRow = row.cloneNode(true)
|
||
newRow.style.cssText = TR_STYLE
|
||
newRow.querySelectorAll('td, th').forEach((item) => {
|
||
item.innerHTML = '<p><br></p>'
|
||
item.removeAttribute('colspan')
|
||
item.removeAttribute('rowspan')
|
||
item.setAttribute('border', '1')
|
||
item.style.cssText = item.tagName.toLowerCase() === 'th' ? TH_STYLE : TD_STYLE
|
||
})
|
||
row.before(newRow)
|
||
return newRow.cells[cell.cellIndex] || newRow.cells[0]
|
||
}
|
||
|
||
export function addRowAfter(cell) {
|
||
const row = cell.closest('tr')
|
||
const newRow = row.cloneNode(true)
|
||
newRow.style.cssText = TR_STYLE
|
||
newRow.querySelectorAll('td, th').forEach((item) => {
|
||
item.innerHTML = '<p><br></p>'
|
||
item.removeAttribute('colspan')
|
||
item.removeAttribute('rowspan')
|
||
item.setAttribute('border', '1')
|
||
item.style.cssText = item.tagName.toLowerCase() === 'th' ? TH_STYLE : TD_STYLE
|
||
})
|
||
row.after(newRow)
|
||
return newRow.cells[cell.cellIndex] || newRow.cells[0]
|
||
}
|
||
|
||
export function deleteRow(cell) {
|
||
const row = cell.closest('tr')
|
||
const section = row?.parentElement
|
||
if (!row || !section || section.querySelectorAll('tr').length <= 1) return null
|
||
const focusCell =
|
||
row.nextElementSibling?.cells[cell.cellIndex] ||
|
||
row.previousElementSibling?.cells[cell.cellIndex] ||
|
||
null
|
||
row.remove()
|
||
return focusCell
|
||
}
|
||
|
||
export function addColBefore(cell) {
|
||
const index = cell.cellIndex
|
||
const table = cell.closest('table')
|
||
table.querySelectorAll('tr').forEach((row) => {
|
||
const refCell = row.cells[index]
|
||
if (!refCell) return
|
||
refCell.before(createEmptyCell(refCell.tagName.toLowerCase()))
|
||
})
|
||
return cell.parentElement.cells[index]
|
||
}
|
||
|
||
export function addColAfter(cell) {
|
||
const index = cell.cellIndex
|
||
const table = cell.closest('table')
|
||
table.querySelectorAll('tr').forEach((row) => {
|
||
const refCell = row.cells[index]
|
||
if (!refCell) return
|
||
refCell.after(createEmptyCell(refCell.tagName.toLowerCase()))
|
||
})
|
||
return cell.parentElement.cells[index + 1]
|
||
}
|
||
|
||
export function deleteCol(cell) {
|
||
const index = cell.cellIndex
|
||
const table = cell.closest('table')
|
||
if (cell.parentElement.cells.length <= 1) return null
|
||
let focusCell = null
|
||
table.querySelectorAll('tr').forEach((row) => {
|
||
const target = row.cells[index]
|
||
if (!target) return
|
||
if (row === cell.parentElement) {
|
||
focusCell = row.cells[index + 1] || row.cells[index - 1] || null
|
||
}
|
||
target.remove()
|
||
})
|
||
return focusCell
|
||
}
|
||
|
||
export function deleteTable(cell) {
|
||
cell.closest('table')?.remove()
|
||
}
|