Duplicate date and selection form fields for Form Editor, where we don't use redesigned components

[MAILPOET-2787]
This commit is contained in:
Ján Mikláš
2020-09-09 14:07:24 +02:00
committed by Veljko V
parent 9d7b7bbd75
commit 73be596b64
4 changed files with 656 additions and 2 deletions

View File

@ -0,0 +1,324 @@
import React from 'react';
import moment from 'moment';
import PropTypes from 'prop-types';
import classnames from 'classnames';
function FormFieldDateYear(props) {
const yearsRange = 100;
const years = [];
if (props.placeholder !== undefined) {
years.push((
<option value="" key={0}>{ props.placeholder }</option>
));
}
const currentYear = moment().year();
for (let i = currentYear; i >= currentYear - yearsRange; i -= 1) {
years.push((
<option
key={i}
value={i}
>
{ i }
</option>
));
}
return (
<select
name={`${props.name}[year]`}
value={props.year}
onChange={props.onValueChange}
className={classnames({ mailpoet_date_year: props.addDefaultClasses })}
>
{ years }
</select>
);
}
FormFieldDateYear.propTypes = {
name: PropTypes.string.isRequired,
placeholder: PropTypes.string.isRequired,
onValueChange: PropTypes.func.isRequired,
year: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]).isRequired,
addDefaultClasses: PropTypes.bool.isRequired,
};
function FormFieldDateMonth(props) {
const months = [];
if (props.placeholder !== undefined) {
months.push((
<option value="" key={0}>{ props.placeholder }</option>
));
}
for (let i = 1; i <= 12; i += 1) {
months.push((
<option
key={i}
value={i}
>
{ props.monthNames[i - 1] }
</option>
));
}
return (
<select
name={`${props.name}[month]`}
value={props.month}
onChange={props.onValueChange}
className={classnames({ mailpoet_date_month: props.addDefaultClasses })}
>
{ months }
</select>
);
}
FormFieldDateMonth.propTypes = {
name: PropTypes.string.isRequired,
placeholder: PropTypes.string.isRequired,
onValueChange: PropTypes.func.isRequired,
month: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]).isRequired,
monthNames: PropTypes.arrayOf(PropTypes.string).isRequired,
addDefaultClasses: PropTypes.bool.isRequired,
};
function FormFieldDateDay(props) {
const days = [];
if (props.placeholder !== undefined) {
days.push((
<option value="" key={0}>{ props.placeholder }</option>
));
}
for (let i = 1; i <= 31; i += 1) {
days.push((
<option
key={i}
value={i}
>
{ i }
</option>
));
}
return (
<select
name={`${props.name}[day]`}
value={props.day}
onChange={props.onValueChange}
className={classnames({ mailpoet_date_day: props.addDefaultClasses })}
>
{ days }
</select>
);
}
FormFieldDateDay.propTypes = {
name: PropTypes.string.isRequired,
placeholder: PropTypes.string.isRequired,
onValueChange: PropTypes.func.isRequired,
day: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
]).isRequired,
addDefaultClasses: PropTypes.bool.isRequired,
};
class FormFieldDate extends React.Component {
constructor(props) {
super(props);
this.state = {
year: '',
month: '',
day: '',
};
this.onValueChange = this.onValueChange.bind(this);
}
componentDidMount() {
this.extractDateParts();
}
componentDidUpdate(prevProps) {
if (
(this.props.item !== undefined && prevProps.item !== undefined)
&& (this.props.item.id !== prevProps.item.id)
) {
this.extractDateParts();
}
}
onValueChange(e) {
// extract property from name
const matches = e.target.name.match(/(.*?)\[(.*?)\]/);
let field = null;
let property = null;
if (matches !== null && matches.length === 3) {
[, field, property] = matches;
const value = Number(e.target.value);
this.setState({
[`${property}`]: value,
}, () => {
this.props.onValueChange({
target: {
name: field,
value: this.formatValue(),
},
});
});
}
}
formatValue() {
const dateType = this.props.field.params.date_type;
let value;
switch (dateType) {
case 'year_month_day':
value = {
year: this.state.year,
month: this.state.month,
day: this.state.day,
};
break;
case 'year_month':
value = {
year: this.state.year,
month: this.state.month,
};
break;
case 'month':
value = {
month: this.state.month,
};
break;
case 'year':
value = {
year: this.state.year,
};
break;
default:
value = {
value: 'invalid type',
};
break;
}
return value;
}
extractDateParts() {
const value = (this.props.item[this.props.field.name] !== undefined)
? this.props.item[this.props.field.name].trim()
: '';
if (value === '') {
return;
}
const dateTime = moment(value);
this.setState({
year: dateTime.format('YYYY'),
month: dateTime.format('M'),
day: dateTime.format('D'),
});
}
render() {
const monthNames = window.mailpoet_month_names || [];
const dateFormats = window.mailpoet_date_formats || {};
const dateType = this.props.field.params.date_type;
let dateFormat = dateFormats[dateType][0];
if (this.props.field.params.date_format) {
dateFormat = this.props.field.params.date_format;
}
const dateSelects = dateFormat.split('/');
const fields = dateSelects.map((type) => {
switch (type) {
case 'YYYY':
return (
<FormFieldDateYear
onValueChange={this.onValueChange}
key="year"
name={this.props.field.name}
addDefaultClasses={this.props.addDefaultClasses}
year={this.state.year}
placeholder={this.props.field.year_placeholder}
/>
);
case 'MM':
return (
<FormFieldDateMonth
onValueChange={this.onValueChange}
key="month"
name={this.props.field.name}
addDefaultClasses={this.props.addDefaultClasses}
month={this.state.month}
monthNames={monthNames}
placeholder={this.props.field.month_placeholder}
/>
);
case 'DD':
return (
<FormFieldDateDay
onValueChange={this.onValueChange}
key="day"
name={this.props.field.name}
addDefaultClasses={this.props.addDefaultClasses}
day={this.state.day}
placeholder={this.props.field.day_placeholder}
/>
);
default:
return <div>Invalid date type</div>;
}
});
return (
<div>
{fields}
</div>
);
}
}
FormFieldDate.propTypes = {
item: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
field: PropTypes.shape({
name: PropTypes.string,
day_placeholder: PropTypes.string,
month_placeholder: PropTypes.string,
year_placeholder: PropTypes.string,
params: PropTypes.object, // eslint-disable-line react/forbid-prop-types
}).isRequired,
onValueChange: PropTypes.func.isRequired,
addDefaultClasses: PropTypes.bool,
};
FormFieldDate.defaultProps = {
addDefaultClasses: false,
};
export default FormFieldDate;

View File

@ -12,7 +12,7 @@ import { useDispatch, useSelect } from '@wordpress/data';
import ParagraphEdit from '../paragraph_edit.jsx';
import CustomFieldSettings from './custom_field_settings.jsx';
import FormFieldDate from '../../../form/fields/date.jsx';
import FormFieldDate from './date.jsx';
import formatLabel from '../label_formatter.jsx';
import mapCustomFieldFormData from '../map_custom_field_form_data.jsx';

View File

@ -12,7 +12,7 @@ import React from 'react';
import MailPoet from 'mailpoet';
import classnames from 'classnames';
import PropTypes from 'prop-types';
import Selection from '../../../form/fields/selection.jsx';
import Selection from './selection.jsx';
import FormTitle from '../form_title';
const BasicSettingsPanel = ({ onToggle, isOpened }) => {

View File

@ -0,0 +1,330 @@
import React from 'react';
import jQuery from 'jquery';
import _ from 'underscore';
import 'react-dom';
import 'select2';
import PropTypes from 'prop-types';
class Selection extends React.Component {
constructor(props) {
super(props);
this.selectRef = React.createRef();
}
componentDidMount() {
if (this.isSelect2Component()) {
this.setupSelect2();
}
}
componentDidUpdate(prevProps) {
if ((this.props.item !== undefined && prevProps.item !== undefined)
&& (this.props.item.id !== prevProps.item.id)
) {
jQuery(`#${this.selectRef.current.id}`)
.val(this.getSelectedValues())
.trigger('change');
}
if (this.isSelect2Initialized()
&& (this.getFieldId(this.props) !== this.getFieldId(prevProps))
&& this.props.field.resetSelect2OnUpdate !== undefined
) {
this.resetSelect2();
}
}
componentWillUnmount() {
if (this.isSelect2Component()) {
this.destroySelect2();
}
}
getFieldId = (data) => {
const props = data || this.props;
return props.field.id || props.field.name;
};
getSelectedValues = () => {
if (this.props.field.selected !== undefined) {
return this.props.field.selected(this.props.item);
}
if (this.props.item !== undefined && this.props.field.name !== undefined) {
if (this.allowMultipleValues()) {
if (_.isArray(this.props.item[this.props.field.name])) {
return this.props.item[this.props.field.name].map((item) => item.id);
}
} else {
return this.props.item[this.props.field.name];
}
}
return null;
};
getItems = () => {
let items;
if (typeof (window[`mailpoet_${this.props.field.endpoint}`]) !== 'undefined') {
items = window[`mailpoet_${this.props.field.endpoint}`];
} else if (this.props.field.values !== undefined) {
items = this.props.field.values;
}
if (_.isArray(items)) {
if (this.props.field.filter !== undefined) {
items = items.filter(this.props.field.filter);
}
}
return items;
};
getLabel = (item) => {
if (this.props.field.getLabel !== undefined) {
return this.props.field.getLabel(item, this.props.item);
}
return item.name;
};
getSearchLabel = (item) => {
if (this.props.field.getSearchLabel !== undefined) {
return this.props.field.getSearchLabel(item, this.props.item);
}
return null;
};
getValue = (item) => {
if (this.props.field.getValue !== undefined) {
return this.props.field.getValue(item, this.props.item);
}
return item.id;
};
setupSelect2 = () => {
if (this.isSelect2Initialized()) {
return;
}
let select2Options = {
disabled: this.props.disabled || false,
width: (this.props.width || ''),
placeholder: {
id: '', // the value of the option
text: this.props.field.placeholder,
},
templateResult: function templateResult(item) {
if (item.element && item.element.selected) {
return null;
}
if (item.title) {
return item.title;
}
return item.text;
},
};
const remoteQuery = this.props.field.remoteQuery || null;
if (remoteQuery) {
select2Options = Object.assign(select2Options, {
ajax: {
url: window.ajaxurl,
type: 'POST',
dataType: 'json',
data: function data(params) {
return {
action: 'mailpoet',
api_version: window.mailpoet_api_version,
token: window.mailpoet_token,
endpoint: remoteQuery.endpoint,
method: remoteQuery.method,
data: Object.assign(
remoteQuery.data,
{ query: params.term }
),
};
},
processResults: function processResults(response) {
let results;
if (!_.has(response, 'data')) {
results = [];
} else {
results = response.data.map((item) => {
const id = item.id || item.value;
const text = item.name || item.text;
return { id, text };
});
}
return { results };
},
},
minimumInputLength: remoteQuery.minimumInputLength || 2,
});
}
if (this.props.field.extendSelect2Options !== undefined) {
select2Options = Object.assign(select2Options, this.props.field.extendSelect2Options);
}
const select2 = jQuery(`#${this.selectRef.current.id}`).select2(select2Options);
let hasRemoved = false;
select2.on('select2:unselecting', () => {
hasRemoved = true;
});
select2.on('select2:opening', (e) => {
if (hasRemoved === true) {
hasRemoved = false;
e.preventDefault();
}
});
select2.on('change', this.handleChange);
};
resetSelect2 = () => {
this.destroySelect2();
this.setupSelect2();
};
destroySelect2 = () => {
if (this.isSelect2Initialized()) {
jQuery(`#${this.selectRef.current.id}`).select2('destroy');
this.cleanupAfterSelect2();
}
};
cleanupAfterSelect2 = () => {
// remove DOM elements created by Select2 that are not tracked by React
jQuery(`#${this.selectRef.current.id}`)
.find('option:not(.default)')
.remove();
// unbind events (https://select2.org/programmatic-control/methods#event-unbinding)
jQuery(`#${this.selectRef.current.id}`)
.off('select2:unselecting')
.off('select2:opening');
};
allowMultipleValues = () => (this.props.field.multiple === true);
isSelect2Initialized = () => (jQuery(`#${this.selectRef.current.id}`).hasClass('select2-hidden-accessible') === true);
isSelect2Component = () => this.allowMultipleValues() || this.props.field.forceSelect2;
handleChange = (e) => {
if (this.props.onValueChange === undefined) return;
const valueTextPair = jQuery(`#${this.selectRef.current.id}`).children(':selected').map(function element() {
return { id: jQuery(this).val(), text: jQuery(this).text() };
});
const value = (this.props.field.multiple) ? _.pluck(valueTextPair, 'id') : _.pluck(valueTextPair, 'id').toString();
const transformedValue = this.transformChangedValue(value, valueTextPair);
this.props.onValueChange({
target: {
value: transformedValue,
name: this.props.field.name,
id: e.target.id,
},
});
};
// When it's impossible to represent the desired value in DOM,
// this function may be used to transform the placeholder value into
// desired value.
transformChangedValue = (value, textValuePair) => {
if (typeof this.props.field.transformChangedValue === 'function') {
return this.props.field.transformChangedValue.call(this, value, textValuePair);
}
return value;
};
insertEmptyOption = () => {
// https://select2.org/placeholders
// For single selects only, in order for the placeholder value to appear,
// we must have a blank <option> as the first option in the <select> control.
if (this.allowMultipleValues()) return undefined;
if (this.props.field.placeholder) {
return (
<option className="default" /> // eslint-disable-line jsx-a11y/control-has-associated-label
);
}
return undefined;
};
render() {
const items = this.getItems(this.props.field);
const selectedValues = this.getSelectedValues();
const options = items.map((item) => {
const label = this.getLabel(item);
const searchLabel = this.getSearchLabel(item);
const value = this.getValue(item);
return (
<option
key={`option-${item.id}`}
className="default"
value={value}
title={searchLabel}
>
{ label }
</option>
);
});
return (
<select
id={this.getFieldId()}
ref={this.selectRef}
disabled={this.props.field.disabled}
data-placeholder={this.props.field.placeholder}
multiple={this.props.field.multiple}
defaultValue={selectedValues}
{...this.props.field.validation}// eslint-disable-line react/jsx-props-no-spreading
>
{ this.insertEmptyOption() }
{ options }
</select>
);
}
}
Selection.propTypes = {
onValueChange: PropTypes.func,
field: PropTypes.shape({
name: PropTypes.string.isRequired,
values: PropTypes.oneOfType([
PropTypes.object,
PropTypes.array,
]),
getLabel: PropTypes.func,
resetSelect2OnUpdate: PropTypes.bool,
selected: PropTypes.func,
endpoint: PropTypes.string,
filter: PropTypes.func,
getSearchLabel: PropTypes.func,
getValue: PropTypes.func,
placeholder: PropTypes.string,
remoteQuery: PropTypes.object,
extendSelect2Options: PropTypes.object,
multiple: PropTypes.bool,
forceSelect2: PropTypes.bool,
transformChangedValue: PropTypes.func,
disabled: PropTypes.bool,
validation: PropTypes.object,
}).isRequired,
item: PropTypes.object, // eslint-disable-line react/forbid-prop-types
disabled: PropTypes.bool,
width: PropTypes.string,
};
Selection.defaultProps = {
onValueChange: function onValueChange() {
// no-op
},
disabled: false,
width: '',
item: undefined,
};
export default Selection;