diff --git a/CHANGELOG.md b/CHANGELOG.md
index dd607663..40e35884 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,9 +4,17 @@ 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).
+#### [6.0.0](https://github.com/eea/volto-slate/compare/5.4.1...6.0.0)
+
+- Use block data form [`#240`](https://github.com/eea/volto-slate/pull/240)
+- Add poweruser menu [`#234`](https://github.com/eea/volto-slate/pull/234)
+- Release 6.0.0 [`de78cb5`](https://github.com/eea/volto-slate/commit/de78cb5495e0a8ea262ff45b05b87f900dccf5eb)
+
#### [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)
+> 19 March 2022
+
+- chore(cypress): Fix paste html [`#237`](https://github.com/eea/volto-slate/pull/237)
#### [5.4.0](https://github.com/eea/volto-slate/compare/5.3.5...5.4.0)
diff --git a/Jenkinsfile b/Jenkinsfile
index f2bfaedb..f13f1313 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -184,10 +184,10 @@ pipeline {
unstash "xunit-reports"
unstash "cypress-coverage"
def scannerHome = tool 'SonarQubeScanner';
- def nodeJS = tool 'NodeJS11';
+ def nodeJS = tool 'NodeJS';
withSonarQubeEnv('Sonarqube') {
sh '''sed -i "s#/opt/frontend/my-volto-project/src/addons/${GIT_NAME}/##g" xunit-reports/coverage/lcov.info'''
- sh "export PATH=$PATH:${scannerHome}/bin:${nodeJS}/bin; sonar-scanner -Dsonar.javascript.lcov.reportPaths=./xunit-reports/coverage/lcov.info,./cypress-coverage/coverage/lcov.info -Dsonar.sources=./src -Dsonar.projectKey=$GIT_NAME-$BRANCH_NAME -Dsonar.projectVersion=$BRANCH_NAME-$BUILD_NUMBER"
+ sh "export PATH=${scannerHome}/bin:${nodeJS}/bin:$PATH; sonar-scanner -Dsonar.javascript.lcov.reportPaths=./xunit-reports/coverage/lcov.info,./cypress-coverage/coverage/lcov.info -Dsonar.sources=./src -Dsonar.projectKey=$GIT_NAME-$BRANCH_NAME -Dsonar.projectVersion=$BRANCH_NAME-$BUILD_NUMBER"
sh '''try=2; while [ \$try -gt 0 ]; do curl -s -XPOST -u "${SONAR_AUTH_TOKEN}:" "${SONAR_HOST_URL}api/project_tags/set?project=${GIT_NAME}-${BRANCH_NAME}&tags=${SONARQUBE_TAGS},${BRANCH_NAME}" > set_tags_result; if [ \$(grep -ic error set_tags_result ) -eq 0 ]; then try=0; else cat set_tags_result; echo "... Will retry"; sleep 60; try=\$(( \$try - 1 )); fi; done'''
}
}
diff --git a/package.json b/package.json
index 003b9973..d361c684 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "volto-slate",
- "version": "5.4.1",
+ "version": "6.0.0",
"description": "Slate.js integration with Volto",
"main": "src/index.js",
"author": "European Environment Agency: IDM2 A-Team",
diff --git a/src/blocks/Text/DefaultTextBlockEditor.jsx b/src/blocks/Text/DefaultTextBlockEditor.jsx
index 7489eb69..5d5e7330 100644
--- a/src/blocks/Text/DefaultTextBlockEditor.jsx
+++ b/src/blocks/Text/DefaultTextBlockEditor.jsx
@@ -9,7 +9,7 @@ import { Dimmer, Loader, Message, Segment } from 'semantic-ui-react';
import { flattenToAppURL, getBaseUrl } from '@plone/volto/helpers';
import config from '@plone/volto/registry';
import {
- InlineForm,
+ BlockDataForm,
SidebarPortal,
BlockChooserButton,
} from '@plone/volto/components';
@@ -23,6 +23,7 @@ import {
} from 'volto-slate/utils';
import { Transforms } from 'slate';
+import PersistentSlashMenu from './SlashMenu';
import ShortcutListing from './ShortcutListing';
import MarkdownIntroduction from './MarkdownIntroduction';
import { handleKey } from './keyboard';
@@ -86,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
@@ -231,6 +243,7 @@ export const DefaultTextBlockEditor = (props) => {
onKeyDown={handleKey}
selected={selected}
placeholder={placeholder}
+ slateSettings={slateSettings}
/>
{DEBUG ?
{block}
: ''}
>
@@ -264,7 +277,7 @@ export const DefaultTextBlockEditor = (props) => {
<>
- {
@@ -274,6 +287,7 @@ export const DefaultTextBlockEditor = (props) => {
});
}}
formData={data}
+ block={block}
/>
>
)}
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
new file mode 100644
index 00000000..c9659165
--- /dev/null
+++ b/src/blocks/Text/SlashMenu.jsx
@@ -0,0 +1,164 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { filter, isEmpty } from 'lodash';
+import { Menu } from 'semantic-ui-react';
+import { useIntl, FormattedMessage } from 'react-intl';
+import { Icon } from '@plone/volto/components';
+
+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,
+ onMutateBlock,
+ selected,
+ availableBlocks,
+}) => {
+ const intl = useIntl();
+
+ return (
+
+
+
+ );
+};
+
+SlashMenu.propTypes = {
+ currentBlock: PropTypes.string.isRequired,
+ onInsertBlock: PropTypes.func,
+ selected: PropTypes.number,
+ blocksConfig: PropTypes.arrayOf(PropTypes.any),
+};
+
+/**
+ * A SlashMenu wrapper implemented as a volto-slate PersistentHelper.
+ */
+const PersistentSlashMenu = ({ editor }) => {
+ 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 c1fc9257..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
@@ -219,6 +223,19 @@ class SlateEditor extends Component {
(acc, apply) => apply(acc),
this.state.editor,
);
+
+ // Reset selection if field is reset
+ if (
+ editor.selection &&
+ this.props.value.length === 1 &&
+ this.props.value[0].children.length === 1 &&
+ this.props.value[0].children[0].text === ''
+ ) {
+ Transforms.select(editor, {
+ anchor: { path: [0, 0], offset: 0 },
+ focus: { path: [0, 0], offset: 0 },
+ });
+ }
this.editor = editor;
if (testingEditorRef) {
@@ -239,21 +256,25 @@ class SlateEditor extends Component {
{selected ? (
<>
- {Object.keys(slate.elementToolbarButtons).map((t) => {
- return (
-
- {slate.elementToolbarButtons[t].map((Btn) => {
- return ;
- })}
-
- );
- })}
+ {Object.keys(slateSettings.elementToolbarButtons).map(
+ (t, i) => {
+ return (
+
+ {slateSettings.elementToolbarButtons[t].map(
+ (Btn, b) => {
+ return ;
+ },
+ )}
+
+ );
+ },
+ )}
>
) : (
''
@@ -299,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/less/editor.less b/src/editor/less/editor.less
index 4b5ab5e7..0c4362ad 100644
--- a/src/editor/less/editor.less
+++ b/src/editor/less/editor.less
@@ -145,4 +145,29 @@
}
}
+.power-user-menu {
+ position: absolute;
+ z-index: 10;
+ top: 29px;
+ left: -9px;
+ width: 210px;
+ background-color: rgba(255, 255, 255, 0.975);
+ border-radius: 2px;
+ box-shadow: 0 0 8px rgba(0, 0, 0, 0.1), 0 2px 4px rgba(0, 0, 0, 0.05);
+
+ .ui.menu {
+ border: 0;
+ border-radius: 2px;
+
+ .icon {
+ margin-right: 12px;
+ vertical-align: middle;
+ }
+
+ .item.active {
+ background: #efefef !important;
+ }
+ }
+}
+
.loadAddonOverrides();