@plone/volto
Version:
Volto
216 lines (197 loc) • 5.34 kB
JSX
import React, { useCallback, useEffect, useMemo, 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 { usePrevious } from '@plone/volto/helpers/Utils/usePrevious';
import config from '@plone/volto/registry';
import { P } from '@plone/volto-slate/constants';
import cx from 'classnames';
const messages = defineMessages({
title: {
id: 'Type the heading…',
defaultMessage: 'Type the heading…',
},
});
/**
* A component for inserting an inline textline field for blocks
*/
export const TextLineEdit = (props) => {
const {
block,
blockNode,
data,
detached,
editable,
index,
metadata,
onAddBlock,
onChangeBlock,
onChangeField,
onFocusNextBlock,
onFocusPreviousBlock,
onSelectBlock,
properties,
selected,
renderTag,
renderClassName,
fieldDataName,
fieldName = 'title',
placeholder,
} = props;
const [editor] = useState(withReact(createEditor()));
const [initialValue] = useState([
{
type: P,
children: [
{
text:
data?.[fieldDataName] ||
metadata?.[fieldName] ||
properties?.[fieldName] ||
'',
},
],
},
]);
const intl = useIntl();
const prevSelected = usePrevious(selected);
const text = useMemo(
() =>
data?.[fieldDataName] ||
metadata?.['title'] ||
properties?.['title'] ||
'',
[data, fieldDataName, metadata, properties],
);
const resultantPlaceholder = useMemo(
() =>
placeholder || data.placeholder || intl.formatMessage(messages['title']),
[placeholder, 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
ReactEditor.focus(editor);
Transforms.select(editor, Editor.end(editor, []));
}
}
}, [prevSelected, selected, editor]);
useEffect(() => {
// undo/redo handler
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) {
if (fieldDataName) {
onChangeBlock(block, {
...data,
[fieldDataName]: newText,
});
} else {
onChangeField(fieldName, newText);
}
}
}, [
data,
block,
editor,
fieldDataName,
fieldName,
onChangeField,
onChangeBlock,
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 RenderTag = renderTag || 'h1';
const renderElement = useCallback(
({ attributes, children }) => {
return (
<RenderTag {...attributes} className={cx(renderClassName)}>
{children}
</RenderTag>
);
},
[renderClassName],
);
if (typeof window.__SERVER__ !== 'undefined') {
return <div />;
}
return (
<Slate editor={editor} onChange={handleChange} initialValue={initialValue}>
<Editable
readOnly={!editable}
onKeyDown={handleKeyDown}
placeholder={resultantPlaceholder}
renderElement={renderElement}
onFocus={handleFocus}
aria-multiline="false"
></Editable>
</Slate>
);
};
TextLineEdit.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,
renderTag: PropTypes.string,
renderClassName: PropTypes.string,
fieldDataName: PropTypes.string,
};
TextLineEdit.defaultProps = {
detached: false,
editable: true,
};
export default TextLineEdit;