From 77cb2726751c45bd42bee6f4d141a7c203f5eb2e Mon Sep 17 00:00:00 2001 From: Adam Silverstein Date: Thu, 29 Aug 2019 12:39:20 -0600 Subject: [PATCH 1/3] add a table-cells-merge icon --- packages/components/src/dashicon/index.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/components/src/dashicon/index.js b/packages/components/src/dashicon/index.js index cd7ecdd622245..0de0d637d20c2 100644 --- a/packages/components/src/dashicon/index.js +++ b/packages/components/src/dashicon/index.js @@ -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; From 12a7f0213a8b5e9d774d2ecc1c0e0aa3bbdb715f Mon Sep 17 00:00:00 2001 From: Adam Silverstein Date: Thu, 29 Aug 2019 13:13:59 -0600 Subject: [PATCH 2/3] drag selection, merging --- packages/block-library/src/table/edit.js | 167 +++++++++++++++++++++-- 1 file changed, 152 insertions(+), 15 deletions(-) diff --git a/packages/block-library/src/table/edit.js b/packages/block-library/src/table/edit.js index b86c0e282d7f9..b100de5b2b6fd 100644 --- a/packages/block-library/src/table/edit.js +++ b/packages/block-library/src/table/edit.js @@ -110,14 +110,99 @@ 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. + */ + onMerge() { + const { selectedCells } = this.state; + 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; + for ( let x = smallerColumn; x <= largerColumn; x++ ) { + for ( let y = smallerRow; y <= largerRow; y++ ) { + if ( ! ( x === smallerColumn && y === smallerRow ) ) { + delete ( attributes.body[ y ].cells[ x ] ); + } + } + } + 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. * @@ -363,23 +448,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', @@ -417,6 +526,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, + }, ]; } @@ -435,19 +550,19 @@ export class TableEdit extends Component { } const Tag = `t${ name }`; - const { selectedCell } = this.state; + const { selectedCell, selectedCells } = this.state; return ( { rows.map( ( { cells }, 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, @@ -457,15 +572,38 @@ export class TableEdit extends Component { return ( { + 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(); + } } } } > @@ -484,12 +622,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: [] } ); } } From a18810d7e151a132421e2103826f432f545cdc4d Mon Sep 17 00:00:00 2001 From: Adam Silverstein Date: Thu, 29 Aug 2019 13:47:43 -0600 Subject: [PATCH 3/3] Handle merging and unmerging --- packages/block-library/src/table/edit.js | 52 ++++++++++++++---------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/packages/block-library/src/table/edit.js b/packages/block-library/src/table/edit.js index b100de5b2b6fd..9049924ae7e6c 100644 --- a/packages/block-library/src/table/edit.js +++ b/packages/block-library/src/table/edit.js @@ -129,32 +129,42 @@ export class TableEdit extends Component { } /** - * Handle cell merging. + * Handle cell merging and unmerging. */ onMerge() { - const { selectedCells } = this.state; - 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; - for ( let x = smallerColumn; x <= largerColumn; x++ ) { - for ( let y = smallerRow; y <= largerRow; y++ ) { - if ( ! ( x === smallerColumn && y === smallerRow ) ) { - delete ( attributes.body[ y ].cells[ x ] ); + 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 += '
' + 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, + } ); } - setAttributes( attributes ); - this.setState( { - selectedCells: [], - mergedCells: selectedCells, - } ); } /**