/**
* @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/
import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters';
import {
downcastInsertCell,
downcastInsertRow,
downcastInsertTable,
downcastRemoveRow,
downcastTableHeadingColumnsChange,
downcastTableHeadingRowsChange
} from '../../src/converters/downcast';
import upcastTable, { upcastTableCell } from '../../src/converters/upcasttable';
/**
* Returns a model representation of a table shorthand notation:
*
* modelTable( [
* [ '00' ] // first row
* [ '10' ] // second row
* ] );
*
* will output:
*
* '
'
*
* Each table row passed in `tableData` array is represented as an array of strings or objects. A string defines text contents of a cell.
*
* Passing an object allows to pass additional table cell attributes:
*
* const tableCellData = {
* colspan: 2,
* rowspan: 4,
* contents: 'foo' // text contents of a cell
* };
*
* @param {Array.|Object>} tableData
* @param {Object} [attributes] Optional table attributes: `headingRows` and `headingColumns`.
*
* @returns {String}
*/
export function modelTable( tableData, attributes ) {
const tableRows = makeRows( tableData, {
cellElement: 'tableCell',
rowElement: 'tableRow',
headingElement: 'tableCell',
wrappingElement: 'paragraph',
enforceWrapping: true
} );
return ``;
}
/**
* Returns a view representation of a table shorthand notation:
*
* viewTable( [
* [ '00', '01' ] // first row
* [ '10', '11' ] // second row
* ] );
*
* will output:
*
* ''
*
* Each table row passed in `tableData` array is represented as an array of strings or objects. A string defines text contents of a cell.
*
* Passing an object allows to pass additional table cell attributes:
*
* const tableCellData = {
* colspan: 2,
* rowspan: 4,
* isHeading: true, // will render table cell as `` element
* contents: 'foo' // text contents of a cell
* };
*
* @param {Array.>} tableData The table data array.
* @param {Object} [attributes] Optional table attributes: `headingRows` and `headingColumns` - passing them will properly render rows
* in `` or `` sections.
*
* @returns {String}
*/
export function viewTable( tableData, attributes = {} ) {
const headingRows = attributes.headingRows || 0;
const asWidget = !!attributes.asWidget;
const thead = headingRows > 0 ? `${ makeRows( tableData.slice( 0, headingRows ), {
cellElement: 'th',
rowElement: 'tr',
headingElement: 'th',
wrappingElement: asWidget ? 'span' : 'p',
enforceWrapping: asWidget,
asWidget
} ) } ` : '';
const tbody = tableData.length > headingRows ?
` ${ makeRows( tableData.slice( headingRows ), {
cellElement: 'td',
rowElement: 'tr',
headingElement: 'th',
wrappingElement: asWidget ? 'span' : 'p',
enforceWrapping: asWidget,
asWidget
} ) } ` : '';
const figureAttributes = asWidget ? 'class="ck-widget ck-widget_selectable table" contenteditable="false"' : 'class="table"';
const widgetHandler = '
';
return `${ asWidget ? widgetHandler : '' } `;
}
/**
* Formats model or view table - useful for chai assertions debugging.
*
* @param {String} tableString
* @returns {String}
*/
export function formatTable( tableString ) {
return tableString
.replace( //g, '\n\n ' )
.replace( //g, '\n\n ' )
.replace( / /g, '\n \n ' )
.replace( //g, '\n \n ' )
.replace( /<\/tableRow>/g, '\n' )
.replace( /<\/thead>/g, '\n' )
.replace( /<\/tbody>/g, '\n ' )
.replace( /<\/tr>/g, '\n' )
.replace( /<\/table>/g, '\n' )
.replace( /<\/figure>/g, '\n' );
}
/**
* Returns formatted model table string.
*
* @param {Array.} tableData
* @param {Object} [attributes]
* @returns {String}
*/
export function formattedModelTable( tableData, attributes ) {
const tableString = modelTable( tableData, attributes );
return formatTable( tableString );
}
/**
* Returns formatted view table string.
*
* @param {Array.} tableData
* @param {Object} [attributes]
* @returns {String}
*/
export function formattedViewTable( tableData, attributes ) {
return formatTable( viewTable( tableData, attributes ) );
}
export function defaultSchema( schema, registerParagraph = true ) {
schema.register( 'table', {
allowWhere: '$block',
allowAttributes: [ 'headingRows', 'headingColumns' ],
isLimit: true,
isObject: true
} );
schema.register( 'tableRow', {
allowIn: 'table',
isLimit: true
} );
schema.register( 'tableCell', {
allowIn: 'tableRow',
allowAttributes: [ 'colspan', 'rowspan' ],
isLimit: true
} );
// Allow all $block content inside table cell.
schema.extend( '$block', { allowIn: 'tableCell' } );
// Disallow table in table.
schema.addChildCheck( ( context, childDefinition ) => {
if ( childDefinition.name == 'table' && Array.from( context.getNames() ).includes( 'table' ) ) {
return false;
}
} );
// Disallow image in table.
schema.addChildCheck( ( context, childDefinition ) => {
if ( childDefinition.name == 'image' && Array.from( context.getNames() ).includes( 'table' ) ) {
return false;
}
} );
if ( registerParagraph ) {
schema.register( 'paragraph', { inheritAllFrom: '$block' } );
}
}
export function defaultConversion( conversion, asWidget = false ) {
conversion.elementToElement( { model: 'paragraph', view: 'p' } );
// Table conversion.
conversion.for( 'upcast' ).add( upcastTable() );
conversion.for( 'downcast' ).add( downcastInsertTable( { asWidget } ) );
// Table row conversion.
conversion.for( 'upcast' ).add( upcastElementToElement( { model: 'tableRow', view: 'tr' } ) );
conversion.for( 'downcast' ).add( downcastInsertRow( { asWidget } ) );
conversion.for( 'downcast' ).add( downcastRemoveRow( { asWidget } ) );
// Table cell conversion.
conversion.for( 'upcast' ).add( upcastTableCell( 'td' ) );
conversion.for( 'upcast' ).add( upcastTableCell( 'th' ) );
conversion.for( 'downcast' ).add( downcastInsertCell( { asWidget } ) );
// Table attributes conversion.
conversion.attributeToAttribute( { model: 'colspan', view: 'colspan' } );
conversion.attributeToAttribute( { model: 'rowspan', view: 'rowspan' } );
conversion.for( 'downcast' ).add( downcastTableHeadingColumnsChange( { asWidget } ) );
conversion.for( 'downcast' ).add( downcastTableHeadingRowsChange( { asWidget } ) );
}
// Formats table cell attributes
//
// @param {Object} attributes Attributes of a cell.
function formatAttributes( attributes ) {
let attributesString = '';
if ( attributes ) {
const entries = Object.entries( attributes );
if ( entries.length ) {
attributesString = ' ' + entries.map( entry => `${ entry[ 0 ] }="${ entry[ 1 ] }"` ).join( ' ' );
}
}
return attributesString;
}
// Formats passed table data to a set of table rows.
function makeRows( tableData, options ) {
const { cellElement, rowElement, headingElement, wrappingElement, enforceWrapping, asWidget } = options;
return tableData
.reduce( ( previousRowsString, tableRow ) => {
const tableRowString = tableRow.reduce( ( tableRowString, tableCellData ) => {
const isObject = typeof tableCellData === 'object';
let contents = isObject ? tableCellData.contents : tableCellData;
let resultingCellElement = cellElement;
if ( isObject ) {
if ( tableCellData.isHeading ) {
resultingCellElement = headingElement;
}
delete tableCellData.contents;
delete tableCellData.isHeading;
}
const attributes = isObject ? tableCellData : {};
if ( asWidget ) {
attributes.class = 'ck-editor__editable ck-editor__nested-editable';
attributes.contenteditable = 'true';
}
if ( !( contents.replace( '[', '' ).replace( ']', '' ).startsWith( '<' ) ) && enforceWrapping ) {
contents = `<${ wrappingElement }>${ contents }${ wrappingElement }>`;
}
const formattedAttributes = formatAttributes( attributes );
tableRowString += `<${ resultingCellElement }${ formattedAttributes }>${ contents }${ resultingCellElement }>`;
return tableRowString;
}, '' );
return `${ previousRowsString }<${ rowElement }>${ tableRowString }${ rowElement }>`;
}, '' );
}