@wordpress/block-library
Version:
Block library for the WordPress editor.
348 lines (317 loc) • 9.15 kB
JavaScript
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 | undefined} 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 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 = Object.fromEntries(
Object.entries( state ).filter( ( [ key ] ) =>
[ 'head', 'body', 'foot' ].includes( key )
)
);
const { sectionName: selectionSectionName, rowIndex: selectionRowIndex } =
selection;
return Object.fromEntries(
Object.entries( tableSections ).map( ( [ sectionName, section ] ) => {
if (
selectionSectionName &&
selectionSectionName !== sectionName
) {
return [ sectionName, section ];
}
return [
sectionName,
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 ? 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 =
firstRow?.cells?.[ index ] ?? {};
const inheritedAttributes = Object.fromEntries(
Object.entries( firstCellInColumn ).filter(
( [ key ] ) =>
INHERITED_COLUMN_ATTRIBUTES.includes( key )
)
);
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 = Object.fromEntries(
Object.entries( state ).filter( ( [ key ] ) =>
[ 'head', 'body', 'foot' ].includes( key )
)
);
return Object.fromEntries(
Object.entries( tableSections ).map( ( [ sectionName, section ] ) => {
// Bail early if the table section is empty.
if ( isEmptyTableSection( section ) ) {
return [ sectionName, section ];
}
return [
sectionName,
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 = Object.fromEntries(
Object.entries( state ).filter( ( [ key ] ) =>
[ 'head', 'body', 'foot' ].includes( key )
)
);
return Object.fromEntries(
Object.entries( tableSections ).map( ( [ sectionName, section ] ) => {
// Bail early if the table section is empty.
if ( isEmptyTableSection( section ) ) {
return [ sectionName, section ];
}
return [
sectionName,
section
.map( ( row ) => ( {
cells:
row.cells.length >= columnIndex
? row.cells.filter(
( cell, index ) => index !== columnIndex
)
: row.cells,
} ) )
.filter( ( row ) => row.cells.length ),
];
} )
);
}
/**
* Toggles the existence 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 = 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 || section.every( 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 );
}