Skip to content

Commit 3b1bb7c

Browse files
authored
Merge pull request #30 from contentstack/feat/EB-579--merge-cells
Feat/eb 579 merge cells
2 parents a5d6df4 + c0857fe commit 3b1bb7c

File tree

5 files changed

+357
-32
lines changed

5 files changed

+357
-32
lines changed

src/fromRedactor.tsx

Lines changed: 198 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,28 @@ 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.innerHTML += "<tbody></tbody>"
217+
}
218+
}
219+
else if (['TBODY', 'THEAD'].includes(el.nodeName)) {
220+
const row = el.querySelector('tr')
221+
if (!row) {
222+
el.innerHTML += "<tr></tr>"
223+
}
224+
}
225+
else if (el.nodeName === 'TR') {
226+
const cell = el.querySelector('th, td')
227+
if (!cell) {
228+
const cellType = el.parentElement.nodeName === 'THEAD' ? 'th' : 'td'
229+
el.innerHTML += `<${cellType}></${cellType}>`
230+
231+
}
232+
}
211233
const { nodeName } = el
212234
let parent = el
213235
if(el.nodeName === "BODY"){
@@ -613,46 +635,111 @@ export const fromRedactor = (el: any, options?:IHtmlToJsonOptions) : IAnyObject
613635
})
614636
}
615637
if (nodeName === 'TABLE') {
616-
let row = 0
638+
const row = el.querySelectorAll('TR').length
617639
let table_child = ['THEAD', 'TBODY']
618640
let cell_type = ['TH', 'TD']
619641
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-
})
642+
643+
const colElementLength = el.getElementsByTagName('COLGROUP')[0]?.children?.length ?? 0
644+
col = Math.max(...Array.from(el.getElementsByTagName('TR')).map((row: any) => row.children.length), colElementLength)
632645
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)))
646+
647+
Array.from(el.childNodes).forEach((child: any) => {
648+
if (child?.nodeName === 'COLGROUP') {
649+
let colGroupWidth = Array<number>(col).fill(250)
650+
let totalWidth = parseFloat(child.getAttribute('data-width')) || col * 250
651+
Array.from(child.children).forEach((child: any, index) => {
652+
if (child?.nodeName === 'COL') {
653+
let width = child?.style?.width ?? '250px'
654+
if (width.substr(-1) === '%') {
655+
colGroupWidth[index] = (parseFloat(width.slice(0, width.length - 1)) * totalWidth) / 100
656+
} else if (width.substr(-2) === 'px') {
657+
colGroupWidth[index] = parseFloat(width.slice(0, width.length - 2))
658+
}
642659
}
643660
})
644661
colWidths = colGroupWidth
645662
}
663+
})
664+
let tableHead : any
665+
let tableBody: any
666+
667+
children.forEach((tableChild: any) => {
668+
if (tableChild?.type === 'thead') {
669+
tableHead = tableChild
670+
return
671+
}
672+
if (tableChild?.type === 'tbody') {
673+
tableBody = tableChild
674+
return
675+
}
676+
});
677+
678+
let disabledCols = [...tableHead?.attrs?.disabledCols ?? [], ...tableBody?.attrs?.disabledCols ?? []]
679+
delete tableHead?.attrs?.disabledCols
680+
delete tableBody?.attrs?.disabledCols
681+
682+
const tableAttrs = {
683+
...elementAttrs.attrs,
684+
rows: row,
685+
cols: col,
686+
colWidths: colWidths,
687+
}
688+
689+
if(!isEmpty(disabledCols)){
690+
tableAttrs['disabledCols'] = Array.from(new Set(disabledCols))
691+
}
692+
646693
elementAttrs = {
647694
...elementAttrs,
648-
attrs: {
649-
...elementAttrs.attrs,
650-
rows: row,
651-
cols: col,
652-
colWidths: colWidths
653-
}
695+
attrs: tableAttrs
654696
}
655697
}
698+
if (["THEAD", "TBODY"].includes(nodeName)) {
699+
const rows = children as any[]
700+
const disabledCols = rows.flatMap(row => {
701+
const { disabledCols } = row.attrs
702+
delete row['attrs']['disabledCols']
703+
return disabledCols ?? []
704+
})
705+
elementAttrs.attrs['disabledCols'] = disabledCols
706+
}
707+
if (nodeName === "TBODY") {
708+
709+
const rows = children
710+
711+
addVoidCellsAndApplyAttributes(rows)
712+
713+
children = getTbodyChildren(rows)
714+
}
715+
716+
if (nodeName === "TR") {
717+
718+
const cells = children.filter((child:any) => ['th', 'td'].includes(child.type)) as any[]
719+
720+
721+
const disabledCols = cells.flatMap((cell, cellIndex) => {
722+
let { colSpan } = cell.attrs
723+
if (!colSpan) return []
724+
colSpan = parseInt(colSpan)
725+
return Array(colSpan).fill(0).map((_, i) => cellIndex + i)
726+
})
727+
728+
if (disabledCols?.length)
729+
elementAttrs.attrs['disabledCols'] = disabledCols
730+
731+
732+
}
733+
if (['TD', 'TH'].includes(nodeName)) {
734+
const { colSpan = 1, rowSpan } = elementAttrs?.['attrs']
735+
736+
return [
737+
jsx('element', elementAttrs, children),
738+
...Array(colSpan - 1)
739+
.fill(0)
740+
.map((_) => emptyCell(nodeName.toLowerCase(), rowSpan ? { inducedRowSpan: rowSpan } : {}))
741+
]
742+
}
656743
if (nodeName === 'P') {
657744
if (
658745
elementAttrs?.attrs?.["redactor-attributes"]?.['data-checked'] &&
@@ -809,3 +896,84 @@ export const getNestedValueIfAvailable = (value: string) => {
809896
return value;
810897
}
811898
};
899+
900+
901+
const spanningAttrs = (el: HTMLElement) => {
902+
const attrs = {}
903+
const rowSpan = parseInt(el.getAttribute('rowspan') ?? '1')
904+
const colSpan = parseInt(el.getAttribute('colspan') ?? '1')
905+
if (rowSpan > 1) attrs['rowSpan'] = rowSpan
906+
if (colSpan > 1) attrs['colSpan'] = colSpan
907+
908+
return attrs
909+
}
910+
const emptyCell = (cellType: string, attrs = {}) => {
911+
return jsx('element', { type: cellType, attrs: { void: true, ...attrs } }, [{ text: '' }])
912+
}
913+
914+
const addVoidCellsAndApplyAttributes = (rows: any[]) => {
915+
rows.forEach((row, currIndex) => {
916+
const cells = row.children as any[]
917+
918+
cells.forEach((cell, cellIndex) => {
919+
if (!cell || !cell.attrs) return
920+
921+
const { rowSpan, inducedRowSpan } = cell.attrs
922+
923+
let span = rowSpan ?? inducedRowSpan ?? 0
924+
925+
if (!span || span < 2) return
926+
const nextRow = rows[currIndex + 1]
927+
if (!nextRow) {
928+
delete cell?.attrs?.inducedRowSpan
929+
return
930+
}
931+
932+
// set inducedRowSpan on cell in row at cellIndex
933+
span--
934+
nextRow?.children?.splice(cellIndex, 0,
935+
emptyCell('td',
936+
(span > 1) ? { inducedRowSpan: span } : {}
937+
))
938+
939+
// Include next row in trgrp
940+
nextRow['attrs']['included'] = true
941+
942+
// Make a new trgrp
943+
if (rowSpan) {
944+
row['attrs']['origin'] = true
945+
}
946+
947+
delete cell?.['attrs']?.['inducedRowSpan']
948+
949+
})
950+
})
951+
}
952+
953+
954+
function getTbodyChildren (rows: any[]) {
955+
const newTbodyChildren = rows.reduce((tBodyChildren, row, rowIndex) => {
956+
957+
const { included, origin } = row.attrs
958+
const l = tBodyChildren.length
959+
960+
if (included || origin) {
961+
962+
if (origin && !included) {
963+
tBodyChildren.push(jsx('element', { type: 'trgrp' }, row))
964+
}
965+
if (included) {
966+
tBodyChildren[l - 1].children.push(row)
967+
968+
}
969+
delete row['attrs']['included']
970+
delete row['attrs']['origin']
971+
return tBodyChildren
972+
}
973+
974+
tBodyChildren.push(row)
975+
return tBodyChildren
976+
977+
}, [])
978+
return newTbodyChildren
979+
}

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)