From 49c65b9f8c6df42717abb088064a55fb8abbe1ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20Van=C2=A0Dorpe?= Date: Thu, 23 Aug 2018 15:43:04 +0200 Subject: [PATCH] Rewrite table block to use a simpler RichText value (#8767) * Rewrite table block to use a simpler RichText value * Remove TinyMCE table plugin * Add basic tests * Restore drop down table controls * move files to package * Fixes * More polish * Polish initial prompt again. * Add docs * Table: Address my own feedback from review * Componnet: Add isDisabled support for DropdownMenu controls * Table block: Fix an issue when removing all columns does not prompt table creation * Revert part of 19b7cb3a8fb1ab6b761a92c73ab6a22fe7ca3f8e --- lib/client-assets.php | 6 - packages/block-library/package.json | 1 + packages/block-library/src/table/edit.js | 418 +++++++++++++++++ packages/block-library/src/table/editor.scss | 8 +- packages/block-library/src/table/index.js | 144 +++--- packages/block-library/src/table/state.js | 159 +++++++ .../block-library/src/table/table-block.js | 125 ----- .../table/test/__snapshots__/index.js.snap | 83 ++-- .../block-library/src/table/test/index.js | 2 +- .../block-library/src/table/test/state.js | 288 ++++++++++++ packages/block-library/src/table/theme.scss | 1 - packages/blocks/src/api/parser.js | 8 +- .../components/src/base-control/style.scss | 15 +- .../components/src/dropdown-menu/README.md | 2 +- .../components/src/dropdown-menu/index.js | 4 + .../full-content/fixtures/core__table.json | 433 +++++++++--------- 16 files changed, 1230 insertions(+), 467 deletions(-) create mode 100644 packages/block-library/src/table/edit.js create mode 100644 packages/block-library/src/table/state.js delete mode 100644 packages/block-library/src/table/table-block.js create mode 100644 packages/block-library/src/table/test/state.js diff --git a/lib/client-assets.php b/lib/client-assets.php index 2bee5c2c7d8ac7..437bdfe20e2992 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -554,7 +554,6 @@ function gutenberg_register_scripts_and_styles() { array( 'lodash', 'tinymce-latest-lists', - 'tinymce-latest-table', 'wp-a11y', 'wp-api-fetch', 'wp-blob', @@ -796,11 +795,6 @@ function gutenberg_register_vendor_scripts() { 'https://unpkg.com/tinymce@' . $tinymce_version . '/plugins/lists/plugin' . $suffix . '.js', array( 'wp-tinymce' ) ); - gutenberg_register_vendor_script( - 'tinymce-latest-table', - 'https://unpkg.com/tinymce@' . $tinymce_version . '/plugins/table/plugin' . $suffix . '.js', - array( 'wp-tinymce' ) - ); gutenberg_register_vendor_script( 'lodash', 'https://unpkg.com/lodash@4.17.5/lodash' . $suffix . '.js' diff --git a/packages/block-library/package.json b/packages/block-library/package.json index 2a4ff968c3d971..ccda95f0e8b06a 100644 --- a/packages/block-library/package.json +++ b/packages/block-library/package.json @@ -46,6 +46,7 @@ "url": "^0.11.0" }, "devDependencies": { + "deep-freeze": "^0.0.1", "enzyme": "^3.3.0", "react-test-renderer": "^16.4.1" }, diff --git a/packages/block-library/src/table/edit.js b/packages/block-library/src/table/edit.js new file mode 100644 index 00000000000000..f733d988138ae5 --- /dev/null +++ b/packages/block-library/src/table/edit.js @@ -0,0 +1,418 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { Fragment, Component } from '@wordpress/element'; +import { InspectorControls, BlockControls, RichText } from '@wordpress/editor'; +import { __ } from '@wordpress/i18n'; +import { + PanelBody, + ToggleControl, + TextControl, + Button, + Toolbar, + DropdownMenu, +} from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { + createTable, + updateCellContent, + insertRow, + deleteRow, + insertColumn, + deleteColumn, +} from './state'; + +export default class TableEdit extends Component { + constructor() { + super( ...arguments ); + + this.onCreateTable = this.onCreateTable.bind( this ); + this.onChangeFixedLayout = this.onChangeFixedLayout.bind( this ); + this.onChange = this.onChange.bind( this ); + this.onChangeInitialColumnCount = this.onChangeInitialColumnCount.bind( this ); + this.onChangeInitialRowCount = this.onChangeInitialRowCount.bind( this ); + this.renderSection = this.renderSection.bind( this ); + this.getTableControls = this.getTableControls.bind( this ); + this.onInsertRow = this.onInsertRow.bind( this ); + this.onInsertRowBefore = this.onInsertRowBefore.bind( this ); + this.onInsertRowAfter = this.onInsertRowAfter.bind( this ); + this.onDeleteRow = this.onDeleteRow.bind( this ); + this.onInsertColumn = this.onInsertColumn.bind( this ); + this.onInsertColumnBefore = this.onInsertColumnBefore.bind( this ); + this.onInsertColumnAfter = this.onInsertColumnAfter.bind( this ); + this.onDeleteColumn = this.onDeleteColumn.bind( this ); + + this.state = { + initialRowCount: 2, + initialColumnCount: 2, + selectedCell: null, + }; + } + + /** + * Updates the initial column count used for table creation. + * + * @param {number} initialColumnCount New initial column count. + */ + onChangeInitialColumnCount( initialColumnCount ) { + this.setState( { initialColumnCount } ); + } + + /** + * Updates the initial row count used for table creation. + * + * @param {number} initialRowCount New initial row count. + */ + onChangeInitialRowCount( initialRowCount ) { + this.setState( { initialRowCount } ); + } + + /** + * Creates a table based on dimensions in local state. + */ + onCreateTable() { + const { setAttributes } = this.props; + let { initialRowCount, initialColumnCount } = this.state; + + initialRowCount = parseInt( initialRowCount, 10 ) || 2; + initialColumnCount = parseInt( initialColumnCount, 10 ) || 2; + + setAttributes( createTable( { + rowCount: initialRowCount, + columnCount: initialColumnCount, + } ) ); + } + + /** + * Toggles whether the table has a fixed layout or not. + */ + onChangeFixedLayout() { + const { attributes, setAttributes } = this.props; + const { hasFixedLayout } = attributes; + + setAttributes( { hasFixedLayout: ! hasFixedLayout } ); + } + + /** + * Changes the content of the currently selected cell. + * + * @param {Array} content A RichText content value. + */ + onChange( content ) { + const { selectedCell } = this.state; + + if ( ! selectedCell ) { + return; + } + + const { attributes, setAttributes } = this.props; + const { section, rowIndex, columnIndex } = selectedCell; + + setAttributes( updateCellContent( attributes, { + section, + rowIndex, + columnIndex, + content, + } ) ); + } + + /** + * Inserts a row at the currently selected row index, plus `delta`. + * + * @param {number} delta Offset for selected row index at which to insert. + */ + onInsertRow( delta ) { + const { selectedCell } = this.state; + + if ( ! selectedCell ) { + return; + } + + const { attributes, setAttributes } = this.props; + const { section, rowIndex } = selectedCell; + + this.setState( { selectedCell: null } ); + setAttributes( insertRow( attributes, { + section, + rowIndex: rowIndex + delta, + } ) ); + } + + /** + * Inserts a row before the currently selected row. + */ + onInsertRowBefore() { + this.onInsertRow( 0 ); + } + + /** + * Inserts a row after the currently selected row. + */ + onInsertRowAfter() { + this.onInsertRow( 1 ); + } + + /** + * Deletes the currently selected row. + */ + onDeleteRow() { + const { selectedCell } = this.state; + + if ( ! selectedCell ) { + return; + } + + const { attributes, setAttributes } = this.props; + const { section, rowIndex } = selectedCell; + + this.setState( { selectedCell: null } ); + setAttributes( deleteRow( attributes, { section, rowIndex } ) ); + } + + /** + * Inserts a column at the currently selected column index, plus `delta`. + * + * @param {number} delta Offset for selected column index at which to insert. + */ + onInsertColumn( delta = 0 ) { + const { selectedCell } = this.state; + + if ( ! selectedCell ) { + return; + } + + const { attributes, setAttributes } = this.props; + const { section, columnIndex } = selectedCell; + + this.setState( { selectedCell: null } ); + setAttributes( insertColumn( attributes, { + section, + columnIndex: columnIndex + delta, + } ) ); + } + + /** + * Inserts a column before the currently selected column. + */ + onInsertColumnBefore() { + this.onInsertColumn( 0 ); + } + + /** + * Inserts a column after the currently selected column. + */ + onInsertColumnAfter() { + this.onInsertColumn( 1 ); + } + + /** + * Deletes the currently selected column. + */ + onDeleteColumn() { + const { selectedCell } = this.state; + + if ( ! selectedCell ) { + return; + } + + const { attributes, setAttributes } = this.props; + const { section, columnIndex } = selectedCell; + + this.setState( { selectedCell: null } ); + setAttributes( deleteColumn( attributes, { section, columnIndex } ) ); + } + + /** + * Creates an onFocus handler for a specified cell. + * + * @param {Object} selectedCell Object with `section`, `rowIndex`, and + * `columnIndex` properties. + * + * @return {Function} Function to call on focus. + */ + createOnFocus( selectedCell ) { + return () => { + this.setState( { selectedCell } ); + }; + } + + /** + * Gets the table controls to display in the block toolbar. + * + * @return {Array} Table controls. + */ + getTableControls() { + const { selectedCell } = this.state; + + return [ + { + icon: 'table-row-before', + title: __( 'Add Row Before' ), + isDisabled: ! selectedCell, + onClick: this.onInsertRowBefore, + }, + { + icon: 'table-row-after', + title: __( 'Add Row After' ), + isDisabled: ! selectedCell, + onClick: this.onInsertRowAfter, + }, + { + icon: 'table-row-delete', + title: __( 'Delete Row' ), + isDisabled: ! selectedCell, + onClick: this.onDeleteRow, + }, + { + icon: 'table-col-before', + title: __( 'Add Column Before' ), + isDisabled: ! selectedCell, + onClick: this.onInsertColumnBefore, + }, + { + icon: 'table-col-after', + title: __( 'Add Column After' ), + isDisabled: ! selectedCell, + onClick: this.onInsertColumnAfter, + }, + { + icon: 'table-col-delete', + title: __( 'Delete Column' ), + isDisabled: ! selectedCell, + onClick: this.onDeleteColumn, + }, + ]; + } + + /** + * Renders a table section. + * + * @param {string} options.type Section type: head, body, or foot. + * @param {Array} options.rows The rows to render. + * + * @return {Object} React element for the section. + */ + renderSection( { type, rows } ) { + if ( ! rows.length ) { + return null; + } + + const Tag = `t${ type }`; + const { selectedCell } = this.state; + + return ( + + { rows.map( ( { cells }, rowIndex ) => + + { cells.map( ( { content, tag: CellTag }, columnIndex ) => { + const isSelected = selectedCell && ( + type === selectedCell.section && + rowIndex === selectedCell.rowIndex && + columnIndex === selectedCell.columnIndex + ); + + const cell = { + section: type, + rowIndex, + columnIndex, + }; + + const classes = classnames( { + 'is-selected': isSelected, + } ); + + return ( + + + + ); + } ) } + + ) } + + ); + } + + componentDidUpdate() { + const { isSelected } = this.props; + const { selectedCell } = this.state; + + if ( ! isSelected && selectedCell ) { + this.setState( { selectedCell: null } ); + } + } + + render() { + const { attributes, className } = this.props; + const { initialRowCount, initialColumnCount } = this.state; + const { hasFixedLayout, head, body, foot } = attributes; + const isEmpty = ! head.length && ! body.length && ! foot.length; + const Section = this.renderSection; + + if ( isEmpty ) { + return ( +
+ + + + + ); + } + + const classes = classnames( className, { + 'has-fixed-layout': hasFixedLayout, + } ); + + return ( + + + + + + + + + + + + +
+
+
+
+
+ ); + } +} diff --git a/packages/block-library/src/table/editor.scss b/packages/block-library/src/table/editor.scss index 3c48c591874d36..6a22682b0937c7 100644 --- a/packages/block-library/src/table/editor.scss +++ b/packages/block-library/src/table/editor.scss @@ -10,8 +10,10 @@ border: $border-width solid currentColor; } - td[data-mce-selected="1"], - th[data-mce-selected="1"] { - background-color: $light-gray-300; + td.is-selected, + th.is-selected { + border-color: $blue-medium-500; + box-shadow: inset 0 0 0 1px $blue-medium-500; + border-style: double; } } diff --git a/packages/block-library/src/table/index.js b/packages/block-library/src/table/index.js index aefe196de67d24..a59e70bee183a0 100644 --- a/packages/block-library/src/table/index.js +++ b/packages/block-library/src/table/index.js @@ -7,24 +7,15 @@ import classnames from 'classnames'; * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { Fragment } from '@wordpress/element'; import { getPhrasingContentSchema } from '@wordpress/blocks'; -import { - RichText, - InspectorControls, -} from '@wordpress/editor'; - -import { - PanelBody, - ToggleControl, -} from '@wordpress/components'; +import { RichText } from '@wordpress/editor'; /** * Internal dependencies */ -import TableBlock from './table-block'; +import edit from './edit'; -const tableContentSchema = { +const tableContentPasteSchema = { tr: { children: { th: { @@ -37,22 +28,51 @@ const tableContentSchema = { }, }; -const tableSchema = { +const tablePasteSchema = { table: { children: { thead: { - children: tableContentSchema, + children: tableContentPasteSchema, }, tfoot: { - children: tableContentSchema, + children: tableContentPasteSchema, }, tbody: { - children: tableContentSchema, + children: tableContentPasteSchema, }, }, }, }; +function getTableSectionAttributeSchema( section ) { + return { + type: 'array', + default: [], + source: 'query', + selector: `t${ section } tr`, + query: { + cells: { + type: 'array', + default: [], + source: 'query', + selector: 'td,th', + query: { + content: { + type: 'array', + default: [], + source: 'children', + }, + tag: { + type: 'string', + default: 'td', + source: 'tag', + }, + }, + }, + }, + }; +} + export const name = 'core/table'; export const settings = { @@ -62,21 +82,13 @@ export const settings = { category: 'formatting', attributes: { - content: { - type: 'array', - source: 'children', - selector: 'table', - default: [ - -

-

- , - ], - }, hasFixedLayout: { type: 'boolean', default: false, }, + head: getTableSectionAttributeSchema( 'head' ), + body: getTableSectionAttributeSchema( 'body' ), + foot: getTableSectionAttributeSchema( 'foot' ), }, supports: { @@ -88,57 +100,51 @@ export const settings = { { type: 'raw', selector: 'table', - schema: tableSchema, + schema: tablePasteSchema, }, ], }, - edit( { attributes, setAttributes, isSelected, className } ) { - const { content, hasFixedLayout } = attributes; - const toggleFixedLayout = () => { - setAttributes( { hasFixedLayout: ! hasFixedLayout } ); - }; + edit, - const classes = classnames( - className, - { - 'has-fixed-layout': hasFixedLayout, - }, - ); + save( { attributes } ) { + const { hasFixedLayout, head, body, foot } = attributes; + const isEmpty = ! head.length && ! body.length && ! foot.length; - return ( - - - - - - - { - setAttributes( { content: nextContent } ); - } } - content={ content } - className={ classes } - isSelected={ isSelected } - /> - - ); - }, + if ( isEmpty ) { + return null; + } - save( { attributes } ) { - const { content, hasFixedLayout } = attributes; - const classes = classnames( - { - 'has-fixed-layout': hasFixedLayout, - }, - ); + const classes = classnames( { + 'has-fixed-layout': hasFixedLayout, + } ); + + const Section = ( { type, rows } ) => { + if ( ! rows.length ) { + return null; + } + + const Tag = `t${ type }`; + + return ( + + { rows.map( ( { cells }, rowIndex ) => + + { cells.map( ( { content, tag }, cellIndex ) => + + ) } + + ) } + + ); + }; return ( - + +
+
+
+
); }, }; diff --git a/packages/block-library/src/table/state.js b/packages/block-library/src/table/state.js new file mode 100644 index 00000000000000..559ba46f3d5ed6 --- /dev/null +++ b/packages/block-library/src/table/state.js @@ -0,0 +1,159 @@ +/** + * External dependencies + */ +import { times } from 'lodash'; + +/** + * Creates a table state. + * + * @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: times( rowCount, () => ( { + cells: times( columnCount, () => ( { + content: [], + tag: 'td', + } ) ), + } ) ), + }; +} + +/** + * Updates cell content in the table state. + * + * @param {Object} state Current table state. + * @param {string} options.section Section of the cell to update. + * @param {number} options.rowIndex Row index of the cell to update. + * @param {number} options.columnIndex Column index of the cell to update. + * @param {Array} options.content Content to set for the cell. + * + * @return {Object} New table state. + */ +export function updateCellContent( state, { + section, + rowIndex, + columnIndex, + content, +} ) { + return { + [ section ]: state[ section ].map( ( row, currentRowIndex ) => { + if ( currentRowIndex !== rowIndex ) { + return row; + } + + return { + cells: row.cells.map( ( cell, currentColumnIndex ) => { + if ( currentColumnIndex !== columnIndex ) { + return cell; + } + + return { + ...cell, + content, + }; + } ), + }; + } ), + }; +} + +/** + * Inserts a row in the table state. + * + * @param {Object} state Current table state. + * @param {string} options.section Section in which to insert the row. + * @param {number} options.rowIndex Row index at which to insert the row. + * + * @return {Object} New table state. + */ +export function insertRow( state, { + section, + rowIndex, +} ) { + const cellCount = state[ section ][ 0 ].cells.length; + + return { + [ section ]: [ + ...state[ section ].slice( 0, rowIndex ), + { + cells: times( cellCount, () => ( { + content: [], + tag: 'td', + } ) ), + }, + ...state[ section ].slice( rowIndex ), + ], + }; +} + +/** + * Deletes a row from the table state. + * + * @param {Object} state Current table state. + * @param {string} options.section Section in which to delete the row. + * @param {number} options.rowIndex Row index to delete. + * + * @return {Object} New table state. + */ +export function deleteRow( state, { + section, + rowIndex, +} ) { + return { + [ section ]: state[ section ].filter( ( row, index ) => index !== rowIndex ), + }; +} + +/** + * Inserts a column in the table state. + * + * @param {Object} state Current table state. + * @param {string} options.section Section in which to insert the column. + * @param {number} options.columnIndex Column index at which to insert the column. + * + * @return {Object} New table state. + */ +export function insertColumn( state, { + section, + columnIndex, +} ) { + return { + [ section ]: state[ section ].map( ( row ) => ( { + cells: [ + ...row.cells.slice( 0, columnIndex ), + { + content: [], + tag: 'td', + }, + ...row.cells.slice( columnIndex ), + ], + } ) ), + }; +} + +/** + * Deletes a column from the table state. + * + * @param {Object} state Current table state. + * @param {string} options.section Section in which to delete the column. + * @param {number} options.columnIndex Column index to delete. + * + * @return {Object} New table state. + */ +export function deleteColumn( state, { + section, + columnIndex, +} ) { + return { + [ section ]: state[ section ].map( ( row ) => ( { + cells: row.cells.filter( ( cell, index ) => index !== columnIndex ), + } ) ).filter( ( row ) => row.cells.length ), + }; +} diff --git a/packages/block-library/src/table/table-block.js b/packages/block-library/src/table/table-block.js deleted file mode 100644 index 18fffae1ef747d..00000000000000 --- a/packages/block-library/src/table/table-block.js +++ /dev/null @@ -1,125 +0,0 @@ -/** - * WordPress dependencies - */ -import { Component, Fragment } from '@wordpress/element'; -import { Toolbar, DropdownMenu } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; -import { - BlockControls, - RichText, -} from '@wordpress/editor'; - -function isTableSelected( editor ) { - return editor.dom.getParent( - editor.selection.getStart( true ), - 'table', - editor.getBody().parentNode - ); -} - -function selectFirstCell( editor ) { - const cell = editor.getBody().querySelector( 'td,th' ); - if ( cell ) { - cell.focus(); - editor.selection.select( cell, true ); - editor.selection.collapse( false ); - } -} - -function execCommand( command ) { - return ( editor ) => { - if ( editor ) { - if ( ! isTableSelected( editor ) ) { - selectFirstCell( editor ); - } - editor.execCommand( command ); - } - }; -} - -const TABLE_CONTROLS = [ - { - icon: 'table-row-before', - title: __( 'Add Row Before' ), - onClick: execCommand( 'mceTableInsertRowBefore' ), - }, - { - icon: 'table-row-after', - title: __( 'Add Row After' ), - onClick: execCommand( 'mceTableInsertRowAfter' ), - }, - { - icon: 'table-row-delete', - title: __( 'Delete Row' ), - onClick: execCommand( 'mceTableDeleteRow' ), - }, - { - icon: 'table-col-before', - title: __( 'Add Column Before' ), - onClick: execCommand( 'mceTableInsertColBefore' ), - }, - { - icon: 'table-col-after', - title: __( 'Add Column After' ), - onClick: execCommand( 'mceTableInsertColAfter' ), - }, - { - icon: 'table-col-delete', - title: __( 'Delete Column' ), - onClick: execCommand( 'mceTableDeleteCol' ), - }, -]; - -export default class TableBlock extends Component { - constructor() { - super(); - this.handleSetup = this.handleSetup.bind( this ); - this.state = { - editor: null, - }; - } - - handleSetup( editor, isSelected ) { - // select the end of the first table cell - editor.on( 'init', () => { - if ( isSelected ) { - selectFirstCell( editor ); - } - } ); - this.setState( { editor } ); - } - - render() { - const { content, onChange, className, isSelected } = this.props; - - return ( - - ( { - ...settings, - plugins: ( settings.plugins || [] ).concat( 'table' ), - table_tab_navigation: false, - } ) } - unstableOnSetup={ ( editor ) => this.handleSetup( editor, isSelected ) } - onChange={ onChange } - value={ content } - /> - - - ( { - ...control, - onClick: () => control.onClick( this.state.editor ), - } ) ) } - /> - - - - ); - } -} diff --git a/packages/block-library/src/table/test/__snapshots__/index.js.snap b/packages/block-library/src/table/test/__snapshots__/index.js.snap index e336bc498289ee..6361bb4649ea48 100644 --- a/packages/block-library/src/table/test/__snapshots__/index.js.snap +++ b/packages/block-library/src/table/test/__snapshots__/index.js.snap @@ -1,41 +1,54 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`core/embed block edit matches snapshot 1`] = ` -
-
-
-
+
+
+
+ Column Count + +
-
+
+
+ + +
+
+ + `; diff --git a/packages/block-library/src/table/test/index.js b/packages/block-library/src/table/test/index.js index 0b8a1985b06aa4..cfd3b3916a6ffa 100644 --- a/packages/block-library/src/table/test/index.js +++ b/packages/block-library/src/table/test/index.js @@ -4,7 +4,7 @@ import { name, settings } from '../'; import { blockEditRender } from '../../test/helpers'; -describe( 'core/embed', () => { +describe( 'core/table', () => { test( 'block edit matches snapshot', () => { const wrapper = blockEditRender( name, settings ); diff --git a/packages/block-library/src/table/test/state.js b/packages/block-library/src/table/test/state.js new file mode 100644 index 00000000000000..a0e84c4626c7e4 --- /dev/null +++ b/packages/block-library/src/table/test/state.js @@ -0,0 +1,288 @@ +/** + * External dependencies + */ +import deepFreeze from 'deep-freeze'; + +/** + * Internal dependencies + */ +import { + createTable, + updateCellContent, + insertRow, + deleteRow, + insertColumn, + deleteColumn, +} from '../state'; + +const table = deepFreeze( { + body: [ + { + cells: [ + { + content: [], + tag: 'td', + }, + { + content: [], + tag: 'td', + }, + ], + }, + { + cells: [ + { + content: [], + tag: 'td', + }, + { + content: [], + tag: 'td', + }, + ], + }, + ], +} ); + +const tableWithContent = deepFreeze( { + body: [ + { + cells: [ + { + content: [], + tag: 'td', + }, + { + content: [], + tag: 'td', + }, + ], + }, + { + cells: [ + { + content: [], + tag: 'td', + }, + { + content: [ 'test' ], + tag: 'td', + }, + ], + }, + ], +} ); + +describe( 'createTable', () => { + it( 'should create a table', () => { + const state = createTable( { rowCount: 2, columnCount: 2 } ); + + expect( state ).toEqual( table ); + } ); +} ); + +describe( 'updateCellContent', () => { + it( 'should update cell content', () => { + const state = updateCellContent( table, { + section: 'body', + rowIndex: 1, + columnIndex: 1, + content: [ 'test' ], + } ); + + expect( state ).toEqual( tableWithContent ); + } ); +} ); + +describe( 'insertRow', () => { + it( 'should insert row', () => { + const state = insertRow( tableWithContent, { + section: 'body', + rowIndex: 2, + } ); + + const expected = { + body: [ + { + cells: [ + { + content: [], + tag: 'td', + }, + { + content: [], + tag: 'td', + }, + ], + }, + { + cells: [ + { + content: [], + tag: 'td', + }, + { + content: [ 'test' ], + tag: 'td', + }, + ], + }, + { + cells: [ + { + content: [], + tag: 'td', + }, + { + content: [], + tag: 'td', + }, + ], + }, + ], + }; + + expect( state ).toEqual( expected ); + } ); +} ); + +describe( 'insertColumn', () => { + it( 'should insert column', () => { + const state = insertColumn( tableWithContent, { + section: 'body', + columnIndex: 2, + } ); + + const expected = { + body: [ + { + cells: [ + { + content: [], + tag: 'td', + }, + { + content: [], + tag: 'td', + }, + { + content: [], + tag: 'td', + }, + ], + }, + { + cells: [ + { + content: [], + tag: 'td', + }, + { + content: [ 'test' ], + tag: 'td', + }, + { + content: [], + tag: 'td', + }, + ], + }, + ], + }; + + expect( state ).toEqual( expected ); + } ); +} ); + +describe( 'deleteRow', () => { + it( 'should delete row', () => { + const state = deleteRow( tableWithContent, { + section: 'body', + rowIndex: 0, + } ); + + const expected = { + body: [ + { + cells: [ + { + content: [], + tag: 'td', + }, + { + content: [ 'test' ], + tag: 'td', + }, + ], + }, + ], + }; + + expect( state ).toEqual( expected ); + } ); +} ); + +describe( 'deleteColumn', () => { + it( 'should delete column', () => { + const state = deleteColumn( tableWithContent, { + section: 'body', + columnIndex: 0, + } ); + + const expected = { + body: [ + { + cells: [ + { + content: [], + tag: 'td', + }, + ], + }, + { + cells: [ + { + content: [ 'test' ], + tag: 'td', + }, + ], + }, + ], + }; + + expect( state ).toEqual( expected ); + } ); + + it( 'should delete all rows when only one column present', () => { + const tableWithOneColumn = { + body: [ + { + cells: [ + { + content: [], + tag: 'td', + }, + ], + }, + { + cells: [ + { + content: [ 'test' ], + tag: 'td', + }, + ], + }, + ], + }; + const state = deleteColumn( tableWithOneColumn, { + section: 'body', + columnIndex: 0, + } ); + + const expected = { + body: [], + }; + + expect( state ).toEqual( expected ); + } ); +} ); diff --git a/packages/block-library/src/table/theme.scss b/packages/block-library/src/table/theme.scss index b2cc6ceedd2587..86872dfa9e84c3 100644 --- a/packages/block-library/src/table/theme.scss +++ b/packages/block-library/src/table/theme.scss @@ -1,6 +1,5 @@ .wp-block-table { overflow-x: auto; - display: block; border-collapse: collapse; width: 100%; diff --git a/packages/blocks/src/api/parser.js b/packages/blocks/src/api/parser.js index e0a3c03e9c89c7..16353552810019 100644 --- a/packages/blocks/src/api/parser.js +++ b/packages/blocks/src/api/parser.js @@ -18,7 +18,7 @@ import { getBlockType, getUnknownTypeHandlerName } from './registration'; import { createBlock } from './factory'; import { isValidBlock } from './validation'; import { getCommentDelimitedContent } from './serializer'; -import { attr, html, text, query, node, children } from './matchers'; +import { attr, html, text, query, node, children, prop } from './matchers'; /** * Higher-order hpq matcher which enhances an attribute matcher to return true @@ -115,6 +115,11 @@ export function matcherFromSource( sourceConfig ) { case 'query': const subMatchers = mapValues( sourceConfig.query, matcherFromSource ); return query( sourceConfig.selector, subMatchers ); + case 'tag': + return flow( [ + prop( sourceConfig.selector, 'nodeName' ), + ( value ) => value.toLowerCase(), + ] ); default: // eslint-disable-next-line no-console console.error( `Unknown source type "${ sourceConfig.source }"` ); @@ -160,6 +165,7 @@ export function getBlockAttribute( attributeKey, attributeSchema, innerHTML, com case 'children': case 'node': case 'query': + case 'tag': value = parseWithAttributeSchema( innerHTML, attributeSchema ); break; } diff --git a/packages/components/src/base-control/style.scss b/packages/components/src/base-control/style.scss index 5d53940b337893..6472df6fa23e90 100644 --- a/packages/components/src/base-control/style.scss +++ b/packages/components/src/base-control/style.scss @@ -1,6 +1,19 @@ +.components-base-control { + font-family: $default-font; + font-size: $default-font-size; +} + +.components-base-control__field { + margin-bottom: $grid-size; + + .components-panel__row & { + margin-bottom: inherit; + } +} + .components-base-control__label { display: block; - margin-bottom: 5px; + margin-bottom: $grid-size-small; } .components-base-control__help { diff --git a/packages/components/src/dropdown-menu/README.md b/packages/components/src/dropdown-menu/README.md index 59aadcc36d6ece..4d934a3ee92045 100644 --- a/packages/components/src/dropdown-menu/README.md +++ b/packages/components/src/dropdown-menu/README.md @@ -64,7 +64,7 @@ A human-readable label to present as accessibility text on the focused collapsed An array of objects describing the options to be shown in the expanded menu. -Each object should include an `icon` [Dashicon](https://developer.wordpress.org/resource/dashicons/) slug string, a human-readable `title` string, and an `onClick` function callback to invoke when the option is selected. +Each object should include an `icon` [Dashicon](https://developer.wordpress.org/resource/dashicons/) slug string, a human-readable `title` string, `isDisabled` boolean flag and an `onClick` function callback to invoke when the option is selected. - Type: `Array` - Required: Yes diff --git a/packages/components/src/dropdown-menu/index.js b/packages/components/src/dropdown-menu/index.js index 771464917ceab2..0ec348097c7db2 100644 --- a/packages/components/src/dropdown-menu/index.js +++ b/packages/components/src/dropdown-menu/index.js @@ -68,6 +68,9 @@ function DropdownMenu( { { + if ( control.isDisabled ) { + return; + } event.stopPropagation(); onClose(); if ( control.onClick ) { @@ -77,6 +80,7 @@ function DropdownMenu( { className="components-dropdown-menu__menu-item" icon={ control.icon } role="menuitem" + disabled={ control.isDisabled } > { control.title } diff --git a/test/integration/full-content/fixtures/core__table.json b/test/integration/full-content/fixtures/core__table.json index f6a6c42c9c5453..3adc4861093f7e 100644 --- a/test/integration/full-content/fixtures/core__table.json +++ b/test/integration/full-content/fixtures/core__table.json @@ -4,260 +4,245 @@ "name": "core/table", "isValid": true, "attributes": { - "content": [ + "hasFixedLayout": false, + "head": [ { - "type": "thead", - "children": [ + "cells": [ { - "type": "tr", - "children": [ - { - "type": "th", - "children": [ - "Version" - ] - }, - { - "type": "th", - "children": [ - "Musician" - ] - }, - { - "type": "th", - "children": [ - "Date" - ] + "content": [ + "Version" + ], + "tag": "th" + }, + { + "content": [ + "Musician" + ], + "tag": "th" + }, + { + "content": [ + "Date" + ], + "tag": "th" + } + ] + } + ], + "body": [ + { + "cells": [ + { + "content": [ + { + "type": "a", + "props": { + "href": "https://wordpress.org/news/2003/05/wordpress-now-available/", + "children": [ + ".70" + ] + } } - ] + ], + "tag": "td" + }, + { + "content": [ + "No musician chosen." + ], + "tag": "td" + }, + { + "content": [ + "May 27, 2003" + ], + "tag": "td" } ] }, { - "type": "tbody", - "children": [ + "cells": [ { - "type": "tr", - "children": [ - { - "type": "td", - "children": [ - { - "type": "a", - "attributes": { - "href": "https://wordpress.org/news/2003/05/wordpress-now-available/" - }, - "children": [ - ".70" - ] - } - ] - }, - { - "type": "td", - "children": [ - "No musician chosen." - ] - }, - { - "type": "td", - "children": [ - "May 27, 2003" - ] + "content": [ + { + "type": "a", + "props": { + "href": "https://wordpress.org/news/2004/01/wordpress-10/", + "children": [ + "1.0" + ] + } } - ] + ], + "tag": "td" }, { - "type": "tr", - "children": [ - { - "type": "td", - "children": [ - { - "type": "a", - "attributes": { - "href": "https://wordpress.org/news/2004/01/wordpress-10/" - }, - "children": [ - "1.0" - ] - } - ] - }, - { - "type": "td", - "children": [ - "Miles Davis" - ] - }, - { - "type": "td", - "children": [ - "January 3, 2004" - ] - } - ] + "content": [ + "Miles Davis" + ], + "tag": "td" }, { - "type": "tr", - "children": [ - { - "type": "td", - "children": [ - "Lots of versions skipped, see ", - { - "type": "a", - "attributes": { - "href": "https://codex.wordpress.org/WordPress_Versions" - }, - "children": [ - "the full list" - ] - } - ] - }, - { - "type": "td", - "children": [ - "…" - ] - }, - { - "type": "td", - "children": [ - "…" - ] + "content": [ + "January 3, 2004" + ], + "tag": "td" + } + ] + }, + { + "cells": [ + { + "content": [ + "Lots of versions skipped, see ", + { + "type": "a", + "props": { + "href": "https://codex.wordpress.org/WordPress_Versions", + "children": [ + "the full list" + ] + } } - ] + ], + "tag": "td" }, { - "type": "tr", - "children": [ - { - "type": "td", - "children": [ - { - "type": "a", - "attributes": { - "href": "https://wordpress.org/news/2015/12/clifford/" - }, - "children": [ - "4.4" - ] - } - ] - }, - { - "type": "td", - "children": [ - "Clifford Brown" - ] - }, - { - "type": "td", - "children": [ - "December 8, 2015" - ] + "content": [ + "…" + ], + "tag": "td" + }, + { + "content": [ + "…" + ], + "tag": "td" + } + ] + }, + { + "cells": [ + { + "content": [ + { + "type": "a", + "props": { + "href": "https://wordpress.org/news/2015/12/clifford/", + "children": [ + "4.4" + ] + } } - ] + ], + "tag": "td" }, { - "type": "tr", - "children": [ - { - "type": "td", - "children": [ - { - "type": "a", - "attributes": { - "href": "https://wordpress.org/news/2016/04/coleman/" - }, - "children": [ - "4.5" - ] - } - ] - }, - { - "type": "td", - "children": [ - "Coleman Hawkins" - ] - }, - { - "type": "td", - "children": [ - "April 12, 2016" - ] + "content": [ + "Clifford Brown" + ], + "tag": "td" + }, + { + "content": [ + "December 8, 2015" + ], + "tag": "td" + } + ] + }, + { + "cells": [ + { + "content": [ + { + "type": "a", + "props": { + "href": "https://wordpress.org/news/2016/04/coleman/", + "children": [ + "4.5" + ] + } } - ] + ], + "tag": "td" }, { - "type": "tr", - "children": [ - { - "type": "td", - "children": [ - { - "type": "a", - "attributes": { - "href": "https://wordpress.org/news/2016/08/pepper/" - }, - "children": [ - "4.6" - ] - } - ] - }, - { - "type": "td", - "children": [ - "Pepper Adams" - ] - }, - { - "type": "td", - "children": [ - "August 16, 2016" - ] + "content": [ + "Coleman Hawkins" + ], + "tag": "td" + }, + { + "content": [ + "April 12, 2016" + ], + "tag": "td" + } + ] + }, + { + "cells": [ + { + "content": [ + { + "type": "a", + "props": { + "href": "https://wordpress.org/news/2016/08/pepper/", + "children": [ + "4.6" + ] + } } - ] + ], + "tag": "td" }, { - "type": "tr", - "children": [ - { - "type": "td", - "children": [ - { - "type": "a", - "attributes": { - "href": "https://wordpress.org/news/2016/12/vaughan/" - }, - "children": [ - "4.7" - ] - } - ] - }, - { - "type": "td", - "children": [ - "Sarah Vaughan" - ] - }, - { - "type": "td", - "children": [ - "December 6, 2016" - ] + "content": [ + "Pepper Adams" + ], + "tag": "td" + }, + { + "content": [ + "August 16, 2016" + ], + "tag": "td" + } + ] + }, + { + "cells": [ + { + "content": [ + { + "type": "a", + "props": { + "href": "https://wordpress.org/news/2016/12/vaughan/", + "children": [ + "4.7" + ] + } } - ] + ], + "tag": "td" + }, + { + "content": [ + "Sarah Vaughan" + ], + "tag": "td" + }, + { + "content": [ + "December 6, 2016" + ], + "tag": "td" } ] } ], - "hasFixedLayout": false + "foot": [] }, "innerBlocks": [], "originalContent": "
VersionMusicianDate
.70No musician chosen.May 27, 2003
1.0Miles DavisJanuary 3, 2004
Lots of versions skipped, see the full list
4.4Clifford BrownDecember 8, 2015
4.5Coleman HawkinsApril 12, 2016
4.6Pepper AdamsAugust 16, 2016
4.7Sarah VaughanDecember 6, 2016
"