Skip to content

Commit

Permalink
🔀 Merge branch 'main' into trunk
Browse files Browse the repository at this point in the history
  • Loading branch information
MatzeKitt committed Jun 17, 2024
2 parents f20c256 + 3e1372c commit de064f1
Show file tree
Hide file tree
Showing 27 changed files with 1,228 additions and 65 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
9 changes: 2 additions & 7 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <hey.security@epiph.yt>. 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)
3 changes: 2 additions & 1 deletion assets/js/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ) {
Expand All @@ -220,7 +221,7 @@ document.addEventListener( 'DOMContentLoaded', () => {
messageContainer.textContent = message;
// then replace all newlines with <br />
messageContainer.innerHTML = nl2br( messageContainer.innerHTML );
messageContainer.setAttribute( 'aria-live', 'assertive' );
messageContainer.setAttribute( 'aria-live', ariaLiveType );

if ( isHtml ) {
messageContainer.innerHTML = message;
Expand Down
102 changes: 102 additions & 0 deletions assets/js/multi-field.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
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', onInput );
multiFieldInput.addEventListener( 'paste', handlePaste );
}
} );

const onInput = ( 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;
}

/**
* 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( /\)$/, '' );
}
70 changes: 60 additions & 10 deletions assets/js/validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -125,13 +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' );
console.log( innerError );
innerError.id = field.id + '__inline-error';
field.setAttribute( 'aria-describedby', ( ( field.getAttribute( 'aria-describedby' ) || '' ) + ' ' + innerError.id ).trim() );
}

for ( const form of forms ) {
form.validator = validator;

Expand All @@ -155,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 ) {
Expand Down Expand Up @@ -185,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 );
Expand Down Expand Up @@ -245,7 +291,7 @@ document.addEventListener( 'DOMContentLoaded', function() {
'is-error-notice',
'screen-reader-text',
);
invalidFieldNotice.setAttribute( 'aria-live', 'assertive' );
invalidFieldNotice.ariaLive = 'assertive';
form.appendChild( invalidFieldNotice );
}

Expand Down Expand Up @@ -319,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;
}
Expand Down
35 changes: 35 additions & 0 deletions assets/style/form.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
flex: 0 0 100%;
}

.form-block__element {
margin-bottom: 0;
}

&.is-type-checkbox,
&.is-type-radio {
align-items: flex-start;
Expand All @@ -32,6 +36,37 @@
}
}

.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%;
padding: 0;
}

> .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;
Expand Down
44 changes: 44 additions & 0 deletions assets/style/twenty-twenty-four.scss
Original file line number Diff line number Diff line change
@@ -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,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10' fill='currentColor'><polygon points='0,0 10,0 5,5'/></svg>") 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;
}
}
}
4 changes: 2 additions & 2 deletions form-block.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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/' );
Expand Down
5 changes: 5 additions & 0 deletions inc/blocks/class-form.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
Loading

0 comments on commit de064f1

Please sign in to comment.