@wordpress/block-library
Version:
Block library for the WordPress editor.
325 lines (294 loc) • 8.54 kB
JavaScript
/**
* External dependencies
*/
import { get, mapValues, every, pick } from 'lodash';
const INHERITED_COLUMN_ATTRIBUTES = [ 'align' ];
/**
* Creates a table state.
*
* @param {Object} options
* @param {number} options.rowCount Row count for the table to create.
* @param {number} options.columnCount Column count for the table to create.
*
* @return {Object} New table state.
*/
export function createTable( { rowCount, columnCount } ) {
return {
body: Array.from( { length: rowCount } ).map( () => ( {
cells: Array.from( { length: columnCount } ).map( () => ( {
content: '',
tag: 'td',
} ) ),
} ) ),
};
}
/**
* Returns the first row in the table.
*
* @param {Object} state Current table state.
*
* @return {Object} The first table row.
*/
export function getFirstRow( state ) {
if ( ! isEmptyTableSection( state.head ) ) {
return state.head[ 0 ];
}
if ( ! isEmptyTableSection( state.body ) ) {
return state.body[ 0 ];
}
if ( ! isEmptyTableSection( state.foot ) ) {
return state.foot[ 0 ];
}
}
/**
* Gets an attribute for a cell.
*
* @param {Object} state Current table state.
* @param {Object} cellLocation The location of the cell
* @param {string} attributeName The name of the attribute to get the value of.
*
* @return {*} The attribute value.
*/
export function getCellAttribute( state, cellLocation, attributeName ) {
const { sectionName, rowIndex, columnIndex } = cellLocation;
return get( state, [
sectionName,
rowIndex,
'cells',
columnIndex,
attributeName,
] );
}
/**
* Returns updated cell attributes after applying the `updateCell` function to the selection.
*
* @param {Object} state The block attributes.
* @param {Object} selection The selection of cells to update.
* @param {Function} updateCell A function to update the selected cell attributes.
*
* @return {Object} New table state including the updated cells.
*/
export function updateSelectedCell( state, selection, updateCell ) {
if ( ! selection ) {
return state;
}
const tableSections = pick( state, [ 'head', 'body', 'foot' ] );
const { sectionName: selectionSectionName, rowIndex: selectionRowIndex } =
selection;
return mapValues( tableSections, ( section, sectionName ) => {
if ( selectionSectionName && selectionSectionName !== sectionName ) {
return section;
}
return section.map( ( row, rowIndex ) => {
if ( selectionRowIndex && selectionRowIndex !== rowIndex ) {
return row;
}
return {
cells: row.cells.map( ( cellAttributes, columnIndex ) => {
const cellLocation = {
sectionName,
columnIndex,
rowIndex,
};
if ( ! isCellSelected( cellLocation, selection ) ) {
return cellAttributes;
}
return updateCell( cellAttributes );
} ),
};
} );
} );
}
/**
* Returns whether the cell at `cellLocation` is included in the selection `selection`.
*
* @param {Object} cellLocation An object containing cell location properties.
* @param {Object} selection An object containing selection properties.
*
* @return {boolean} True if the cell is selected, false otherwise.
*/
export function isCellSelected( cellLocation, selection ) {
if ( ! cellLocation || ! selection ) {
return false;
}
switch ( selection.type ) {
case 'column':
return (
selection.type === 'column' &&
cellLocation.columnIndex === selection.columnIndex
);
case 'cell':
return (
selection.type === 'cell' &&
cellLocation.sectionName === selection.sectionName &&
cellLocation.columnIndex === selection.columnIndex &&
cellLocation.rowIndex === selection.rowIndex
);
}
}
/**
* Inserts a row in the table state.
*
* @param {Object} state Current table state.
* @param {Object} options
* @param {string} options.sectionName Section in which to insert the row.
* @param {number} options.rowIndex Row index at which to insert the row.
* @param {number} options.columnCount Column count for the table to create.
*
* @return {Object} New table state.
*/
export function insertRow( state, { sectionName, rowIndex, columnCount } ) {
const firstRow = getFirstRow( state );
const cellCount =
columnCount === undefined
? get( firstRow, [ 'cells', 'length' ] )
: columnCount;
// Bail early if the function cannot determine how many cells to add.
if ( ! cellCount ) {
return state;
}
return {
[ sectionName ]: [
...state[ sectionName ].slice( 0, rowIndex ),
{
cells: Array.from( { length: cellCount } ).map(
( _, index ) => {
const firstCellInColumn = get(
firstRow,
[ 'cells', index ],
{}
);
const inheritedAttributes = pick(
firstCellInColumn,
INHERITED_COLUMN_ATTRIBUTES
);
return {
...inheritedAttributes,
content: '',
tag: sectionName === 'head' ? 'th' : 'td',
};
}
),
},
...state[ sectionName ].slice( rowIndex ),
],
};
}
/**
* Deletes a row from the table state.
*
* @param {Object} state Current table state.
* @param {Object} options
* @param {string} options.sectionName Section in which to delete the row.
* @param {number} options.rowIndex Row index to delete.
*
* @return {Object} New table state.
*/
export function deleteRow( state, { sectionName, rowIndex } ) {
return {
[ sectionName ]: state[ sectionName ].filter(
( row, index ) => index !== rowIndex
),
};
}
/**
* Inserts a column in the table state.
*
* @param {Object} state Current table state.
* @param {Object} options
* @param {number} options.columnIndex Column index at which to insert the column.
*
* @return {Object} New table state.
*/
export function insertColumn( state, { columnIndex } ) {
const tableSections = pick( state, [ 'head', 'body', 'foot' ] );
return mapValues( tableSections, ( section, sectionName ) => {
// Bail early if the table section is empty.
if ( isEmptyTableSection( section ) ) {
return section;
}
return section.map( ( row ) => {
// Bail early if the row is empty or it's an attempt to insert past
// the last possible index of the array.
if ( isEmptyRow( row ) || row.cells.length < columnIndex ) {
return row;
}
return {
cells: [
...row.cells.slice( 0, columnIndex ),
{
content: '',
tag: sectionName === 'head' ? 'th' : 'td',
},
...row.cells.slice( columnIndex ),
],
};
} );
} );
}
/**
* Deletes a column from the table state.
*
* @param {Object} state Current table state.
* @param {Object} options
* @param {number} options.columnIndex Column index to delete.
*
* @return {Object} New table state.
*/
export function deleteColumn( state, { columnIndex } ) {
const tableSections = pick( state, [ 'head', 'body', 'foot' ] );
return mapValues( tableSections, ( section ) => {
// Bail early if the table section is empty.
if ( isEmptyTableSection( section ) ) {
return section;
}
return section
.map( ( row ) => ( {
cells:
row.cells.length >= columnIndex
? row.cells.filter(
( cell, index ) => index !== columnIndex
)
: row.cells,
} ) )
.filter( ( row ) => row.cells.length );
} );
}
/**
* Toggles the existance of a section.
*
* @param {Object} state Current table state.
* @param {string} sectionName Name of the section to toggle.
*
* @return {Object} New table state.
*/
export function toggleSection( state, sectionName ) {
// Section exists, replace it with an empty row to remove it.
if ( ! isEmptyTableSection( state[ sectionName ] ) ) {
return { [ sectionName ]: [] };
}
// Get the length of the first row of the body to use when creating the header.
const columnCount = get( state, [ 'body', 0, 'cells', 'length' ], 1 );
// Section doesn't exist, insert an empty row to create the section.
return insertRow( state, { sectionName, rowIndex: 0, columnCount } );
}
/**
* Determines whether a table section is empty.
*
* @param {Object} section Table section state.
*
* @return {boolean} True if the table section is empty, false otherwise.
*/
export function isEmptyTableSection( section ) {
return ! section || ! section.length || every( section, isEmptyRow );
}
/**
* Determines whether a table row is empty.
*
* @param {Object} row Table row state.
*
* @return {boolean} True if the table section is empty, false otherwise.
*/
export function isEmptyRow( row ) {
return ! ( row.cells && row.cells.length );
}