diff --git a/assets/js/src/form_editor/blocks/custom_date/date.jsx b/assets/js/src/form_editor/blocks/custom_date/date.jsx
new file mode 100644
index 0000000000..e89d5eb664
--- /dev/null
+++ b/assets/js/src/form_editor/blocks/custom_date/date.jsx
@@ -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((
+
+ ));
+ }
+
+ const currentYear = moment().year();
+ for (let i = currentYear; i >= currentYear - yearsRange; i -= 1) {
+ years.push((
+
+ ));
+ }
+ return (
+
+ );
+}
+
+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((
+
+ ));
+ }
+
+ for (let i = 1; i <= 12; i += 1) {
+ months.push((
+
+ ));
+ }
+ return (
+
+ );
+}
+
+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((
+
+ ));
+ }
+
+ for (let i = 1; i <= 31; i += 1) {
+ days.push((
+
+ ));
+ }
+
+ return (
+
+ );
+}
+
+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 (
+
+ );
+
+ case 'MM':
+ return (
+
+ );
+
+ case 'DD':
+ return (
+
+ );
+
+ default:
+ return
Invalid date type
;
+ }
+ });
+
+ return (
+
+ {fields}
+
+ );
+ }
+}
+
+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;
diff --git a/assets/js/src/form_editor/blocks/custom_date/edit.jsx b/assets/js/src/form_editor/blocks/custom_date/edit.jsx
index 37cafd35a6..1fac5fa4d2 100644
--- a/assets/js/src/form_editor/blocks/custom_date/edit.jsx
+++ b/assets/js/src/form_editor/blocks/custom_date/edit.jsx
@@ -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';
diff --git a/assets/js/src/form_editor/components/form_settings/basic_settings_panel.jsx b/assets/js/src/form_editor/components/form_settings/basic_settings_panel.jsx
index 6c879ab9f8..f54ec43a58 100644
--- a/assets/js/src/form_editor/components/form_settings/basic_settings_panel.jsx
+++ b/assets/js/src/form_editor/components/form_settings/basic_settings_panel.jsx
@@ -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 }) => {
diff --git a/assets/js/src/form_editor/components/form_settings/selection.jsx b/assets/js/src/form_editor/components/form_settings/selection.jsx
new file mode 100644
index 0000000000..daddce700c
--- /dev/null
+++ b/assets/js/src/form_editor/components/form_settings/selection.jsx
@@ -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