@lyra/form-builder
Version:
Lyra form builder
449 lines (364 loc) • 14.2 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 _reactDom = require('react-dom');
var _reactDom2 = _interopRequireDefault(_reactDom);
var _slateBase64Serializer = require('slate-base64-serializer');
var _slateBase64Serializer2 = _interopRequireDefault(_slateBase64Serializer);
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
var _slate = require('slate');
var _slateReact = require('slate-react');
var _router = require('part:@lyra/base/router');
var _linkIcon = require('part:@lyra/base/link-icon');
var _linkIcon2 = _interopRequireDefault(_linkIcon);
var _stacked = require('part:@lyra/components/utilities/stacked');
var _stacked2 = _interopRequireDefault(_stacked);
var _escapable = require('part:@lyra/components/utilities/escapable');
var _escapable2 = _interopRequireDefault(_escapable);
var _reactTippy = require('react-tippy');
var _classnames = require('classnames');
var _classnames2 = _interopRequireDefault(_classnames);
var _resolveTypeName = require('../../../utils/resolveTypeName');
var _PatchEvent = require('../../../PatchEvent');
var _pathUtils = require('../../../utils/pathUtils');
var _DeleteButton = require('../DeleteButton');
var _DeleteButton2 = _interopRequireDefault(_DeleteButton);
var _EditButton = require('../EditButton');
var _EditButton2 = _interopRequireDefault(_EditButton);
var _InvalidValueInput = require('../../InvalidValueInput');
var _InvalidValueInput2 = _interopRequireDefault(_InvalidValueInput);
var _Preview = require('../../../Preview');
var _Preview2 = _interopRequireDefault(_Preview);
var _InlineObject = require('./styles/InlineObject.css');
var _InlineObject2 = _interopRequireDefault(_InlineObject);
var _ViewButton = require('../ViewButton');
var _ViewButton2 = _interopRequireDefault(_ViewButton);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/*:: import type {
BlockContentFeatures,
SlateValue,
Type,
SlateChange,
Marker
} from '../typeDefs'*/
/*:: import type {Node} from 'react'*/
/*:: type Props = {
attributes: {},
blockContentFeatures: BlockContentFeatures,
children: Node,
editor: Editor,
editorIsFocused: boolean,
editorValue: SlateValue,
hasFormBuilderFocus: boolean,
isSelected: boolean,
markers: Marker[],
node: Block,
onChange: (change: SlateChange) => void,
onFocus: (nextPath: []) => void,
onPatch: (event: PatchEvent) => void,
readOnly?: boolean,
type: ?Type
}*/
/*:: type State = {
isDragging: boolean,
menuOpen: boolean
}*/
const NOOP = () => {};
function shouldUpdateDropTarget(range, dropTarget) {
if (!dropTarget) {
return true;
}
return range.focusOffset !== dropTarget.range.focusOffset;
}
class InlineObject extends _react2.default.Component /*:: <Props, State>*/ {
constructor(...args) {
var _temp;
return _temp = super(...args), this.rootElement = null, this.state = {
isDragging: false,
menuOpen: false
}, this.handleDragStart = event => {
const node = this.props.node;
this.setState({ isDragging: true });
this.addDragHandlers();
const element = _reactDom2.default.findDOMNode(this.previewContainer);
const encoded = _slateBase64Serializer2.default.serializeNode(node, { preserveKeys: true });
(0, _slateReact.setEventTransfer)(event, 'node', encoded);
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setDragImage(element, element.clientWidth / 2, -10);
}, this.handleDragLeave = event => {
if (event.target === this._editorNode) {
this.resetDropTarget();
}
}, this.handleDragOverOtherNode = event => {
if (!this.state.isDragging) {
return;
}
const targetDOMNode = event.target;
// As the event is registered on the editor parent node
// ignore the event if it is coming from from the editor node itself
if (targetDOMNode === this._editorNode) {
return;
}
var _props = this.props;
const editorValue = _props.editorValue,
onChange = _props.onChange;
const range = (0, _slateReact.getEventRange)(event, editorValue);
if (range === null || typeof range.focusOffset === undefined) {
return;
}
const targetNode = editorValue.document.getDescendant(range.focusKey);
// If we are dragging over another inline return
if (editorValue.document.getClosestInline(targetNode.key)) {
return;
}
// If we are dragging over a custom type block return
const block = editorValue.document.getClosestBlock(range.focusKey);
if (block && block.type !== 'contentBlock') {
return;
}
const moveCursorChange = this.moveCursor(range, targetNode);
const finalRange = moveCursorChange.value.selection;
if (shouldUpdateDropTarget(finalRange, this._dropTarget)) {
this._dropTarget = { node: targetNode, range: finalRange };
onChange(moveCursorChange);
}
}, this.handleDragEnd = event => {
this.setState({ isDragging: false });
var _props2 = this.props;
const onChange = _props2.onChange,
node = _props2.node,
editorValue = _props2.editorValue;
const target = this._dropTarget;
// Return if this is our node
if (!target || target.node === node) {
this.resetDropTarget();
return;
}
const change = editorValue.change().select(target.range).removeNodeByKey(node.key).insertInline(node).collapseToEndOf(node).focus();
onChange(change);
this.resetDropTarget();
}, this.handleInvalidValue = (event /*: PatchEvent*/) => {
let _event = event;
var _props3 = this.props;
const editorValue = _props3.editorValue,
onPatch = _props3.onPatch;
const focusBlock = editorValue.focusBlock;
const value = this.getValue();
const path = [{ _key: focusBlock.key }, 'children', { _key: value._key }];
path.reverse().forEach(part => {
_event = _event.prefixAll(part);
});
onPatch(_event, value);
}, this.handleRemoveValue = event => {
event.preventDefault();
event.stopPropagation();
var _props4 = this.props;
const editorValue = _props4.editorValue,
node = _props4.node,
onChange = _props4.onChange;
const change = editorValue.change();
onChange(change.removeNodeByKey(node.key).focus());
}, this.handleCancelEvent = event => {
event.stopPropagation();
event.preventDefault();
}, this.handleEditStart = event => {
event.stopPropagation();
var _props5 = this.props;
const node = _props5.node,
onFocus = _props5.onFocus,
onChange = _props5.onChange,
editorValue = _props5.editorValue;
const focusBlock = editorValue.focusBlock;
const change = editorValue.change().collapseToEndOf(node).focus().blur();
onChange(change, () => onFocus([{ _key: focusBlock.key }, 'children', { _key: node.key }, _pathUtils.FOCUS_TERMINATOR]));
}, this.handleView = event => {
event.stopPropagation();
var _props6 = this.props;
const node = _props6.node,
onFocus = _props6.onFocus,
editorValue = _props6.editorValue;
const focusBlock = editorValue.focusBlock;
onFocus([{ _key: focusBlock.key }, 'children', { _key: node.key }, _pathUtils.FOCUS_TERMINATOR]);
}, this.handleCloseMenu = () => {
this.setState({ menuOpen: false });
}, this.refFormBuilderBlock = formBuilderBlock => {
this.formBuilderBlock = formBuilderBlock;
}, this.refPreview = previewContainer => {
this.previewContainer = previewContainer;
}, this.handleShowMenu = () => {
this.setState({ menuOpen: true });
}, _temp;
}
componentDidMount() {
const editor = this.props.editor;
this._editorNode = _reactDom2.default.findDOMNode(editor);
}
componentWillUnmount() {
this.removeDragHandlers();
}
addDragHandlers() {
this._editorNode.addEventListener('dragover', this.handleDragOverOtherNode);
this._editorNode.addEventListener('dragleave', this.handleDragLeave);
}
removeDragHandlers() {
this._editorNode.removeEventListener('dragover', this.handleDragOverOtherNode);
this._editorNode.removeEventListener('dragleave', this.handleDragLeave);
}
// Remove the drop target if we leave the editors nodes
resetDropTarget() {
this._dropTarget = null;
}
moveCursor(range, node) {
const editorValue = this.props.editorValue;
let theOffset = range.focusOffset;
// Check if it is acceptable to move the cursor here
const nextChars = editorValue.document.getCharactersAtRange(_slate.Range.create({
anchorKey: node.key,
focusKey: node.key,
anchorOffset: theOffset - 1,
focusOffset: theOffset,
isFocused: true,
isBackward: false
}));
if (!nextChars.size) {
theOffset = 0;
}
const change = editorValue.change().collapseToStartOf(node).move(theOffset).focus();
return change;
}
getValue() {
return this.props.node.data.get('value');
}
renderMenu(value) {
const readOnly = this.props.readOnly;
return _react2.default.createElement(
_stacked2.default,
null,
isActive => {
return _react2.default.createElement(
_escapable2.default,
{ onEscape: isActive ? this.handleCloseMenu : NOOP },
_react2.default.createElement(
'div',
{ className: _InlineObject2.default.functions },
value._ref && _react2.default.createElement(
_router.IntentLink,
{
className: _InlineObject2.default.linkToReference,
intent: 'edit',
params: { id: value._ref }
},
_react2.default.createElement(_linkIcon2.default, null)
),
readOnly && _react2.default.createElement(
_ViewButton2.default,
{
title: 'View this object',
onClick: this.handleView
},
'View'
),
!readOnly && _react2.default.createElement(
_EditButton2.default,
{
title: 'Edit this object',
onClick: this.handleEditStart
},
'Edit'
),
!readOnly && _react2.default.createElement(
_DeleteButton2.default,
{
title: 'Remove this object',
onClick: this.handleRemoveValue
},
'Delete'
)
)
);
}
);
}
renderPreview(value) {
const type = this.props.type;
const menuOpen = this.state.menuOpen;
const valueKeys = value ? Object.keys(value) : [];
const isEmpty = !value || (0, _isEqual3.default)(valueKeys.sort(), ['_key', '_type'].sort());
return _react2.default.createElement(
_reactTippy.Tooltip,
{
arrow: true,
theme: 'light',
trigger: 'manual',
open: menuOpen,
position: 'bottom',
interactive: true,
useContext: true,
duration: 100,
style: { padding: 0, display: 'inline-block', minWidth: '1em' },
unmountHTMLWhenHide: true,
onRequestClose: menuOpen && this.handleCloseMenu,
html: this.renderMenu(value)
},
!isEmpty && _react2.default.createElement(_Preview2.default, { type: type, value: value, layout: 'inline' }),
isEmpty && _react2.default.createElement(
'span',
null,
'Click to edit'
)
);
}
render() {
var _props7 = this.props;
const attributes = _props7.attributes,
node = _props7.node,
editorValue = _props7.editorValue,
isSelected = _props7.isSelected,
readOnly = _props7.readOnly,
markers = _props7.markers,
blockContentFeatures = _props7.blockContentFeatures;
const value = this.getValue();
const valueType = (0, _resolveTypeName.resolveTypeName)(value);
const validTypes = blockContentFeatures.types.inlineObjects.map(objType => objType.name);
if (!validTypes.includes(valueType)) {
return _react2.default.createElement(
'div',
{ onClick: this.handleCancelEvent },
_react2.default.createElement(_InvalidValueInput2.default, {
validTypes: validTypes,
actualType: valueType,
value: value,
onChange: this.handleInvalidValue
})
);
}
const validation = markers.filter(marker => marker.type === 'validation');
const errors = validation.filter(marker => marker.level === 'error');
const classname = (0, _classnames2.default)([_InlineObject2.default.root, editorValue.selection.hasFocusIn(node) && _InlineObject2.default.focused, isSelected && _InlineObject2.default.selected, errors.length > 0 && _InlineObject2.default.hasErrors]);
return _react2.default.createElement(
'span',
_extends({}, attributes, {
onDragStart: this.handleDragStart,
onDragEnd: this.handleDragEnd,
onDragEnter: this.handleCancelEvent,
onDragLeave: this.handleCancelEvent,
onDrop: this.handleCancelEvent,
draggable: !readOnly,
ref: this.refFormBuilderBlock,
className: classname,
onClick: this.handleShowMenu
}),
_react2.default.createElement(
'span',
{ ref: this.refPreview, className: _InlineObject2.default.previewContainer },
this.renderPreview(value)
)
);
}
}
exports.default = InlineObject;