import { HelperInterface , TableModel , TableModelCol , TableModelEmptyCol , TableOptions , } from '../types' ; import isInteger from 'lodash/isInteger' ; import { $ , EDITABLE_SELECTOR , DATA_TRANSIENT_ATTRIBUTES , isNode , NodeInterface , transformCustomTags , EditorInterface , } from '@aomao/engine' ; import Template from './template' ; class Helper implements HelperInterface { private clipboard ? : { html : string ; text : string ; } ; #editor : EditorInterface ; constructor ( editor ) { this . #editor = editor ; } isEmptyModelCol ( model ) { return model && ( model as TableModelEmptyCol ) . isEmpty ; } param table returns getTableModel ( table ) { let model = [ ] ; const tableElement = table . get < HTMLTableElement > ( ) ! ; const rows = tableElement . rows ; const rowCount = rows . length ; for ( let r = 0 ; r < rowCount ; r ++ ) { const tr = rows [ r ] ; const cells = tr . cells ; const cellCount = cells . length ; for ( let c = 0 ; c < cellCount ; c ++ ) { const td = cells [ c ] ; let { rowSpan , colSpan } = td ; rowSpan = rowSpan === void 0 ? 1 : rowSpan ; colSpan = colSpan === void 0 ? 1 : colSpan ; const isMulti = rowSpan > 1 || colSpan > 1 ; model [ r ] = model [ r ] || [ ] ; let _c = c ; while ( model [ r ] [ _c ] ) { _c ++ ; } model [ r ] [ _c ] = { rowSpan : rowSpan , colSpan : colSpan , isMulti : isMulti , element : td , } ; if ( isMulti ) { let _rowCount = rowSpan ; while ( _rowCount > 0 ) { let colCount = colSpan ; while ( colCount > 0 ) { if ( colCount !== 1 || _rowCount !== 1 ) { const rowIndex = r + _rowCount - 1 ; const colIndex = _c + colCount - 1 ; model [ rowIndex ] = model [ rowIndex ] || [ ] ; model [ rowIndex ] [ colIndex ] = { isEmpty : true , parent : { row : r , col : _c , } , element : null , } ; } colCount -- ; } _rowCount -- ; } } } } const colCounts = model . map ( ( trModel ) => { return trModel . length ; } ) ; const MaxColCount = Math . max . apply ( Math , [ ... colCounts ] ) ; model . forEach ( ( trModel ) => { if ( trModel . length < MaxColCount ) { let addCount = MaxColCount - trModel . length ; while ( addCount -- ) { trModel . push ( { rowSpan : 1 , colSpan : 1 , isShadow : true , element : null , } ) ; } } for ( let i = 0 ; i < MaxColCount ; i ++ ) { if ( ! trModel [ i ] ) { trModel [ i ] = { rowSpan : 1 , colSpan : 1 , isShadow : true , element : null , } ; } } } ) ; const result = { rows : model . length , cols : MaxColCount , width : tableElement . offsetWidth , height : tableElement . offsetHeight , table : model , } ; return result ; } param table returns normalize ( table ) { this . trimStartTr ( table ) ; this . fixNumberTr ( table ) ; table . addClass ( 'data-table' ) ; table . attributes ( DATA_TRANSIENT_ATTRIBUTES , 'class' ) ; const width = table . css ( 'width' ) ; if ( parseInt ( width ) === 0 ) { table . css ( 'width' , 'auto' ) ; } table . css ( 'background-color' , '' ) ; const model = this . getTableModel ( table ) ; let cols = table . find ( 'col' ) ; if ( cols . length !== 0 ) { for ( let c = cols . length - 1 ; c >= 0 ; c -- ) { const colElement = cols [ c ] as HTMLTableColElement ; const _width = cols . eq ( c ) ?. attributes ( 'width' ) ; if ( _width ) { cols . eq ( c ) ?. attributes ( 'width' , parseInt ( _width ) ) ; } if ( colElement . span > 1 ) { let addCount = colElement . span - 1 ; while ( addCount -- ) { cols [ c ] . parentNode ?. insertBefore ( cols [ c ] . cloneNode ( ) , cols [ c ] ) ; } } } cols = table . find ( 'col' ) ; if ( cols . length < model . cols ) { const lastCol = cols . length - 1 ; let colsAddCount = model . cols - cols . length ; while ( colsAddCount -- ) { cols [ 0 ] . parentNode ?. appendChild ( cols [ lastCol ] . cloneNode ( ) ) ; } } table . find ( 'col' ) . attributes ( 'span' , 1 ) ; } else { let colgroup = table . find ( 'colgroup' ) [ 0 ] ; if ( ! colgroup ) { colgroup = document . createElement ( 'colgroup' ) ; } table . prepend ( colgroup ) ; const widths = ( function ( table ) { const tr = table . find ( 'tr' ) [ 0 ] ; const tds = $ ( tr ) . find ( 'td' ) ; const widthArray = [ ] ; tds . each ( ( _ , index ) => { const $td = tds . eq ( index ) ; if ( ! $td ) return ; let colWidth = $td . attributes ( 'data-colwidth' ) ; let tdWidth = $td . attributes ( 'width' ) ; const tdColSpan = ( $td [ 0 ] as HTMLTableDataCellElement ) . colSpan ; if ( colWidth ) { colWidth = colWidth . split ( ',' ) ; } else if ( tdWidth ) { tdWidth = parseInt ( tdWidth ) / tdColSpan ; } for ( let o = 0 ; tdColSpan > o ; o ++ ) { if ( colWidth && colWidth [ o ] ) { widthArray . push ( parseInt ( colWidth [ o ] ) ) ; } else if ( tdWidth ) { widthArray . push ( parseInt ( tdWidth . toString ( ) ) ) ; } else { widthArray . push ( undefined ) ; } } } ) ; const td = table . find ( 'td' ) ; td . removeAttributes ( 'data-colwidth' ) ; td . removeAttributes ( 'width' ) ; return widthArray ; } ) ( table ) ; const col = document . createElement ( 'col' ) ; for ( let f = 0 ; model . cols > f ; f ++ ) { const node = col . cloneNode ( ) ; if ( widths [ f ] ) { ( node as HTMLElement ) . setAttribute ( 'width' , ( widths [ f ] || '' ) . toString ( ) ) ; } colgroup . appendChild ( node ) ; } } const tableElement = table . get < HTMLTableElement > ( ) ! ; model . table . forEach ( ( tr , r ) => { if ( ! tableElement . rows [ r ] ) { tableElement . insertRow ( r ) ; } const shadow = tr . filter ( ( td ) => { return this . isEmptyModelCol ( td ) ? false : td . isShadow ; } ) ; let shadowCount = shadow . length ; while ( shadowCount -- ) { if ( r === 0 ) { tableElement . rows [ r ] . insertCell ( 0 ) ; } else { tableElement . rows [ r ] . insertCell ( ) ; } } } ) ; const trs = table . find ( 'tr' ) ; trs . each ( ( _ , index ) => { const $tr = trs . eq ( index ) ; if ( ! $tr ) return ; let height = parseInt ( $tr . css ( 'height' ) ) ; height = height || this . #editor . plugin . findPlugin < TableOptions > ( 'table' ) ?. options . rowMinHeight || 0 ; $tr . css ( 'height' , height + 'px' ) ; } ) ; const tds = table . find ( 'td' ) ; tds . each ( ( _ , index ) => { const tdNode = tds . eq ( index ) ; if ( ! tdNode ) return ; tdNode . attributes ( DATA_TRANSIENT_ATTRIBUTES , 'table-cell-selection' ) ; let editableElement = tdNode . find ( EDITABLE_SELECTOR ) ; if ( editableElement . length === 0 ) { const content = tdNode . html ( ) ; tdNode . empty ( ) ; tdNode . append ( Template . EmptyCell ) ; editableElement = tdNode . find ( EDITABLE_SELECTOR ) ; editableElement . html ( content ) ; } editableElement . find ( 'p' ) . each ( ( pNode ) => { if ( pNode . childNodes . length === 0 ) { pNode . appendChild ( document . createElement ( 'br' ) ) ; } } ) ; } ) ; return table ; } param { nodeModel } table trimStartTr ( table ) { const tr = table . find ( 'tr' ) ; const first = tr . eq ( 0 ) ; if ( first && first . children ( ) . length === 0 ) { first . remove ( ) ; } } fixNumberTr ( table ) { const tableElement = table . get < HTMLTableElement > ( ) ! ; const rows = tableElement . rows ; const rowCount = rows . length ; let colCounts = [ ] ; let firstColCount = 0 ; let cellCounts = [ ] ; let totalCellCounts = 0 ; let emptyCounts = 0 ; let maxCellCounts = 0 ; for ( let r = 0 ; r < rowCount ; r ++ ) { const row = rows [ r ] ; const cells = row . cells ; let cellCountThisRow = 0 ; for ( let c = 0 ; c < cells . length ; c ++ ) { const { rowSpan , colSpan } = cells [ c ] ; totalCellCounts += rowSpan * colSpan ; cellCountThisRow += colSpan ; if ( rowSpan > 1 ) { emptyCounts += ( rowSpan - 1 ) * colSpan ; } } cellCounts [ r ] = cellCountThisRow ; if ( r === 0 ) { firstColCount = cellCountThisRow ; } maxCellCounts = Math . max ( cellCountThisRow , maxCellCounts ) ; } const isNumber1 = isInteger ( totalCellCounts / firstColCount ) ; const isNumber2 = firstColCount === maxCellCounts ; const isNumber = isNumber1 && isNumber2 ; if ( isNumber ) { let lossCellCounts = 0 ; cellCounts . forEach ( ( cellCount ) => { lossCellCounts += maxCellCounts - cellCount ; } ) ; if ( lossCellCounts !== emptyCounts ) { const missCellCounts = emptyCounts - lossCellCounts ; if ( isInteger ( missCellCounts / maxCellCounts ) ) { let lossRowIndex = [ ] ; for ( let _r = 0 ; _r < rowCount ; _r ++ ) { const _row = rows [ _r ] ; const _cells = _row . cells ; let realRow = _r + lossRowIndex . length ; while ( colCounts [ realRow ] === maxCellCounts ) { lossRowIndex . push ( realRow ) ; realRow ++ ; } for ( let _c2 = 0 ; _c2 < _cells . length ; _c2 ++ ) { const { rowSpan , colSpan } = _cells [ _c2 ] ; if ( rowSpan > 1 ) { for ( let rr = 1 ; rr < rowSpan ; rr ++ ) { colCounts [ realRow + rr ] = ( colCounts [ realRow + rr ] || 0 ) + colSpan ; } } } } lossRowIndex . forEach ( ( row ) => { tableElement . insertRow ( row ) ; } ) ; } } } } fixDragEvent ( event ) { event . dataTransfer ?. clearData ( ) ; event . dataTransfer ?. setData ( 'aomao' , '' ) ; if ( event . dataTransfer ) { event . dataTransfer . effectAllowed = 'all' ; } } param from param to copyCss ( from , to ) { if ( isNode ( from ) ) from = $ ( from ) ; if ( isNode ( to ) ) to = $ ( to ) ; to . css ( 'text-align' , from . css ( 'text-align' ) ) ; to . css ( 'vertical-align' , from . css ( 'vertical-align' ) ) ; let tdBgColor = from . css ( 'background-color' ) ; tdBgColor = tdBgColor !== 'rgba(0, 0, 0, 0)' ? tdBgColor : '#fff' ; to . css ( 'background-color' , tdBgColor ) ; to . css ( 'color' , from . css ( 'color' ) ) ; to . css ( 'font-weight' , from . css ( 'font-weight' ) ) ; } param from param to copyTo ( from , to ) { if ( isNode ( from ) ) from = $ ( from ) ; if ( isNode ( to ) ) to = $ ( to ) ; let editableElement = to . find ( EDITABLE_SELECTOR ) ; if ( editableElement . length === 0 ) { to . html ( Template . EmptyCell ) ; editableElement = to . find ( EDITABLE_SELECTOR ) ; } editableElement . html ( transformCustomTags ( from . html ( ) ) ) ; if ( to . name === 'td' && to . find ( Template . TABLE_TD_BG_CLASS ) . length === 0 ) { to . append ( $ ( Template . CellBG ) ) ; } if ( to . name === 'td' ) { to . attributes ( 'data-transient-attributes' , 'table-cell-selection' ) ; } } param html copyHTML ( html ) { this . clipboard = { html : html , text : $ ( html ) . get < HTMLElement > ( ) ?. innerText || '' , } ; } returns getCopyData ( ) { return this . clipboard ; } trimBlankSpan ( node ) { const len = node . length ; let nodelist = [ ] ; let i = 0 ; let j = len - 1 ; while ( node [ i ] && ( node [ i ] as HTMLElement ) . tagName . toLowerCase ( ) === 'span' && ( node [ i ] as HTMLElement ) . innerText . trim ( ) === '' ) { i ++ ; } while ( node [ j ] && ( node [ j ] as HTMLElement ) . tagName . toLowerCase ( ) === 'span' && ( node [ j ] as HTMLElement ) . innerText . trim ( ) === '' ) { j -- ; } if ( i <= j ) { for ( let k = i ; k <= j ; k ++ ) { nodelist . push ( node [ k ] ) ; } } if ( nodelist . length ) { return $ ( nodelist ) ; } return node ; } param { NodeInterface } table return { NodeInterface } normalizeTable ( table ) { this . trimStartTr ( table ) ; this . fixNumberTr ( table ) ; table . addClass ( 'data-table' ) ; const width = table . css ( 'width' ) ; if ( parseInt ( width ) === 0 ) { table . css ( 'width' , 'auto' ) ; } else if ( ! width . endsWith ( '%' ) ) { table . css ( 'width' , parseInt ( width , 10 ) + 'px' ) ; } table . css ( 'background-color' , '' ) ; const model = this . getTableModel ( table ) ; let cols = table . find ( 'col' ) ; if ( cols . length !== 0 ) { for ( let c = cols . length - 1 ; c >= 0 ; c -- ) { const colElement = cols [ c ] as HTMLTableColElement ; const _width = cols . eq ( c ) ?. attributes ( 'width' ) ; if ( _width && ! _width . endsWith ( '%' ) ) { const widthValue = parseInt ( _width ) ; if ( widthValue !== NaN ) cols . eq ( c ) ?. attributes ( 'width' , widthValue ) ; } if ( colElement . span > 1 ) { let addCount = colElement . span - 1 ; while ( addCount -- ) { cols [ c ] . parentNode ?. insertBefore ( cols [ c ] . cloneNode ( ) , cols [ c ] ) ; } } } cols = table . find ( 'col' ) ; if ( cols . length < model . cols ) { const lastCol = cols . length - 1 ; let colsAddCount = model . cols - cols . length ; while ( colsAddCount -- ) { cols [ 0 ] . parentNode ?. appendChild ( cols [ lastCol ] . cloneNode ( ) ) ; } } table . find ( 'col' ) . attributes ( 'span' , 1 ) ; } else { let colgroup = table . find ( 'colgroup' ) [ 0 ] ; if ( ! colgroup ) { colgroup = document . createElement ( 'colgroup' ) ; } table . prepend ( colgroup ) ; const widths = ( function ( table ) { const tr = table . find ( 'tr' ) [ 0 ] ; const tds = $ ( tr ) . find ( 'td' ) ; const widthArray = [ ] ; tds . each ( ( _ , index ) => { const tdNode = tds . eq ( index ) ; if ( ! tdNode ) return ; let colWidth = tdNode . attributes ( 'data-colwidth' ) ; let tdWidth = tdNode . attributes ( 'width' ) ; const tdColSpan = ( tdNode [ 0 ] as HTMLTableDataCellElement ) . colSpan ; if ( colWidth ) { colWidth = colWidth . split ( ',' ) ; } else if ( tdWidth ) { tdWidth = parseInt ( tdWidth ) / tdColSpan ; } for ( let o = 0 ; tdColSpan > o ; o ++ ) { if ( colWidth && colWidth [ o ] ) { widthArray . push ( parseInt ( colWidth [ o ] ) ) ; } else if ( tdWidth ) { widthArray . push ( parseInt ( tdWidth . toString ( ) ) ) ; } else { widthArray . push ( undefined ) ; } } } ) ; const td = table . find ( 'td' ) ; td . removeAttributes ( 'data-colwidth' ) ; td . removeAttributes ( 'width' ) ; return widthArray ; } ) ( table ) ; const col = document . createElement ( 'col' ) ; for ( let f = 0 ; model . cols > f ; f ++ ) { const node = col . cloneNode ( ) ; if ( widths [ f ] ) { ( node as HTMLElement ) . setAttribute ( 'width' , ( widths [ f ] || '' ) . toString ( ) ) ; } colgroup . appendChild ( node ) ; } } const tableElement = table . get < HTMLTableElement > ( ) ! ; model . table . forEach ( ( tr , r ) => { if ( ! tableElement . rows [ r ] ) { tableElement . insertRow ( r ) ; } const shadow = tr . filter ( ( td ) => { return ( td as TableModelCol ) . isShadow ; } ) ; let shadowCount = shadow . length ; while ( shadowCount -- ) { if ( r === 0 ) { tableElement . rows [ r ] . insertCell ( 0 ) ; } else { tableElement . rows [ r ] . insertCell ( ) ; } } } ) ; const trs = table . find ( 'tr' ) ; trs . each ( ( _ , index ) => { const $tr = trs . eq ( index ) ; if ( ! $tr ) return ; let height = parseInt ( $tr . css ( 'height' ) ) ; height = height || this . #editor . plugin . findPlugin < TableOptions > ( 'table' ) ?. options . rowMinHeight || 0 ; $tr . css ( 'height' , height + 'px' ) ; } ) ; return table ; } } export default Helper ;