UNPKG

@plone/volto

Version:
216 lines (197 loc) 5.34 kB
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;