From a377f1e888a36caa75ef33341752e2300cf2d12e Mon Sep 17 00:00:00 2001 From: Tiberiu Ichim Date: Sun, 3 Apr 2022 21:25:29 +0300 Subject: [PATCH] Add poweruser menu review (#238) * New features on table block (#235) * New features on table block * Add 2 new boolean options to table configuration: 'Show headers' and 'Make the table sortable' * Add text align option to table configuration * Clean up * Update tests * Add 'Vertical align' option * Update tests * [JENKINS] - Fix tests to work with Volto 15 * Bump version to 5.4.0 * set showHeaders as true by default * Replace showHeaders with hideHeaders * use align widget * Update tests * fix(table style): change display to inline-block for headers content * Add comment to public.less Co-authored-by: Alin Voinea * Automated release 5.4.0 * Add Sonarqube tag using clms-frontend addons list * chore(cypress): Fix paste html * Automated release 5.4.1 * Allow passing custom slate settings * WIP * WIP * Seems to be working * Properly select first option in menu * Add some instructions on slash menu * Try to cancel Esc * Add node about esc canceling * Add one more note * Return from component Co-authored-by: Alin Voinea Co-authored-by: Miu Razvan Co-authored-by: EEA Jenkins <@users.noreply.github.com> Co-authored-by: EEA Jenkins --- CHANGELOG.md | 14 ++ Jenkinsfile | 2 +- .../integration/27-block-slate-paste-html.js | 4 +- jest-addon.config.js | 1 + package.json | 2 +- src/blocks/Table/TableBlockEdit.jsx | 189 ++++++++++++++++-- src/blocks/Table/TableBlockView.jsx | 101 +++++++++- src/blocks/Table/TableBlockView.test.js | 1 + .../__snapshots__/TableBlockEdit.test.js.snap | 7 + .../__snapshots__/TableBlockView.test.js.snap | 14 +- src/blocks/Text/DefaultTextBlockEditor.jsx | 109 ++-------- src/blocks/Text/ShortcutListing.jsx | 3 + src/blocks/Text/SlashMenu.jsx | 123 +++++++++++- src/blocks/Text/index.js | 6 + src/blocks/Text/keyboard/cancelEsc.js | 7 + src/blocks/Text/keyboard/index.js | 2 + src/blocks/Text/keyboard/slashMenu.js | 16 ++ src/editor/SlateEditor.jsx | 42 ++-- src/editor/plugins/Table/less/public.less | 8 + 19 files changed, 494 insertions(+), 157 deletions(-) create mode 100644 src/blocks/Text/keyboard/cancelEsc.js create mode 100644 src/blocks/Text/keyboard/slashMenu.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d49cc0a..dd607663 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,22 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +#### [5.4.1](https://github.com/eea/volto-slate/compare/5.4.0...5.4.1) + +- chore(cypress): Fix paste html [`0bebd22`](https://github.com/eea/volto-slate/commit/0bebd222609ad8186d12be93c230ac1bc1b3c16d) + +#### [5.4.0](https://github.com/eea/volto-slate/compare/5.3.5...5.4.0) + +> 15 March 2022 + +- New features on table block (#235) [`#236`](https://github.com/eea/volto-slate/pull/236) +- New features on table block [`#235`](https://github.com/eea/volto-slate/pull/235) + #### [5.3.5](https://github.com/eea/volto-slate/compare/5.3.4...5.3.5) +> 8 March 2022 + +- Develop [`#232`](https://github.com/eea/volto-slate/pull/232) - Do not override the whole Title block, but only the view/edit components. [`#231`](https://github.com/eea/volto-slate/pull/231) - Do not override the whole Title block, but only the view/edit components. (#231) [`#230`](https://github.com/eea/volto-slate/issues/230) diff --git a/Jenkinsfile b/Jenkinsfile index 3783ec5f..f2bfaedb 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -5,7 +5,7 @@ pipeline { GIT_NAME = "volto-slate" NAMESPACE = "" DEPENDENCIES = "volto-slate:asCypressDefault" - SONARQUBE_TAGS = "volto.eea.europa.eu,climate-energy.eea.europa.eu,forest.eea.europa.eu,biodiversity.europa.eu,www.eea.europa.eu-ims,sustainability.eionet.europa.eu,clms.land.copernicus.eu,industry.eea.europa.eu,water.europa.eu-freshwater,demo-www.eea.europa.eu" + SONARQUBE_TAGS = "volto.eea.europa.eu,climate-energy.eea.europa.eu,forest.eea.europa.eu,biodiversity.europa.eu,www.eea.europa.eu-ims,sustainability.eionet.europa.eu,clms.land.copernicus.eu,industry.eea.europa.eu,water.europa.eu-freshwater,demo-www.eea.europa.eu,clmsdemo.devel6cph.eea.europa.eu" } stages { diff --git a/cypress/integration/27-block-slate-paste-html.js b/cypress/integration/27-block-slate-paste-html.js index 0e84938f..bcfca976 100644 --- a/cypress/integration/27-block-slate-paste-html.js +++ b/cypress/integration/27-block-slate-paste-html.js @@ -31,11 +31,11 @@ describe('Block Tests: external text containing html contents/tags ', () => { // Save cy.toolbarSave(); - cy.get('[id="page-document"] > p:nth-child(3)') + cy.get('[id="page-document"] > p:nth-of-type(2)') .children() .its('length') .should('be.gte', 1); - cy.get('[id="page-document"] > p:nth-child(4)') + cy.get('[id="page-document"] > p:nth-of-type(3)') .children() .should('have.length', 0); }); 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..003b9973 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "volto-slate", - "version": "5.3.5", + "version": "5.4.1", "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/blocks/Text/DefaultTextBlockEditor.jsx b/src/blocks/Text/DefaultTextBlockEditor.jsx index 7374709d..1fa73970 100644 --- a/src/blocks/Text/DefaultTextBlockEditor.jsx +++ b/src/blocks/Text/DefaultTextBlockEditor.jsx @@ -1,6 +1,5 @@ import ReactDOM from 'react-dom'; import React from 'react'; -import { filter, isEmpty, keys, remove } from 'lodash'; import { readAsDataURL } from 'promise-file-reader'; import Dropzone from 'react-dropzone'; import { defineMessages, useIntl } from 'react-intl'; @@ -24,7 +23,7 @@ import { } from 'volto-slate/utils'; import { Transforms } from 'slate'; -import SlashMenu from './SlashMenu'; +import PersistentSlashMenu from './SlashMenu'; import ShortcutListing from './ShortcutListing'; import MarkdownIntroduction from './MarkdownIntroduction'; import { handleKey } from './keyboard'; @@ -77,7 +76,6 @@ export const DefaultTextBlockEditor = (props) => { const [showDropzone, setShowDropzone] = React.useState(false); const [uploading, setUploading] = React.useState(false); const [newImageId, setNewImageId] = React.useState(null); - const [slashMenuSelected, setSlashMenuSelected] = React.useState(0); const prevReq = React.useRef(null); @@ -89,6 +87,17 @@ export const DefaultTextBlockEditor = (props) => { [props], ); + const slateSettings = React.useMemo( + () => ({ + ...config.settings.slate, + persistentHelpers: [ + ...config.settings.slate.persistentHelpers, + PersistentSlashMenu, + ], + }), + [], + ); + const onDrop = React.useCallback( (files) => { // TODO: need to fix setUploading, treat uploading indicator @@ -191,81 +200,6 @@ export const DefaultTextBlockEditor = (props) => { } }, [onSelectBlock, selected, block]); - const slashCommand = data.plaintext?.trim().match(/^\/([a-z]*)$/); - - const useAllowedBlocks = !isEmpty(allowedBlocks); - - const filteredBlocksConfig = filter(blocksConfig, (item) => { - if (useAllowedBlocks) { - return allowedBlocks.includes(item.id); - } else { - return typeof item.restricted === 'function' - ? !item.restricted({ properties, block: item }) - : !item.restricted; - } - }).sort((a, b) => (a.title < b.title ? -1 : 1)); - - // Remove non matching - if (slashCommand) { - remove( - filteredBlocksConfig, - (block) => block.title.toLowerCase().indexOf(slashCommand[1]) === -1, - ); - } - - const slashMenuSize = keys(filteredBlocksConfig).length; - if (slashMenuSelected > slashMenuSize - 1) { - setSlashMenuSelected(slashMenuSize - 1); - } - - const onKeyDown = ({ editor, event }) => { - if (slashCommand) { - switch (event.key) { - case 'ArrowUp': - setSlashMenuSelected( - slashMenuSelected === 0 ? slashMenuSize - 1 : slashMenuSelected - 1, - ); - event.preventDefault(); - break; - case 'ArrowDown': - setSlashMenuSelected( - slashMenuSelected >= slashMenuSize - 1 ? 0 : slashMenuSelected + 1, - ); - event.preventDefault(); - break; - case 'Enter': - if (slashMenuSize > 0) { - onInsertBlock( - block, - { - '@type': filteredBlocksConfig[slashMenuSelected].id, - }, - { - value: [ - { - children: [ - { - text: '', - }, - ], - type: 'p', - }, - ], - plaintext: '', - }, - ); - } - event.preventDefault(); - break; - default: - handleKey({ editor, event }); - break; - } - } else { - handleKey({ editor, event }); - } - }; - return (
<> @@ -306,9 +240,10 @@ export const DefaultTextBlockEditor = (props) => { debug={DEBUG} onFocus={handleFocus} onChange={(value, editor) => onEditorChange(value, editor)} - onKeyDown={onKeyDown} + onKeyDown={handleKey} selected={selected} placeholder={placeholder} + slateSettings={slateSettings} /> {DEBUG ?
{block}
: ''} @@ -332,22 +267,6 @@ export const DefaultTextBlockEditor = (props) => { /> )} - {selected && slashCommand && !disableNewBlocks && ( - { - onSelectBlock(onInsertBlock(id, value)); - }} - onMutateBlock={onMutateBlock} - allowedBlocks={allowedBlocks} - blocksConfig={filteredBlocksConfig} - properties={properties} - search={slashCommand[1]} - selected={slashMenuSelected} - /> - )} -
{instructions ? ( diff --git a/src/blocks/Text/ShortcutListing.jsx b/src/blocks/Text/ShortcutListing.jsx index 665dfdff..41c09dce 100644 --- a/src/blocks/Text/ShortcutListing.jsx +++ b/src/blocks/Text/ShortcutListing.jsx @@ -12,6 +12,9 @@ const ShortcutListing = (props) => { + + Type a slash (/) to change block type + {Object.entries(hotkeys || {}).map(([shortcut, { format, type }]) => ( {`${shortcut}: ${format}`} ))} diff --git a/src/blocks/Text/SlashMenu.jsx b/src/blocks/Text/SlashMenu.jsx index d8a38921..c9659165 100644 --- a/src/blocks/Text/SlashMenu.jsx +++ b/src/blocks/Text/SlashMenu.jsx @@ -1,29 +1,52 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { map } from 'lodash'; +import { filter, isEmpty } from 'lodash'; import { Menu } from 'semantic-ui-react'; import { useIntl, FormattedMessage } from 'react-intl'; import { Icon } from '@plone/volto/components'; -import config from '@plone/volto/registry'; + +const emptySlateBlock = () => ({ + value: [ + { + children: [ + { + text: '', + }, + ], + type: 'p', + }, + ], + plaintext: '', +}); + +const useIsMounted = () => { + const ref = React.useRef(); + React.useEffect(() => { + ref.current = true; + return () => (ref.current = false); + }, []); + return ref.current; +}; const SlashMenu = ({ currentBlock, - onInsertBlock, + onMutateBlock, selected, - blocksConfig = config.blocks.blocksConfig, + availableBlocks, }) => { const intl = useIntl(); return (
- {map(blocksConfig, (block, index) => ( + {availableBlocks.map((block, index) => ( { - onInsertBlock(currentBlock, { '@type': block.id }); + // onInsertBlock(currentBlock, { '@type': block.id }); + onMutateBlock(currentBlock, { '@type': block.id }); e.stopPropagation(); }} > @@ -34,7 +57,7 @@ const SlashMenu = ({ })} ))} - {blocksConfig.length === 0 && ( + {availableBlocks.length === 0 && ( { + const props = editor.getBlockProps(); + const { + block, + blocksConfig, + data, + onMutateBlock, + properties, + selected, + allowedBlocks, + detached, + } = props; + const disableNewBlocks = data?.disableNewBlocks || detached; + + const [slashMenuSelected, setSlashMenuSelected] = React.useState(0); + + const useAllowedBlocks = !isEmpty(allowedBlocks); + const slashCommand = data.plaintext?.trim().match(/^\/([a-z]*)$/); + + const availableBlocks = React.useMemo( + () => + filter(blocksConfig, (item) => + useAllowedBlocks + ? allowedBlocks.includes(item.id) + : typeof item.restricted === 'function' + ? !item.restricted({ properties, block: item }) + : !item.restricted, + ) + .filter( + // TODO: make it work with intl? + (block) => slashCommand && block.id.indexOf(slashCommand[1]) === 0, + ) + .sort((a, b) => (a.title < b.title ? -1 : 1)), + [allowedBlocks, blocksConfig, properties, slashCommand, useAllowedBlocks], + ); + + const slashMenuSize = availableBlocks.length; + const show = selected && slashCommand && !disableNewBlocks; + + const isMounted = useIsMounted(); + + React.useEffect(() => { + if (isMounted && show && slashMenuSelected > slashMenuSize - 1) { + setSlashMenuSelected(slashMenuSize - 1); + } + }, [show, slashMenuSelected, isMounted, slashMenuSize]); + + editor.showSlashMenu = show; + + editor.slashEnter = () => + slashMenuSize > 0 && + onMutateBlock( + block, + { + '@type': availableBlocks[slashMenuSelected].id, + }, + emptySlateBlock(), + ); + + editor.slashArrowUp = () => + setSlashMenuSelected( + slashMenuSelected === 0 ? slashMenuSize - 1 : slashMenuSelected - 1, + ); + + editor.slashArrowDown = () => + setSlashMenuSelected( + slashMenuSelected >= slashMenuSize - 1 ? 0 : slashMenuSelected + 1, + ); + + return show ? ( + + ) : ( + '' + ); +}; + +export default PersistentSlashMenu; diff --git a/src/blocks/Text/index.js b/src/blocks/Text/index.js index 3a5f8248..014adda1 100644 --- a/src/blocks/Text/index.js +++ b/src/blocks/Text/index.js @@ -16,6 +16,8 @@ import { moveListItemUp, traverseBlocks, unwrapEmptyString, + slashMenu, + cancelEsc, } from './keyboard'; import { withDeleteSelectionOnEnter } from 'volto-slate/editor/extensions'; import { @@ -58,14 +60,17 @@ export default (config) => { joinWithNextBlock, // Delete at end of block joins with next block ], Enter: [ + slashMenu, unwrapEmptyString, softBreak, // Handles shift+Enter as a newline (
) ], ArrowUp: [ + slashMenu, moveListItemUp, // Move up a list with with Ctrl+up goUp, // Select previous block ], ArrowDown: [ + slashMenu, moveListItemDown, // Move down a list item with Ctrl+down goDown, // Select next block ], @@ -73,6 +78,7 @@ export default (config) => { indentListItems, // and behaviour for list items traverseBlocks, ], + Escape: [cancelEsc], }, textblockDetachedKeyboardHandlers: { Enter: [ diff --git a/src/blocks/Text/keyboard/cancelEsc.js b/src/blocks/Text/keyboard/cancelEsc.js new file mode 100644 index 00000000..a60ab827 --- /dev/null +++ b/src/blocks/Text/keyboard/cancelEsc.js @@ -0,0 +1,7 @@ +export const cancelEsc = ({ editor, event }) => { + // TODO: this doesn't work, escape canceling doesn't work. + event.stopPropagation(); + event.nativeEvent.stopImmediatePropagation(); + event.preventDefault(); + return true; +}; diff --git a/src/blocks/Text/keyboard/index.js b/src/blocks/Text/keyboard/index.js index 247980b2..78538983 100644 --- a/src/blocks/Text/keyboard/index.js +++ b/src/blocks/Text/keyboard/index.js @@ -8,6 +8,8 @@ export * from './moveListItems'; export * from './softBreak'; export * from './traverseBlocks'; export * from './unwrapEmptyString'; +export * from './slashMenu'; +export * from './cancelEsc'; /** * Takes all the handlers from `slate.textblockKeyboardHandlers` that are diff --git a/src/blocks/Text/keyboard/slashMenu.js b/src/blocks/Text/keyboard/slashMenu.js new file mode 100644 index 00000000..b3074b64 --- /dev/null +++ b/src/blocks/Text/keyboard/slashMenu.js @@ -0,0 +1,16 @@ +export const slashMenu = ({ editor, event }) => { + if (!editor.showSlashMenu) return; + + const { slashArrowUp, slashArrowDown, slashEnter } = editor; + + const handlers = { + ArrowUp: slashArrowUp, + ArrowDown: slashArrowDown, + Enter: slashEnter, + }; + + const handler = handlers[event.key]; + if (handler) handler(); + + return true; +}; diff --git a/src/editor/SlateEditor.jsx b/src/editor/SlateEditor.jsx index 629dd0b3..bd241a48 100644 --- a/src/editor/SlateEditor.jsx +++ b/src/editor/SlateEditor.jsx @@ -64,12 +64,12 @@ class SlateEditor extends Component { const uid = uuid(); // used to namespace the editor's plugins - const { slate } = config.settings; + this.slateSettings = props.slateSettings || config.settings.slate; this.state = { editor: this.createEditor(uid), - showExpandedToolbar: config.settings.slate.showExpandedToolbar, - internalValue: this.props.value || slate.defaultValue(), + showExpandedToolbar: this.slateSettings.showExpandedToolbar, + internalValue: this.props.value || this.slateSettings.defaultValue(), uid, }; @@ -85,6 +85,10 @@ class SlateEditor extends Component { } createEditor(uid) { + // extensions are "editor plugins" or "editor wrappers". It's a similar + // similar to OOP inheritance, where a callable creates a new copy of the + // editor, while replacing or adding new capabilities to that editor. + // Extensions are purely JS, no React components. const editor = makeEditor({ extensions: this.props.extensions }); // When the editor loses focus it no longer has a valid selections. This @@ -110,7 +114,7 @@ class SlateEditor extends Component { multiDecorator([node, path]) { // Decorations (such as higlighting node types, selection, etc). - const { runtimeDecorators = [] } = config.settings.slate; + const { runtimeDecorators = [] } = this.slateSettings; return runtimeDecorators.reduce( (acc, deco) => deco(this.state.editor, [node, path], acc), [], @@ -210,7 +214,7 @@ class SlateEditor extends Component { className, renderExtensions = [], } = this.props; - const { slate } = config.settings; + const slateSettings = this.slateSettings; // renderExtensions is needed because the editor is memoized, so if these // extensions need an updated state (for example to insert updated @@ -252,21 +256,25 @@ class SlateEditor extends Component { {selected ? ( <> - {Object.keys(slate.elementToolbarButtons).map((t, i) => { - return ( - - {slate.elementToolbarButtons[t].map((Btn, b) => { - return ; - })} - - ); - })} + {Object.keys(slateSettings.elementToolbarButtons).map( + (t, i) => { + return ( + + {slateSettings.elementToolbarButtons[t].map( + (Btn, b) => { + return ; + }, + )} + + ); + }, + )} ) : ( '' @@ -312,13 +320,13 @@ class SlateEditor extends Component { }, 200); }} onKeyDown={(event) => { - const handled = handleHotKeys(editor, event, slate); + const handled = handleHotKeys(editor, event, slateSettings); if (handled) return; onKeyDown && onKeyDown({ editor, event }); }} /> {selected && - slate.persistentHelpers.map((Helper, i) => { + slateSettings.persistentHelpers.map((Helper, i) => { return ; })} {this.props.debug ? ( 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; + } +}