@lyra/form-builder
Version:
Lyra form builder
542 lines (441 loc) • 18.7 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _isEqual2 = require('lodash/isEqual');
var _isEqual3 = _interopRequireDefault(_isEqual2);
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
var _reactDom = require('react-dom');
var _reactDom2 = _interopRequireDefault(_reactDom);
var _slateSoftBreak = require('slate-soft-break');
var _slateSoftBreak2 = _interopRequireDefault(_slateSoftBreak);
var _slateReact = require('slate-react');
var _isHotkey = require('is-hotkey');
var _blockTools = require('@lyra/block-tools');
var _slateInsertBlockOnEnter = require('slate-insert-block-on-enter');
var _slateInsertBlockOnEnter2 = _interopRequireDefault(_slateInsertBlockOnEnter);
var _onPaste = require('part:@lyra/form-builder/input/block-editor/on-paste?');
var _onPaste2 = _interopRequireDefault(_onPaste);
var _onCopy = require('part:@lyra/form-builder/input/block-editor/on-copy?');
var _onCopy2 = _interopRequireDefault(_onCopy);
var _changeToPatches = require('./utils/changeToPatches');
var _pathUtils = require('../../utils/pathUtils');
var _createNodeValidator = require('./utils/createNodeValidator');
var _createNodeValidator2 = _interopRequireDefault(_createNodeValidator);
var _findInlineByAnnotationKey = require('./utils/findInlineByAnnotationKey');
var _findInlineByAnnotationKey2 = _interopRequireDefault(_findInlineByAnnotationKey);
var _PatchEvent = require('../../../PatchEvent');
var _PatchEvent2 = _interopRequireDefault(_PatchEvent);
var _ListItemOnEnterKeyPlugin = require('./plugins/ListItemOnEnterKeyPlugin');
var _ListItemOnEnterKeyPlugin2 = _interopRequireDefault(_ListItemOnEnterKeyPlugin);
var _ListItemOnTabKeyPlugin = require('./plugins/ListItemOnTabKeyPlugin');
var _ListItemOnTabKeyPlugin2 = _interopRequireDefault(_ListItemOnTabKeyPlugin);
var _OnDropPlugin = require('./plugins/OnDropPlugin');
var _OnDropPlugin2 = _interopRequireDefault(_OnDropPlugin);
var _PastePlugin = require('./plugins/PastePlugin');
var _PastePlugin2 = _interopRequireDefault(_PastePlugin);
var _SetMarksOnKeyComboPlugin = require('./plugins/SetMarksOnKeyComboPlugin');
var _SetMarksOnKeyComboPlugin2 = _interopRequireDefault(_SetMarksOnKeyComboPlugin);
var _TextBlockOnEnterKeyPlugin = require('./plugins/TextBlockOnEnterKeyPlugin');
var _TextBlockOnEnterKeyPlugin2 = _interopRequireDefault(_TextBlockOnEnterKeyPlugin);
var _UpdateCustomNodesPlugin = require('./plugins/UpdateCustomNodesPlugin');
var _UpdateCustomNodesPlugin2 = _interopRequireDefault(_UpdateCustomNodesPlugin);
var _UndoRedoPlugin = require('./plugins/UndoRedoPlugin');
var _UndoRedoPlugin2 = _interopRequireDefault(_UndoRedoPlugin);
var _BlockObject = require('./nodes/BlockObject');
var _BlockObject2 = _interopRequireDefault(_BlockObject);
var _ContentBlock = require('./nodes/ContentBlock');
var _ContentBlock2 = _interopRequireDefault(_ContentBlock);
var _Decorator = require('./nodes/Decorator');
var _Decorator2 = _interopRequireDefault(_Decorator);
var _InlineObject = require('./nodes/InlineObject');
var _InlineObject2 = _interopRequireDefault(_InlineObject);
var _Span = require('./nodes/Span');
var _Span2 = _interopRequireDefault(_Span);
var _Editor = require('./styles/Editor.css');
var _Editor2 = _interopRequireDefault(_Editor);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/*:: import type {
Block,
BlockContentFeatures,
Marker,
SlateChange,
SlateComponentProps,
SlateMarkProps,
SlateValue,
Type
} from './typeDefs'*/
/*:: import type {Node} from 'react'*/
/*:: type Props = {
blockContentFeatures: BlockContentFeatures,
editorValue: SlateValue,
fullscreen: boolean,
markers: Marker[],
onBlur: (nextPath: []) => void,
onChange: (change: SlateChange) => void,
onEditorBlur: void => void,
onEditorFocus: void => void,
onFocus: (nextPath: []) => void,
onFormBuilderInputBlur: (nextPath: []) => void,
onFormBuilderInputFocus: (nextPath: []) => void,
onPaste?: (
event: SyntheticEvent,
path: [],
type: Type,
value: ?Value
) => {insert?: Value, path?: []},
onPatch: (event: PatchEvent) => void,
onToggleFullScreen: void => void,
readOnly?: boolean,
renderBlockActions?: (block: Block) => React.Node,
renderCustomMarkers?: (Marker[]) => React.Node,
setFocus: void => void,
type: Type,
undoRedoStack: {undo: [], redo: []},
value: Block[]
}*/
class Editor extends _react2.default.Component /*:: <Props>*/ {
constructor(props /*: Props*/) {
super(props);
_initialiseProps.call(this);
this._plugins = [(0, _ListItemOnEnterKeyPlugin2.default)({ defaultBlock: _blockTools.EDITOR_DEFAULT_BLOCK_TYPE }), (0, _ListItemOnTabKeyPlugin2.default)(), (0, _TextBlockOnEnterKeyPlugin2.default)({ defaultBlock: _blockTools.EDITOR_DEFAULT_BLOCK_TYPE }), (0, _SetMarksOnKeyComboPlugin2.default)({
decorators: props.blockContentFeatures.decorators.map(item => item.value)
}), (0, _slateSoftBreak2.default)({
onlyIn: [_blockTools.EDITOR_DEFAULT_BLOCK_TYPE.type],
shift: true
}), (0, _PastePlugin2.default)({ blockContentType: props.type }), (0, _slateInsertBlockOnEnter2.default)(_blockTools.EDITOR_DEFAULT_BLOCK_TYPE), (0, _UpdateCustomNodesPlugin2.default)(), (0, _OnDropPlugin2.default)(), (0, _UndoRedoPlugin2.default)({
stack: props.undoRedoStack,
onChange: props.onChange,
editorValue: props.editorValue,
blockContentType: props.type
})];
this._validateNode = (0, _createNodeValidator2.default)(props.type, this.getValue);
}
componentDidMount() {
var _props = this.props;
const editorValue = _props.editorValue,
focusPath = _props.focusPath;
if ((focusPath || []).length) {
const block = editorValue.document.getDescendant(focusPath[0]._key);
if (!block) {
return;
}
// Wait for things to get finshed rendered before scrolling there
setTimeout(() => window.requestAnimationFrame(() => {
const element = (0, _slateReact.findDOMNode)(block);
element.scrollIntoView({
behavior: 'instant',
block: 'center',
inline: 'nearest'
});
}), 0);
}
}
// When focusPath has changed, but the editorValue has another focusBlock,
// select the block according to the focusPath and scroll there
// eslint-disable-next-line complexity
componentWillReceiveProps(nextProps /*: Props*/) {
const focusPath = nextProps.focusPath,
editorValue = nextProps.editorValue,
onChange = nextProps.onChange,
readOnly = nextProps.readOnly;
if (!focusPath || focusPath.length === 0) {
return;
}
const focusPathIsSingleBlock = editorValue.focusBlock && (0, _isEqual3.default)(focusPath, [{ _key: editorValue.focusBlock.key }]);
const focusPathChanged = !(0, _isEqual3.default)(this.props.focusPath, nextProps.focusPath);
const change = editorValue.change();
const block = editorValue.document.getDescendant(focusPath[0]._key);
let inline;
if (focusPathChanged && !focusPathIsSingleBlock) {
if (focusPath[1] && focusPath[1] === 'children' && focusPath[2]) {
// Inline object
inline = editorValue.document.getDescendant(focusPath[2]._key);
this.scrollIntoView(change, inline);
} else if (
// Annotation
focusPath[1] && focusPath[1] === 'markDefs' && focusPath[2] && (inline = (0, _findInlineByAnnotationKey2.default)(focusPath[2]._key, block))) {
this.scrollIntoView(change, inline);
} else if (block) {
// Regular block
this.scrollIntoView(change, block);
}
} else if (focusPathChanged && !readOnly) {
// Must be here to set focus after editing interfaces are closed
change.focus();
onChange(change);
}
}
scrollIntoView(change, node) {
var _props2 = this.props;
const onChange = _props2.onChange,
readOnly = _props2.readOnly;
const element = (0, _slateReact.findDOMNode)(node);
element.scrollIntoView({
behavior: 'instant',
block: 'center',
inline: 'nearest'
});
if (!readOnly) {
change.collapseToEndOf(node);
onChange(change);
}
}
// When user changes the selection in the editor, update focusPath accordingly.
getEditor() {
return this._editor;
}
render() {
var _props3 = this.props;
const editorValue = _props3.editorValue,
fullscreen = _props3.fullscreen,
readOnly = _props3.readOnly,
markers = _props3.markers,
renderBlockActions = _props3.renderBlockActions,
value = _props3.value;
const hasMarkers = markers.filter(marker => marker.path.length > 0).length > 0;
// Figure out if we have any block actions
let hasBlockActions = false;
if (renderBlockActions && value) {
this._blockActionsMap = {};
value.forEach(block => {
const actions = renderBlockActions(block);
if (actions) {
this._blockActionsMap[block._key] = actions;
}
});
hasBlockActions = Object.keys(this._blockActionsMap).length > 0;
}
const classNames = [_Editor2.default.root, (hasBlockActions || hasMarkers) && _Editor2.default.hasBlockExtras, fullscreen ? _Editor2.default.fullscreen : null].filter(Boolean);
return _react2.default.createElement(
'div',
{ className: classNames.join(' ') },
_react2.default.createElement(_slateReact.Editor, {
spellCheck: false,
className: _Editor2.default.editor,
ref: this.refEditor,
value: editorValue,
onChange: this.handleChange,
onFocus: this.handleEditorFocus,
onCopy: this.handleCopy,
onPaste: this.handlePaste,
onKeyDown: this.handleOnKeyDown,
validateNode: this._validateNode,
plugins: this._plugins,
readOnly: readOnly,
renderNode: this.renderNode,
renderMark: this.renderMark
}),
_react2.default.createElement('div', {
className: _Editor2.default.blockDragMarker,
ref: this.refBlockDragMarker,
style: { display: 'none' }
})
);
}
}
exports.default = Editor;
var _initialiseProps = function _initialiseProps() {
this._blockActionsMap = {};
this._editor = null;
this._plugins = [];
this.handleChange = (change /*: slateChange*/) => {
var _props4 = this.props;
const onChange = _props4.onChange,
onFocus = _props4.onFocus,
focusPath = _props4.focusPath;
const focusBlock = change.value.focusBlock;
const path = [];
if (focusBlock) {
path.push({ _key: focusBlock.key });
}
if (path.length && focusPath && focusPath.length === 1) {
return onChange(change, () => onFocus(path));
}
return onChange(change);
};
this.handleEditorFocus = () => {
const setFocus = this.props.setFocus;
setFocus();
};
this.getValue = () => {
return this.props.value;
};
this.refEditor = (editor /*: ?SlateEditor*/) => {
this._editor = editor;
};
this.handleShowBlockDragMarker = (pos /*: string*/, node /*: HTMLDivElement*/) => {
const editorDOMNode = _reactDom2.default.findDOMNode(this._editor);
if (editorDOMNode instanceof HTMLElement) {
const editorRect = editorDOMNode.getBoundingClientRect();
const elemRect = node.getBoundingClientRect();
const topPos = elemRect.top - editorRect.top;
const bottomPos = topPos + (elemRect.bottom - elemRect.top);
const top = pos === 'after' ? `${parseInt(bottomPos, 10)}px` : `${parseInt(topPos, 10)}px`;
if (this._blockDragMarker) {
this._blockDragMarker.style.display = 'block';
this._blockDragMarker.style.top = top;
}
}
};
this.handleHideBlockDragMarker = () => {
if (this._blockDragMarker) {
this._blockDragMarker.style.display = 'none';
}
};
this.handlePaste = event => {
var _props5 = this.props;
const focusPath = _props5.focusPath,
onPatch = _props5.onPatch,
value = _props5.value,
type = _props5.type;
const onPaste = this.props.onPaste || _onPaste2.default;
if (onPaste) {
const editorValue = this.props.editorValue;
const change = editorValue.change();
var _change$value = change.value;
const focusBlock = _change$value.focusBlock,
focusKey = _change$value.focusKey,
focusText = _change$value.focusText,
focusInline = _change$value.focusInline;
const path = [];
if (focusBlock) {
path.push({ _key: focusBlock.key });
}
if (focusInline || focusText) {
path.push('children');
path.push({ _key: focusKey });
}
const result = onPaste({ event, value, path, type });
if (result && result.insert) {
onPatch(_PatchEvent2.default.from([(0, _PatchEvent.insert)([result.insert], 'after', result.path || focusPath)]));
return result.insert;
}
}
return undefined;
};
this.handleCopy = event => {
if (_onCopy2.default) {
return (0, _onCopy2.default)({ event });
}
return undefined;
};
this.handleOnKeyDown = event => {
const isFullscreenKey = (0, _isHotkey.isKeyHotkey)('mod+enter');
const onToggleFullScreen = this.props.onToggleFullScreen;
if (isFullscreenKey(event)) {
event.preventDefault();
event.stopPropagation();
onToggleFullScreen();
return true;
}
return undefined;
};
this.refBlockDragMarker = (blockDragMarker /*: ?HTMLDivElement*/) => {
this._blockDragMarker = blockDragMarker;
};
this.renderNode = (props /*: SlateComponentProps*/) => {
var _props6 = this.props;
const blockContentFeatures = _props6.blockContentFeatures,
editorValue = _props6.editorValue,
focusPath = _props6.focusPath,
markers = _props6.markers,
onChange = _props6.onChange,
onFocus = _props6.onFocus,
onPatch = _props6.onPatch,
readOnly = _props6.readOnly,
value = _props6.value,
renderCustomMarkers = _props6.renderCustomMarkers,
type = _props6.type;
const node = props.node;
let childMarkers = markers.filter(marker => marker.path.length > 0).filter(marker => marker.path[0]._key === node.data.get('_key'));
let ObjectClass = _BlockObject2.default;
let ObjectType = blockContentFeatures.types.blockObjects.find(memberType => memberType.name === node.type);
if (node.object === 'inline') {
ObjectClass = _InlineObject2.default;
ObjectType = blockContentFeatures.types.inlineObjects.find(memberType => memberType.name === node.type);
childMarkers = markers.filter(marker => marker.path[2] && marker.path[2]._key === node.data.get('_key'));
}
if (node.type === 'span') {
childMarkers = markers.filter(marker => marker.path[2] && marker.path[2]._key === node.data.get('_key'));
// Add any markers for related markDefs here as well
let annotations;
if (annotations = node.data.get('annotations')) {
const block = editorValue.document.getParent(node.key);
Object.keys(annotations).forEach(key => {
childMarkers = childMarkers.concat(markers.filter(marker => marker.path[0]._key === block.key && marker.path[1] === 'markDefs' && marker.path[2]._key === annotations[key]._key));
});
}
}
// Set prop on blocks that are included in focusPath
let hasFormBuilderFocus = false;
if (node.object === 'block') {
hasFormBuilderFocus = focusPath ? (0, _pathUtils.hasItemFocus)(focusPath, { _key: node.key }) : false;
}
switch (node.type) {
case 'contentBlock':
return _react2.default.createElement(_ContentBlock2.default, _extends({}, props, {
editorValue: editorValue,
onChange: onChange,
onFocus: onFocus,
blockContentFeatures: blockContentFeatures,
hasFormBuilderFocus: hasFormBuilderFocus,
markers: childMarkers,
readOnly: readOnly,
blockActions: this._blockActionsMap[node.key],
renderCustomMarkers: renderCustomMarkers,
block: value ? value.find(blk => blk._key === node.key) : (0, _blockTools.editorValueToBlocks)({ document: { nodes: [node.toJSON(_changeToPatches.VALUE_TO_JSON_OPTS)] } }, type)[0]
}));
case 'span':
return _react2.default.createElement(
_Span2.default,
{
attributes: props.attributes,
blockContentFeatures: blockContentFeatures,
editorValue: editorValue,
markers: childMarkers,
node: props.node,
onChange: onChange,
onFocus: onFocus,
onPatch: onPatch,
readOnly: readOnly,
type: blockContentFeatures.types.span
},
props.children
);
default:
return _react2.default.createElement(ObjectClass, {
attributes: props.attributes,
blockContentFeatures: blockContentFeatures,
editor: props.editor,
editorValue: editorValue,
hasFormBuilderFocus: hasFormBuilderFocus,
isSelected: props.isSelected,
markers: childMarkers,
node: props.node,
onChange: onChange,
onDrag: this.handleDrag,
onFocus: onFocus,
onHideBlockDragMarker: this.handleHideBlockDragMarker,
onPatch: onPatch,
onShowBlockDragMarker: this.handleShowBlockDragMarker,
readOnly: readOnly,
blockActions: this._blockActionsMap[node.key],
renderCustomMarkers: renderCustomMarkers,
type: ObjectType
});
}
};
this.renderMark = (props /*: SlateMarkProps*/) => {
const blockContentFeatures = this.props.blockContentFeatures;
const type = props.mark.type;
const decorator = blockContentFeatures.decorators.find(item => item.value === type);
const CustomComponent = decorator && decorator.blockEditor && decorator.blockEditor.render ? decorator.blockEditor.render : null;
if (CustomComponent) {
return _react2.default.createElement(CustomComponent, props);
}
return decorator ? _react2.default.createElement(_Decorator2.default, props) : null;
};
};