Skip to content
This repository has been archived by the owner on Oct 25, 2022. It is now read-only.

Commit

Permalink
Add poweruser menu (#234)
Browse files Browse the repository at this point in the history
* Added poweruser menu.

* Fix poweruser menu.

* [JENKINS] - Fix stylelint

* 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 <contact@avoinea.com>

* 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 <contact@avoinea.com>
Co-authored-by: Miu Razvan <miu.razvan28@gmail.com>
Co-authored-by: EEA Jenkins <@users.noreply.github.com>
Co-authored-by: EEA Jenkins <eea-jenkins@users.noreply.github.com>

Co-authored-by: Alin Voinea <contact@avoinea.com>
Co-authored-by: Tiberiu Ichim <tiberiuichim@users.noreply.github.com>
Co-authored-by: Miu Razvan <miu.razvan28@gmail.com>
Co-authored-by: EEA Jenkins <eea-jenkins@users.noreply.github.com>

Author: Rob Gietema
  • Loading branch information
robgietema committed Apr 4, 2022
1 parent cce494f commit b93deab
Show file tree
Hide file tree
Showing 9 changed files with 274 additions and 17 deletions.
13 changes: 13 additions & 0 deletions src/blocks/Text/DefaultTextBlockEditor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -231,6 +243,7 @@ export const DefaultTextBlockEditor = (props) => {
onKeyDown={handleKey}
selected={selected}
placeholder={placeholder}
slateSettings={slateSettings}
/>
{DEBUG ? <div>{block}</div> : ''}
</>
Expand Down
3 changes: 3 additions & 0 deletions src/blocks/Text/ShortcutListing.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ const ShortcutListing = (props) => {

<Segment secondary attached>
<List>
<List.Item>
Type a slash (<em>/</em>) to change block type
</List.Item>
{Object.entries(hotkeys || {}).map(([shortcut, { format, type }]) => (
<List.Item key={shortcut}>{`${shortcut}: ${format}`}</List.Item>
))}
Expand Down
164 changes: 164 additions & 0 deletions src/blocks/Text/SlashMenu.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="power-user-menu">
<Menu vertical fluid borderless>
{availableBlocks.map((block, index) => (
<Menu.Item
key={block.id}
className={block.id}
active={index === selected}
onClick={(e) => {
// onInsertBlock(currentBlock, { '@type': block.id });
onMutateBlock(currentBlock, { '@type': block.id });
e.stopPropagation();
}}
>
<Icon name={block.icon} size="24px" />
{intl.formatMessage({
id: block.title,
defaultMessage: block.title,
})}
</Menu.Item>
))}
{availableBlocks.length === 0 && (
<Menu.Item>
<FormattedMessage
id="No matching blocks"
defaultMessage="No matching blocks"
/>
</Menu.Item>
)}
</Menu>
</div>
);
};

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 ? (
<SlashMenu
currentBlock={block}
onMutateBlock={onMutateBlock}
availableBlocks={availableBlocks}
selected={slashMenuSelected}
/>
) : (
''
);
};

export default PersistentSlashMenu;
6 changes: 6 additions & 0 deletions src/blocks/Text/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import {
moveListItemUp,
traverseBlocks,
unwrapEmptyString,
slashMenu,
cancelEsc,
} from './keyboard';
import { withDeleteSelectionOnEnter } from 'volto-slate/editor/extensions';
import {
Expand Down Expand Up @@ -58,21 +60,25 @@ export default (config) => {
joinWithNextBlock, // Delete at end of block joins with next block
],
Enter: [
slashMenu,
unwrapEmptyString,
softBreak, // Handles shift+Enter as a newline (<br/>)
],
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
],
Tab: [
indentListItems, // <tab> and <c-tab> behaviour for list items
traverseBlocks,
],
Escape: [cancelEsc],
},
textblockDetachedKeyboardHandlers: {
Enter: [
Expand Down
7 changes: 7 additions & 0 deletions src/blocks/Text/keyboard/cancelEsc.js
Original file line number Diff line number Diff line change
@@ -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;
};
2 changes: 2 additions & 0 deletions src/blocks/Text/keyboard/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions src/blocks/Text/keyboard/slashMenu.js
Original file line number Diff line number Diff line change
@@ -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;
};
55 changes: 38 additions & 17 deletions src/editor/SlateEditor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};

Expand All @@ -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
Expand All @@ -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),
[],
Expand Down Expand Up @@ -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
Expand All @@ -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) {
Expand All @@ -239,21 +256,25 @@ class SlateEditor extends Component {
<EditorContext.Provider value={editor}>
<Slate
editor={editor}
value={this.props.value || slate.defaultValue()}
value={this.props.value || slateSettings.defaultValue()}
onChange={this.handleChange}
>
{selected ? (
<>
<InlineToolbar editor={editor} className={className} />
{Object.keys(slate.elementToolbarButtons).map((t) => {
return (
<Toolbar elementType={t}>
{slate.elementToolbarButtons[t].map((Btn) => {
return <Btn editor={editor} />;
})}
</Toolbar>
);
})}
{Object.keys(slateSettings.elementToolbarButtons).map(
(t, i) => {
return (
<Toolbar elementType={t} key={i}>
{slateSettings.elementToolbarButtons[t].map(
(Btn, b) => {
return <Btn editor={editor} key={b} />;
},
)}
</Toolbar>
);
},
)}
</>
) : (
''
Expand Down Expand Up @@ -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 <Helper key={i} editor={editor} />;
})}
{this.props.debug ? (
Expand Down
Loading

0 comments on commit b93deab

Please sign in to comment.