Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Enable table cell merge and unmerge #17261

Draft
wants to merge 3 commits into
base: trunk
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 162 additions & 15 deletions packages/block-library/src/table/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,14 +110,109 @@ export class TableEdit extends Component {
this.onToggleFooterSection = this.onToggleFooterSection.bind( this );
this.onChangeColumnAlignment = this.onChangeColumnAlignment.bind( this );
this.getCellAlignment = this.getCellAlignment.bind( this );
this.rangeSelect = this.rangeSelect.bind( this );
this.onMouseDown = this.onMouseDown.bind( this );
this.onMouseUp = this.onMouseUp.bind( this );
this.onMouseOver = this.onMouseOver.bind( this );
this.onMerge = this.onMerge.bind( this );

this.state = {
initialRowCount: 2,
initialColumnCount: 2,
selectedCell: null,
selectedCells: [],
initialSelection: null,
isSelecting: false,
finalSelection: null,
mergedCells: false,
};
}

/**
* Handle cell merging and unmerging.
*/
onMerge() {
const { selectedCells, mergedCells } = this.state;

// If we already have merged cells, unmerge them!
if ( mergedCells ) {

} else {
// Merge the cells.
const { attributes, setAttributes } = this.props;
const { initialSelection, finalSelection } = this.state;
const largerColumn = initialSelection.columnIndex > finalSelection.columnIndex ? initialSelection.columnIndex : finalSelection.columnIndex;
const smallerColumn = initialSelection.columnIndex < finalSelection.columnIndex ? initialSelection.columnIndex : finalSelection.columnIndex;
const largerRow = initialSelection.rowIndex > finalSelection.rowIndex ? initialSelection.rowIndex : finalSelection.rowIndex;
const smallerRow = initialSelection.rowIndex < finalSelection.rowIndex ? initialSelection.rowIndex : finalSelection.rowIndex;
const colspan = largerColumn - smallerColumn + 1;
const rowspan = largerRow - smallerRow + 1;
attributes.body[ smallerRow ].cells[ smallerColumn ].colspan = colspan;
attributes.body[ smallerRow ].cells[ smallerColumn ].rowspan = rowspan;
let mergedContent = attributes.body[ smallerRow ].cells[ smallerColumn ].content;
for ( let x = smallerColumn; x <= largerColumn; x++ ) {
for ( let y = smallerRow; y <= largerRow; y++ ) {
if ( ! ( x === smallerColumn && y === smallerRow ) ) {
mergedContent += '<br />' + attributes.body[ y ].cells[ x ].content;
delete ( attributes.body[ y ].cells[ x ] );
}
}
}
attributes.body[ smallerRow ].cells[ smallerColumn ].content = mergedContent;
setAttributes( attributes );
this.setState( {
selectedCells: [],
mergedCells: selectedCells,
} );
}
}

/**
* Handle onMouseDown events.
*
* @param {Object} cellLocation Object with `section`, `rowIndex`, and `columnIndex` properties.
*/
onMouseDown( cellLocation ) {
const { isSelecting } = this.state;
if ( ! isSelecting ) {
this.setState( {
isSelecting: true,
initialSelection: cellLocation,
selectedCells: [],
} );
}
}

/**
* Handle onMouseUp events.
*
* @param {Object} cellLocation Object with `section`, `rowIndex`, and `columnIndex` properties.
*/
onMouseUp( cellLocation ) {
const { isSelecting } = this.state;
if ( isSelecting ) {
this.setState( {
isSelecting: false,
finalSelection: cellLocation,
} );
}
}

/**
* Handle onMouseOver events.
*
* @param {Object} cellLocation Object with `section`, `rowIndex`, and `columnIndex` properties.
*/
onMouseOver( cellLocation ) {
const {
isSelecting,
} = this.state;

if ( isSelecting ) {
this.rangeSelect( cellLocation );
}
}

/**
* Updates the initial column count used for table creation.
*
Expand Down Expand Up @@ -363,23 +458,47 @@ export class TableEdit extends Component {
*/
createOnFocus( cellLocation ) {
return () => {
const { selectedCells } = this.state;
selectedCells[ `${ cellLocation.columnIndex }::${ cellLocation.rowIndex }` ] = true;
this.setState( {
selectedCell: {
...cellLocation,
type: 'cell',
},
selectedCells,
} );
};
}

/**
* Select a range.
*
* @param {Object} cellLocation Object with `section`, `rowIndex`, and `columnIndex` properties.
*/
rangeSelect( cellLocation ) {
const { initialSelection, selectedCell } = this.state;
const selectedCells = [];
const currentCell = cellLocation || selectedCell;
const largerColumn = initialSelection.columnIndex > currentCell.columnIndex ? initialSelection.columnIndex : currentCell.columnIndex;
const smallerColumn = initialSelection.columnIndex < currentCell.columnIndex ? initialSelection.columnIndex : currentCell.columnIndex;
const largerRow = initialSelection.rowIndex > currentCell.rowIndex ? initialSelection.rowIndex : currentCell.rowIndex;
const smallerRow = initialSelection.rowIndex < currentCell.rowIndex ? initialSelection.rowIndex : currentCell.rowIndex;

for ( let x = smallerColumn; x <= largerColumn; x++ ) {
for ( let y = smallerRow; y <= largerRow; y++ ) {
selectedCells[ `${ x }::${ y }` ] = true;
}
}
this.setState( { selectedCells } );
}

/**
* Gets the table controls to display in the block toolbar.
*
* @return {Array} Table controls.
*/
getTableControls() {
const { selectedCell } = this.state;

const { selectedCell, selectedCells, mergedCells } = this.state;
return [
{
icon: 'table-row-before',
Expand Down Expand Up @@ -417,6 +536,12 @@ export class TableEdit extends Component {
isDisabled: ! selectedCell,
onClick: this.onDeleteColumn,
},
{
icon: 'table-cells-merge',
title: mergedCells ? __( 'Unmerge Cells' ) : __( 'Merge Cells' ),
isDisabled: 0 === Object.values( selectedCells ),
onClick: this.onMerge,
},
];
}

Expand All @@ -435,19 +560,19 @@ export class TableEdit extends Component {
}

const Tag = `t${ name }`;
const { selectedCell } = this.state;
const { selectedCell, selectedCells } = this.state;

return (
<Tag>
{ rows.map( ( { cells }, rowIndex ) => (
<tr key={ rowIndex }>
{ cells.map( ( { content, tag: CellTag, scope, align }, columnIndex ) => {
{ cells.map( ( { content, tag: CellTag, scope, align, colspan, rowspan }, columnIndex ) => {
const cellLocation = {
sectionName: name,
rowIndex,
columnIndex,
};
const isSelected = isCellSelected( cellLocation, selectedCell );
const isSelected = isCellSelected( cellLocation, selectedCell ) || selectedCells[ `${ cellLocation.columnIndex }::${ cellLocation.rowIndex }` ];

const cellClasses = classnames( {
'is-selected': isSelected,
Expand All @@ -457,15 +582,38 @@ export class TableEdit extends Component {

return (
<CellTag
colSpan={ colspan }
rowSpan={ rowspan }
key={ columnIndex }
className={ cellClasses }
scope={ CellTag === 'th' ? scope : undefined }
onMouseDown={ () => {
this.onMouseDown( cellLocation );
} }
onMouseUp={ () => {
this.onMouseUp( cellLocation );
} }
onMouseOver={ () => {
this.onMouseOver( cellLocation );
} }
onFocus={ this.createOnFocus( cellLocation ) }
onClick={ ( event ) => {
// When a cell is selected, forward focus to the child RichText. This solves an issue where the
// user may click inside a cell, but outside of the RichText, resulting in nothing happening.
const richTextElement = event && event.target && event.target.querySelector( `.${ richTextClassName }` );
if ( richTextElement ) {
richTextElement.focus();
// If the shift key is depressed when clicking, and an existing cell is selected, do a multi-select
// betweeen them.
if ( event.shiftKey && isSelected ) {
// Select range.
this.rangeSelect();
event.preventDefault();
} else {
// Clear multi-selection and set initial selection.
this.setState( { selectedCells: [], initialSelection: cellLocation } );

// When a cell is selected, forward focus to the child RichText. This solves an issue where the
// user may click inside a cell, but outside of the RichText, resulting in nothing happening.
const richTextElement = event && event.target && event.target.querySelector( `.${ richTextClassName }` );
if ( richTextElement ) {
richTextElement.focus();
}
}
} }
>
Expand All @@ -484,12 +632,11 @@ export class TableEdit extends Component {
);
}

componentDidUpdate() {
componentDidUpdate( prevProps ) {
const { isSelected } = this.props;
const { selectedCell } = this.state;

if ( ! isSelected && selectedCell ) {
this.setState( { selectedCell: null } );
const { wasIsSelected } = prevProps;
if ( ! isSelected && wasIsSelected ) {
this.setState( { selectedCell: null, selectedCells: [] } );
}
}

Expand Down
3 changes: 3 additions & 0 deletions packages/components/src/dashicon/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,9 @@ export default class Dashicon extends Component {
case 'table-col-before':
path = 'M6.4 3.776v3.648H2.752v1.792H6.4v3.648h1.728V9.216h3.712V7.424H8.128V3.776zM0 17.92V0h20.48v17.92H0zM12.8 1.28H1.28v14.08H12.8V1.28zm6.4 0h-5.12v3.84h5.12V1.28zm0 5.12h-5.12v3.84h5.12V6.4zm0 5.12h-5.12v3.84h5.12v-3.84z';
break;
case 'table-cells-merge':
path = 'M5 10H3V4h8v2H5v4zm14 8h-6v2h8v-6h-2v4zM5 18v-4H3v6h8v-2H5zM21 4h-8v2h6v4h2V4zM8 13v2l3-3-3-3v2H3v2h5zm8-2V9l-3 3 3 3v-2h5v-2h-5z';
break;
case 'table-col-delete':
path = 'M6.4 9.98L7.68 8.7v-.256L6.4 7.164V9.98zm6.4-1.532l1.28-1.28V9.92L12.8 8.64v-.192zm7.68 9.472V0H0v17.92h20.48zm-1.28-2.56h-5.12v-1.024l-.256.256-1.024-1.024v1.792H7.68v-1.792l-1.024 1.024-.256-.256v1.024H1.28V1.28H6.4v2.368l.704-.704.576.576V1.216h5.12V3.52l.96-.96.32.32V1.216h5.12V15.36zm-5.76-2.112l-3.136-3.136-3.264 3.264-1.536-1.536 3.264-3.264L5.632 5.44l1.536-1.536 3.136 3.136 3.2-3.2 1.536 1.536-3.2 3.2 3.136 3.136-1.536 1.536z';
break;
Expand Down