From 8e25aa121e3233784804c21cdd322e4b0afc8cce Mon Sep 17 00:00:00 2001 From: Matthias Kittsteiner Date: Wed, 31 Jan 2024 17:09:04 +0100 Subject: [PATCH 01/27] =?UTF-8?q?=F0=9F=94=87=20Remove=20debug=20log?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/js/validation.js | 1 - 1 file changed, 1 deletion(-) diff --git a/assets/js/validation.js b/assets/js/validation.js index d897bc7..50d8e3c 100644 --- a/assets/js/validation.js +++ b/assets/js/validation.js @@ -127,7 +127,6 @@ document.addEventListener( 'DOMContentLoaded', function() { const setAriaDescribedBy = ( field ) => { const innerError = field.parentNode.querySelector( '.inline-error' ); - console.log( innerError ); innerError.id = field.id + '__inline-error'; field.setAttribute( 'aria-describedby', ( ( field.getAttribute( 'aria-describedby' ) || '' ) + ' ' + innerError.id ).trim() ); } From f2d938119379a21114372fd27ef8b5c401137049 Mon Sep 17 00:00:00 2001 From: Matthias Kittsteiner Date: Mon, 15 Apr 2024 13:55:22 +0200 Subject: [PATCH 02/27] =?UTF-8?q?=F0=9F=93=9D=20Upgrade=20"Tested=20up=20t?= =?UTF-8?q?o"=20to=206.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- readme.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.txt b/readme.txt index eca1f60..8541fdc 100644 --- a/readme.txt +++ b/readme.txt @@ -3,7 +3,7 @@ Contributors: epiphyt, kittmedia Tags: contact, form, contact form, gutenberg, block editor Requires at least: 6.3 Stable tag: 1.3.0 -Tested up to: 6.4 +Tested up to: 6.5 Requires PHP: 7.4 License: GPL2 License URI: https://www.gnu.org/licenses/gpl-2.0.html From a356dcb9e7e06fdad23b14ed8db193d68c984ac2 Mon Sep 17 00:00:00 2001 From: Matthias Kittsteiner Date: Fri, 3 May 2024 16:53:29 +0200 Subject: [PATCH 03/27] =?UTF-8?q?=F0=9F=94=92=EF=B8=8F=20Update=20security?= =?UTF-8?q?=20policy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 +++ SECURITY.md | 9 ++------- readme.txt | 3 +++ 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 6b7715d..85db31d 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,9 @@ The code is open source and hosted on [GitHub](https://github.com/epiphyt/form-b We are [Epiphyt](https://epiph.yt/), your friendly neighborhood WordPress plugin shop from southern Germany. +## Security + +For security related information, please consult the [security policy](SECURITY.md). ## License diff --git a/SECURITY.md b/SECURITY.md index 0885776..e08fe13 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -9,11 +9,6 @@ We usually only support the latest major version. | 1.3.x | :white_check_mark: | | < 1.3 | :x: | -## Reporting a Vulnerability +## How can I report security bugs? -Please report any vulnerability via . You should receive -an answer within 24 hours. You will be informed about if the vulnerability is -accepted or declined. If accepted and fixed, we will thank you in the changelog. - -If desired, we can also mention you in any other channel we use to announce an -update, e.g. in a blog post or via Mastodon/Twitter. +You can report security bugs through the Patchstack Vulnerability Disclosure Program. The Patchstack team help validate, triage and handle any security vulnerabilities. [Report a security vulnerability.](https://patchstack.com/database/vdp/form-block) diff --git a/readme.txt b/readme.txt index 8541fdc..ba03001 100644 --- a/readme.txt +++ b/readme.txt @@ -88,6 +88,9 @@ The code is open source and hosted on [GitHub](https://github.com/epiphyt/form-b We are [Epiphyt](https://epiph.yt/), your friendly neighborhood WordPress plugin shop from southern Germany. += How can I report security bugs? = + +You can report security bugs through the Patchstack Vulnerability Disclosure Program. The Patchstack team help validate, triage and handle any security vulnerabilities. [Report a security vulnerability.](https://patchstack.com/database/vdp/form-block) == Changelog == From 1a91d015fea90ec476b9ba7f4bdcd0177fe47a4f Mon Sep 17 00:00:00 2001 From: Matthias Kittsteiner Date: Sun, 12 May 2024 11:01:27 +0200 Subject: [PATCH 04/27] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Use=20aria=20attribu?= =?UTF-8?q?te=20instead=20of=20property?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/js/validation.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/validation.js b/assets/js/validation.js index 50d8e3c..3c7f927 100644 --- a/assets/js/validation.js +++ b/assets/js/validation.js @@ -244,7 +244,7 @@ document.addEventListener( 'DOMContentLoaded', function() { 'is-error-notice', 'screen-reader-text', ); - invalidFieldNotice.setAttribute( 'aria-live', 'assertive' ); + invalidFieldNotice.ariaLive = 'assertive'; form.appendChild( invalidFieldNotice ); } From 394a9fecc3cb9594ac823733a11a9cb292404c25 Mon Sep 17 00:00:00 2001 From: Matthias Kittsteiner Date: Sun, 12 May 2024 11:06:51 +0200 Subject: [PATCH 05/27] =?UTF-8?q?=E2=9C=A8=20Add=20all=20input=20types=20S?= =?UTF-8?q?ee=20#36?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/input/html-data.js | 147 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) diff --git a/src/input/html-data.js b/src/input/html-data.js index 4d86d05..c292edb 100644 --- a/src/input/html-data.js +++ b/src/input/html-data.js @@ -26,6 +26,40 @@ const types = applyFilters( 'value', ], }, + color: { + allowedAttributes: [ + 'ariaDescription', + 'autoComplete', + 'disabled', + 'label', + ], + }, + date: { + allowedAttributes: [ + 'ariaDescription', + 'autoComplete', + 'disabled', + 'label', + 'max', + 'min', + 'readOnly', + 'required', + 'step', + ], + }, + 'datetime-local': { + allowedAttributes: [ + 'ariaDescription', + 'autoComplete', + 'disabled', + 'label', + 'max', + 'min', + 'readOnly', + 'required', + 'step', + ], + }, email: { allowedAttributes: [ 'autoComplete', @@ -54,6 +88,36 @@ const types = applyFilters( 'required', ], }, + hidden: { + allowedAttributes: [], + }, + image: { + allowedAttributes: [ + 'ariaDescription', + 'alt', + 'autoComplete', + 'disabled', + 'height', + 'label', + 'readOnly', + 'required', + 'src', + 'width', + ], + }, + month: { + allowedAttributes: [ + 'ariaDescription', + 'autoComplete', + 'disabled', + 'label', + 'max', + 'min', + 'readOnly', + 'required', + 'step', + ], + }, number: { allowedAttributes: [ 'autoComplete', @@ -67,6 +131,21 @@ const types = applyFilters( 'step', ], }, + password: { + allowedAttributes: [ + 'ariaDescription', + 'autoComplete', + 'disabled', + 'label', + 'maxLength', + 'minLength', + 'pattern', + 'placeholder', + 'readOnly', + 'required', + 'size', + ], + }, radio: { allowedAttributes: [ 'checked', @@ -76,12 +155,39 @@ const types = applyFilters( 'value', ], }, + range: { + allowedAttributes: [ + 'ariaDescription', + 'autoComplete', + 'disabled', + 'label', + 'max', + 'min', + 'step', + ], + }, reset: { allowedAttributes: [ 'disabled', 'value', ], }, + search: { + allowedAttributes: [ + 'ariaDescription', + 'autoComplete', + 'dirname', + 'disabled', + 'label', + 'maxLength', + 'minLength', + 'pattern', + 'placeholder', + 'readOnly', + 'required', + 'size', + ], + }, submit: { allowedAttributes: [ 'disabled', @@ -102,6 +208,19 @@ const types = applyFilters( 'size', ], }, + time: { + allowedAttributes: [ + 'ariaDescription', + 'autoComplete', + 'disabled', + 'label', + 'max', + 'min', + 'readOnly', + 'required', + 'step', + ], + }, text: { allowedAttributes: [ 'autoComplete', @@ -117,5 +236,33 @@ const types = applyFilters( 'size', ], }, + url: { + allowedAttributes: [ + 'ariaDescription', + 'autoComplete', + 'disabled', + 'label', + 'maxLength', + 'minLength', + 'pattern', + 'placeholder', + 'readOnly', + 'required', + 'size', + ], + }, + week: { + allowedAttributes: [ + 'ariaDescription', + 'autoComplete', + 'disabled', + 'label', + 'max', + 'min', + 'readOnly', + 'required', + 'step', + ], + }, }, ); From c68ac9ab2e30cb9340db8e0994e5b9f91b6445ee Mon Sep 17 00:00:00 2001 From: Matthias Kittsteiner Date: Wed, 29 May 2024 07:06:01 +0200 Subject: [PATCH 06/27] =?UTF-8?q?=E2=99=BF=EF=B8=8F=20Be=20more=20polite?= =?UTF-8?q?=20for=20non-error=20live=20messages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/js/form.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/assets/js/form.js b/assets/js/form.js index 6d515a0..2f794f8 100644 --- a/assets/js/form.js +++ b/assets/js/form.js @@ -204,6 +204,7 @@ document.addEventListener( 'DOMContentLoaded', () => { * @param {Boolean} isHtml Whether the message is raw HTML */ function setSubmitMessage( form, messageType, message, isHtml ) { + const ariaLiveType = messageType === 'error' ? 'assertive' : 'polite'; let messageContainer = form.querySelector( '.form-block__message-container' ); if ( ! messageContainer ) { @@ -220,7 +221,7 @@ document.addEventListener( 'DOMContentLoaded', () => { messageContainer.textContent = message; // then replace all newlines with
messageContainer.innerHTML = nl2br( messageContainer.innerHTML ); - messageContainer.setAttribute( 'aria-live', 'assertive' ); + messageContainer.setAttribute( 'aria-live', ariaLiveType ); if ( isHtml ) { messageContainer.innerHTML = message; From 5dd511422999f08c6b73aa07c6e320528cb36870 Mon Sep 17 00:00:00 2001 From: Matthias Kittsteiner Date: Sat, 15 Jun 2024 11:21:13 +0200 Subject: [PATCH 07/27] =?UTF-8?q?=F0=9F=94=A5=20Remove=20superfluous=20use?= =?UTF-8?q?=20statements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- inc/class-admin.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/inc/class-admin.php b/inc/class-admin.php index 4282cd9..886c554 100644 --- a/inc/class-admin.php +++ b/inc/class-admin.php @@ -3,11 +3,6 @@ namespace epiphyt\Form_Block; use function add_action; -use function plugin_dir_path; -use function plugin_dir_url; -use function wp_enqueue_script; -use function wp_enqueue_style; -use const EPI_FORM_BLOCK_FILE; /** * Form Block admin class. From 5b8b76befac0f9dfdd115158d65ad5c8ee3b4270 Mon Sep 17 00:00:00 2001 From: Matthias Kittsteiner Date: Sat, 15 Jun 2024 11:22:03 +0200 Subject: [PATCH 08/27] =?UTF-8?q?=E2=9C=A8=20Add=20editor=20settings=20for?= =?UTF-8?q?=20custom=20date=20field=20See=20#38?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 1 - src/data/attributes.js | 3 + src/input/attributes.js | 7 ++ src/input/controls.js | 10 ++ src/input/edit.js | 16 ++- src/input/html-data.js | 14 +++ src/input/modules/custom-date/controls.js | 20 ++++ src/input/modules/custom-date/editor.scss | 15 +++ src/input/modules/custom-date/index.js | 121 ++++++++++++++++++++++ 9 files changed, 201 insertions(+), 6 deletions(-) create mode 100644 src/input/modules/custom-date/controls.js create mode 100644 src/input/modules/custom-date/editor.scss create mode 100644 src/input/modules/custom-date/index.js diff --git a/package-lock.json b/package-lock.json index 29c8ede..2fbd5a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,6 @@ "packages": { "": { "name": "form-block", - "version": "1.0.0-dev", "devDependencies": { "@wordpress/block-editor": "^11.1.0", "@wordpress/blocks": "^12.1.0", diff --git a/src/data/attributes.js b/src/data/attributes.js index 8b54c28..99c6097 100644 --- a/src/data/attributes.js +++ b/src/data/attributes.js @@ -12,6 +12,9 @@ export const attributes = applyFilters( description: __( 'Whether the element is checked by default.', 'form-block' ), label: _x( 'Checked', 'HTML attribute name', 'form-block' ), }, + customDate: { + controlType: 'custom-date', + }, disabled: { controlType: 'toggle', description: __( 'Whether the form element is disabled and will not be submitted by sending the form.', 'form-block' ), diff --git a/src/input/attributes.js b/src/input/attributes.js index ae62234..3766c9a 100644 --- a/src/input/attributes.js +++ b/src/input/attributes.js @@ -7,6 +7,13 @@ const attributes = { source: 'attribute', type: 'boolean', }, + customDate: { + default: { + showPlaceholder: true, + value: {}, + }, + type: 'object', + }, disabled: { attribute: 'disabled', selector: 'input', diff --git a/src/input/controls.js b/src/input/controls.js index 2cfa3cd..6d9d67d 100644 --- a/src/input/controls.js +++ b/src/input/controls.js @@ -19,6 +19,7 @@ import { } from '../data/attributes'; import { getSanitizedAttributeValue, stripSpecialChars } from '../data/util'; import { getTypes, isAllowedAttribute } from './html-data'; +import CustomDateControls from './modules/custom-date/controls'; export default function Controls( props ) { const { @@ -92,6 +93,15 @@ export default function Controls( props ) { } ); switch ( inputAttributes[ attribute ].controlType ) { + case 'custom-date': + return ( + + ); case 'number': return ( setAttributes( { value } ) } - { ...elementProps } - /> + { isCustomDate( type ) + ? + : setAttributes( { value } ) } + { ...elementProps } + /> + } } diff --git a/src/input/html-data.js b/src/input/html-data.js index c292edb..fb0b2bd 100644 --- a/src/input/html-data.js +++ b/src/input/html-data.js @@ -47,6 +47,20 @@ const types = applyFilters( 'step', ], }, + 'date-custom': { + allowedAttributes: [ + 'ariaDescription', + 'autoComplete', + 'customDate', + 'disabled', + 'label', + 'max', + 'min', + 'readOnly', + 'required', + 'step', + ], + }, 'datetime-local': { allowedAttributes: [ 'ariaDescription', diff --git a/src/input/modules/custom-date/controls.js b/src/input/modules/custom-date/controls.js new file mode 100644 index 0000000..1ff9e2e --- /dev/null +++ b/src/input/modules/custom-date/controls.js @@ -0,0 +1,20 @@ +import { ToggleControl } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +export default function CustomDateControls( { attribute, props, updateValue } ) { + return ( + <> + { + let updatedValue = structuredClone( props.attributes[ attribute ] ); + updatedValue.showPlaceholder = newValue; + + updateValue( updatedValue, attribute ); + } } + /> + + ); +} diff --git a/src/input/modules/custom-date/editor.scss b/src/input/modules/custom-date/editor.scss new file mode 100644 index 0000000..7293c00 --- /dev/null +++ b/src/input/modules/custom-date/editor.scss @@ -0,0 +1,15 @@ +@import '~@wordpress/base-styles/variables'; + +.form-block__date-custom { + .form-block__date-custom--separator { + margin-block-end: $grid-unit-10; + + &:last-child { + display: none; + } + } + + .is-type-year { + flex-grow: 2; + } +} diff --git a/src/input/modules/custom-date/index.js b/src/input/modules/custom-date/index.js new file mode 100644 index 0000000..b6793ca --- /dev/null +++ b/src/input/modules/custom-date/index.js @@ -0,0 +1,121 @@ +import { + Flex, + FlexBlock, + FlexItem, + TextControl, +} from '@wordpress/components'; +import { Fragment } from '@wordpress/element'; +import { addFilter, applyFilters } from '@wordpress/hooks'; +import { __, _x } from '@wordpress/i18n'; + +import './editor.scss'; + +const getAllowedInputTypes = () => { + let types = [ + 'date-custom', + 'datetime-local-custom', + 'month-custom', + 'time-custom', + 'week-custom', + ]; + + types = applyFilters( + 'formBlock.module.datePicker.allowedInputTypes', + types + ); + + return types; +} + +const addControlTypes = ( controlTypes, props ) => { + const { + attributes: { + type, + }, + } = props; + + if ( ! isCustomDate( type ) ) { + return controlTypes; + } + + controlTypes.push( { + attributeName: 'customDate', + attributes: {}, + } ); + + return controlTypes; +} + +addFilter( 'formBlock.input.controlTypes', 'form-block/custom-date/add-control-types', addControlTypes ); + +export const isCustomDate = ( type ) => getAllowedInputTypes().includes( type ); + +export function CustomDate( { props, elementProps } ) { + const { + attributes: { + customDate, + label, + type, + }, + setAttributes, + } = props; + const { + showPlaceholder, + value, + } = customDate; + const fieldData = { + day: { + label: __( 'Day', 'form-block' ), + placeholder: _x( 'DD', 'date field placeholder', 'form-block' ), + }, + month: { + label: __( 'Month', 'form-block' ), + placeholder: _x( 'MM', 'date field placeholder', 'form-block' ), + }, + year: { + label: __( 'Year', 'form-block' ), + placeholder: _x( 'YYYY', 'date field placeholder', 'form-block' ), + }, + } + + const onFieldUpdate = ( field, fieldValue ) => { + let newValue = structuredClone( customDate ); + newValue.value[ field ] = fieldValue; + + setAttributes( { customDate: newValue } ); + } + + switch ( type ) { + case 'date-custom': + const fields = _x( 'month, day, year', 'date order in lowercase', 'form-block' ).split( ', ' ); + + return ( +
+ { label } + + + { fields.map( ( field, index ) => ( + + + onFieldUpdate( field, value ) } + { ...elementProps } + placeholder={ showPlaceholder ? fieldData[ field ].placeholder : '' } + value={ value ? ( value[ field ] || '' ) : '' } + /> + + + + { _x( '/', 'date separator', 'form-block' ) } + + + ) ) } + +
+ ); + } + + return null; +} From 04184e98fe44d50e6fe39ca6712f0dc7ae674ef9 Mon Sep 17 00:00:00 2001 From: Matthias Kittsteiner Date: Sat, 15 Jun 2024 11:30:01 +0200 Subject: [PATCH 09/27] =?UTF-8?q?=F0=9F=92=84=20Allow=20displaying=20label?= =?UTF-8?q?s=20See=20#38?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/input/attributes.js | 1 + src/input/modules/custom-date/controls.js | 20 ++++++++++++++------ src/input/modules/custom-date/editor.scss | 4 +++- src/input/modules/custom-date/index.js | 5 +++-- 4 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/input/attributes.js b/src/input/attributes.js index 3766c9a..3befb25 100644 --- a/src/input/attributes.js +++ b/src/input/attributes.js @@ -9,6 +9,7 @@ const attributes = { }, customDate: { default: { + showLabel: false, showPlaceholder: true, value: {}, }, diff --git a/src/input/modules/custom-date/controls.js b/src/input/modules/custom-date/controls.js index 1ff9e2e..0817649 100644 --- a/src/input/modules/custom-date/controls.js +++ b/src/input/modules/custom-date/controls.js @@ -2,18 +2,26 @@ import { ToggleControl } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; export default function CustomDateControls( { attribute, props, updateValue } ) { + const updateSettings = ( field, newValue ) => { + let updatedValue = structuredClone( props.attributes[ attribute ] ); + updatedValue[ field ] = newValue; + + updateValue( updatedValue, attribute ); + }; + return ( <> { - let updatedValue = structuredClone( props.attributes[ attribute ] ); - updatedValue.showPlaceholder = newValue; - - updateValue( updatedValue, attribute ); - } } + onChange={ ( newValue ) => updateSettings( 'showPlaceholder', newValue ) } + /> + updateSettings( 'showLabel', newValue ) } /> ); diff --git a/src/input/modules/custom-date/editor.scss b/src/input/modules/custom-date/editor.scss index 7293c00..5a53724 100644 --- a/src/input/modules/custom-date/editor.scss +++ b/src/input/modules/custom-date/editor.scss @@ -2,7 +2,9 @@ .form-block__date-custom { .form-block__date-custom--separator { - margin-block-end: $grid-unit-10; + font-size: $default-font-size; + margin-block-end: $grid-unit-10; // match input margin + padding-block: 6px; // match input padding &:last-child { display: none; diff --git a/src/input/modules/custom-date/index.js b/src/input/modules/custom-date/index.js index b6793ca..3592d1a 100644 --- a/src/input/modules/custom-date/index.js +++ b/src/input/modules/custom-date/index.js @@ -60,6 +60,7 @@ export function CustomDate( { props, elementProps } ) { setAttributes, } = props; const { + showLabel, showPlaceholder, value, } = customDate; @@ -93,12 +94,12 @@ export function CustomDate( { props, elementProps } ) {
{ label } - + { fields.map( ( field, index ) => ( onFieldUpdate( field, value ) } { ...elementProps } From 7c8a4b5087dd07cb9a46745adf3594831f7926f2 Mon Sep 17 00:00:00 2001 From: Matthias Kittsteiner Date: Sat, 15 Jun 2024 11:43:04 +0200 Subject: [PATCH 10/27] =?UTF-8?q?=E2=9C=A8=20Add=20custom=20datetime-local?= =?UTF-8?q?=20field=20See=20#38?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/input/html-data.js | 14 +++- src/input/modules/custom-date/editor.scss | 4 - src/input/modules/custom-date/index.js | 89 +++++++++++++++-------- 3 files changed, 71 insertions(+), 36 deletions(-) diff --git a/src/input/html-data.js b/src/input/html-data.js index fb0b2bd..8273e43 100644 --- a/src/input/html-data.js +++ b/src/input/html-data.js @@ -54,11 +54,8 @@ const types = applyFilters( 'customDate', 'disabled', 'label', - 'max', - 'min', 'readOnly', 'required', - 'step', ], }, 'datetime-local': { @@ -74,6 +71,17 @@ const types = applyFilters( 'step', ], }, + 'datetime-local-custom': { + allowedAttributes: [ + 'ariaDescription', + 'autoComplete', + 'customDate', + 'disabled', + 'label', + 'readOnly', + 'required', + ], + }, email: { allowedAttributes: [ 'autoComplete', diff --git a/src/input/modules/custom-date/editor.scss b/src/input/modules/custom-date/editor.scss index 5a53724..da27dae 100644 --- a/src/input/modules/custom-date/editor.scss +++ b/src/input/modules/custom-date/editor.scss @@ -5,10 +5,6 @@ font-size: $default-font-size; margin-block-end: $grid-unit-10; // match input margin padding-block: 6px; // match input padding - - &:last-child { - display: none; - } } .is-type-year { diff --git a/src/input/modules/custom-date/index.js b/src/input/modules/custom-date/index.js index 3592d1a..7ecc1f0 100644 --- a/src/input/modules/custom-date/index.js +++ b/src/input/modules/custom-date/index.js @@ -64,18 +64,37 @@ export function CustomDate( { props, elementProps } ) { showPlaceholder, value, } = customDate; + let fields; const fieldData = { day: { label: __( 'Day', 'form-block' ), placeholder: _x( 'DD', 'date field placeholder', 'form-block' ), + separatorAfter: _x( '/', 'date separator', 'form-block' ), + separatorBefore: '', + }, + hour: { + label: __( 'Hours', 'form-block' ), + placeholder: _x( 'HH', 'date field placeholder', 'form-block' ), + separatorAfter: _x( ':', 'time separator', 'form-block' ), + separatorBefore: _x( 'at', 'date and time separator', 'form-block' ), + }, + minute: { + label: __( 'Minutes', 'form-block' ), + placeholder: _x( 'MM', 'date field placeholder', 'form-block' ), + separatorAfter: '', + separatorBefore: '', }, month: { label: __( 'Month', 'form-block' ), placeholder: _x( 'MM', 'date field placeholder', 'form-block' ), + separatorAfter: _x( '/', 'date separator', 'form-block' ), + separatorBefore: '', }, year: { label: __( 'Year', 'form-block' ), placeholder: _x( 'YYYY', 'date field placeholder', 'form-block' ), + separatorAfter: '', + separatorBefore: '', }, } @@ -88,35 +107,47 @@ export function CustomDate( { props, elementProps } ) { switch ( type ) { case 'date-custom': - const fields = _x( 'month, day, year', 'date order in lowercase', 'form-block' ).split( ', ' ); - - return ( -
- { label } - - - { fields.map( ( field, index ) => ( - - - onFieldUpdate( field, value ) } - { ...elementProps } - placeholder={ showPlaceholder ? fieldData[ field ].placeholder : '' } - value={ value ? ( value[ field ] || '' ) : '' } - /> - - - - { _x( '/', 'date separator', 'form-block' ) } - - - ) ) } - -
- ); + fields = _x( 'month, day, year', 'date order in lowercase', 'form-block' ).split( ', ' ); + break; + case 'datetime-local-custom': + fields = _x( 'month, day, year, hour, minute', 'date order in lowercase', 'form-block' ).split( ', ' ); + break; } - return null; + return ( +
+ { label } + + + { fields.map( ( field, index ) => ( + + { fieldData[ field ].separatorBefore + ? + { fieldData[ field ].separatorBefore } + + : null + } + + + onFieldUpdate( field, value ) } + { ...elementProps } + placeholder={ showPlaceholder ? fieldData[ field ].placeholder : '' } + value={ value ? ( value[ field ] || '' ) : '' } + /> + + + { fieldData[ field ].separatorAfter + ? + { fieldData[ field ].separatorAfter } + + : null + } + + ) ) } + +
+ ); } From 46715d36453bef5763ce3123d5b8e229ea5c2c84 Mon Sep 17 00:00:00 2001 From: Matthias Kittsteiner Date: Sat, 15 Jun 2024 11:46:55 +0200 Subject: [PATCH 11/27] =?UTF-8?q?=E2=9C=A8=20Add=20custom=20time=20field?= =?UTF-8?q?=20See=20#38?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/input/html-data.js | 11 +++++++++++ src/input/modules/custom-date/editor.scss | 4 ++++ src/input/modules/custom-date/index.js | 3 +++ 3 files changed, 18 insertions(+) diff --git a/src/input/html-data.js b/src/input/html-data.js index 8273e43..b11b363 100644 --- a/src/input/html-data.js +++ b/src/input/html-data.js @@ -243,6 +243,17 @@ const types = applyFilters( 'step', ], }, + 'time-custom': { + allowedAttributes: [ + 'ariaDescription', + 'autoComplete', + 'customDate', + 'disabled', + 'label', + 'readOnly', + 'required', + ], + }, text: { allowedAttributes: [ 'autoComplete', diff --git a/src/input/modules/custom-date/editor.scss b/src/input/modules/custom-date/editor.scss index da27dae..a5464a7 100644 --- a/src/input/modules/custom-date/editor.scss +++ b/src/input/modules/custom-date/editor.scss @@ -5,6 +5,10 @@ font-size: $default-font-size; margin-block-end: $grid-unit-10; // match input margin padding-block: 6px; // match input padding + + &.is-before:first-child { + display: none; + } } .is-type-year { diff --git a/src/input/modules/custom-date/index.js b/src/input/modules/custom-date/index.js index 7ecc1f0..d5030c1 100644 --- a/src/input/modules/custom-date/index.js +++ b/src/input/modules/custom-date/index.js @@ -112,6 +112,9 @@ export function CustomDate( { props, elementProps } ) { case 'datetime-local-custom': fields = _x( 'month, day, year, hour, minute', 'date order in lowercase', 'form-block' ).split( ', ' ); break; + case 'time-custom': + fields = _x( 'hour, minute', 'date order in lowercase', 'form-block' ).split( ', ' ); + break; } return ( From 8fc33e2f113c558a8b4595503f819abbf0387b31 Mon Sep 17 00:00:00 2001 From: Matthias Kittsteiner Date: Sat, 15 Jun 2024 11:48:20 +0200 Subject: [PATCH 12/27] =?UTF-8?q?=E2=9C=A8=20Add=20custom=20month=20field?= =?UTF-8?q?=20See=20#38?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/input/html-data.js | 11 +++++++++++ src/input/modules/custom-date/index.js | 3 +++ 2 files changed, 14 insertions(+) diff --git a/src/input/html-data.js b/src/input/html-data.js index b11b363..a851b2d 100644 --- a/src/input/html-data.js +++ b/src/input/html-data.js @@ -140,6 +140,17 @@ const types = applyFilters( 'step', ], }, + 'month-custom': { + allowedAttributes: [ + 'ariaDescription', + 'autoComplete', + 'customDate', + 'disabled', + 'label', + 'readOnly', + 'required', + ], + }, number: { allowedAttributes: [ 'autoComplete', diff --git a/src/input/modules/custom-date/index.js b/src/input/modules/custom-date/index.js index d5030c1..5392c68 100644 --- a/src/input/modules/custom-date/index.js +++ b/src/input/modules/custom-date/index.js @@ -112,6 +112,9 @@ export function CustomDate( { props, elementProps } ) { case 'datetime-local-custom': fields = _x( 'month, day, year, hour, minute', 'date order in lowercase', 'form-block' ).split( ', ' ); break; + case 'month-custom': + fields = _x( 'month, year', 'date order in lowercase', 'form-block' ).split( ', ' ); + break; case 'time-custom': fields = _x( 'hour, minute', 'date order in lowercase', 'form-block' ).split( ', ' ); break; From 77d9aca720d0b69ceae44b044b71206b04ca45aa Mon Sep 17 00:00:00 2001 From: Matthias Kittsteiner Date: Sat, 15 Jun 2024 11:50:09 +0200 Subject: [PATCH 13/27] =?UTF-8?q?=E2=9C=A8=20Add=20custom=20week=20field?= =?UTF-8?q?=20See=20#38?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/input/html-data.js | 11 +++++++++++ src/input/modules/custom-date/index.js | 9 +++++++++ 2 files changed, 20 insertions(+) diff --git a/src/input/html-data.js b/src/input/html-data.js index a851b2d..96b76ae 100644 --- a/src/input/html-data.js +++ b/src/input/html-data.js @@ -308,5 +308,16 @@ const types = applyFilters( 'step', ], }, + 'week-custom': { + allowedAttributes: [ + 'ariaDescription', + 'autoComplete', + 'customDate', + 'disabled', + 'label', + 'readOnly', + 'required', + ], + }, }, ); diff --git a/src/input/modules/custom-date/index.js b/src/input/modules/custom-date/index.js index 5392c68..25b2139 100644 --- a/src/input/modules/custom-date/index.js +++ b/src/input/modules/custom-date/index.js @@ -90,6 +90,12 @@ export function CustomDate( { props, elementProps } ) { separatorAfter: _x( '/', 'date separator', 'form-block' ), separatorBefore: '', }, + week: { + label: __( 'Weak', 'form-block' ), + placeholder: _x( 'WK', 'date field placeholder', 'form-block' ), + separatorAfter: _x( '/', 'date separator', 'form-block' ), + separatorBefore: '', + }, year: { label: __( 'Year', 'form-block' ), placeholder: _x( 'YYYY', 'date field placeholder', 'form-block' ), @@ -118,6 +124,9 @@ export function CustomDate( { props, elementProps } ) { case 'time-custom': fields = _x( 'hour, minute', 'date order in lowercase', 'form-block' ).split( ', ' ); break; + case 'week-custom': + fields = _x( 'week, year', 'date order in lowercase', 'form-block' ).split( ', ' ); + break; } return ( From 511c11ddbaee28cbbe4bb6ed4cd33ca173578ed3 Mon Sep 17 00:00:00 2001 From: Matthias Kittsteiner Date: Sat, 15 Jun 2024 16:05:06 +0200 Subject: [PATCH 14/27] =?UTF-8?q?=E2=9C=A8=20Add=20frontend=20for=20custom?= =?UTF-8?q?=20date=20fields=20See=20#38?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/style/form.scss | 34 ++++ inc/class-form-block.php | 13 ++ inc/modules/class-custom-date.php | 251 +++++++++++++++++++++++++ src/input/modules/custom-date/index.js | 50 +---- 4 files changed, 304 insertions(+), 44 deletions(-) create mode 100644 inc/modules/class-custom-date.php diff --git a/assets/style/form.scss b/assets/style/form.scss index e63b714..8832f49 100644 --- a/assets/style/form.scss +++ b/assets/style/form.scss @@ -12,6 +12,10 @@ flex: 0 0 100%; } + .form-block__element { + margin-bottom: 0; + } + &.is-type-checkbox, &.is-type-radio { align-items: flex-start; @@ -32,6 +36,36 @@ } } +.form-block__input-container { + align-items: center; + column-gap: 8px; + display: flex; + + > .form-block__source { + max-width: 75px; + + .is-sub-type-year & { + max-width: 120px; + } + } +} + +.form-block__input-group { + border: 0; + column-gap: 8px; + display: flex; + flex-wrap: wrap; + padding: 0; + + > legend { + flex: 0 0 100%; + } + + > .form-block__element:first-of-type .form-block__date-custom--separator.is-before { + display: none; + } +} + .form-block__message-container { &.is-type-loading { align-items: center; diff --git a/inc/class-form-block.php b/inc/class-form-block.php index 3acdff7..360f4c3 100644 --- a/inc/class-form-block.php +++ b/inc/class-form-block.php @@ -2,6 +2,7 @@ namespace epiphyt\Form_Block; use DOMDocument; +use epiphyt\Form_Block\modules\Custom_Date; use epiphyt\Form_Block\block_data\Data as Block_Data_Data; use epiphyt\Form_Block\blocks\Form; use epiphyt\Form_Block\blocks\Input; @@ -19,6 +20,13 @@ final class Form_Block { const MAX_INT = 2147483647; + /** + * @var array Registered Modules + */ + public array $modules = [ + Custom_Date::class, + ]; + /** * @var array List of block name attributes */ @@ -46,6 +54,11 @@ public function init(): void { Input::get_instance()->init(); Select::get_instance()->init(); Textarea::get_instance()->init(); + + foreach ( $this->modules as $key => $asset ) { + $this->modules[ $key ] = new $asset(); + $this->modules[ $key ]->init(); + } } /** diff --git a/inc/modules/class-custom-date.php b/inc/modules/class-custom-date.php new file mode 100644 index 0000000..32c369a --- /dev/null +++ b/inc/modules/class-custom-date.php @@ -0,0 +1,251 @@ + $field ) { + $container = $dom->createElement( 'div' ); + $input_container = $dom->createElement( 'div' ); + $input_node = $dom->createElement( 'input' ); + $label_classes = 'form-block__label is-input-label'; + $label_content_node = $dom->createElement( 'span', $field['label'] ); + $label_node = $dom->createElement( 'label' ); + $separators = []; + + foreach ( $field['separator'] as $position => $value ) { + if ( ! empty( $value ) ) { + $separators[ $position ] = $dom->createElement( 'span' ); + $separators[ $position ]->setAttribute( 'class', 'form-block__date-custom--separator is-' . $position ); + $separators[ $position ]->textContent = $value; + } + } + + $input_node->setAttribute( 'class', $field_data['class'] ); + $input_node->setAttribute( 'id', $field_data['id'] . '-' . $type ); + $input_node->setAttribute( 'name', $field_data['name'] . '[' . $type . ']' ); + $input_node->setAttribute( 'type', 'text' ); + $input_node->setAttribute( 'value', $block_data['value'][ $type ] ?? '' ); + + if ( $field_data['is_required'] ) { + $input_node->setAttribute( 'required', '' ); + } + + if ( $block_data['showPlaceholder'] ) { + $input_node->setAttribute( 'placeholder', $field['placeholder'] ); + } + + if ( empty( $block_data['showLabel'] ) ) { + $label_classes .= ' screen-reader-text'; + } + + $container->setAttribute( 'class', 'form-block__element is-type-text is-sub-type-' . $type ); + $input_container->setAttribute( 'class', 'form-block__input-container' ); + $label_content_node->setAttribute( 'class', 'form-block__label-content' ); + $label_node->setAttribute( 'class', $label_classes ); + $label_node->setAttribute( 'for', $field_data['id'] . '-' . $type ); + $label_node->appendChild( $label_content_node ); + + if ( ! empty( $separators['before'] ) ) { + $input_container->appendChild( $separators['before'] ); + } + + $input_container->appendChild( $input_node ); + + if ( ! empty( $separators['after'] ) ) { + $input_container->appendChild( $separators['after'] ); + } + + $container->appendChild( $input_container ); + $container->appendChild( $label_node ); + $element->appendChild( $container ); + } + } + + /** + * Enqueue editor assets. + */ + public static function enqueue_editor_assets(): void { + \wp_localize_script( 'form-block-input-editor-script', 'formBlockInputCustomDate', self::get_field_data() ); + } + + /** + * Get field data. + * + * @param array $order Field data order + * @return array Field data + */ + public static function get_field_data( array $order = [] ): array { + $fields = [ + 'day' => [ + 'label' => \__( 'Day', 'form-block' ), + 'placeholder' => \_x( 'DD', 'date field placeholder', 'form-block' ), + 'separator' => [ + 'after' => \_x( '/', 'date separator', 'form-block' ), + 'before' => '', + ], + ], + 'hour' => [ + 'label' => \__( 'Hours', 'form-block' ), + 'placeholder' => \_x( 'HH', 'date field placeholder', 'form-block' ), + 'separator' => [ + 'after' => \_x( ':', 'time separator', 'form-block' ), + 'before' => \_x( 'at', 'date and time separator', 'form-block' ), + ], + ], + 'minute' => [ + 'label' => \__( 'Minutes', 'form-block' ), + 'placeholder' => \_x( 'MM', 'date field placeholder', 'form-block' ), + 'separator' => [ + 'after' => '', + 'before' => '', + ], + ], + 'month' => [ + 'label' => \__( 'Month', 'form-block' ), + 'placeholder' => \_x( 'MM', 'date field placeholder', 'form-block' ), + 'separator' => [ + 'after' => \_x( '/', 'date separator', 'form-block' ), + 'before' => '', + ], + ], + 'week' => [ + 'label' => \__( 'Week', 'form-block' ), + 'placeholder' => \_x( 'WK', 'date field placeholder', 'form-block' ), + 'separator' => [ + 'after' => \_x( '/', 'date separator', 'form-block' ), + 'before' => '', + ], + ], + 'year' => [ + 'label' => \__( 'Year', 'form-block' ), + 'placeholder' => \_x( 'YYYY', 'date field placeholder', 'form-block' ), + 'separator' => [ + 'after' => '', + 'before' => '', + ], + ], + ]; + + if ( empty( $order ) ) { + return $fields; + } + + return \array_merge( \array_flip( $order ), $fields ); + } + + /** + * Set markup for a custom date field. + * + * @param string $block_content The block content + * @param array $block Block attributes + * @return string Updated block content + */ + public static function set_markup( string $block_content, array $block ): string { + $dom = new DOMDocument(); + $dom->loadHTML( + '' . $block_content . '', + \LIBXML_HTML_NOIMPLIED | \LIBXML_HTML_NODEFDTD + ); + $xpath = new DOMXPath( $dom ); + /** @var \DOMElement $input_node */ + $input_node = $xpath->query( '//div[contains(@class, "wp-block-form-block-input")]//input' )->item( 0 ); + $label_node = $xpath->query( '//div[contains(@class, "wp-block-form-block-input")]//label' )->item( 0 ); + $label_content_node = $xpath->query( '//div[contains(@class, "wp-block-form-block-input")]//span[contains(@class, "form-block__label-content")]' )->item( 0 ); + $field_data = [ + 'class' => $input_node->getAttribute( 'class' ), + 'id' => $input_node->getAttribute( 'id' ), + 'is_required' => $input_node->hasAttribute( 'required' ) ? true : false, + 'name' => $input_node->getAttribute( 'name' ), + 'type' => $input_node->getAttribute( 'type' ), + ]; + + if ( ! \str_ends_with( $field_data['type'], '-custom' ) ) { + return $block_content; + } + + $fieldset = $dom->createElement( 'fieldset' ); + $legend = $dom->createElement( 'legend' ); + $legend_content = $dom->createElement( 'span', $label_content_node->textContent ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + $legend_content->setAttribute( 'class', 'form-block__label-content' ); + $legend->appendChild( $legend_content ); + + if ( $field_data['is_required'] ) { + $required_symbol = $dom->createElement( 'span' ); + $required_symbol->setAttribute( 'class', 'is-required' ); + $required_symbol->setAttribute( 'aria-hidden', 'true' ); + $required_symbol->textContent = '*'; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + $legend->appendChild( $required_symbol ); + } + + $fieldset->appendChild( $legend ); + $fieldset->setAttribute( 'class', 'form-block__input-group' ); + + switch ( $field_data['type'] ) { + case 'date-custom': + $order = \explode( ', ', \_x( 'month, day, year', 'date order in lowercase', 'form-block' ) ); + break; + case 'datetime-local-custom': + $order = \explode( ', ', \_x( 'month, day, year, hour, minute', 'date order in lowercase', 'form-block' ) ); + break; + case 'month-custom': + $order = \explode( ', ', \_x( 'month, year', 'date order in lowercase', 'form-block' ) ); + break; + case 'time-custom': + $order = \explode( ', ', \_x( 'hour, minute', 'date order in lowercase', 'form-block' ) ); + break; + case 'week-custom': + $order = \explode( ', ', \_x( 'week, year', 'date order in lowercase', 'form-block' ) ); + break; + } + + if ( ! isset( $order ) ) { + return $block_content; + } + + $fields = \array_intersect_key( self::get_field_data( $order ), \array_flip( $order ) ); + $block_data = \wp_parse_args( + $block['attrs']['customDate'] ?? [], + [ + 'showPlaceholder' => true, + 'showLabel' => false, + 'value' => [], + ] + ); + + self::add_date_fields( $fields, $dom, $fieldset, $field_data, $block_data ); + $input_node->parentNode->appendChild( $fieldset ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + $input_node->parentNode->removeChild( $input_node ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + $label_node->parentNode->removeChild( $label_node ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + + return str_replace( [ '', '' ], '', $dom->saveHTML( $dom->documentElement ) ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + } +} diff --git a/src/input/modules/custom-date/index.js b/src/input/modules/custom-date/index.js index 25b2139..1fac6ab 100644 --- a/src/input/modules/custom-date/index.js +++ b/src/input/modules/custom-date/index.js @@ -65,44 +65,6 @@ export function CustomDate( { props, elementProps } ) { value, } = customDate; let fields; - const fieldData = { - day: { - label: __( 'Day', 'form-block' ), - placeholder: _x( 'DD', 'date field placeholder', 'form-block' ), - separatorAfter: _x( '/', 'date separator', 'form-block' ), - separatorBefore: '', - }, - hour: { - label: __( 'Hours', 'form-block' ), - placeholder: _x( 'HH', 'date field placeholder', 'form-block' ), - separatorAfter: _x( ':', 'time separator', 'form-block' ), - separatorBefore: _x( 'at', 'date and time separator', 'form-block' ), - }, - minute: { - label: __( 'Minutes', 'form-block' ), - placeholder: _x( 'MM', 'date field placeholder', 'form-block' ), - separatorAfter: '', - separatorBefore: '', - }, - month: { - label: __( 'Month', 'form-block' ), - placeholder: _x( 'MM', 'date field placeholder', 'form-block' ), - separatorAfter: _x( '/', 'date separator', 'form-block' ), - separatorBefore: '', - }, - week: { - label: __( 'Weak', 'form-block' ), - placeholder: _x( 'WK', 'date field placeholder', 'form-block' ), - separatorAfter: _x( '/', 'date separator', 'form-block' ), - separatorBefore: '', - }, - year: { - label: __( 'Year', 'form-block' ), - placeholder: _x( 'YYYY', 'date field placeholder', 'form-block' ), - separatorAfter: '', - separatorBefore: '', - }, - } const onFieldUpdate = ( field, fieldValue ) => { let newValue = structuredClone( customDate ); @@ -136,9 +98,9 @@ export function CustomDate( { props, elementProps } ) { { fields.map( ( field, index ) => ( - { fieldData[ field ].separatorBefore + { formBlockInputCustomDate[ field ].separator.before ? - { fieldData[ field ].separatorBefore } + { formBlockInputCustomDate[ field ].separator.before } : null } @@ -146,17 +108,17 @@ export function CustomDate( { props, elementProps } ) { onFieldUpdate( field, value ) } { ...elementProps } - placeholder={ showPlaceholder ? fieldData[ field ].placeholder : '' } + placeholder={ showPlaceholder ? formBlockInputCustomDate[ field ].placeholder : '' } value={ value ? ( value[ field ] || '' ) : '' } /> - { fieldData[ field ].separatorAfter + { formBlockInputCustomDate[ field ].separator.after ? - { fieldData[ field ].separatorAfter } + { formBlockInputCustomDate[ field ].separator.after } : null } From 9ad42f5226955971d0dbc7925823b2987066f134 Mon Sep 17 00:00:00 2001 From: Matthias Kittsteiner Date: Sat, 15 Jun 2024 16:19:29 +0200 Subject: [PATCH 15/27] =?UTF-8?q?=F0=9F=92=84=20Add=20styling=20for=20Twen?= =?UTF-8?q?ty=20Twenty-Four=20See=20#39?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/style/twenty-twenty-four.scss | 44 ++++++++++++++++++++++++++++ inc/class-theme-styles.php | 13 ++++++-- webpack.config.js | 1 + 3 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 assets/style/twenty-twenty-four.scss diff --git a/assets/style/twenty-twenty-four.scss b/assets/style/twenty-twenty-four.scss new file mode 100644 index 0000000..e90596b --- /dev/null +++ b/assets/style/twenty-twenty-four.scss @@ -0,0 +1,44 @@ +.form-block__element { + input:not([type="reset"]):not([type="submit"]), + textarea { + border: 1px solid #949494; + font-family: inherit; + font-size: 1em; + } + + input:not([type="checkbox"]):not([type="radio"]):not([type="reset"]):not([type="submit"]), + textarea { + box-sizing: border-box; + display: block; + padding: calc(.667em + 2px); + width: 100%; + } + + input[type="reset"], + input[type="submit"] { + align-self: flex-start; + } + + select { + -moz-appearance: none; + -webkit-appearance: none; + appearance: none; + background: url("data:image/svg+xml;utf8,") no-repeat right calc(.667em + 2px) top 56%; + border: 1px solid #949494; + border-radius: 0; + font-size: 1em; + line-height: 1.6; + padding: calc(.667em + 2px) calc(1.333em + 12px) calc(.667em + 2px) calc(.667em + 2px); + } + + &.is-type-checkbox, + &.is-type-radio { + input { + margin-top: .35em; + } + + .form-block__label { + margin-left: .3em; + } + } +} diff --git a/inc/class-theme-styles.php b/inc/class-theme-styles.php index 16364ee..821d9df 100644 --- a/inc/class-theme-styles.php +++ b/inc/class-theme-styles.php @@ -61,7 +61,10 @@ public function is_theme( $name ) { * @return array Updated block styles */ public function register_block_styles( array $styles ): array { - if ( $this->is_theme( 'Twenty Twenty-Three' ) ) { + if ( $this->is_theme( 'Twenty Twenty-Four' ) ) { + $styles[] = 'form-block-twenty-twenty-four'; + } + else if ( $this->is_theme( 'Twenty Twenty-Three' ) ) { $styles[] = 'form-block-twenty-twenty-three'; } else if ( $this->is_theme( 'Twenty Twenty-Two' ) ) { @@ -78,7 +81,13 @@ public function register_styles(): void { $is_debug = defined( 'WP_DEBUG' ) && WP_DEBUG; $suffix = ( $is_debug ? '' : '.min' ); - if ( $this->is_theme( 'Twenty Twenty-Three' ) ) { + if ( $this->is_theme( 'Twenty Twenty-Four' ) ) { + $file_path = \plugin_dir_path( EPI_FORM_BLOCK_FILE ) . 'assets/style/build/twenty-twenty-four' . $suffix . '.css'; + $file_url = \plugin_dir_url( EPI_FORM_BLOCK_FILE ) . 'assets/style/build/twenty-twenty-four' . $suffix . '.css'; + + \wp_register_style( 'form-block-twenty-twenty-four', $file_url, [ 'form-block' ], $is_debug ? \filemtime( $file_path ) : \FORM_BLOCK_VERSION ); + } + else if ( $this->is_theme( 'Twenty Twenty-Three' ) ) { $file_path = plugin_dir_path( EPI_FORM_BLOCK_FILE ) . 'assets/style/build/twenty-twenty-three' . $suffix . '.css'; $file_url = plugin_dir_url( EPI_FORM_BLOCK_FILE ) . 'assets/style/build/twenty-twenty-three' . $suffix . '.css'; diff --git a/webpack.config.js b/webpack.config.js index 7e99436..9faffa7 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -12,6 +12,7 @@ const jsFiles = { }; const scssFiles = { 'form': path.resolve( process.cwd(), 'assets/style', 'form.scss' ), + 'twenty-twenty-four': path.resolve( process.cwd(), 'assets/style', 'twenty-twenty-four.scss' ), 'twenty-twenty-three': path.resolve( process.cwd(), 'assets/style', 'twenty-twenty-three.scss' ), 'twenty-twenty-two': path.resolve( process.cwd(), 'assets/style', 'twenty-twenty-two.scss' ), }; From 3314db9f4c1100e4464ebb62efbad2fe1d7a4f80 Mon Sep 17 00:00:00 2001 From: Matthias Kittsteiner Date: Sun, 16 Jun 2024 10:35:45 +0200 Subject: [PATCH 16/27] =?UTF-8?q?=E2=9C=A8=20Format=20custom=20date=20fiel?= =?UTF-8?q?ds=20in=20the=20output=20See=20#38?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- inc/form-data/class-validation.php | 7 ++- inc/modules/class-custom-date.php | 99 ++++++++++++++++++++++++------ 2 files changed, 86 insertions(+), 20 deletions(-) diff --git a/inc/form-data/class-validation.php b/inc/form-data/class-validation.php index 16dc9b4..ada046a 100644 --- a/inc/form-data/class-validation.php +++ b/inc/form-data/class-validation.php @@ -92,7 +92,12 @@ private function by_attributes( $value, array $attributes ): array { $validated = sanitize_textarea_field( $value ); break; default: - $validated = sanitize_text_field( $value ); + if ( \is_array( $value ) ) { + $validated = \array_map( 'sanitize_text_field', $value ); + } + else { + $validated = sanitize_text_field( $value ); + } break; } diff --git a/inc/modules/class-custom-date.php b/inc/modules/class-custom-date.php index 32c369a..7d7a649 100644 --- a/inc/modules/class-custom-date.php +++ b/inc/modules/class-custom-date.php @@ -5,6 +5,7 @@ use DOMElement; use DOMException; use DOMXPath; +use epiphyt\Form_Block\form_data\Data; /** * Custom date module. @@ -14,11 +15,23 @@ * @package epiphyt\Form_Block */ final class Custom_Date { + /** + * @var array Supported custom date field types + */ + public static array $field_types = [ + 'date-custom', + 'datetime-local-custom', + 'month-custom', + 'time-custom', + 'week-custom', + ]; + /** * Initialize the class. */ public function init(): void { \add_action( 'enqueue_block_editor_assets', [ self::class, 'enqueue_editor_assets' ] ); + \add_filter( 'form_block_output_field_value', [ self::class, 'set_output_format' ], 10, 3 ); \add_filter( 'render_block_form-block/input', [ self::class, 'set_markup' ], 15, 2 ); } @@ -162,6 +175,37 @@ public static function get_field_data( array $order = [] ): array { return \array_merge( \array_flip( $order ), $fields ); } + /** + * Get the field order. + * + * @param string $type Field type + * @return array Field order + */ + public static function get_field_order( string $type ): array { + switch ( $type ) { + case 'date-custom': + $order = \explode( ', ', \_x( 'month, day, year', 'date order in lowercase', 'form-block' ) ); + break; + case 'datetime-local-custom': + $order = \explode( ', ', \_x( 'month, day, year, hour, minute', 'date order in lowercase', 'form-block' ) ); + break; + case 'month-custom': + $order = \explode( ', ', \_x( 'month, year', 'date order in lowercase', 'form-block' ) ); + break; + case 'time-custom': + $order = \explode( ', ', \_x( 'hour, minute', 'date order in lowercase', 'form-block' ) ); + break; + case 'week-custom': + $order = \explode( ', ', \_x( 'week, year', 'date order in lowercase', 'form-block' ) ); + break; + default: + $order = []; + break; + } + + return $order; + } + /** * Set markup for a custom date field. * @@ -208,26 +252,9 @@ public static function set_markup( string $block_content, array $block ): string $fieldset->appendChild( $legend ); $fieldset->setAttribute( 'class', 'form-block__input-group' ); + $order = self::get_field_order( $field_data['type'] ); - switch ( $field_data['type'] ) { - case 'date-custom': - $order = \explode( ', ', \_x( 'month, day, year', 'date order in lowercase', 'form-block' ) ); - break; - case 'datetime-local-custom': - $order = \explode( ', ', \_x( 'month, day, year, hour, minute', 'date order in lowercase', 'form-block' ) ); - break; - case 'month-custom': - $order = \explode( ', ', \_x( 'month, year', 'date order in lowercase', 'form-block' ) ); - break; - case 'time-custom': - $order = \explode( ', ', \_x( 'hour, minute', 'date order in lowercase', 'form-block' ) ); - break; - case 'week-custom': - $order = \explode( ', ', \_x( 'week, year', 'date order in lowercase', 'form-block' ) ); - break; - } - - if ( ! isset( $order ) ) { + if ( empty( $order ) ) { return $block_content; } @@ -248,4 +275,38 @@ public static function set_markup( string $block_content, array $block ): string return str_replace( [ '', '' ], '', $dom->saveHTML( $dom->documentElement ) ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase } + + /** + * Set the output format. + * + * @param mixed $value Post value + * @param string $name Field name + * @param array $field_data Form field data + * @return mixed Output in proper format + */ + public static function set_output_format( mixed $value, string $name, array $field_data ): mixed { + $field = Data::get_instance()->get_field_data_by_name( $name, $field_data['fields'] ); + + if ( ! \in_array( $field['type'], self::$field_types, true ) ) { + return $value; + } + + $field_order = self::get_field_order( $field['type'] ); + $format_data = \array_intersect_key( self::get_field_data( $field_order ), \array_flip( $field_order ) ); + $output = ''; + + foreach ( $format_data as $format_type => $field_format ) { + // don't start with a separator + if ( ! empty( $output ) ) { + $output .= $field_format['separator']['before'] ?? ''; + } + + if ( ! empty( $value[ $format_type ] ) && \is_string( $value[ $format_type ] ) ) { + $output .= $value[ $format_type ]; + $output .= $field_format['separator']['after'] ?? ''; + } + } + + return $output; + } } From 48146b80d9b37975d4b02bdcb0f6052579f32e8e Mon Sep 17 00:00:00 2001 From: Matthias Kittsteiner Date: Sun, 16 Jun 2024 10:48:54 +0200 Subject: [PATCH 17/27] =?UTF-8?q?=E2=99=BF=EF=B8=8F=20Only=20update=20aria?= =?UTF-8?q?-describedby=20if=20the=20error=20ID=20doesn't=20exist=20yet=20?= =?UTF-8?q?See=20#40?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/js/validation.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/assets/js/validation.js b/assets/js/validation.js index 3c7f927..6ca2a0e 100644 --- a/assets/js/validation.js +++ b/assets/js/validation.js @@ -128,7 +128,10 @@ document.addEventListener( 'DOMContentLoaded', function() { const setAriaDescribedBy = ( field ) => { const innerError = field.parentNode.querySelector( '.inline-error' ); innerError.id = field.id + '__inline-error'; - field.setAttribute( 'aria-describedby', ( ( field.getAttribute( 'aria-describedby' ) || '' ) + ' ' + innerError.id ).trim() ); + + if ( ! field.hasAttribute( 'aria-describedby' ) || ! field.getAttribute( 'aria-describedby' ).includes( innerError.id ) ) { + field.setAttribute( 'aria-describedby', ( ( field.getAttribute( 'aria-describedby' ) || '' ) + ' ' + innerError.id ).trim() ); + } } for ( const form of forms ) { From 0d5b370a421dbbaf8bd3db32390b1ff21ff1ad53 Mon Sep 17 00:00:00 2001 From: Matthias Kittsteiner Date: Sun, 16 Jun 2024 13:03:07 +0200 Subject: [PATCH 18/27] =?UTF-8?q?=E2=9C=A8=20Add=20custom=20date=20fields?= =?UTF-8?q?=20validation=20#38?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/js/validation.js | 70 ++++++++++++++++++++++++++----- inc/modules/class-custom-date.php | 53 ++++++++++++++++++++++- 2 files changed, 110 insertions(+), 13 deletions(-) diff --git a/assets/js/validation.js b/assets/js/validation.js index 6ca2a0e..a7ad79a 100644 --- a/assets/js/validation.js +++ b/assets/js/validation.js @@ -96,6 +96,46 @@ FormValidator.prototype.tests.url = function( field, data ) { return this.texts.url; }; +const adjustMultiFieldErrors = ( data ) => { + const parentField = data.field.closest( '.form-block__element' ); + let innerError = document.getElementById( data.field.id + '__inline-error' ); + + if ( innerError ) { + innerError.remove(); + } + + if ( data.valid ) { + data.field.removeAttribute( 'aria-invalid' ); + + return; + } + + const adjacentField = parentField.closest( '.form-block__element:not(.is-sub-element)' ); + innerError = document.createElement( 'div' ); + const labelContent = parentField.querySelector( '.form-block__label-content' ).textContent; + innerError.id = data.field.id + '__inline-error'; + innerError.textContent = labelContent + ': ' + data.error; + innerError.classList.add( 'inline-error' ); + parentField.classList.add( 'form-error' ); + adjacentField.classList.add( 'form-error' ); + adjacentField.appendChild( innerError ); + setAriaDescribedBy( data.field, adjacentField ); + data.field.closest( '.form-block__element' ).querySelector( '.inline-error' ).remove(); + data.field.ariaInvalid = true; + +} + +const setAriaDescribedBy = ( field, parentField ) => { + const fieldToExtend = parentField || field; + const errorId = field.id + '__inline-error'; + const innerError = document.getElementById( errorId ) || fieldToExtend.parentNode.querySelector( '.inline-error:not([id])' ); + innerError.id = errorId; + + if ( ! field.hasAttribute( 'aria-describedby' ) || ! field.getAttribute( 'aria-describedby' ).includes( errorId ) ) { + field.setAttribute( 'aria-describedby', ( ( field.getAttribute( 'aria-describedby' ) || '' ) + ' ' + errorId ).trim() ); + } +} + const validator = new FormValidator( { classes: { alert: 'inline-error', @@ -125,15 +165,6 @@ document.addEventListener( 'DOMContentLoaded', function() { const forms = document.querySelectorAll( '.wp-block-form-block-form' ); let typingTimeout; - const setAriaDescribedBy = ( field ) => { - const innerError = field.parentNode.querySelector( '.inline-error' ); - innerError.id = field.id + '__inline-error'; - - if ( ! field.hasAttribute( 'aria-describedby' ) || ! field.getAttribute( 'aria-describedby' ).includes( innerError.id ) ) { - field.setAttribute( 'aria-describedby', ( ( field.getAttribute( 'aria-describedby' ) || '' ) + ' ' + innerError.id ).trim() ); - } - } - for ( const form of forms ) { form.validator = validator; @@ -157,6 +188,14 @@ document.addEventListener( 'DOMContentLoaded', function() { } } + if ( event.target.closest( '.form-block__input-group' ) ) { + let data = validator.checkField( event.target ); + data.field = event.target; + adjustMultiFieldErrors( data ); + + return; + } + const container = event.target.closest( '[class^="wp-block-form-block-"]' ); if ( container && result.valid ) { @@ -187,8 +226,13 @@ document.addEventListener( 'DOMContentLoaded', function() { let invalidFields = []; const validatorResult = validator.checkAll( this ); - validatorResult.fields.forEach( function( field, index, array ) { - if ( field.field.type !== 'file' ) { + validatorResult.fields.reverse().forEach( function( field, index, array ) { + if ( field.field.closest( '.form-block__input-group' ) ) { + adjustMultiFieldErrors( field ); + + return; + } + else if ( field.field.type !== 'file' ) { if ( ! field.valid ) { setAriaDescribedBy( field.field ); invalidFields.push( field ); @@ -321,6 +365,10 @@ document.addEventListener( 'DOMContentLoaded', function() { if ( formErrors ) { for ( const formError of formErrors ) { + if ( formError.classList.contains( 'has-sub-elements' ) ) { + continue; + } + if ( formError.classList.contains( 'wp-block-form-block-input' ) ) { formError.querySelector( 'input' ).ariaInvalid = true; } diff --git a/inc/modules/class-custom-date.php b/inc/modules/class-custom-date.php index 7d7a649..0095e60 100644 --- a/inc/modules/class-custom-date.php +++ b/inc/modules/class-custom-date.php @@ -63,9 +63,13 @@ private static function add_date_fields( array $fields, DOMDocument $dom, DOMEle } $input_node->setAttribute( 'class', $field_data['class'] ); + $input_node->setAttribute( 'data-validate-length-range', $field['validation']['min-length'] . ',' . $field['validation']['max-length'] ); + $input_node->setAttribute( 'data-validate-minmax', $field['validation']['min'] . ',' . $field['validation']['max'] ); $input_node->setAttribute( 'id', $field_data['id'] . '-' . $type ); + $input_node->setAttribute( 'max', $field['validation']['max'] ); + $input_node->setAttribute( 'min', $field['validation']['min'] ); $input_node->setAttribute( 'name', $field_data['name'] . '[' . $type . ']' ); - $input_node->setAttribute( 'type', 'text' ); + $input_node->setAttribute( 'type', 'number' ); $input_node->setAttribute( 'value', $block_data['value'][ $type ] ?? '' ); if ( $field_data['is_required'] ) { @@ -80,7 +84,7 @@ private static function add_date_fields( array $fields, DOMDocument $dom, DOMEle $label_classes .= ' screen-reader-text'; } - $container->setAttribute( 'class', 'form-block__element is-type-text is-sub-type-' . $type ); + $container->setAttribute( 'class', 'form-block__element is-sub-element is-type-text is-sub-type-' . $type ); $input_container->setAttribute( 'class', 'form-block__input-container' ); $label_content_node->setAttribute( 'class', 'form-block__label-content' ); $label_node->setAttribute( 'class', $label_classes ); @@ -125,6 +129,13 @@ public static function get_field_data( array $order = [] ): array { 'after' => \_x( '/', 'date separator', 'form-block' ), 'before' => '', ], + 'validation' => [ + 'max' => 31, + 'max-length' => 2, + 'min' => 1, + 'min-length' => 2, + 'type' => 'numeric', + ], ], 'hour' => [ 'label' => \__( 'Hours', 'form-block' ), @@ -133,6 +144,13 @@ public static function get_field_data( array $order = [] ): array { 'after' => \_x( ':', 'time separator', 'form-block' ), 'before' => \_x( 'at', 'date and time separator', 'form-block' ), ], + 'validation' => [ + 'max' => 24, + 'max-length' => 2, + 'min' => 0, + 'min-length' => 2, + 'type' => 'numeric', + ], ], 'minute' => [ 'label' => \__( 'Minutes', 'form-block' ), @@ -141,6 +159,13 @@ public static function get_field_data( array $order = [] ): array { 'after' => '', 'before' => '', ], + 'validation' => [ + 'max' => 59, + 'max-length' => 2, + 'min' => 0, + 'min-length' => 2, + 'type' => 'numeric', + ], ], 'month' => [ 'label' => \__( 'Month', 'form-block' ), @@ -149,6 +174,13 @@ public static function get_field_data( array $order = [] ): array { 'after' => \_x( '/', 'date separator', 'form-block' ), 'before' => '', ], + 'validation' => [ + 'max' => 12, + 'max-length' => 2, + 'min' => 1, + 'min-length' => 2, + 'type' => 'numeric', + ], ], 'week' => [ 'label' => \__( 'Week', 'form-block' ), @@ -157,6 +189,13 @@ public static function get_field_data( array $order = [] ): array { 'after' => \_x( '/', 'date separator', 'form-block' ), 'before' => '', ], + 'validation' => [ + 'max' => 53, + 'max-length' => 2, + 'min' => 1, + 'min-length' => 2, + 'type' => 'numeric', + ], ], 'year' => [ 'label' => \__( 'Year', 'form-block' ), @@ -165,6 +204,13 @@ public static function get_field_data( array $order = [] ): array { 'after' => '', 'before' => '', ], + 'validation' => [ + 'max' => 99999, + 'max-length' => 4, + 'min' => 0, + 'min-length' => 4, + 'type' => 'numeric', + ], ], ]; @@ -220,6 +266,8 @@ public static function set_markup( string $block_content, array $block ): string \LIBXML_HTML_NOIMPLIED | \LIBXML_HTML_NODEFDTD ); $xpath = new DOMXPath( $dom ); + /** @var \DOMElement $container_node */ + $container_node = $xpath->query( '//div[contains(@class, "wp-block-form-block-input")]' )->item( 0 ); /** @var \DOMElement $input_node */ $input_node = $xpath->query( '//div[contains(@class, "wp-block-form-block-input")]//input' )->item( 0 ); $label_node = $xpath->query( '//div[contains(@class, "wp-block-form-block-input")]//label' )->item( 0 ); @@ -269,6 +317,7 @@ public static function set_markup( string $block_content, array $block ): string ); self::add_date_fields( $fields, $dom, $fieldset, $field_data, $block_data ); + $container_node->setAttribute( 'class', $container_node->getAttribute( 'class' ) . ' has-sub-elements' ); $input_node->parentNode->appendChild( $fieldset ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase $input_node->parentNode->removeChild( $input_node ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase $label_node->parentNode->removeChild( $label_node ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase From cbdde2ea5985799a81fb5f729b68b03adab7abd7 Mon Sep 17 00:00:00 2001 From: Matthias Kittsteiner Date: Sun, 16 Jun 2024 14:52:00 +0200 Subject: [PATCH 19/27] =?UTF-8?q?=F0=9F=92=84=20Display=20inputs=20in=20ba?= =?UTF-8?q?ckend=20as=20numbers=20for=20custom=20date=20fields?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/input/modules/custom-date/index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/input/modules/custom-date/index.js b/src/input/modules/custom-date/index.js index 1fac6ab..2f750b0 100644 --- a/src/input/modules/custom-date/index.js +++ b/src/input/modules/custom-date/index.js @@ -111,6 +111,7 @@ export function CustomDate( { props, elementProps } ) { label={ formBlockInputCustomDate[ field ].label } onChange={ ( value ) => onFieldUpdate( field, value ) } { ...elementProps } + type="number" placeholder={ showPlaceholder ? formBlockInputCustomDate[ field ].placeholder : '' } value={ value ? ( value[ field ] || '' ) : '' } /> From 9da4e804e4e89a3399d0c235045a0aab415112e0 Mon Sep 17 00:00:00 2001 From: Matthias Kittsteiner Date: Sun, 16 Jun 2024 14:53:53 +0200 Subject: [PATCH 20/27] =?UTF-8?q?=E2=9C=A8=20Add=20support=20for=20auto-ad?= =?UTF-8?q?ding=20leading=20zeros=20in=20custom=20date=20fields=20See=20#3?= =?UTF-8?q?8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/js/multi-field.js | 28 ++++++++++++++++++++++++++++ inc/blocks/class-form.php | 5 +++++ inc/modules/class-custom-date.php | 3 +++ webpack.config.js | 1 + 4 files changed, 37 insertions(+) create mode 100644 assets/js/multi-field.js diff --git a/assets/js/multi-field.js b/assets/js/multi-field.js new file mode 100644 index 0000000..9698615 --- /dev/null +++ b/assets/js/multi-field.js @@ -0,0 +1,28 @@ +document.addEventListener( 'DOMContentLoaded', () => { + const multiFieldInputs = document.querySelectorAll( '.form-block__element.is-sub-element input[data-max-length]' ); + + for ( const multiFieldInput of multiFieldInputs ) { + multiFieldInput.addEventListener( 'input', ( event ) => addLeadingZero( event.currentTarget ) ); + } +} ); + +/** + * Add leading zeros to an element. + * + * @see https://stackoverflow.com/a/72864152/3461955 + * + * @param {HTMLElement} element The element to add zeros to + * @param {string} [attribute='data-max-length'] The attribute to check for + */ +function addLeadingZero( element, attribute = 'data-max-length' ) { + const maxLength = parseInt( element.getAttribute( attribute ) ); + const isNegative = parseInt( element.value ) < 0 + let newValue = ( '0'.repeat( maxLength ) + Math.abs( element.value ).toString() ).slice( -maxLength ); + + if ( isNegative ) { + newValue = '-' + newValue; + } + + element.value = newValue; +} + diff --git a/inc/blocks/class-form.php b/inc/blocks/class-form.php index dea683b..9943da1 100644 --- a/inc/blocks/class-form.php +++ b/inc/blocks/class-form.php @@ -263,6 +263,11 @@ public static function register_block(): void { public function register_frontend_assets(): void { $is_debug = defined( 'WP_DEBUG' ) && WP_DEBUG; $suffix = ( $is_debug ? '' : '.min' ); + $file_path = \plugin_dir_path( \EPI_FORM_BLOCK_FILE ) . 'assets/js/' . ( $is_debug ? '' : 'build/' ) . 'multi-field' . $suffix . '.js'; + $file_url = \plugin_dir_url( \EPI_FORM_BLOCK_FILE ) . 'assets/js/' . ( $is_debug ? '' : 'build/' ) . 'multi-field' . $suffix . '.js'; + + \wp_register_script( 'form-block-multi-field', $file_url, [ 'form-block-form' ], $is_debug ? \filemtime( $file_path ) : \FORM_BLOCK_VERSION, true ); + $file_path = plugin_dir_path( EPI_FORM_BLOCK_FILE ) . 'assets/js/vendor/validator' . $suffix . '.js'; $file_url = plugin_dir_url( EPI_FORM_BLOCK_FILE ) . 'assets/js/vendor/validator' . $suffix . '.js'; diff --git a/inc/modules/class-custom-date.php b/inc/modules/class-custom-date.php index 0095e60..7e40fa0 100644 --- a/inc/modules/class-custom-date.php +++ b/inc/modules/class-custom-date.php @@ -63,6 +63,7 @@ private static function add_date_fields( array $fields, DOMDocument $dom, DOMEle } $input_node->setAttribute( 'class', $field_data['class'] ); + $input_node->setAttribute( 'data-max-length', $field['validation']['max-length'] ); $input_node->setAttribute( 'data-validate-length-range', $field['validation']['min-length'] . ',' . $field['validation']['max-length'] ); $input_node->setAttribute( 'data-validate-minmax', $field['validation']['min'] . ',' . $field['validation']['max'] ); $input_node->setAttribute( 'id', $field_data['id'] . '-' . $type ); @@ -306,6 +307,8 @@ public static function set_markup( string $block_content, array $block ): string return $block_content; } + \wp_enqueue_script( 'form-block-multi-field' ); + $fields = \array_intersect_key( self::get_field_data( $order ), \array_flip( $order ) ); $block_data = \wp_parse_args( $block['attrs']['customDate'] ?? [], diff --git a/webpack.config.js b/webpack.config.js index 9faffa7..e1875bd 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -8,6 +8,7 @@ const mode = isProduction ? 'production' : 'development'; const jsFiles = { 'form': path.resolve( process.cwd(), 'assets/js', 'form.js' ), + 'multi-field': path.resolve( process.cwd(), 'assets/js', 'multi-field.js' ), 'validation': path.resolve( process.cwd(), 'assets/js', 'validation.js' ), }; const scssFiles = { From d28e515cf4a4c2f84958a43cd3f12f8dcd5718bd Mon Sep 17 00:00:00 2001 From: Matthias Kittsteiner Date: Sun, 16 Jun 2024 16:18:22 +0200 Subject: [PATCH 21/27] =?UTF-8?q?=E2=9C=A8=20Handling=20date=20paste=20sup?= =?UTF-8?q?port=20for=20custom=20date=20fields=20See=20#38?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/js/multi-field.js | 76 ++++++++++++++++++++++++++++++- inc/modules/class-custom-date.php | 1 + 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/assets/js/multi-field.js b/assets/js/multi-field.js index 9698615..22d2521 100644 --- a/assets/js/multi-field.js +++ b/assets/js/multi-field.js @@ -2,10 +2,13 @@ document.addEventListener( 'DOMContentLoaded', () => { const multiFieldInputs = document.querySelectorAll( '.form-block__element.is-sub-element input[data-max-length]' ); for ( const multiFieldInput of multiFieldInputs ) { - multiFieldInput.addEventListener( 'input', ( event ) => addLeadingZero( event.currentTarget ) ); + multiFieldInput.addEventListener( 'input', onInput ); + multiFieldInput.addEventListener( 'paste', handlePaste ); } } ); +const onInput = ( event ) => addLeadingZero( event.currentTarget ); + /** * Add leading zeros to an element. * @@ -26,3 +29,74 @@ function addLeadingZero( element, attribute = 'data-max-length' ) { element.value = newValue; } +/** + * Handle pasting into custom date fields. + * + * @param {Event} event Paste event + */ +function handlePaste( event ) { + const currentTarget = event.currentTarget; + const isFirstInput = !! currentTarget.closest( '.form-block__element.is-sub-element:first-of-type' ); + + if ( ! isFirstInput ) { + return; + } + + const container = currentTarget.closest( '.form-block__element:not(.is-sub-element)' ); + const inputs = container.querySelectorAll( 'input' ); + const format = getFormat( inputs ); + const paste = ( event.clipboardData || event.originalEvent.clipboardData || window.clipboardData ).getData( 'text' ) || ''; + const matches = paste.match( new RegExp( format ) ); + + if ( matches ) { + event.preventDefault(); + + for ( let i = 0; i < inputs.length; i++ ) { + inputs[ i ].value = matches[ 2 * i + 1 ]; + } + } +} + +/** + * Get regular expression format from a pasted string. + * + * @param {HTMLCollection} inputs List of inputs + * @returns {string} Regular expression string + */ +function getFormat( inputs ) { + let isFirst = true; + let format = '^'; + + const escape = ( string, symbol ) => { + let newString; + + for ( let i = 0; i < string.length; i++ ) { + newString = symbol + string.charAt( i ); + } + + return newString; + } + + for ( const input of inputs ) { + if ( ! isFirst ) { + if ( input.previousElementSibling ) { + format += ' ' + input.previousElementSibling.textContent + ' '; + } + } + else { + isFirst = false; + } + + format += '([0-9]{' + input.getAttribute( 'data-validate-length-range' ) + '})'; + format += '('; + + if ( input.nextElementSibling ) { + format += escape( input.nextElementSibling.textContent, '\\' ); + } + } + + format = format.replace( /\($/, '' ); + format += '?)'.repeat( inputs.length ); + + return format.replace( /\)$/, '' ); +} diff --git a/inc/modules/class-custom-date.php b/inc/modules/class-custom-date.php index 7e40fa0..bf63058 100644 --- a/inc/modules/class-custom-date.php +++ b/inc/modules/class-custom-date.php @@ -64,6 +64,7 @@ private static function add_date_fields( array $fields, DOMDocument $dom, DOMEle $input_node->setAttribute( 'class', $field_data['class'] ); $input_node->setAttribute( 'data-max-length', $field['validation']['max-length'] ); + $input_node->setAttribute( 'data-type', $type ); $input_node->setAttribute( 'data-validate-length-range', $field['validation']['min-length'] . ',' . $field['validation']['max-length'] ); $input_node->setAttribute( 'data-validate-minmax', $field['validation']['min'] . ',' . $field['validation']['max'] ); $input_node->setAttribute( 'id', $field_data['id'] . '-' . $type ); From 07bd8bb908f645f5c7f10e2c8417b533d54c422a Mon Sep 17 00:00:00 2001 From: Matthias Kittsteiner Date: Sun, 16 Jun 2024 16:25:08 +0200 Subject: [PATCH 22/27] =?UTF-8?q?=F0=9F=92=84=20Make=20input=20types=20tra?= =?UTF-8?q?nslatable=20and=20more=20descriptive=20See=20#41?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/input/controls.js | 4 ++-- src/input/html-data.js | 29 ++++++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/src/input/controls.js b/src/input/controls.js index 6d9d67d..a9d5b54 100644 --- a/src/input/controls.js +++ b/src/input/controls.js @@ -18,7 +18,7 @@ import { getAttributeHelp, } from '../data/attributes'; import { getSanitizedAttributeValue, stripSpecialChars } from '../data/util'; -import { getTypes, isAllowedAttribute } from './html-data'; +import { getTypes, isAllowedAttribute, types } from './html-data'; import CustomDateControls from './modules/custom-date/controls'; export default function Controls( props ) { @@ -210,7 +210,7 @@ export default function Controls( props ) { setAttributes( { type } ) } - options={ getTypes().map( ( type ) => ( { label: type, value: type } ) ) } + options={ getTypes().map( ( type ) => ( { label: types[ type ].label, value: type } ) ) } value={ type } /> { return types[ type ].allowedAttributes; @@ -14,7 +15,7 @@ export const isAllowedAttribute = ( type, attribute ) => { return types[ type ].allowedAttributes.includes( attribute ); } -const types = applyFilters( +export const types = applyFilters( 'formBlock.input.htmlTypes', { checkbox: { @@ -25,6 +26,7 @@ const types = applyFilters( 'required', 'value', ], + label: __( 'Checkbox', 'form-block' ), }, color: { allowedAttributes: [ @@ -33,6 +35,7 @@ const types = applyFilters( 'disabled', 'label', ], + label: __( 'Color selection', 'form-block' ), }, date: { allowedAttributes: [ @@ -46,6 +49,7 @@ const types = applyFilters( 'required', 'step', ], + label: __( 'Datei', 'form-block' ), }, 'date-custom': { allowedAttributes: [ @@ -57,6 +61,7 @@ const types = applyFilters( 'readOnly', 'required', ], + label: __( 'Date with separate fields', 'form-block' ), }, 'datetime-local': { allowedAttributes: [ @@ -70,6 +75,7 @@ const types = applyFilters( 'required', 'step', ], + label: __( 'Date and time', 'form-block' ), }, 'datetime-local-custom': { allowedAttributes: [ @@ -81,6 +87,7 @@ const types = applyFilters( 'readOnly', 'required', ], + label: __( 'Date and time with separate fields', 'form-block' ), }, email: { allowedAttributes: [ @@ -97,6 +104,7 @@ const types = applyFilters( 'required', 'size', ], + label: __( 'E-mail', 'form-block' ), }, file: { allowedAttributes: [ @@ -109,9 +117,11 @@ const types = applyFilters( 'readOnly', 'required', ], + label: __( 'File', 'form-block' ), }, hidden: { allowedAttributes: [], + label: __( 'Hidden', 'form-block' ), }, image: { allowedAttributes: [ @@ -126,6 +136,7 @@ const types = applyFilters( 'src', 'width', ], + label: __( 'Image', 'form-block' ), }, month: { allowedAttributes: [ @@ -139,6 +150,7 @@ const types = applyFilters( 'required', 'step', ], + label: __( 'Month', 'form-block' ), }, 'month-custom': { allowedAttributes: [ @@ -150,6 +162,7 @@ const types = applyFilters( 'readOnly', 'required', ], + label: __( 'Month with separate fields', 'form-block' ), }, number: { allowedAttributes: [ @@ -163,6 +176,7 @@ const types = applyFilters( 'required', 'step', ], + label: __( 'Number', 'form-block' ), }, password: { allowedAttributes: [ @@ -178,6 +192,7 @@ const types = applyFilters( 'required', 'size', ], + label: __( 'Password (input not visible)', 'form-block' ), }, radio: { allowedAttributes: [ @@ -187,6 +202,7 @@ const types = applyFilters( 'required', 'value', ], + label: __( 'Radio button', 'form-block' ), }, range: { allowedAttributes: [ @@ -198,12 +214,14 @@ const types = applyFilters( 'min', 'step', ], + label: __( 'Range', 'form-block' ), }, reset: { allowedAttributes: [ 'disabled', 'value', ], + label: __( 'Reset button', 'form-block' ), }, search: { allowedAttributes: [ @@ -220,12 +238,14 @@ const types = applyFilters( 'required', 'size', ], + label: __( 'Search', 'form-block' ), }, submit: { allowedAttributes: [ 'disabled', 'value', ], + label: __( 'Submit button', 'form-block' ), }, tel: { allowedAttributes: [ @@ -240,6 +260,7 @@ const types = applyFilters( 'required', 'size', ], + label: __( 'Telephone', 'form-block' ), }, time: { allowedAttributes: [ @@ -253,6 +274,7 @@ const types = applyFilters( 'required', 'step', ], + label: __( 'Time', 'form-block' ), }, 'time-custom': { allowedAttributes: [ @@ -264,6 +286,7 @@ const types = applyFilters( 'readOnly', 'required', ], + label: __( 'Time with separate fields', 'form-block' ), }, text: { allowedAttributes: [ @@ -279,6 +302,7 @@ const types = applyFilters( 'required', 'size', ], + label: __( 'Text', 'form-block' ), }, url: { allowedAttributes: [ @@ -294,6 +318,7 @@ const types = applyFilters( 'required', 'size', ], + label: __( 'URL', 'form-block' ), }, week: { allowedAttributes: [ @@ -307,6 +332,7 @@ const types = applyFilters( 'required', 'step', ], + label: __( 'Week', 'form-block' ), }, 'week-custom': { allowedAttributes: [ @@ -318,6 +344,7 @@ const types = applyFilters( 'readOnly', 'required', ], + label: __( 'Week with separate fields', 'form-block' ), }, }, ); From 8e11401b20bf2009a73f4f5a63972a5a0df69280 Mon Sep 17 00:00:00 2001 From: Matthias Kittsteiner Date: Sun, 16 Jun 2024 21:31:15 +0200 Subject: [PATCH 23/27] =?UTF-8?q?=F0=9F=92=84=20Remove=20default=20padding?= =?UTF-8?q?=20from=20fieldset=20legend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/style/form.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/style/form.scss b/assets/style/form.scss index 8832f49..4a2337c 100644 --- a/assets/style/form.scss +++ b/assets/style/form.scss @@ -59,6 +59,7 @@ > legend { flex: 0 0 100%; + padding: 0; } > .form-block__element:first-of-type .form-block__date-custom--separator.is-before { From ec164c75494ddd723a8aff1959506d4bd4765f38 Mon Sep 17 00:00:00 2001 From: Matthias Kittsteiner Date: Mon, 17 Jun 2024 08:22:47 +0200 Subject: [PATCH 24/27] =?UTF-8?q?=F0=9F=90=9B=20Fix=20using=20the=20wizard?= =?UTF-8?q?=20See=20#42?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/form/wizard.js | 67 +++++++++++++++++++++++++++++----------------- 1 file changed, 43 insertions(+), 24 deletions(-) diff --git a/src/form/wizard.js b/src/form/wizard.js index 81f623f..4ccf5ab 100644 --- a/src/form/wizard.js +++ b/src/form/wizard.js @@ -66,15 +66,37 @@ export default function Wizard( props ) { const preparedFields = fields.split( ',' ).map( ( field ) => field.trim() ); for ( const preparedField of preparedFields ) { + let isAdded = false; + const isRequired = preparedField.includes( '*' ); + const fieldLabel = preparedField.replace( '*', '' ); + checkFieldTypeLoop: for ( const fieldType of Object.keys( fieldMatches ) ) { for ( const potentialMatch of fieldMatches[ fieldType ] ) { - console.log( {preparedField, potentialMatch} ); - const isRequired = preparedField.includes( '*' ); - const fieldLabel = preparedField.replace( '*', '' ); + if ( ! preparedField.toLowerCase().includes( potentialMatch ) ) { + continue; + } - if ( preparedField.toLowerCase().includes( potentialMatch ) ) { - if ( fieldType !== 'textarea' ) { + switch ( fieldType ) { + case 'select': + blocks.push( [ + 'form-block/select', + { + label: fieldLabel, + required: isRequired, + }, + ] ); + break; + case 'textarea': + blocks.push( [ + 'form-block/textarea', + { + label: fieldLabel, + required: isRequired, + }, + ] ); + break; + default: let blockAttributes = { label: fieldLabel, required: isRequired, @@ -93,26 +115,23 @@ export default function Wizard( props ) { 'form-block/input', blockAttributes, ] ); - } - else { - blocks.push( [ - 'form-block/textarea', - { - label: fieldLabel, - required: isRequired, - }, - ] ); - } + break; } - else { - blocks.push( [ - 'form-block/input', - { - label: fieldLabel, - required: isRequired, - type: 'text', - }, - ] ); + + isAdded = true; + break checkFieldTypeLoop; + } + } + + if ( ! isAdded ) { + blocks.push( [ + 'form-block/input', + { + label: fieldLabel, + required: isRequired, + type: 'text', + }, + ] ); } break checkFieldTypeLoop; } From 8183074abea5cef34ae3b47cd3a172f2c188bcdd Mon Sep 17 00:00:00 2001 From: Matthias Kittsteiner Date: Mon, 17 Jun 2024 08:23:15 +0200 Subject: [PATCH 25/27] =?UTF-8?q?=E2=9C=A8=20Add=20new=20mappings=20for=20?= =?UTF-8?q?the=20wizard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/form/wizard.js | 68 +++++++++++++++++++++++++++++++++++++++--- src/input/html-data.js | 32 ++++++++++---------- 2 files changed, 80 insertions(+), 20 deletions(-) diff --git a/src/form/wizard.js b/src/form/wizard.js index 4ccf5ab..3f455d7 100644 --- a/src/form/wizard.js +++ b/src/form/wizard.js @@ -14,9 +14,19 @@ const fieldMatches = applyFilters( 'formBlock.wizard.fieldMatches', { checkbox: [ - _x( '?', 'potential form field name in lowercase', 'form-block' ), + _x( 'checkbox', 'potential form field name in lowercase', 'form-block' ), _x( 'consent', 'potential form field name in lowercase', 'form-block' ), ], + color: [ + _x( 'color', 'potential form field name in lowercase', 'form-block' ), + ], + 'datetime-local-custom': [ // override 'date' + _x( 'date time', 'potential form field name in lowercase', 'form-block' ), + _x( 'date and time', 'potential form field name in lowercase', 'form-block' ), + ], + 'date-custom': [ + _x( 'date', 'potential form field name in lowercase', 'form-block' ), + ], email: [ _x( 'e-mail', 'potential form field name in lowercase', 'form-block' ), _x( 'email', 'potential form field name in lowercase', 'form-block' ), @@ -26,9 +36,46 @@ const fieldMatches = applyFilters( _x( 'file', 'potential form field name in lowercase', 'form-block' ), _x( 'upload', 'potential form field name in lowercase', 'form-block' ), ], + hidden: [ + _x( 'hidden', 'potential form field name in lowercase', 'form-block' ), + _x( 'invisible', 'potential form field name in lowercase', 'form-block' ), + ], + image: [ + _x( 'image', 'potential form field name in lowercase', 'form-block' ), + _x( 'picture', 'potential form field name in lowercase', 'form-block' ), + ], + 'month-custom': [ + _x( 'month', 'potential form field name in lowercase', 'form-block' ), + ], + 'number': [ + _x( 'amount', 'potential form field name in lowercase', 'form-block' ), + _x( 'count', 'potential form field name in lowercase', 'form-block' ), + _x( 'int', 'potential form field name in lowercase', 'form-block' ), + _x( 'integer', 'potential form field name in lowercase', 'form-block' ), + _x( 'number', 'potential form field name in lowercase', 'form-block' ), + _x( 'numeric', 'potential form field name in lowercase', 'form-block' ), + ], password: [ _x( 'password', 'potential form field name in lowercase', 'form-block' ), ], + radio: [ + _x( 'choice', 'potential form field name in lowercase', 'form-block' ), + ], + range: [ + _x( 'range', 'potential form field name in lowercase', 'form-block' ), + ], + reset: [ + _x( 'cancel', 'potential form field name in lowercase', 'form-block' ), + _x( 'reset', 'potential form field name in lowercase', 'form-block' ), + ], + search: [ + _x( 'find', 'potential form field name in lowercase', 'form-block' ), + _x( 'search', 'potential form field name in lowercase', 'form-block' ), + ], + select: [ + _x( 'select', 'potential form field name in lowercase', 'form-block' ), + _x( 'selection', 'potential form field name in lowercase', 'form-block' ), + ], tel: [ _x( 'tel', 'potential form field name in lowercase', 'form-block' ), _x( 'phone', 'potential form field name in lowercase', 'form-block' ), @@ -42,7 +89,23 @@ const fieldMatches = applyFilters( _x( 'zip', 'potential form field name in lowercase', 'form-block' ), ], textarea: [ + _x( 'area', 'potential form field name in lowercase', 'form-block' ), _x( 'message', 'potential form field name in lowercase', 'form-block' ), + _x( 'multiline', 'potential form field name in lowercase', 'form-block' ), + _x( 'textarea', 'potential form field name in lowercase', 'form-block' ), + ], + 'time-custom': [ + _x( 'clock', 'potential form field name in lowercase', 'form-block' ), + _x( 'time', 'potential form field name in lowercase', 'form-block' ), + ], + url: [ + _x( 'homepage', 'potential form field name in lowercase', 'form-block' ), + _x( 'link', 'potential form field name in lowercase', 'form-block' ), + _x( 'page', 'potential form field name in lowercase', 'form-block' ), + _x( 'url', 'potential form field name in lowercase', 'form-block' ), + ], + 'week-custom': [ + _x( 'week', 'potential form field name in lowercase', 'form-block' ), ], } ); @@ -132,9 +195,6 @@ export default function Wizard( props ) { type: 'text', }, ] ); - } - break checkFieldTypeLoop; - } } } diff --git a/src/input/html-data.js b/src/input/html-data.js index 9ef4ef0..4365dee 100644 --- a/src/input/html-data.js +++ b/src/input/html-data.js @@ -262,6 +262,22 @@ export const types = applyFilters( ], label: __( 'Telephone', 'form-block' ), }, + text: { + allowedAttributes: [ + 'autoComplete', + 'dirname', + 'disabled', + 'label', + 'maxLength', + 'minLength', + 'pattern', + 'placeholder', + 'readOnly', + 'required', + 'size', + ], + label: __( 'Text', 'form-block' ), + }, time: { allowedAttributes: [ 'ariaDescription', @@ -288,22 +304,6 @@ export const types = applyFilters( ], label: __( 'Time with separate fields', 'form-block' ), }, - text: { - allowedAttributes: [ - 'autoComplete', - 'dirname', - 'disabled', - 'label', - 'maxLength', - 'minLength', - 'pattern', - 'placeholder', - 'readOnly', - 'required', - 'size', - ], - label: __( 'Text', 'form-block' ), - }, url: { allowedAttributes: [ 'ariaDescription', From 977453d35a2a0976a6d15b9c2e3f495f99b02c2f Mon Sep 17 00:00:00 2001 From: Matthias Kittsteiner Date: Mon, 17 Jun 2024 09:57:35 +0200 Subject: [PATCH 26/27] =?UTF-8?q?=E2=99=BF=EF=B8=8F=20Allow=20submitting?= =?UTF-8?q?=20wizard=20on=20enter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/form/wizard.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/form/wizard.js b/src/form/wizard.js index 3f455d7..3c2fef2 100644 --- a/src/form/wizard.js +++ b/src/form/wizard.js @@ -229,6 +229,12 @@ export default function Wizard( props ) { setIsWizardOpen( false ); }; + const onKeyPress = ( event ) => { + if ( event.key === 'Enter' ) { + onInsert(); + } + } + return ( setFields( fields ) } + onKeyPress={ onKeyPress } /> Date: Mon, 17 Jun 2024 10:12:08 +0200 Subject: [PATCH 27/27] =?UTF-8?q?=F0=9F=94=96=20Prepare=20version=201.4.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 9 +++++++++ form-block.php | 4 ++-- readme.txt | 14 ++++++++++++-- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b0f135..632e149 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 1.4.0 +* Added: Custom separated date fields (read [the announcement for more information](https://epiph.yt/en/blog/2024/form-block-1-4-0-release-and-opinions-on-date-pickers/)) +* Added: All supported input types that were previously only part of the Pro version +* Added: Design for Twenty Twenty-Four +* Added: More recognized field names for the form wizard +* Improved: Input type selection is now more descriptive and translatable +* Fixed: `aria-describedby` for error fields is no more added multiple times +* Fixed: Form wizard now returns the proper input fields + ## 1.3.0 * Added: Support block settings like font size, line height and dimensions * Added: By selecting an invalid field, the error message will now be announced to screen readers diff --git a/form-block.php b/form-block.php index 7f12751..fd56c58 100644 --- a/form-block.php +++ b/form-block.php @@ -18,7 +18,7 @@ Plugin Name: Form Block Plugin URI: https://formblock.pro/en/ Description: An extensive yet user-friendly form block. -Version: 1.3.0 +Version: 1.4.0 Author: Epiphyt Author URI: https://epiph.yt License: GPL2 @@ -43,7 +43,7 @@ // exit if ABSPATH is not defined defined( 'ABSPATH' ) || exit; -define( 'FORM_BLOCK_VERSION', '1.3.0' ); +define( 'FORM_BLOCK_VERSION', '1.4.0' ); if ( ! defined( 'EPI_FORM_BLOCK_BASE' ) ) { define( 'EPI_FORM_BLOCK_BASE', WP_PLUGIN_DIR . '/form-block/' ); diff --git a/readme.txt b/readme.txt index ba03001..fdadd3c 100644 --- a/readme.txt +++ b/readme.txt @@ -1,8 +1,8 @@ === Form Block === Contributors: epiphyt, kittmedia -Tags: contact, form, contact form, gutenberg, block editor +Tags: contact, form, contact form, gutenberg, block editor, accessibility Requires at least: 6.3 -Stable tag: 1.3.0 +Stable tag: 1.4.0 Tested up to: 6.5 Requires PHP: 7.4 License: GPL2 @@ -19,6 +19,7 @@ WordPress offers several (contact) form plugins, but most of them are not up-to- = Features = * Fully support of the block editor +* Built with accessibility in mind * Create forms with an unlimited number of fields * Select from a wide variety of field types * Use a predefined form or start from scratch @@ -94,6 +95,15 @@ You can report security bugs through the Patchstack Vulnerability Disclosure Pro == Changelog == += 1.4.0 = +* Added: Custom separated date fields (read [the announcement for more information](https://epiph.yt/en/blog/2024/form-block-1-4-0-release-and-opinions-on-date-pickers/)) +* Added: All supported input types that were previously only part of the Pro version +* Added: Design for Twenty Twenty-Four +* Added: More recognized field names for the form wizard +* Improved: Input type selection is now more descriptive and translatable +* Fixed: `aria-describedby` for error fields is no more added multiple times +* Fixed: Form wizard now returns the proper input fields + = 1.3.0 = * Added: Support block settings like font size, line height and dimensions * Added: By selecting an invalid field, the error message will now be announced to screen readers