decap-cms-core
Version:
Decap CMS core application, see decap-cms package for the main distribution.
359 lines (329 loc) • 10.6 kB
JavaScript
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { Map, List } from 'immutable';
import { oneLine } from 'common-tags';
import { getRemarkPlugins } from '../../../lib/registry';
import ValidationErrorTypes from '../../../constants/validationErrorTypes';
function truthy() {
return { error: false };
}
function isEmpty(value) {
return (
value === null ||
value === undefined ||
(Object.prototype.hasOwnProperty.call(value, 'length') && value.length === 0) ||
(value.constructor === Object && Object.keys(value).length === 0) ||
(List.isList(value) && value.size === 0)
);
}
export default class Widget extends Component {
static propTypes = {
controlComponent: PropTypes.func.isRequired,
field: ImmutablePropTypes.map.isRequired,
hasActiveStyle: PropTypes.bool,
setActiveStyle: PropTypes.func.isRequired,
setInactiveStyle: PropTypes.func.isRequired,
classNameWrapper: PropTypes.string.isRequired,
classNameWidget: PropTypes.string.isRequired,
classNameWidgetActive: PropTypes.string.isRequired,
classNameLabel: PropTypes.string.isRequired,
classNameLabelActive: PropTypes.string.isRequired,
value: PropTypes.oneOfType([
PropTypes.node,
PropTypes.object,
PropTypes.string,
PropTypes.bool,
]),
mediaPaths: ImmutablePropTypes.map.isRequired,
metadata: ImmutablePropTypes.map,
fieldsErrors: ImmutablePropTypes.map,
onChange: PropTypes.func.isRequired,
onValidate: PropTypes.func,
onOpenMediaLibrary: PropTypes.func.isRequired,
onClearMediaControl: PropTypes.func.isRequired,
onRemoveMediaControl: PropTypes.func.isRequired,
onPersistMedia: PropTypes.func.isRequired,
onAddAsset: PropTypes.func.isRequired,
onRemoveInsertedMedia: PropTypes.func.isRequired,
getAsset: PropTypes.func.isRequired,
resolveWidget: PropTypes.func.isRequired,
widget: PropTypes.object.isRequired,
getEditorComponents: PropTypes.func.isRequired,
isFetching: PropTypes.bool,
controlRef: PropTypes.func,
query: PropTypes.func.isRequired,
clearSearch: PropTypes.func.isRequired,
clearFieldErrors: PropTypes.func.isRequired,
queryHits: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
editorControl: PropTypes.elementType.isRequired,
uniqueFieldId: PropTypes.string.isRequired,
loadEntry: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
onValidateObject: PropTypes.func,
isEditorComponent: PropTypes.bool,
isNewEditorComponent: PropTypes.bool,
entry: ImmutablePropTypes.map.isRequired,
isDisabled: PropTypes.bool,
isFieldDuplicate: PropTypes.func,
isFieldHidden: PropTypes.func,
locale: PropTypes.string,
isParentListCollapsed: PropTypes.bool,
};
shouldComponentUpdate(nextProps) {
/**
* Avoid unnecessary rerenders while loading assets.
*/
if (this.props.isLoadingAsset && nextProps.isLoadingAsset) return false;
/**
* Allow widgets to provide their own `shouldComponentUpdate` method.
*/
if (this.wrappedControlShouldComponentUpdate) {
return this.wrappedControlShouldComponentUpdate(nextProps);
}
return (
this.props.value !== nextProps.value ||
this.props.classNameWrapper !== nextProps.classNameWrapper ||
this.props.hasActiveStyle !== nextProps.hasActiveStyle
);
}
processInnerControlRef = ref => {
if (!ref) return;
/**
* If the widget is a container that receives state updates from the store,
* we'll need to get the ref of the actual control via the `react-redux`
* `getWrappedInstance` method. Note that connected widgets must pass
* `withRef: true` to `connect` in the options object.
*/
this.innerWrappedControl = ref.getWrappedInstance ? ref.getWrappedInstance() : ref;
this.wrappedControlValid = this.innerWrappedControl.isValid || truthy;
/**
* Get the `shouldComponentUpdate` method from the wrapped control, and
* provide the control instance is the `this` binding.
*/
const { shouldComponentUpdate: scu } = this.innerWrappedControl;
this.wrappedControlShouldComponentUpdate = scu && scu.bind(this.innerWrappedControl);
};
getValidateValue = () => {
let value = this.innerWrappedControl?.getValidateValue?.() || this.props.value;
// Convert list input widget value to string for validation test
List.isList(value) && (value = value.join(','));
return value;
};
validate = (skipWrapped = false) => {
const value = this.getValidateValue();
const field = this.props.field;
const errors = [];
const validations = [this.validatePresence, this.validatePattern];
if (field.get('meta')) {
validations.push(this.props.validateMetaField);
}
validations.forEach(func => {
const response = func(field, value, this.props.t);
if (response.error) errors.push(response.error);
});
if (skipWrapped) {
if (skipWrapped.error) errors.push(skipWrapped.error);
} else {
const wrappedError = this.validateWrappedControl(field);
if (wrappedError.error) errors.push(wrappedError.error);
}
this.props.onValidate(errors);
};
validatePresence = (field, value) => {
const { t, parentIds } = this.props;
const isRequired = field.get('required', true);
if (isRequired && isEmpty(value)) {
const error = {
type: ValidationErrorTypes.PRESENCE,
parentIds,
message: t('editor.editorControlPane.widget.required', {
fieldLabel: field.get('label', field.get('name')),
}),
};
return { error };
}
return { error: false };
};
validatePattern = (field, value) => {
const { t, parentIds } = this.props;
const pattern = field.get('pattern', false);
if (isEmpty(value)) {
return { error: false };
}
if (pattern && !RegExp(pattern.first()).test(value)) {
const error = {
type: ValidationErrorTypes.PATTERN,
parentIds,
message: t('editor.editorControlPane.widget.regexPattern', {
fieldLabel: field.get('label', field.get('name')),
pattern: pattern.last(),
}),
};
return { error };
}
return { error: false };
};
validateWrappedControl = field => {
const { t, parentIds } = this.props;
if (typeof this.wrappedControlValid !== 'function') {
throw new Error(oneLine`
this.wrappedControlValid is not a function. Are you sure widget
"${field.get('widget')}" is registered?
`);
}
const response = this.wrappedControlValid();
if (typeof response === 'boolean') {
const isValid = response;
return { error: !isValid };
} else if (Object.prototype.hasOwnProperty.call(response, 'error')) {
return response;
} else if (response instanceof Promise) {
response.then(
() => {
this.validate({ error: false });
},
err => {
const error = {
type: ValidationErrorTypes.CUSTOM,
message: `${field.get('label', field.get('name'))} - ${err}.`,
};
this.validate({ error });
},
);
const error = {
type: ValidationErrorTypes.CUSTOM,
parentIds,
message: t('editor.editorControlPane.widget.processing', {
fieldLabel: field.get('label', field.get('name')),
}),
};
return { error };
}
return { error: false };
};
/**
* In case the `onChangeObject` function is frozen by a child widget implementation,
* e.g. when debounced, always get the latest object value instead of using
* `this.props.value` directly.
*/
getObjectValue = () => this.props.value || Map();
/**
* Change handler for fields that are nested within another field.
*/
onChangeObject = (field, newValue, newMetadata) => {
const newObjectValue = this.getObjectValue().set(field.get('name'), newValue);
return this.props.onChange(
newObjectValue,
newMetadata && { [this.props.field.get('name')]: newMetadata },
);
};
setInactiveStyle = () => {
this.props.setInactiveStyle();
if (this.props.field.has('pattern') && !isEmpty(this.getValidateValue())) {
this.validate();
}
};
render() {
const {
controlComponent,
entry,
collection,
config,
field,
value,
mediaPaths,
metadata,
onChange,
onValidateObject,
onOpenMediaLibrary,
onRemoveMediaControl,
onPersistMedia,
onClearMediaControl,
onAddAsset,
onRemoveInsertedMedia,
getAsset,
classNameWrapper,
classNameWidget,
classNameWidgetActive,
classNameLabel,
classNameLabelActive,
setActiveStyle,
hasActiveStyle,
editorControl,
uniqueFieldId,
resolveWidget,
widget,
getEditorComponents,
query,
queryHits,
clearSearch,
clearFieldErrors,
isFetching,
loadEntry,
fieldsErrors,
controlRef,
isEditorComponent,
isNewEditorComponent,
parentIds,
t,
isDisabled,
isFieldDuplicate,
isFieldHidden,
locale,
isParentListCollapsed,
} = this.props;
return React.createElement(controlComponent, {
entry,
collection,
config,
field,
value,
mediaPaths,
metadata,
onChange,
onChangeObject: this.onChangeObject,
onValidateObject,
onOpenMediaLibrary,
onClearMediaControl,
onRemoveMediaControl,
onPersistMedia,
onAddAsset,
onRemoveInsertedMedia,
getAsset,
forID: uniqueFieldId,
ref: this.processInnerControlRef,
validate: this.validate,
classNameWrapper,
classNameWidget,
classNameWidgetActive,
classNameLabel,
classNameLabelActive,
setActiveStyle,
setInactiveStyle: () => this.setInactiveStyle(),
hasActiveStyle,
editorControl,
resolveWidget,
widget,
getEditorComponents,
getRemarkPlugins,
query,
queryHits,
clearSearch,
clearFieldErrors,
isFetching,
loadEntry,
isEditorComponent,
isNewEditorComponent,
fieldsErrors,
controlRef,
parentIds,
t,
isDisabled,
isFieldDuplicate,
isFieldHidden,
locale,
isParentListCollapsed,
});
}
}