@@ -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+ }
0 commit comments