diff options
Diffstat (limited to 'plugins/jetpack/extensions/blocks/contact-form/components')
7 files changed, 686 insertions, 0 deletions
diff --git a/plugins/jetpack/extensions/blocks/contact-form/components/jetpack-contact-form.js b/plugins/jetpack/extensions/blocks/contact-form/components/jetpack-contact-form.js new file mode 100644 index 00000000..952a3934 --- /dev/null +++ b/plugins/jetpack/extensions/blocks/contact-form/components/jetpack-contact-form.js @@ -0,0 +1,266 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; +import emailValidator from 'email-validator'; +import { __, sprintf } from '@wordpress/i18n'; +import { Button, PanelBody, Path, Placeholder, TextControl } from '@wordpress/components'; +import { Component, Fragment } from '@wordpress/element'; +import { compose, withInstanceId } from '@wordpress/compose'; +import { InnerBlocks, InspectorControls } from '@wordpress/editor'; + +/** + * Internal dependencies + */ +import HelpMessage from '../../../shared/help-message'; +import renderMaterialIcon from '../../../shared/render-material-icon'; +import SubmitButton from '../../../shared/submit-button'; + +const ALLOWED_BLOCKS = [ + 'jetpack/markdown', + 'core/paragraph', + 'core/image', + 'core/heading', + 'core/gallery', + 'core/list', + 'core/quote', + 'core/shortcode', + 'core/audio', + 'core/code', + 'core/cover', + 'core/file', + 'core/html', + 'core/separator', + 'core/spacer', + 'core/subhead', + 'core/table', + 'core/verse', + 'core/video', +]; + +class JetpackContactForm extends Component { + constructor( ...args ) { + super( ...args ); + this.onChangeSubject = this.onChangeSubject.bind( this ); + this.onBlurTo = this.onBlurTo.bind( this ); + this.onChangeTo = this.onChangeTo.bind( this ); + this.onChangeSubmit = this.onChangeSubmit.bind( this ); + this.onFormSettingsSet = this.onFormSettingsSet.bind( this ); + this.getToValidationError = this.getToValidationError.bind( this ); + this.renderToAndSubjectFields = this.renderToAndSubjectFields.bind( this ); + this.preventEnterSubmittion = this.preventEnterSubmittion.bind( this ); + this.hasEmailError = this.hasEmailError.bind( this ); + + const to = args[ 0 ].attributes.to ? args[ 0 ].attributes.to : ''; + const error = to + .split( ',' ) + .map( this.getToValidationError ) + .filter( Boolean ); + + this.state = { + toError: error && error.length ? error : null, + }; + } + + getIntroMessage() { + return __( + 'You’ll receive an email notification each time someone fills out the form. Where should it go, and what should the subject line be?', + 'jetpack' + ); + } + + getEmailHelpMessage() { + return __( 'You can enter multiple email addresses separated by commas.', 'jetpack' ); + } + + onChangeSubject( subject ) { + this.props.setAttributes( { subject } ); + } + + getToValidationError( email ) { + email = email.trim(); + if ( email.length === 0 ) { + return false; // ignore the empty emails + } + if ( ! emailValidator.validate( email ) ) { + return { email }; + } + return false; + } + + onBlurTo( event ) { + const error = event.target.value + .split( ',' ) + .map( this.getToValidationError ) + .filter( Boolean ); + if ( error && error.length ) { + this.setState( { toError: error } ); + return; + } + } + + onChangeTo( to ) { + const emails = to.trim(); + if ( emails.length === 0 ) { + this.setState( { toError: null } ); + this.props.setAttributes( { to } ); + return; + } + + this.setState( { toError: null } ); + this.props.setAttributes( { to } ); + } + + onChangeSubmit( submitButtonText ) { + this.props.setAttributes( { submitButtonText } ); + } + + onFormSettingsSet( event ) { + event.preventDefault(); + if ( this.state.toError ) { + // don't submit the form if there are errors. + return; + } + this.props.setAttributes( { hasFormSettingsSet: 'yes' } ); + } + + getfieldEmailError( errors ) { + if ( errors ) { + if ( errors.length === 1 ) { + if ( errors[ 0 ] && errors[ 0 ].email ) { + return sprintf( __( '%s is not a valid email address.', 'jetpack' ), errors[ 0 ].email ); + } + return errors[ 0 ]; + } + + if ( errors.length === 2 ) { + return sprintf( + __( '%s and %s are not a valid email address.', 'jetpack' ), + errors[ 0 ].email, + errors[ 1 ].email + ); + } + const inValidEmails = errors.map( error => error.email ); + return sprintf( + __( '%s are not a valid email address.', 'jetpack' ), + inValidEmails.join( ', ' ) + ); + } + return null; + } + + preventEnterSubmittion( event ) { + if ( event.key === 'Enter' ) { + event.preventDefault(); + event.stopPropagation(); + } + } + + renderToAndSubjectFields() { + const fieldEmailError = this.state.toError; + const { instanceId, attributes } = this.props; + const { subject, to } = attributes; + return ( + <Fragment> + <TextControl + aria-describedby={ `contact-form-${ instanceId }-email-${ + this.hasEmailError() ? 'error' : 'help' + }` } + label={ __( 'Email address', 'jetpack' ) } + placeholder={ __( 'name@example.com', 'jetpack' ) } + onKeyDown={ this.preventEnterSubmittion } + value={ to } + onBlur={ this.onBlurTo } + onChange={ this.onChangeTo } + /> + <HelpMessage isError id={ `contact-form-${ instanceId }-email-error` }> + { this.getfieldEmailError( fieldEmailError ) } + </HelpMessage> + <HelpMessage id={ `contact-form-${ instanceId }-email-help` }> + { this.getEmailHelpMessage() } + </HelpMessage> + + <TextControl + label={ __( 'Email subject line', 'jetpack' ) } + value={ subject } + placeholder={ __( "Let's work together", 'jetpack' ) } + onChange={ this.onChangeSubject } + /> + </Fragment> + ); + } + + hasEmailError() { + const fieldEmailError = this.state.toError; + return fieldEmailError && fieldEmailError.length > 0; + } + + render() { + const { className, attributes } = this.props; + const { hasFormSettingsSet } = attributes; + const formClassnames = classnames( className, 'jetpack-contact-form', { + 'has-intro': ! hasFormSettingsSet, + } ); + + return ( + <Fragment> + <InspectorControls> + <PanelBody title={ __( 'Email feedback settings', 'jetpack' ) }> + { this.renderToAndSubjectFields() } + </PanelBody> + </InspectorControls> + <div className={ formClassnames }> + { ! hasFormSettingsSet && ( + <Placeholder + label={ __( 'Form', 'jetpack' ) } + icon={ renderMaterialIcon( + <Path d="M13 7.5h5v2h-5zm0 7h5v2h-5zM19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V5h14v14zM11 6H6v5h5V6zm-1 4H7V7h3v3zm1 3H6v5h5v-5zm-1 4H7v-3h3v3z" /> + ) } + > + <form onSubmit={ this.onFormSettingsSet }> + <p className="jetpack-contact-form__intro-message">{ this.getIntroMessage() }</p> + { this.renderToAndSubjectFields() } + <p className="jetpack-contact-form__intro-message"> + { __( + '(If you leave these blank, notifications will go to the author with the post or page title as the subject line.)', + 'jetpack' + ) } + </p> + <div className="jetpack-contact-form__create"> + <Button isPrimary type="submit" disabled={ this.hasEmailError() }> + { __( 'Add form', 'jetpack' ) } + </Button> + </div> + </form> + </Placeholder> + ) } + { hasFormSettingsSet && ( + <InnerBlocks + allowedBlocks={ ALLOWED_BLOCKS } + templateLock={ false } + template={ [ + [ + 'jetpack/field-name', + { + required: true, + }, + ], + [ + 'jetpack/field-email', + { + required: true, + }, + ], + [ 'jetpack/field-url', {} ], + [ 'jetpack/field-textarea', {} ], + ] } + /> + ) } + { hasFormSettingsSet && <SubmitButton { ...this.props } /> } + </div> + </Fragment> + ); + } +} + +export default compose( [ withInstanceId ] )( JetpackContactForm ); diff --git a/plugins/jetpack/extensions/blocks/contact-form/components/jetpack-field-checkbox.js b/plugins/jetpack/extensions/blocks/contact-form/components/jetpack-field-checkbox.js new file mode 100644 index 00000000..05e49dc4 --- /dev/null +++ b/plugins/jetpack/extensions/blocks/contact-form/components/jetpack-field-checkbox.js @@ -0,0 +1,62 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { BaseControl, PanelBody, TextControl, ToggleControl } from '@wordpress/components'; +import { Fragment } from '@wordpress/element'; +import { InspectorControls } from '@wordpress/editor'; +import { withInstanceId } from '@wordpress/compose'; + +/** + * Internal dependencies + */ +import JetpackFieldLabel from './jetpack-field-label'; + +const JetpackFieldCheckbox = ( { + instanceId, + required, + label, + setAttributes, + isSelected, + defaultValue, + id, +} ) => { + return ( + <BaseControl + id={ `jetpack-field-checkbox-${ instanceId }` } + className="jetpack-field jetpack-field-checkbox" + label={ + <Fragment> + <input + className="jetpack-field-checkbox__checkbox" + type="checkbox" + disabled + checked={ defaultValue } + /> + <JetpackFieldLabel + required={ required } + label={ label } + setAttributes={ setAttributes } + isSelected={ isSelected } + /> + <InspectorControls> + <PanelBody title={ __( 'Field Settings', 'jetpack' ) }> + <ToggleControl + label={ __( 'Default Checked State', 'jetpack' ) } + checked={ defaultValue } + onChange={ value => setAttributes( { defaultValue: value } ) } + /> + <TextControl + label={ __( 'ID', 'jetpack' ) } + value={ id } + onChange={ value => setAttributes( { id: value } ) } + /> + </PanelBody> + </InspectorControls> + </Fragment> + } + /> + ); +}; + +export default withInstanceId( JetpackFieldCheckbox ); diff --git a/plugins/jetpack/extensions/blocks/contact-form/components/jetpack-field-label.js b/plugins/jetpack/extensions/blocks/contact-form/components/jetpack-field-label.js new file mode 100644 index 00000000..0ee3d7ba --- /dev/null +++ b/plugins/jetpack/extensions/blocks/contact-form/components/jetpack-field-label.js @@ -0,0 +1,35 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { PlainText } from '@wordpress/editor'; +import { ToggleControl } from '@wordpress/components'; + +const JetpackFieldLabel = ( { setAttributes, label, resetFocus, isSelected, required } ) => { + return ( + <div className="jetpack-field-label"> + <PlainText + value={ label } + className="jetpack-field-label__input" + onChange={ value => { + resetFocus && resetFocus(); + setAttributes( { label: value } ); + } } + placeholder={ __( 'Write label…', 'jetpack' ) } + /> + { isSelected && ( + <ToggleControl + label={ __( 'Required', 'jetpack' ) } + className="jetpack-field-label__required" + checked={ required } + onChange={ value => setAttributes( { required: value } ) } + /> + ) } + { ! isSelected && required && ( + <span className="required">{ __( '(required)', 'jetpack' ) }</span> + ) } + </div> + ); +}; + +export default JetpackFieldLabel; diff --git a/plugins/jetpack/extensions/blocks/contact-form/components/jetpack-field-multiple.js b/plugins/jetpack/extensions/blocks/contact-form/components/jetpack-field-multiple.js new file mode 100644 index 00000000..292bdeaf --- /dev/null +++ b/plugins/jetpack/extensions/blocks/contact-form/components/jetpack-field-multiple.js @@ -0,0 +1,122 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { BaseControl, IconButton, PanelBody, TextControl } from '@wordpress/components'; +import { Component, Fragment } from '@wordpress/element'; +import { InspectorControls } from '@wordpress/editor'; +import { withInstanceId } from '@wordpress/compose'; + +/** + * Internal dependencies + */ +import JetpackFieldLabel from './jetpack-field-label'; +import JetpackOption from './jetpack-option'; + +class JetpackFieldMultiple extends Component { + constructor( ...args ) { + super( ...args ); + this.onChangeOption = this.onChangeOption.bind( this ); + this.addNewOption = this.addNewOption.bind( this ); + this.state = { inFocus: null }; + } + + onChangeOption( key = null, option = null ) { + const newOptions = this.props.options.slice( 0 ); + if ( null === option ) { + // Remove a key + newOptions.splice( key, 1 ); + if ( key > 0 ) { + this.setState( { inFocus: key - 1 } ); + } + } else { + // update a key + newOptions.splice( key, 1, option ); + this.setState( { inFocus: key } ); // set the focus. + } + this.props.setAttributes( { options: newOptions } ); + } + + addNewOption( key = null ) { + const newOptions = this.props.options.slice( 0 ); + let inFocus = 0; + if ( 'object' === typeof key ) { + newOptions.push( '' ); + inFocus = newOptions.length - 1; + } else { + newOptions.splice( key + 1, 0, '' ); + inFocus = key + 1; + } + + this.setState( { inFocus: inFocus } ); + this.props.setAttributes( { options: newOptions } ); + } + + render() { + const { type, instanceId, required, label, setAttributes, isSelected, id } = this.props; + let { options } = this.props; + let { inFocus } = this.state; + if ( ! options.length ) { + options = [ '' ]; + inFocus = 0; + } + + return ( + <Fragment> + <BaseControl + id={ `jetpack-field-multiple-${ instanceId }` } + className="jetpack-field jetpack-field-multiple" + label={ + <JetpackFieldLabel + required={ required } + label={ label } + setAttributes={ setAttributes } + isSelected={ isSelected } + resetFocus={ () => this.setState( { inFocus: null } ) } + /> + } + > + <ol + className="jetpack-field-multiple__list" + id={ `jetpack-field-multiple-${ instanceId }` } + > + { options.map( ( option, index ) => ( + <JetpackOption + type={ type } + key={ index } + option={ option } + index={ index } + onChangeOption={ this.onChangeOption } + onAddOption={ this.addNewOption } + isInFocus={ index === inFocus && isSelected } + isSelected={ isSelected } + /> + ) ) } + </ol> + { isSelected && ( + <IconButton + className="jetpack-field-multiple__add-option" + icon="insert" + label={ __( 'Insert option', 'jetpack' ) } + onClick={ this.addNewOption } + > + { __( 'Add option', 'jetpack' ) } + </IconButton> + ) } + </BaseControl> + + <InspectorControls> + <PanelBody title={ __( 'Field Settings', 'jetpack' ) }> + <TextControl + label={ __( 'ID', 'jetpack' ) } + value={ id } + onChange={ value => setAttributes( { id: value } ) } + /> + </PanelBody> + </InspectorControls> + </Fragment> + ); + } +} + +export default withInstanceId( JetpackFieldMultiple ); diff --git a/plugins/jetpack/extensions/blocks/contact-form/components/jetpack-field-textarea.js b/plugins/jetpack/extensions/blocks/contact-form/components/jetpack-field-textarea.js new file mode 100644 index 00000000..e2025941 --- /dev/null +++ b/plugins/jetpack/extensions/blocks/contact-form/components/jetpack-field-textarea.js @@ -0,0 +1,59 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Fragment } from '@wordpress/element'; +import { InspectorControls } from '@wordpress/editor'; +import { PanelBody, TextareaControl, TextControl } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import JetpackFieldLabel from './jetpack-field-label'; + +function JetpackFieldTextarea( { + required, + label, + setAttributes, + isSelected, + defaultValue, + placeholder, + id, +} ) { + return ( + <Fragment> + <div className="jetpack-field"> + <TextareaControl + label={ + <JetpackFieldLabel + required={ required } + label={ label } + setAttributes={ setAttributes } + isSelected={ isSelected } + /> + } + placeholder={ placeholder } + value={ placeholder } + onChange={ value => setAttributes( { placeholder: value } ) } + title={ __( 'Set the placeholder text', 'jetpack' ) } + /> + </div> + <InspectorControls> + <PanelBody title={ __( 'Field Settings', 'jetpack' ) }> + <TextControl + label={ __( 'Default Value', 'jetpack' ) } + value={ defaultValue } + onChange={ value => setAttributes( { defaultValue: value } ) } + /> + <TextControl + label={ __( 'ID', 'jetpack' ) } + value={ id } + onChange={ value => setAttributes( { id: value } ) } + /> + </PanelBody> + </InspectorControls> + </Fragment> + ); +} + +export default JetpackFieldTextarea; diff --git a/plugins/jetpack/extensions/blocks/contact-form/components/jetpack-field.js b/plugins/jetpack/extensions/blocks/contact-form/components/jetpack-field.js new file mode 100644 index 00000000..6a8269ff --- /dev/null +++ b/plugins/jetpack/extensions/blocks/contact-form/components/jetpack-field.js @@ -0,0 +1,62 @@ +/** + * External dependencies + */ +import classNames from 'classnames'; +import { __ } from '@wordpress/i18n'; +import { Fragment } from '@wordpress/element'; +import { InspectorControls } from '@wordpress/editor'; +import { PanelBody, TextControl } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import JetpackFieldLabel from './jetpack-field-label'; + +function JetpackField( { + isSelected, + type, + required, + label, + setAttributes, + defaultValue, + placeholder, + id, +} ) { + return ( + <Fragment> + <div className={ classNames( 'jetpack-field', { 'is-selected': isSelected } ) }> + <TextControl + type={ type } + label={ + <JetpackFieldLabel + required={ required } + label={ label } + setAttributes={ setAttributes } + isSelected={ isSelected } + /> + } + placeholder={ placeholder } + value={ placeholder } + onChange={ value => setAttributes( { placeholder: value } ) } + title={ __( 'Set the placeholder text', 'jetpack' ) } + /> + </div> + <InspectorControls> + <PanelBody title={ __( 'Field Settings', 'jetpack' ) }> + <TextControl + label={ __( 'Default Value', 'jetpack' ) } + value={ defaultValue } + onChange={ value => setAttributes( { defaultValue: value } ) } + /> + <TextControl + label={ __( 'ID', 'jetpack' ) } + value={ id } + onChange={ value => setAttributes( { id: value } ) } + /> + </PanelBody> + </InspectorControls> + </Fragment> + ); +} + +export default JetpackField; diff --git a/plugins/jetpack/extensions/blocks/contact-form/components/jetpack-option.js b/plugins/jetpack/extensions/blocks/contact-form/components/jetpack-option.js new file mode 100644 index 00000000..8cd2792d --- /dev/null +++ b/plugins/jetpack/extensions/blocks/contact-form/components/jetpack-option.js @@ -0,0 +1,80 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { IconButton } from '@wordpress/components'; +import { Component, createRef } from '@wordpress/element'; + +class JetpackOption extends Component { + constructor( ...args ) { + super( ...args ); + this.onChangeOption = this.onChangeOption.bind( this ); + this.onKeyPress = this.onKeyPress.bind( this ); + this.onDeleteOption = this.onDeleteOption.bind( this ); + this.textInput = createRef(); + } + + componentDidMount() { + if ( this.props.isInFocus ) { + this.textInput.current.focus(); + } + } + + componentDidUpdate() { + if ( this.props.isInFocus ) { + this.textInput.current.focus(); + } + } + + onChangeOption( event ) { + this.props.onChangeOption( this.props.index, event.target.value ); + } + + onKeyPress( event ) { + if ( event.key === 'Enter' ) { + this.props.onAddOption( this.props.index ); + event.preventDefault(); + return; + } + + if ( event.key === 'Backspace' && event.target.value === '' ) { + this.props.onChangeOption( this.props.index ); + event.preventDefault(); + return; + } + } + + onDeleteOption() { + this.props.onChangeOption( this.props.index ); + } + + render() { + const { isSelected, option, type } = this.props; + return ( + <li className="jetpack-option"> + { type && type !== 'select' && ( + <input className="jetpack-option__type" type={ type } disabled /> + ) } + <input + type="text" + className="jetpack-option__input" + value={ option } + placeholder={ __( 'Write option…', 'jetpack' ) } + onChange={ this.onChangeOption } + onKeyDown={ this.onKeyPress } + ref={ this.textInput } + /> + { isSelected && ( + <IconButton + className="jetpack-option__remove" + icon="trash" + label={ __( 'Remove option', 'jetpack' ) } + onClick={ this.onDeleteOption } + /> + ) } + </li> + ); + } +} + +export default JetpackOption; |