diff --git a/jest-addon.config.js b/jest-addon.config.js index 7c154993..1e5c80c9 100644 --- a/jest-addon.config.js +++ b/jest-addon.config.js @@ -9,6 +9,7 @@ module.exports = { '@plone/volto/babel': '/node_modules/@plone/volto/babel', '@plone/volto/(.*)$': '/node_modules/@plone/volto/src/$1', '@package/(.*)$': '/src/$1', + '@root/(.*)$': '/src/$1', '@plone/volto-quanta/(.*)$': '/src/addons/volto-quanta/src/$1', '@eeacms/(.*?)/(.*)$': '/src/addons/$1/src/$2', 'volto-slate/(.*)$': '/src/addons/volto-slate/src/$1', diff --git a/package.json b/package.json index bd65cf60..17a5c5e8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "volto-slate", - "version": "5.3.5", + "version": "5.4.0", "description": "Slate.js integration with Volto", "main": "src/index.js", "author": "European Environment Agency: IDM2 A-Team", diff --git a/src/blocks/Table/TableBlockEdit.jsx b/src/blocks/Table/TableBlockEdit.jsx index 3b2c1e03..fdf1d886 100644 --- a/src/blocks/Table/TableBlockEdit.jsx +++ b/src/blocks/Table/TableBlockEdit.jsx @@ -64,6 +64,7 @@ const emptyRow = (cells) => ({ * relevance only in the context in which it is used. */ const initialTable = { + hideHeaders: false, fixed: true, compact: false, basic: false, @@ -129,6 +130,18 @@ const messages = defineMessages({ id: 'Delete col', defaultMessage: 'Delete col', }, + hideHeaders: { + id: 'Hide headers', + defaultMessage: 'Hide headers', + }, + sortable: { + id: 'Make the table sortable', + defaultMessage: 'Make the table sortable', + }, + sortableDescription: { + id: 'Visible only in view mode', + defaultMessage: 'Visible only in view mode', + }, fixed: { id: 'Fixed width table cells', defaultMessage: 'Fixed width table cells', @@ -153,6 +166,38 @@ const messages = defineMessages({ id: 'Stripe alternate rows with color', defaultMessage: 'Stripe alternate rows with color', }, + textAlign: { + id: 'Align text', + defaultMessage: 'Align text', + }, + verticalAlign: { + id: 'Vertical align', + defaultMessage: 'Vertical align', + }, + left: { + id: 'Left', + defaultMessage: 'Left', + }, + center: { + id: 'Center', + defaultMessage: 'Center', + }, + right: { + id: 'Right', + defaultMessage: 'Right', + }, + bottom: { + id: 'Bottom', + defaultMessage: 'Bottom', + }, + middle: { + id: 'Middle', + defaultMessage: 'Middle', + }, + top: { + id: 'Top', + defaultMessage: 'Top', + }, }); /** @@ -200,12 +245,15 @@ class Edit extends Component { constructor(props) { super(props); this.state = { + headers: [], + rows: {}, selected: { row: 0, cell: 0, }, isClient: false, }; + this.onChange = this.onChange.bind(this); this.onSelectCell = this.onSelectCell.bind(this); this.onInsertRowBefore = this.onInsertRowBefore.bind(this); this.onInsertRowAfter = this.onInsertRowAfter.bind(this); @@ -216,6 +264,8 @@ class Edit extends Component { this.onChangeCell = this.onChangeCell.bind(this); this.toggleCellType = this.toggleCellType.bind(this); this.toggleBool = this.toggleBool.bind(this); + this.toggleHideHeaders = this.toggleHideHeaders.bind(this); + this.toggleSortable = this.toggleSortable.bind(this); this.toggleFixed = this.toggleFixed.bind(this); this.toggleCompact = this.toggleCompact.bind(this); this.toggleBasic = this.toggleBasic.bind(this); @@ -254,6 +304,24 @@ class Edit extends Component { } } + /** + * On change + * @method onChange + * @param {string} id Id of modified property. + * @param {any} value New value of modified property. + * @returns {undefined} + */ + onChange(id, value) { + const table = this.props.data.table; + this.props.onChangeBlock(this.props.block, { + ...this.props.data, + table: { + ...table, + [id]: value, + }, + }); + } + /** * Select cell handler * @method onSelectCell @@ -476,6 +544,24 @@ class Edit extends Component { }); } + /** + * Toggle fixed + * @method toggleHideHeaders + * @returns {undefined} + */ + toggleHideHeaders() { + this.toggleBool('hideHeaders'); + } + + /** + * Toggle sortable + * @method toggleSortable + * @returns {undefined} + */ + toggleSortable() { + this.toggleBool('sortable'); + } + /** * Toggle fixed * @method toggleFixed @@ -542,6 +628,10 @@ class Edit extends Component { * @returns {string} Markup for the component. */ render() { + const headers = this.props.data.table?.rows?.[0]?.cells || []; + const rows = + this.props.data.table?.rows?.filter((_, index) => index > 0) || []; + return ( // TODO: use slate-table instead of table, but first copy the CSS styles // to the new name @@ -642,17 +732,54 @@ class Edit extends Component { striped={this.props.data.table.striped} className="slate-table-block" > + {!this.props.data.table.hideHeaders ? ( + + + {headers.map((cell, cellIndex) => ( + + + + ))} + + + ) : ( + '' + )} - {map(this.props.data.table.rows, (row, rowIndex) => ( + {map(rows, (row, rowIndex) => ( {map(row.cells, (cell, cellIndex) => ( + this.toggleHideHeaders()} + /> + this.toggleSortable()} + /> - - - - - + diff --git a/src/blocks/Table/TableBlockView.jsx b/src/blocks/Table/TableBlockView.jsx index 8d344a8a..6091cfd9 100644 --- a/src/blocks/Table/TableBlockView.jsx +++ b/src/blocks/Table/TableBlockView.jsx @@ -3,11 +3,14 @@ * @module volto-slate/blocks/Table/View */ -import React from 'react'; +import React, { useState, useMemo } from 'react'; import PropTypes from 'prop-types'; import { Table } from 'semantic-ui-react'; import { map } from 'lodash'; -import { serializeNodes } from 'volto-slate/editor/render'; +import { + serializeNodes, + serializeNodesToText, +} from 'volto-slate/editor/render'; import { Node } from 'slate'; // TODO: loading LESS files with `volto-slate/...` paths does not work currently @@ -20,6 +23,54 @@ import '../../editor/plugins/Table/less/public.less'; * @param {object} data The table data to render as a table. */ const View = ({ data }) => { + const [state, setState] = useState({ + column: null, + direction: null, + }); + + const headers = useMemo(() => { + return data.table.rows?.[0]?.cells; + }, [data.table.rows]); + + const rows = useMemo(() => { + const items = {}; + if (!data.table.rows) return {}; + data.table.rows.forEach((row, index) => { + if (index > 0) { + items[row.key] = []; + row.cells.forEach((cell, cellIndex) => { + items[row.key][cellIndex] = { + ...cell, + value: + cell.value && Node.string({ children: cell.value }).length > 0 + ? serializeNodes(cell.value) + : '\u00A0', + valueText: + cell.value && Node.string({ children: cell.value }).length > 0 + ? serializeNodesToText(cell.value) + : '\u00A0', + }; + }); + } + }); + return items; + }, [data.table.rows]); + + const sortedRows = useMemo(() => { + if (state.column === null) return Object.keys(rows); + return Object.keys(rows).sort((a, b) => { + const a_text = rows[a][state.column].valueText; + const b_text = rows[b][state.column].valueText; + if (state.direction === 'ascending' ? a_text < b_text : a_text > b_text) { + return -1; + } + if (state.direction === 'ascending' ? a_text > b_text : a_text < b_text) { + return 1; + } + return 0; + }); + }, [state, rows]); + return ( <> {data && data.table && ( @@ -30,22 +81,52 @@ const View = ({ data }) => { celled={data.table.celled} inverted={data.table.inverted} striped={data.table.striped} + sortable={data.table.sortable} className="slate-table-block" > - - {map(data.table.rows, (row) => ( - - {map(row.cells, (cell) => ( - + + {headers.map((cell, index) => ( + { + if (!data.table.sortable) return; + setState({ + column: index, + direction: + state.column !== index + ? 'ascending' + : state.direction === 'ascending' + ? 'descending' + : 'ascending', + }); + }} > {cell.value && Node.string({ children: cell.value }).length > 0 ? serializeNodes(cell.value) : '\u00A0'} - - {/* TODO: above use blockHasValue from the Slate Volto addon block's metadata */} + + ))} + + + ) : ( + '' + )} + + {map(sortedRows, (row) => ( + + {map(rows[row], (cell) => ( + + {cell.value} ))} diff --git a/src/blocks/Table/TableBlockView.test.js b/src/blocks/Table/TableBlockView.test.js index 35ec8cf0..e3280877 100644 --- a/src/blocks/Table/TableBlockView.test.js +++ b/src/blocks/Table/TableBlockView.test.js @@ -39,6 +39,7 @@ test('renders a view table component', () => { ], }, ], + hideHeaders: false, }, }} />, diff --git a/src/blocks/Table/__snapshots__/TableBlockEdit.test.js.snap b/src/blocks/Table/__snapshots__/TableBlockEdit.test.js.snap index 7fac60e7..dd490441 100644 --- a/src/blocks/Table/__snapshots__/TableBlockEdit.test.js.snap +++ b/src/blocks/Table/__snapshots__/TableBlockEdit.test.js.snap @@ -7,6 +7,13 @@ exports[`renders an edit table block component 1`] = ` + + + diff --git a/src/blocks/Table/__snapshots__/TableBlockView.test.js.snap b/src/blocks/Table/__snapshots__/TableBlockView.test.js.snap index da67f3dc..6f61859d 100644 --- a/src/blocks/Table/__snapshots__/TableBlockView.test.js.snap +++ b/src/blocks/Table/__snapshots__/TableBlockView.test.js.snap @@ -4,20 +4,24 @@ exports[`renders a view table component 1`] = `
- - + - + +

My header

-
`; diff --git a/src/editor/plugins/Table/less/public.less b/src/editor/plugins/Table/less/public.less index 6ac5faee..6cc70090 100644 --- a/src/editor/plugins/Table/less/public.less +++ b/src/editor/plugins/Table/less/public.less @@ -19,3 +19,11 @@ table.slate-table { border-bottom: 0.15rem solid @brown; } } + +table.slate-table-block.sortable { + tr th > * { + // Header will contain both the slate and an icon when sorted + // so this will keep them on the same line. + display: inline-block; + } +}