/** * 富文本表格操作与行内样式/属性注入工具 * 注入 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(/]*)?>/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 `` }) // 注入 tr 样式 res = res.replace(/]*)?>/gi, (match, attrs = '') => { if (/style\s*=\s*["']/i.test(attrs)) { return ` `style="${s}; ${TR_STYLE}"`)}>` } return `` }) // 注入 th 属性与样式 res = res.replace(/]*)?>/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 `
` }) // 注入 td 属性与样式 res = res.replace(/]*)?>/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 `` }) return res } export function createEmptyCell(tagName = 'td') { const cell = document.createElement(tagName) cell.innerHTML = '


' 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 = `` for (let r = 0; r < rows; r += 1) { html += `` for (let c = 0; c < cols; c += 1) { if (r === 0) { html += `` } else { html += `` } } html += '' } html += '




' 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 = '


' 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 = '


' 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() }