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

Table Block: Support rowspan attribute in table HTML, including when pasting #46629

Merged
merged 12 commits into from
Feb 1, 2023
15 changes: 15 additions & 0 deletions packages/block-library/src/table/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@
"type": "string",
"source": "attribute",
"attribute": "colspan"
},
"rowspan": {
"type": "string",
"source": "attribute",
"attribute": "rowspan"
}
}
}
Expand Down Expand Up @@ -92,6 +97,11 @@
"type": "string",
"source": "attribute",
"attribute": "colspan"
},
"rowspan": {
"type": "string",
"source": "attribute",
"attribute": "rowspan"
}
}
}
Expand Down Expand Up @@ -132,6 +142,11 @@
"type": "string",
"source": "attribute",
"attribute": "colspan"
},
"rowspan": {
"type": "string",
"source": "attribute",
"attribute": "rowspan"
}
}
}
Expand Down
10 changes: 9 additions & 1 deletion packages/block-library/src/table/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,14 @@ function TableEdit( {
<tr key={ rowIndex }>
{ cells.map(
(
{ content, tag: CellTag, scope, align, colspan },
{
content,
tag: CellTag,
scope,
align,
colspan,
rowspan,
},
columnIndex
) => (
<RichText
Expand All @@ -418,6 +425,7 @@ function TableEdit( {
) }
scope={ CellTag === 'th' ? scope : undefined }
colSpan={ colspan }
rowSpan={ rowspan }
value={ content }
onChange={ onChange }
unstableOnFocus={ () => {
Expand Down
10 changes: 9 additions & 1 deletion packages/block-library/src/table/save.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,14 @@ export default function save( { attributes } ) {
<tr key={ rowIndex }>
{ cells.map(
(
{ content, tag, scope, align, colspan },
{
content,
tag,
scope,
align,
colspan,
rowspan,
},
cellIndex
) => {
const cellClasses = classnames( {
Expand All @@ -66,6 +73,7 @@ export default function save( { attributes } ) {
tag === 'th' ? scope : undefined
}
colSpan={ colspan }
rowSpan={ rowspan }
/>
);
}
Expand Down
20 changes: 20 additions & 0 deletions packages/block-library/src/table/test/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Internal dependencies
*/
import { normalizeRowColSpan } from '../utils';

describe( 'normalizeRowColSpan', () => {
it( 'should convert a value to a string', () => {
expect( normalizeRowColSpan( 2 ) ).toBe( '2' );
expect( normalizeRowColSpan( '2' ) ).toBe( '2' );
expect( normalizeRowColSpan( 2.55 ) ).toBe( '2' );
expect( normalizeRowColSpan( '2.55' ) ).toBe( '2' );
} );

it( 'should return undefined for values not allowed as the rowspan/colspan attributes', () => {
expect( normalizeRowColSpan( -2 ) ).toBe( undefined );
expect( normalizeRowColSpan( '-2' ) ).toBe( undefined );
expect( normalizeRowColSpan( 1 ) ).toBe( undefined );
expect( normalizeRowColSpan( '1' ) ).toBe( undefined );
} );
} );
67 changes: 65 additions & 2 deletions packages/block-library/src/table/transforms.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
/**
* WordPress dependencies
*/
import { createBlock } from '@wordpress/blocks';

/**
* Internal dependencies
*/
import { normalizeRowColSpan } from './utils';

const tableContentPasteSchema = ( { phrasingContentSchema } ) => ( {
tr: {
allowEmpty: true,
children: {
th: {
allowEmpty: true,
children: phrasingContentSchema,
attributes: [ 'scope', 'colspan' ],
attributes: [ 'scope', 'colspan', 'rowspan' ],
},
td: {
allowEmpty: true,
children: phrasingContentSchema,
attributes: [ 'colspan' ],
attributes: [ 'colspan', 'rowspan' ],
},
},
},
Expand Down Expand Up @@ -41,6 +51,59 @@ const transforms = {
type: 'raw',
selector: 'table',
schema: tablePasteSchema,
transform: ( node ) => {
const attributes = Array.from( node.children ).reduce(
( sectionAcc, section ) => {
if ( ! section.children.length ) {
return sectionAcc;
}

const sectionName = section.nodeName
.toLowerCase()
.slice( 1 );

const sectionAttributes = Array.from(
section.children
).reduce( ( rowAcc, row ) => {
if ( ! row.children.length ) {
return rowAcc;
}

const rowAttributes = Array.from(
row.children
).reduce( ( colAcc, col ) => {
const rowspan = normalizeRowColSpan(
col.getAttribute( 'rowspan' )
);
const colspan = normalizeRowColSpan(
col.getAttribute( 'colspan' )
);

colAcc.push( {
tag: col.nodeName.toLowerCase(),
content: col.innerHTML,
rowspan,
colspan,
} );

return colAcc;
}, [] );

rowAcc.push( {
cells: rowAttributes,
} );

return rowAcc;
}, [] );

sectionAcc[ sectionName ] = sectionAttributes;
return sectionAcc;
},
{}
);

return createBlock( 'core/table', attributes );
},
},
],
};
Expand Down
18 changes: 18 additions & 0 deletions packages/block-library/src/table/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Normalize the rowspan/colspan value.
* Returns undefined if the parameter is not a positive number
* or the default value (1) for rowspan/colspan.
*
* @param {number|undefined} rowColSpan rowspan/colspan value.
*
* @return {string|undefined} normalized rowspan/colspan value.
*/
export function normalizeRowColSpan( rowColSpan ) {
t-hamano marked this conversation as resolved.
Show resolved Hide resolved
const parsedValue = parseInt( rowColSpan, 10 );
if ( ! Number.isInteger( parsedValue ) ) {
return undefined;
}
return parsedValue < 0 || parsedValue === 1
? undefined
: parsedValue.toString();
}
131 changes: 79 additions & 52 deletions packages/blocks/src/api/raw-handling/test/paste-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,46 +10,55 @@ import { init as initAndRegisterTableBlock } from '../../../../../block-library/
const tableWithHeaderFooterAndBodyUsingColspan = `
<table>
<thead>
<tr>
<th colspan="2">Colspan 2</th>
<th>Header Cell</th>
</tr>
</thead>
<tfoot>
<tr>
<th colspan="2">Footer Cell</th>
<th>Footer Cell</th>
</tr>
</tfoot>
<tbody>
<tr>
<td colspan="2">Colspan 2</td>
<td>Cell Data</td>
</tr>
</tbody>
<tr>
<th colspan="2">Colspan 2</th>
<th>Header Cell</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="2">Colspan 2</td>
<td>Cell Data</td>
</tr>
</tbody>
<tfoot>
<tr>
<th colspan="2">Colspan 2</th>
<th>Footer Cell</th>
</tr>
</tfoot>
</table>`;

const googleDocsTableWithColspan = `
<meta charset="utf-8"><b style="font-weight:normal;" id="docs-internal-guid-b0f68bdd-7fff-a054-94d1-43c2fdedca2a">
<div dir="ltr" style="margin-left:0pt;" align="left">
<table style="border:none;border-collapse:collapse;">
<colgroup>
<col width="185"/>
<col width="439"/>
</colgroup>
<tbody>
<tr style="height:21pt">
<td colspan="2"
style="border-left:solid #000000 1pt;border-right:solid #000000 1pt;border-bottom:solid #000000 1pt;border-top:solid #000000 1pt;vertical-align:top;padding:5pt 5pt 5pt 5pt;overflow:hidden;overflow-wrap:break-word;">
<p dir="ltr" style="line-height:1.2;margin-top:0pt;margin-bottom:0pt;"><span
style="font-size:11pt;font-family:Arial;color:#000000;background-color:transparent;font-weight:400;font-style:normal;font-variant:normal;text-decoration:none;vertical-align:baseline;white-space:pre;white-space:pre-wrap;">Test colspan</span>
</p></td>
</tr>
</tbody>
</table>
</div>
<br/><br/></b>
`;
const tableWithHeaderFooterAndBodyUsingRowspan = `
<table>
<thead>
<tr>
<th rowspan="2">Rowspan 2</th>
<th>Header Cell</th>
</tr>
<tr>
<th>Header Cell</th>
</tr>
</thead>
<tbody>
<tr>
<td rowspan="2">Rowspan 2</td>
<td>Cell Data</td>
</tr>
<tr>
<td>Cell Data</td>
</tr>
</tbody>
<tfoot>
<tr>
<td rowspan="2">Rowspan 2</td>
<td>Footer Cell</td>
</tr>
<tr>
<td>Footer Cell</td>
</tr>
</tfoot>
</table>`;

describe( 'pasteHandler', () => {
beforeAll( () => {
Expand Down Expand Up @@ -87,7 +96,7 @@ describe( 'pasteHandler', () => {
foot: [
{
cells: [
{ content: 'Footer Cell', tag: 'th', colspan: '2' },
{ content: 'Colspan 2', tag: 'th', colspan: '2' },
{ content: 'Footer Cell', tag: 'th' },
],
},
Expand All @@ -97,33 +106,51 @@ describe( 'pasteHandler', () => {
expect( result.isValid ).toBeTruthy();
} );

it( 'can handle a google docs table with colspan', () => {
it( 'can handle a table with thead, tbody and tfoot using rowspan', () => {
const [ result ] = pasteHandler( {
HTML: googleDocsTableWithColspan,
HTML: tableWithHeaderFooterAndBodyUsingRowspan,
tagName: 'p',
preserveWhiteSpace: false,
} );

expect( console ).toHaveLogged();

expect( result.attributes ).toEqual( {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: why not check the serialised block like elsewhere in this file?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean that the following should be checked with strings containing comments?

<!-- wp:table -->
<figure class="wp-block-table"><table>...</table></figure>
<!-- /wp:table -->

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. You could use .expect().toMatchInlineSnapshot() I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There appears to be no serialized block string in the result variable.
Do I need to generate a serialized block from the result.attirbutes?

hasFixedLayout: false,
caption: '',
head: [
{
cells: [
{ content: 'Rowspan 2', tag: 'th', rowspan: '2' },
{ content: 'Header Cell', tag: 'th' },
],
},
{
cells: [ { content: 'Header Cell', tag: 'th' } ],
},
],
body: [
{
cells: [
{
align: undefined,
colspan: '2',
content: 'Test colspan',
scope: undefined,
tag: 'td',
},
{ content: 'Rowspan 2', tag: 'td', rowspan: '2' },
{ content: 'Cell Data', tag: 'td' },
],
},
{
cells: [ { content: 'Cell Data', tag: 'td' } ],
},
],
foot: [
{
cells: [
{ content: 'Rowspan 2', tag: 'td', rowspan: '2' },
{ content: 'Footer Cell', tag: 'td' },
],
},
{
cells: [ { content: 'Footer Cell', tag: 'td' } ],
},
],
caption: '',
foot: [],
hasFixedLayout: false,
head: [],
} );
expect( result.name ).toEqual( 'core/table' );
expect( result.isValid ).toBeTruthy();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ exports[`Blocks raw handling pasteHandler google-docs-list-only 1`] = `"My first

exports[`Blocks raw handling pasteHandler google-docs-table 1`] = `"<br><br><br><br>One<br>Two<br>Three<br>1<br>2<br>3<br>I<br>II<br>III"`;

exports[`Blocks raw handling pasteHandler google-docs-table-with-colspan 1`] = `"<br><br>Test colspan<br><br>"`;

exports[`Blocks raw handling pasteHandler google-docs-table-with-rowspan 1`] = `"<br><br>Test rowspan<br><br>"`;

exports[`Blocks raw handling pasteHandler google-docs-table-with-comments 1`] = `"<br><br><br><br>One<br>Two<br>Three<br>1<br>2<br>3<br>I<br>II<br>III"`;

exports[`Blocks raw handling pasteHandler google-docs-with-comments 1`] = `"This is a <strong>title</strong><br><br>This is a <em>heading</em><br><br>Formatting test: <strong>bold</strong>, <em>italic</em>, <a href=\\"https://w.org/\\">link</a>, <s>strikethrough</s>, <sup>superscript</sup>, <sub>subscript</sub>, <strong><em>nested</em></strong>.<br><br>A<br>Bulleted<br>Indented<br>List<br><br>One<br>Two<br>Three<br><br><br><br><br>One<br>Two<br>Three<br>1<br>2<br>3<br>I<br>II<br>III<br><br><br><br><br><br>An image:<br><br><img src=\\"https://lh4.googleusercontent.com/ID\\" width=\\"544\\" height=\\"184\\"><br><br>"`;
Expand Down
Loading