From 73be596b64a5facbdd3b7b567d184c656de1c8db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ja=CC=81n=20Mikla=CC=81s=CC=8C?= Date: Wed, 9 Sep 2020 14:07:24 +0200 Subject: [PATCH] Duplicate date and selection form fields for Form Editor, where we don't use redesigned components [MAILPOET-2787] --- .../form_editor/blocks/custom_date/date.jsx | 324 +++++++++++++++++ .../form_editor/blocks/custom_date/edit.jsx | 2 +- .../form_settings/basic_settings_panel.jsx | 2 +- .../components/form_settings/selection.jsx | 330 ++++++++++++++++++ 4 files changed, 656 insertions(+), 2 deletions(-) create mode 100644 assets/js/src/form_editor/blocks/custom_date/date.jsx create mode 100644 assets/js/src/form_editor/components/form_settings/selection.jsx 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