Skip to content

Commit 3c5bb64

Browse files
committed
feat: Add support for rowspan and colspan
1 parent 224ee64 commit 3c5bb64

File tree

2 files changed

+220
-31
lines changed

2 files changed

+220
-31
lines changed

src/fromRedactor.tsx

Lines changed: 199 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ const ELEMENT_TAGS: IHtmlToJsonElementTags = {
5151
THEAD: (el: HTMLElement) => ({ type: 'thead', attrs: {} }),
5252
TBODY: (el: HTMLElement) => ({ type: 'tbody', attrs: {} }),
5353
TR: (el: HTMLElement) => ({ type: 'tr', attrs: {} }),
54-
TD: (el: HTMLElement) => ({ type: 'td', attrs: {} }),
55-
TH: (el: HTMLElement) => ({ type: 'th', attrs: {} }),
54+
TD: (el: HTMLElement) => ({ type: 'td', attrs: { ...spanningAttrs(el) } }),
55+
TH: (el: HTMLElement) => ({ type: 'th', attrs: { ...spanningAttrs(el) } }),
5656
// FIGURE: (el: HTMLElement) => ({ type: 'reference', attrs: { default: true, "display-type": "display", "type": "asset" } }),
5757

5858
FIGURE: (el: HTMLElement) => {
@@ -208,6 +208,29 @@ export const fromRedactor = (el: any, options?:IHtmlToJsonOptions) : IAnyObject
208208
} else if (el.nodeName === 'COLGROUP') {
209209
return null
210210
}
211+
else if (el.nodeName === "TABLE") {
212+
const tbody = el.querySelector('tbody')
213+
const thead = el.querySelector('thead')
214+
215+
if (!tbody && !thead) {
216+
el.append(document.createElement('tbody'))
217+
}
218+
}
219+
else if (['TBODY', 'THEAD'].includes(el.nodeName)) {
220+
const row = el.querySelector('tr')
221+
if (!row) {
222+
const tr = document.createElement('tr')
223+
el.append(tr)
224+
}
225+
}
226+
else if (el.nodeName === 'TR') {
227+
const cell = el.querySelector('th, td')
228+
if (!cell) {
229+
const cellType = el.parentElement.nodeName === 'THEAD' ? 'th' : 'td'
230+
const cell = document.createElement(cellType)
231+
el.append(cell)
232+
}
233+
}
211234
const { nodeName } = el
212235
let parent = el
213236
if(el.nodeName === "BODY"){
@@ -613,46 +636,111 @@ export const fromRedactor = (el: any, options?:IHtmlToJsonOptions) : IAnyObject
613636
})
614637
}
615638
if (nodeName === 'TABLE') {
616-
let row = 0
639+
const row = el.querySelectorAll('TR').length
617640
let table_child = ['THEAD', 'TBODY']
618641
let cell_type = ['TH', 'TD']
619642
let col = 0
620-
Array.from(el.childNodes).forEach((child: any) => {
621-
if (table_child.includes(child.nodeName)) {
622-
row += child.childNodes.length
623-
}
624-
})
625-
let rowElement = el.getElementsByTagName('TR')[0]
626-
if (rowElement)
627-
Array.from(rowElement.childNodes).forEach((child: any) => {
628-
if (cell_type.includes(child.nodeName)) {
629-
col += 1
630-
}
631-
})
643+
644+
const colElementLength = el.getElementsByTagName('COLGROUP')[0]?.children?.length ?? 0
645+
col = Math.max(...Array.from(el.getElementsByTagName('TR')).map((row: any) => row.children.length), colElementLength)
632646
let colWidths: Array<any> = Array.from({ length: col }).fill(250)
633-
if (el?.childNodes?.[0]?.nodeName === 'COLGROUP') {
634-
let colGroupWidth: Array<any> = []
635-
let totalWidth = parseFloat(el.childNodes[0].getAttribute('data-width')) || col * 250
636-
Array.from(el.childNodes[0].childNodes).forEach((child: any) => {
637-
let width = child?.style?.width || '250px'
638-
if (width.slice(width.length - 1) === '%') {
639-
colGroupWidth.push((parseFloat(width.slice(0, width.length - 1)) * totalWidth) / 100)
640-
} else if (width.slice(width.length - 2) === 'px') {
641-
colGroupWidth.push(parseFloat(width.slice(0, width.length - 2)))
647+
648+
Array.from(el.childNodes).forEach((child: any) => {
649+
if (child?.nodeName === 'COLGROUP') {
650+
let colGroupWidth = Array<number>(col).fill(250)
651+
let totalWidth = parseFloat(child.getAttribute('data-width')) || col * 250
652+
Array.from(child.children).forEach((child: any, index) => {
653+
if (child?.nodeName === 'COL') {
654+
let width = child?.style?.width ?? '250px'
655+
if (width.substr(-1) === '%') {
656+
colGroupWidth[index] = (parseFloat(width.slice(0, width.length - 1)) * totalWidth) / 100
657+
} else if (width.substr(-2) === 'px') {
658+
colGroupWidth[index] = parseFloat(width.slice(0, width.length - 2))
659+
}
642660
}
643661
})
644662
colWidths = colGroupWidth
645663
}
664+
})
665+
let tableHead : any
666+
let tableBody: any
667+
668+
children.forEach((tableChild: any) => {
669+
if (tableChild?.type === 'thead') {
670+
tableHead = tableChild
671+
return
672+
}
673+
if (tableChild?.type === 'tbody') {
674+
tableBody = tableChild
675+
return
676+
}
677+
});
678+
679+
let disabledCols = [...tableHead?.attrs?.disabledCols ?? [], ...tableBody?.attrs?.disabledCols ?? []]
680+
delete tableHead?.attrs?.disabledCols
681+
delete tableBody?.attrs?.disabledCols
682+
683+
const tableAttrs = {
684+
...elementAttrs.attrs,
685+
rows: row,
686+
cols: col,
687+
colWidths: colWidths,
688+
}
689+
690+
if(!isEmpty(disabledCols)){
691+
tableAttrs['disabledCols'] = Array.from(new Set(disabledCols))
692+
}
693+
646694
elementAttrs = {
647695
...elementAttrs,
648-
attrs: {
649-
...elementAttrs.attrs,
650-
rows: row,
651-
cols: col,
652-
colWidths: colWidths
653-
}
696+
attrs: tableAttrs
654697
}
655698
}
699+
if (["THEAD", "TBODY"].includes(nodeName)) {
700+
const rows = children as any[]
701+
const disabledCols = rows.flatMap(row => {
702+
const { disabledCols } = row.attrs
703+
delete row['attrs']['disabledCols']
704+
return disabledCols ?? []
705+
})
706+
elementAttrs.attrs['disabledCols'] = disabledCols
707+
}
708+
if (nodeName === "TBODY") {
709+
710+
const rows = children
711+
712+
addVoidCellsAndApplyAttributes(rows)
713+
714+
children = getTbodyChildren(rows)
715+
}
716+
717+
if (nodeName === "TR") {
718+
719+
const cells = children.filter((child:any) => ['th', 'td'].includes(child.type)) as any[]
720+
721+
722+
const disabledCols = cells.flatMap((cell, cellIndex) => {
723+
let { colSpan } = cell.attrs
724+
if (!colSpan) return []
725+
colSpan = parseInt(colSpan)
726+
return Array(colSpan).fill(0).map((_, i) => cellIndex + i)
727+
})
728+
729+
if (disabledCols.length)
730+
elementAttrs.attrs['disabledCols'] = disabledCols
731+
732+
733+
}
734+
if (['TD', 'TH'].includes(nodeName)) {
735+
const { colSpan = 1, rowSpan } = elementAttrs?.['attrs']
736+
737+
return [
738+
jsx('element', elementAttrs, children),
739+
...Array(colSpan - 1)
740+
.fill(0)
741+
.map((_) => emptyCell(nodeName.toLowerCase(), rowSpan ? { inducedRowSpan: rowSpan } : {}))
742+
]
743+
}
656744
if (nodeName === 'P') {
657745
if (
658746
elementAttrs?.attrs?.["redactor-attributes"]?.['data-checked'] &&
@@ -809,3 +897,84 @@ export const getNestedValueIfAvailable = (value: string) => {
809897
return value;
810898
}
811899
};
900+
901+
902+
const spanningAttrs = (el: HTMLElement) => {
903+
const attrs = {}
904+
const rowSpan = parseInt(el.getAttribute('rowspan') ?? '1')
905+
const colSpan = parseInt(el.getAttribute('colspan') ?? '1')
906+
if (rowSpan > 1) attrs['rowSpan'] = rowSpan
907+
if (colSpan > 1) attrs['colSpan'] = colSpan
908+
909+
return attrs
910+
}
911+
const emptyCell = (cellType: string, attrs = {}) => {
912+
return jsx('element', { type: cellType, attrs: { void: true, ...attrs } }, [{ text: '' }])
913+
}
914+
915+
const addVoidCellsAndApplyAttributes = (rows: any[]) => {
916+
rows.forEach((row, currIndex) => {
917+
const cells = row.children as any[]
918+
919+
cells.forEach((cell, cellIndex) => {
920+
if (!cell || !cell.attrs) return
921+
922+
const { rowSpan, inducedRowSpan } = cell.attrs
923+
924+
let span = rowSpan ?? inducedRowSpan ?? 0
925+
926+
if (!span || span < 2) return
927+
const nextRow = rows[currIndex + 1]
928+
if (!nextRow) {
929+
delete cell?.attrs?.inducedRowSpan
930+
return
931+
}
932+
933+
// set inducedRowSpan on cell in row at cellIndex
934+
span--
935+
nextRow?.children?.splice(cellIndex, 0,
936+
emptyCell('td',
937+
(span > 1) ? { inducedRowSpan: span } : {}
938+
))
939+
940+
// Include next row in trgrp
941+
nextRow['attrs']['included'] = true
942+
943+
// Make a new trgrp
944+
if (rowSpan) {
945+
row['attrs']['origin'] = true
946+
}
947+
948+
delete cell?.['attrs']?.['inducedRowSpan']
949+
950+
})
951+
})
952+
}
953+
954+
955+
function getTbodyChildren (rows: any[]) {
956+
const newTbodyChildren = rows.reduce((tBodyChildren, row, rowIndex) => {
957+
958+
const { included, origin } = row.attrs
959+
const l = tBodyChildren.length
960+
961+
if (included || origin) {
962+
963+
if (origin && !included) {
964+
tBodyChildren.push(jsx('element', { type: 'trgrp' }, row))
965+
}
966+
if (included) {
967+
tBodyChildren[l - 1].children.push(row)
968+
969+
}
970+
delete row['attrs']['included']
971+
delete row['attrs']['origin']
972+
return tBodyChildren
973+
}
974+
975+
tBodyChildren.push(row)
976+
return tBodyChildren
977+
978+
}, [])
979+
return newTbodyChildren
980+
}

src/toRedactor.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ const ELEMENT_TYPES: IJsonToHtmlElementTags = {
8080
tr: (attrs: any, child: any) => {
8181
return `<tr${attrs}>${child}</tr>`
8282
},
83+
trgrp: (attrs: any, child: any) => {
84+
return child
85+
},
8386
td: (attrs: any, child: any) => {
8487
return `<td${attrs}>${child}</td>`
8588
},
@@ -399,6 +402,15 @@ export const toRedactor = (jsonValue: any,options?:IJsonToHtmlOptions) : string
399402
delete allattrs['cols']
400403
delete allattrs['colWidths']
401404
}
405+
if (allattrs['disabledCols']) {
406+
delete allattrs['disabledCols']
407+
}
408+
if (allattrs['colSpan']) {
409+
delete allattrs['colSpan']
410+
}
411+
if (allattrs['rowSpan']) {
412+
delete allattrs['rowSpan']
413+
}
402414

403415
attrsJson = { ...attrsJson, ...allattrs, style: style }
404416
if (jsonValue['type'] === 'reference') {
@@ -482,7 +494,10 @@ export const toRedactor = (jsonValue: any,options?:IJsonToHtmlOptions) : string
482494
if (!(setCol.size === 1 && jsonValue.attrs.cols * setCol.values().next().value === totalWidth)) {
483495
let col = ''
484496
Array.from(colWidths).forEach(
485-
(child, index) => (col += `<col style="width:${(colWidths[index] / totalWidth) * 100}%"/>`)
497+
(colWidth, index) => {
498+
const width = (colWidth as number / totalWidth) * 100
499+
col += `<col style="width:${width}%"/>`
500+
}
486501
)
487502
let colgroup = `<colgroup data-width='${totalWidth}'>${col}</colgroup>`
488503
children = colgroup + children
@@ -521,6 +536,11 @@ export const toRedactor = (jsonValue: any,options?:IJsonToHtmlOptions) : string
521536
return children
522537
}
523538
}
539+
540+
if(['td','th'].includes(jsonValue['type'])){
541+
if(jsonValue?.['attrs']?.['void']) return ''
542+
}
543+
524544
attrs = (attrs.trim() ? ' ' : '') + attrs.trim()
525545

526546
return ELEMENT_TYPES[orgType || jsonValue['type']](attrs, children,jsonValue, figureStyles)

0 commit comments

Comments
 (0)