@lyra/form-builder
Version:
Lyra form builder
370 lines (304 loc) • 12.4 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _uniq2 = require('lodash/uniq');
var _uniq3 = _interopRequireDefault(_uniq2);
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 _class, _temp, _initialiseProps;
var _react = require('react');
var _react2 = _interopRequireDefault(_react);
var _generateHelpUrl = require('@lyra/generate-help-url');
var _generateHelpUrl2 = _interopRequireDefault(_generateHelpUrl);
var _default = require('part:@lyra/components/formfields/default');
var _default2 = _interopRequireDefault(_default);
var _withPatchSubscriber = require('../../utils/withPatchSubscriber');
var _withPatchSubscriber2 = _interopRequireDefault(_withPatchSubscriber);
var _PatchEvent = require('../../PatchEvent');
var _InvalidValueInput = require('../InvalidValueInput');
var _InvalidValueInput2 = _interopRequireDefault(_InvalidValueInput);
var _resolveTypeName = require('../../utils/resolveTypeName');
var _Input = require('./Input');
var _Input2 = _interopRequireDefault(_Input);
var _createSelectionOperation = require('./utils/createSelectionOperation');
var _createSelectionOperation2 = _interopRequireDefault(_createSelectionOperation);
var _changeToPatches = require('./utils/changeToPatches');
var _changeToPatches2 = _interopRequireDefault(_changeToPatches);
var _deserialize = require('./utils/deserialize');
var _deserialize2 = _interopRequireDefault(_deserialize);
var _patchesToChange = require('./utils/patchesToChange');
var _patchesToChange2 = _interopRequireDefault(_patchesToChange);
var _SyncWrapper = require('./styles/SyncWrapper.css');
var _SyncWrapper2 = _interopRequireDefault(_SyncWrapper);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; }
/*:: import type {Block, BlockArrayType, SlateValue, Marker} from './typeDefs'*/
function findBlockType(type) {
return type.of.find(ofType => ofType.name === 'block');
}
function isDeprecatedBlockSchema(type) {
const blockType = findBlockType(type);
if (blockType.span !== undefined) {
return 'deprecatedSpan';
}
if (type.of.find(memberType => memberType.options && memberType.options.inline)) {
return 'deprecatedInline';
}
return false;
}
function isDeprecatedBlockValue(value) {
if (!value || !Array.isArray(value)) {
return false;
}
const block = value.find(item => item._type === 'block');
if (block && Object.keys(block).includes('spans')) {
return true;
}
return false;
}
function isInvalidBlockValue(value) {
if (Array.isArray(value)) {
return false;
}
if (typeof value === 'undefined') {
return false;
}
return true;
}
/*:: type Props = {
focusPath: [],
markers: Marker[],
onBlur: (nextPath: []) => void,
onChange: PatchEvent => void,
onFocus: (nextPath: []) => void,
onPaste?: (
event: SyntheticEvent,
path: [],
type: Type,
value: ?Value
) => {insert?: Value, path?: []},
onPatch: (event: PatchEvent) => void,
readOnly?: boolean,
renderBlockActions?: (block: Block) => React.Node,
renderCustomMarkers?: (Marker[]) => React.Node,
schema: Schema,
subscribe: (() => void) => void,
type: BlockArrayType,
value: Block[]
}*/
/*:: type State = {
deprecatedSchema: boolean,
deprecatedBlockValue: boolean,
invalidBlockValue: boolean,
editorValue: SlateValue,
initialFocusPath?: []
}*/
exports.default = (0, _withPatchSubscriber2.default)((_temp = _class = class SyncWrapper extends _react2.default.PureComponent /*:: <Props, State>*/ {
constructor(props) {
super(props);
_initialiseProps.call(this);
const value = props.value,
type = props.type;
const deprecatedSchema = isDeprecatedBlockSchema(type);
const deprecatedBlockValue = isDeprecatedBlockValue(value);
const invalidBlockValue = isInvalidBlockValue(value);
this.state = {
deprecatedSchema,
deprecatedBlockValue,
invalidBlockValue,
initialFocusPath: props.focusPath,
editorValue: deprecatedSchema || deprecatedBlockValue || invalidBlockValue ? (0, _deserialize2.default)([], type) : (0, _deserialize2.default)(value, type)
};
this.unsubscribe = props.subscribe(this.handleDocumentPatches);
}
// Keep track of what the editor value is (as seen in the editor) before it is changed by something.
componentDidMount() {
if (this.state.initialFocusPath) {
setTimeout(() => {
this.props.onFocus(this.state.initialFocusPath);
}, 0);
}
}
focus() {
if (this._input) {
this._input.focus();
}
}
// eslint-disable-next-line complexity
componentWillUnmount() {
this.unsubscribe();
}
// eslint-disable-next-line complexity
render() {
var _state = this.state;
const editorValue = _state.editorValue,
deprecatedSchema = _state.deprecatedSchema,
deprecatedBlockValue = _state.deprecatedBlockValue,
invalidBlockValue = _state.invalidBlockValue;
var _props = this.props;
const onChange = _props.onChange,
rest = _objectWithoutProperties(_props, ['onChange']);
var _props2 = this.props;
const type = _props2.type,
value = _props2.value;
const isDeprecated = deprecatedSchema || deprecatedBlockValue;
return _react2.default.createElement(
'div',
{ className: _SyncWrapper2.default.root },
!isDeprecated && !invalidBlockValue && _react2.default.createElement(_Input2.default, _extends({
editorValue: editorValue,
onChange: this.handleEditorChange,
onPatch: this.handleFormBuilderPatch,
undoRedoStack: this._undoRedoStack,
ref: this.refInput
}, rest)),
invalidBlockValue && _react2.default.createElement(_InvalidValueInput2.default, {
validTypes: type.of.map(mType => mType.name),
actualType: (0, _resolveTypeName.resolveTypeName)(value),
value: value,
onChange: this.handleInvalidValue
}),
isDeprecated && _react2.default.createElement(
_default2.default,
{ label: type.title },
_react2.default.createElement(
'div',
{ className: _SyncWrapper2.default.disabledEditor },
_react2.default.createElement(
'strong',
null,
'Heads up!'
),
_react2.default.createElement(
'p',
null,
'You\'re using a new version of the Studio with',
deprecatedSchema && " a block schema that hasn't been updated.",
deprecatedSchema && deprecatedBlockValue && ' Also block text needs to be updated.',
deprecatedBlockValue && !deprecatedSchema && " block text that hasn't been updated."
),
deprecatedSchema === 'deprecatedInline' && _react2.default.createElement(
'p',
null,
_react2.default.createElement(
'a',
{
href: (0, _generateHelpUrl2.default)('migrate-to-block-inline-types'),
rel: 'noopener noreferrer',
target: '_blank'
},
'Migrate schema to block.children inline types'
)
),
deprecatedSchema === 'deprecatedSpan' && _react2.default.createElement(
'p',
null,
_react2.default.createElement(
'a',
{
href: (0, _generateHelpUrl2.default)('migrate-to-block-children'),
rel: 'noopener noreferrer',
target: '_blank'
},
'Migrate schema to block.children'
)
)
)
)
);
}
}, _class.defaultProps = {
markers: [] }, _initialiseProps = function _initialiseProps() {
this._input = null;
this._select = null;
this._undoRedoStack = { undo: [], redo: [] };
this._beforeChangeEditorValue = null;
this.handleEditorChange = (change /*: SlateChange*/, callback /*: void => void*/) => {
var _props3 = this.props;
const value = _props3.value,
onChange = _props3.onChange,
type = _props3.type;
this._beforeChangeEditorValue = this.state.editorValue;
this.setState({ editorValue: change.value });
const patches = (0, _changeToPatches2.default)(this._beforeChangeEditorValue, change, value, type);
this._select = (0, _createSelectionOperation2.default)(change);
// Do the change
if (patches.length) {
onChange(_PatchEvent.PatchEvent.from(patches));
}
if (callback) {
callback(change);
return change;
}
return change;
};
this.handleFormBuilderPatch = (event /*: PatchEvent*/) => {
var _props4 = this.props;
const onChange = _props4.onChange,
type = _props4.type;
const editorValue = this.state.editorValue;
const change = (0, _patchesToChange2.default)(event.patches, editorValue, null, type);
this.setState({ editorValue: change.value });
return onChange(event);
};
this.handleDocumentPatches = ({ patches, shouldReset, snapshot }) => {
var _props5 = this.props;
const type = _props5.type,
focusPath = _props5.focusPath;
const hasRemotePatches = patches.some(patch => patch.origin === 'remote');
const hasInsertUnsetPatches = patches.some(patch => ['insert', 'unset'].includes(patch.type));
const hasMultipleDestinations = (0, _uniq3.default)(patches.map(patch => patch.path[0] && patch.path[0]._key).filter(Boolean)).length > 1;
const hasComplexity = patches.length > 3;
// Some heuristics for when we should set a new state or just trust that the editor
// state is in sync with the formbuilder value. As setting a new state may be a performance
// hog, we don't want to do it for the most basic changes (like entering a new character).
// TODO: force sync the state every now and then just to be 100% sure we are in sync.
const shouldSetNewState = hasRemotePatches || hasInsertUnsetPatches || hasMultipleDestinations || hasComplexity || shouldReset;
const localPatches = patches.filter(patch => patch.origin === 'local');
// Handle undo/redo
if (localPatches.length) {
const lastPatch = localPatches.slice(-1)[0];
// Until the FormBuilder can support some kind of patch tagging,
// we create a void patch with key 'undoRedoVoidPatch' in changesToPatches
// to know if this is undo/redo operation or not.
const isUndoRedoPatch = lastPatch && lastPatch.path[0] && lastPatch.path[0]._key === 'undoRedoVoidPatch';
if (!isUndoRedoPatch) {
this._undoRedoStack.undo.push({
patches: localPatches,
// Use the _beforeChangeEditorValue here, because at this point we could be
// in the middle of changes, and the state.editorValue may be in a flux state
editorValue: this._beforeChangeEditorValue,
select: this._select
});
// Redo stack must be reset here
this._undoRedoStack.redo = [];
}
}
// Set a new editorValue from the snapshot,
// and restore the user's selection
if (snapshot && shouldSetNewState) {
const editorValue = (0, _deserialize2.default)(snapshot, type);
const change = editorValue.change();
if (this._select) {
// eslint-disable-next-line max-depth
try {
change.applyOperations([this._select]);
} catch (err) {
// eslint-disable-next-line max-depth
if (!err.message.match('Could not find a descendant')) {
console.error(err); // eslint-disable-line no-console
}
}
}
// Keep the editor focused as we insert the new value
if ((focusPath || []).length === 1) {
change.focus();
}
this.setState({ editorValue: change.value });
}
};
this.refInput = (input /*: Input*/) => {
this._input = input;
};
this.handleInvalidValue = () => {};
}, _temp));