| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267 |
- /**
- * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
- /**
- * @module table/converters/downcasttable
- */
- import ViewPosition from '@ckeditor/ckeditor5-engine/src/view/position';
- /**
- * Model table element to view table element conversion helper.
- *
- * This conversion helper creates whole table element with child elements.
- *
- * @returns {Function} Conversion helper.
- */
- export default function downcastTable() {
- return dispatcher => dispatcher.on( 'insert:table', ( evt, data, conversionApi ) => {
- const table = data.item;
- if ( !conversionApi.consumable.consume( table, 'insert' ) ) {
- return;
- }
- // The <thead> and <tbody> elements are created on the fly when needed by inner `getTableSection()` function.
- let tHead, tBody;
- const tableElement = conversionApi.writer.createContainerElement( 'table' );
- const headingRows = parseInt( table.getAttribute( 'headingRows' ) ) || 0;
- const tableRows = Array.from( table.getChildren() );
- const cellSpans = new CellSpans();
- for ( const tableRow of tableRows ) {
- const rowIndex = tableRows.indexOf( tableRow );
- const tableSectionElement = getTableSection( rowIndex, headingRows, tableElement, conversionApi );
- downcastTableRow( tableRow, rowIndex, cellSpans, tableSectionElement, conversionApi );
- // Drop table cell spans information for downcasted row.
- cellSpans.drop( rowIndex );
- }
- const viewPosition = conversionApi.mapper.toViewPosition( data.range.start );
- conversionApi.mapper.bindElements( table, tableElement );
- conversionApi.writer.insert( viewPosition, tableElement );
- // Creates if not existing and returns <tbody> or <thead> element for given rowIndex.
- function getTableSection( rowIndex, headingRows, tableElement, conversionApi ) {
- if ( headingRows && rowIndex < headingRows ) {
- if ( !tHead ) {
- tHead = createTableSection( 'thead', tableElement, conversionApi );
- }
- return tHead;
- }
- if ( !tBody ) {
- tBody = createTableSection( 'tbody', tableElement, conversionApi );
- }
- return tBody;
- }
- }, { priority: 'normal' } );
- }
- // Downcast converter for tableRow model element. Converts tableCells as well.
- //
- // @param {module:engine/model/element~Element} tableRow
- // @param {Number} rowIndex
- // @param {CellSpans} cellSpans
- // @param {module:engine/view/containerelement~ContainerElement} tableSection
- // @param {Object} conversionApi
- function downcastTableRow( tableRow, rowIndex, cellSpans, tableSection, conversionApi ) {
- // Will always consume since we're converting <tableRow> element from a parent <table>.
- conversionApi.consumable.consume( tableRow, 'insert' );
- const trElement = conversionApi.writer.createContainerElement( 'tr' );
- conversionApi.mapper.bindElements( tableRow, trElement );
- conversionApi.writer.insert( ViewPosition.createAt( tableSection, 'end' ), trElement );
- // Defines tableCell horizontal position in table.
- // Might be different then position of tableCell in parent tableRow
- // as tableCells from previous rows might overlaps current row's cells.
- let columnIndex = 0;
- const headingRows = tableRow.parent.getAttribute( 'headingRows' ) || 0;
- const headingColumns = tableRow.parent.getAttribute( 'headingColumns' ) || 0;
- for ( const tableCell of Array.from( tableRow.getChildren() ) ) {
- // Check whether current columnIndex is overlapped by table cells from previous rows.
- columnIndex = cellSpans.getNextFreeColumnIndex( rowIndex, columnIndex );
- const colspan = tableCell.hasAttribute( 'colspan' ) ? parseInt( tableCell.getAttribute( 'colspan' ) ) : 1;
- const rowspan = tableCell.hasAttribute( 'rowspan' ) ? parseInt( tableCell.getAttribute( 'rowspan' ) ) : 1;
- cellSpans.recordSpans( rowIndex, columnIndex, rowspan, colspan );
- // Will always consume since we're converting <tableRow> element from a parent <table>.
- conversionApi.consumable.consume( tableCell, 'insert' );
- const cellElementName = getCellElementName( rowIndex, columnIndex, headingRows, headingColumns );
- const cellElement = conversionApi.writer.createContainerElement( cellElementName );
- conversionApi.mapper.bindElements( tableCell, cellElement );
- conversionApi.writer.insert( ViewPosition.createAt( trElement, 'end' ), cellElement );
- // Skip to next "free" column index.
- columnIndex += colspan;
- }
- }
- // Creates table section at the end of a table.
- //
- // @param {String} elementName
- // @param {module:engine/view/element~Element} tableElement
- // @param conversionApi
- // @return {module:engine/view/containerelement~ContainerElement}
- function createTableSection( elementName, tableElement, conversionApi ) {
- const tableChildElement = conversionApi.writer.createContainerElement( elementName );
- conversionApi.writer.insert( ViewPosition.createAt( tableElement, 'end' ), tableChildElement );
- return tableChildElement;
- }
- // Returns `th` for heading cells and `td` for other cells.
- // It is based on tableCell location (rowIndex x columnIndex) and the sizes of column & row headings sizes.
- //
- // @param {Number} rowIndex
- // @param {Number} columnIndex
- // @param {Number} headingRows
- // @param {Number} headingColumns
- // @returns {String}
- function getCellElementName( rowIndex, columnIndex, headingRows, headingColumns ) {
- // Column heading are all tableCells in the first `columnHeading` rows.
- const isHeadingForAColumn = headingRows && headingRows > rowIndex;
- // So a whole row gets <th> element.
- if ( isHeadingForAColumn ) {
- return 'th';
- }
- // Row heading are tableCells which columnIndex is lower then headingColumns.
- const isHeadingForARow = headingColumns && headingColumns > columnIndex;
- return isHeadingForARow ? 'th' : 'td';
- }
- /**
- * Holds information about spanned table cells.
- *
- * @private
- */
- class CellSpans {
- /**
- * Creates CellSpans instance.
- */
- constructor() {
- /**
- * Holds table cell spans mapping.
- *
- * @type {Map<Number, Number>}
- * @private
- */
- this._spans = new Map();
- }
- /**
- * Returns proper column index if a current cell index is overlapped by other (has a span defined).
- *
- * @param {Number} row
- * @param {Number} column
- * @return {Number} Returns current column or updated column index.
- */
- getNextFreeColumnIndex( row, column ) {
- let span = this._check( row, column ) || 0;
- // Offset current table cell columnIndex by spanning cells from rows above.
- while ( span ) {
- column += span;
- span = this._check( row, column );
- }
- return column;
- }
- /**
- * Updates spans based on current table cell height & width. Spans with height <= 1 will not be recorded.
- *
- * For instance if a table cell at row 0 and column 0 has height of 3 and width of 2 we're setting spans:
- *
- * 0 1 2 3 4 5
- * 0:
- * 1: 2
- * 2: 2
- * 3:
- *
- * Adding another spans for a table cell at row 2 and column 1 that has height of 2 and width of 4 will update above to:
- *
- * 0 1 2 3 4 5
- * 0:
- * 1: 2
- * 2: 2
- * 3: 4
- *
- * The above span mapping was calculated from a table below (cells 03 & 12 were not added as their height is 1):
- *
- * +----+----+----+----+----+----+
- * | 00 | 02 | 03 | 05 |
- * | +--- +----+----+----+
- * | | 12 | 24 | 25 |
- * | +----+----+----+----+
- * | | 22 |
- * |----+----+ +
- * | 31 | 32 | |
- * +----+----+----+----+----+----+
- *
- * @param {Number} rowIndex
- * @param {Number} columnIndex
- * @param {Number} height
- * @param {Number} width
- */
- recordSpans( rowIndex, columnIndex, height, width ) {
- // This will update all rows below up to row height with value of span width.
- for ( let rowToUpdate = rowIndex + 1; rowToUpdate < rowIndex + height; rowToUpdate++ ) {
- if ( !this._spans.has( rowToUpdate ) ) {
- this._spans.set( rowToUpdate, new Map() );
- }
- const rowSpans = this._spans.get( rowToUpdate );
- rowSpans.set( columnIndex, width );
- }
- }
- /**
- * Removes row from mapping.
- *
- * @param {Number} rowIndex
- */
- drop( rowIndex ) {
- if ( this._spans.has( rowIndex ) ) {
- this._spans.delete( rowIndex );
- }
- }
- /**
- * Checks if given table cell is spanned by other.
- *
- * @param {Number} rowIndex
- * @param {Number} columnIndex
- * @return {Boolean|Number} Returns false or width of a span.
- * @private
- */
- _check( rowIndex, columnIndex ) {
- if ( !this._spans.has( rowIndex ) ) {
- return false;
- }
- const rowSpans = this._spans.get( rowIndex );
- return rowSpans.has( columnIndex ) ? rowSpans.get( columnIndex ) : false;
- }
- }
|