@plone/volto
Version:
Volto
190 lines (169 loc) • 4.73 kB
JSX
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { Editor, Node, Transforms, Range, createEditor } from 'slate';
import { ReactEditor, Editable, Slate, withReact } from 'slate-react';
import PropTypes from 'prop-types';
import { defineMessages, useIntl } from 'react-intl';
import config from '@plone/volto/registry';
import { P } from '@plone/volto-slate/constants';
const messages = defineMessages({
title: {
id: 'Type the title…',
defaultMessage: 'Type the title…',
},
});
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
export const TitleBlockEdit = (props) => {
const {
block,
blockNode,
data,
detached,
editable,
index,
metadata,
onAddBlock,
onChangeField,
onFocusNextBlock,
onFocusPreviousBlock,
onSelectBlock,
properties,
selected,
} = props;
const [editor] = useState(withReact(createEditor()));
const [initialValue] = useState([
{
type: P,
children: [{ text: metadata?.['title'] || properties?.['title'] || '' }],
},
]);
const intl = useIntl();
const prevSelected = usePrevious(selected);
const text = useMemo(
() => metadata?.['title'] || properties?.['title'] || '',
[metadata, properties],
);
const placeholder = useMemo(
() => data.placeholder || intl.formatMessage(messages['title']),
[data.placeholder, intl],
);
const disableNewBlocks = useMemo(() => detached, [detached]);
useEffect(() => {
if (!prevSelected && selected) {
if (editor.selection && Range.isCollapsed(editor.selection)) {
// keep selection
ReactEditor.focus(editor);
} else {
// nothing is selected, move focus to end
// make sure that the editor is focused
setTimeout(() => {
const focused = ReactEditor.focus(editor);
if (!focused) {
ReactEditor.focus(editor);
Transforms.select(editor, Editor.end(editor, []));
}
}, 0);
}
}
}, [prevSelected, selected, editor]);
useEffect(() => {
// undo/redo handlerr
const oldText = Node.string(editor);
if (oldText !== text) {
Transforms.insertText(editor, text, {
at: [0, 0],
});
}
}, [editor, text]);
const handleChange = useCallback(() => {
const newText = Node.string(editor);
if (newText !== text) {
onChangeField('title', newText);
}
}, [editor, onChangeField, text]);
const handleKeyDown = useCallback(
(ev) => {
if (ev.key === 'Return' || ev.key === 'Enter') {
ev.preventDefault();
if (!disableNewBlocks) {
onSelectBlock(
onAddBlock(config.settings.defaultBlockType, index + 1),
);
}
} else if (ev.key === 'ArrowUp') {
ev.preventDefault();
onFocusPreviousBlock(block, blockNode.current);
} else if (ev.key === 'ArrowDown') {
ev.preventDefault();
onFocusNextBlock(block, blockNode.current);
}
},
[
index,
blockNode,
disableNewBlocks,
onSelectBlock,
onAddBlock,
onFocusPreviousBlock,
onFocusNextBlock,
block,
],
);
const handleFocus = useCallback(() => {
onSelectBlock(block);
}, [block, onSelectBlock]);
const renderElement = useCallback(({ attributes, children }) => {
return (
<h1 {...attributes} className="documentFirstHeading">
{children}
</h1>
);
}, []);
if (typeof window.__SERVER__ !== 'undefined') {
return <div />;
}
return (
<Slate editor={editor} onChange={handleChange} initialValue={initialValue}>
<Editable
readOnly={!editable}
onKeyDown={handleKeyDown}
placeholder={placeholder}
renderElement={renderElement}
onFocus={handleFocus}
aria-multiline="false"
></Editable>
</Slate>
);
};
TitleBlockEdit.propTypes = {
properties: PropTypes.objectOf(PropTypes.any).isRequired,
selected: PropTypes.bool.isRequired,
block: PropTypes.string.isRequired,
index: PropTypes.number.isRequired,
onChangeField: PropTypes.func.isRequired,
onSelectBlock: PropTypes.func.isRequired,
onDeleteBlock: PropTypes.func.isRequired,
onAddBlock: PropTypes.func.isRequired,
onFocusPreviousBlock: PropTypes.func.isRequired,
onFocusNextBlock: PropTypes.func.isRequired,
data: PropTypes.objectOf(PropTypes.any).isRequired,
editable: PropTypes.bool,
detached: PropTypes.bool,
blockNode: PropTypes.any,
};
TitleBlockEdit.defaultProps = {
detached: false,
editable: true,
};
export default TitleBlockEdit;