diff --git a/fame-lahjoitukset.php b/fame-lahjoitukset.php index c3cb253..0c02b6e 100644 --- a/fame-lahjoitukset.php +++ b/fame-lahjoitukset.php @@ -7,7 +7,7 @@ /** * Plugin Name: Lahjoitin * Description: Wordpress plugin for Fame lahjoitukset system. - * Version: 1.1.1 + * Version: 1.1.2 * Requires at least: 6.7 * Requires PHP: 8.3 * Author: Fame Helsinki diff --git a/src/Blocks/contact-form/block.json b/src/Blocks/contact-form/block.json index 7f81f17..06474a5 100644 --- a/src/Blocks/contact-form/block.json +++ b/src/Blocks/contact-form/block.json @@ -9,130 +9,44 @@ "parent": ["famehelsinki/donation-form"], "example": {}, "attributes": { - "showLegend": { - "type": "boolean", - "default": true - }, - "legend": { - "type": "string", - "source": "text", - "selector": "legend.fame-form__legend", - "default": "Contacts" - }, - "contact": { - "type": "boolean", - "source": "attribute", - "selector": ".fame-form__group--required:has(input[name=\"email\"])", - "attribute": "class", - "default": false - }, - "showAddress": { - "type": "boolean", - "source": "attribute", - "selector": "input[name=\"address\"]", - "attribute": "name", - "default": true - }, - "showPhone": { - "type": "boolean", - "source": "attribute", - "selector": "input[name=\"phone\"]", - "attribute": "name", - "default": true - }, - "first_name_label": { - "type": "string", - "source": "html", - "selector": ".fame-form__label[for=\"contact-first_name\"]", - "default": "First name" - }, - "last_name_label": { - "type": "string", - "source": "html", - "selector": ".fame-form__label[for=\"contact-last_name\"]", - "default": "Last name" - }, - "name_help": { - "type": "string", - "source": "html", - "selector": "#contact-name-help", - "default": "" - }, - "email_label": { - "type": "string", - "source": "html", - "selector": ".fame-form__label[for=\"contact-email\"]", - "default": "Email" - }, - "email_help": { - "type": "string", - "source": "html", - "selector": "#contact-email-help", - "default": "" - }, - "address_label": { - "type": "string", - "source": "html", - "selector": ".fame-form__label[for=\"contact-address\"]", - "default": "Address" - }, - "address_help": { - "type": "string", - "source": "html", - "selector": "#contact-address-help", - "default": "" - }, - "city_label": { - "type": "string", - "source": "html", - "selector": ".fame-form__label[for=\"contact-city\"]", - "default": "City" - }, - "city_postal_code_help": { - "type": "string", - "source": "html", - "selector": "#contact-city_postal_code-help", - "default": "City" - }, - "postal_code_label": { - "type": "string", - "source": "html", - "selector": ".fame-form__label[for=\"contact-postal_code\"]", - "default": "Postal code" - }, - "postal_code_help": { - "type": "string", - "source": "html", - "selector": "#contact-postal_code-help", - "default": "Postal code" - }, - "phone_label": { - "type": "string", - "source": "html", - "selector": ".fame-form__label[for=\"contact-phone\"]", - "default": "Phone" - }, - "phone_help": { - "type": "string", - "source": "html", - "selector": "#contact-phone-help", - "default": "" - } + "showLegend": { "type": "boolean", "default": true }, + "legend": { "type": "string", "default": "Contacts" }, + + "contact": { "type": "boolean", "default": false }, + "showAddress": { "type": "boolean", "default": true }, + "showPhone": { "type": "boolean", "default": true }, + + "first_name_label": { "type": "string", "default": "First name" }, + "last_name_label": { "type": "string", "default": "Last name" }, + "name_help": { "type": "string", "default": "" }, + + "email_label": { "type": "string", "default": "Email" }, + "email_help": { "type": "string", "default": "" }, + + "address_label": { "type": "string", "default": "Address" }, + "address_help": { "type": "string", "default": "" }, + + "city_label": { "type": "string", "default": "City" }, + "city_postal_code_help": { "type": "string", "default": "City" }, + + "postal_code_label": { "type": "string", "default": "Postal code" }, + "postal_code_help": { "type": "string", "default": "Postal code" }, + + "phone_label": { "type": "string", "default": "Phone" }, + "phone_help": { "type": "string", "default": "" }, + + "show": { "type": "boolean", "default": true }, + "legendAlign": { "type": "string", "default": "left" } }, "supports": { "multiple": false, - "color": { - "background": false, - "text": true - }, + "color": { "background": false, "text": true }, "html": false, - "typography": { - "fontSize": true - } + "typography": { "fontSize": true } }, "icon": "id", "textdomain": "fame_lahjoitukset", "editorScript": "file:./index.js", "editorStyle": "file:./index.css", - "style": "file:./style-index.css" + "render": "file:./render.php" } diff --git a/src/Blocks/contact-form/edit.css b/src/Blocks/contact-form/edit.css index e0e860d..f0edd7b 100644 --- a/src/Blocks/contact-form/edit.css +++ b/src/Blocks/contact-form/edit.css @@ -1,9 +1,6 @@ .contact-form .fame-form__fake-input { border: var(--form-element-border); padding: var(--form-element-padding); - - /* This is pretty ugly, should set line height - * somewhere, so this should be 1em * line-height */ height: 1.7em; } @@ -14,4 +11,3 @@ .fame-form__help { font-size: 14px !important; } - diff --git a/src/Blocks/contact-form/edit.tsx b/src/Blocks/contact-form/edit.tsx index 16f2774..790888c 100644 --- a/src/Blocks/contact-form/edit.tsx +++ b/src/Blocks/contact-form/edit.tsx @@ -1,6 +1,12 @@ import React from 'react' import { __ } from '@wordpress/i18n' -import { InspectorControls, RichText, useBlockProps } from '@wordpress/block-editor' +import { + InspectorControls, + RichText, + useBlockProps, + AlignmentToolbar, + BlockControls, +} from '@wordpress/block-editor' import { PanelBody, TextControl, ToggleControl } from '@wordpress/components' import ContactInputControl from './ContactInputControl.tsx' import ContactInputGroup from './ContactInputGroup.tsx' @@ -13,97 +19,149 @@ import { EditProps } from '../common/types.ts' * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#edit */ export default function Edit({ attributes, setAttributes }: EditProps): React.JSX.Element { - const { contact, showAddress, showPhone, showLegend, legend } = attributes + const { contact, showAddress, showPhone, showLegend, legend, legendAlign = 'left' } = attributes + const { show = true } = attributes as { show?: boolean } return ( <> + + setAttributes({ legendAlign: next || 'left' })} + /> + setAttributes({ contact })} - /> - setAttributes({ showAddress })} - /> - setAttributes({ showPhone })} - /> - setAttributes({ showLegend: checked })} - /> - setAttributes({ legend: value })} + label={__('Show contact fields', 'fame_lahjoitukset')} + checked={attributes.show} + onChange={value => setAttributes({ show: value })} /> + + {show && ( + <> + setAttributes({ contact: value })} + /> + setAttributes({ showAddress: value })} + /> + setAttributes({ showPhone: value })} + /> + setAttributes({ showLegend: checked })} + help={__( + 'If disabled, the legend is marked visually hidden.', + 'fame_lahjoitukset' + )} + /> + setAttributes({ legend: value })} + /> + + )}
- {showLegend && ( - setAttributes({ legend: value })} - /> - )} - - - {showAddress && ( + {show ? ( <> - setAttributes({ legend: value })} + style={{ + textAlign: legendAlign as React.CSSProperties['textAlign'], + }} + /> + )} + - + {showAddress && ( + <> + + + + )} + {showPhone && ( + + )} - )} - {showPhone && ( - + ) : ( +
+ {__( + 'The contact form is not in use. Use the toggle in the sidebar to enable it.', + 'fame_lahjoitukset' + )} +
)}
diff --git a/src/Blocks/contact-form/render.php b/src/Blocks/contact-form/render.php new file mode 100644 index 0000000..f1a95f9 --- /dev/null +++ b/src/Blocks/contact-form/render.php @@ -0,0 +1,139 @@ +|null $attributes */ +$attributes = $attributes ?? []; + +$show = array_key_exists('show', $attributes) ? (bool) $attributes['show'] : true; +if (!$show) { + return; +} + +$showLegend = array_key_exists('showLegend', $attributes) ? (bool) $attributes['showLegend'] : true; +$legend = isset($attributes['legend']) && trim((string) $attributes['legend']) !== '' + ? (string) $attributes['legend'] + : __('Contacts', 'fame_lahjoitukset'); + +$contact = array_key_exists('contact', $attributes) ? (bool) $attributes['contact'] : false; +$showAddress = array_key_exists('showAddress', $attributes) ? (bool) $attributes['showAddress'] : true; +$showPhone = array_key_exists('showPhone', $attributes) ? (bool) $attributes['showPhone'] : true; + +$wrapper_attrs = get_block_wrapper_attributes([ + 'class' => 'fame-form__fieldset', +]); + +$get = static function (array $attrs, string $key, string $fallback = ''): string { + return isset($attrs[$key]) ? (string) $attrs[$key] : $fallback; +}; + +$render_input = static function ( + array $attrs, + string $name, + string $type, + bool $required = false, + ?string $ariaDescribedBy = null +) use ($get): void { + $label = $get($attrs, "{$name}_label", ''); + $help = $get($attrs, "{$name}_help", ''); + + $aria_id = $ariaDescribedBy ?: ($help !== '' ? "contact-{$name}-help" : ''); +?> +
+ + + + class="fame-form__input" + id="" + + aria-describedby="" + /> + + + " class="fame-form__help"> + + + +
+ +
+ + + + + + + +
+ +
> + + + + + + 'first_name', 'type' => 'text', 'required' => $contact], + ['name' => 'last_name', 'type' => 'text', 'required' => $contact], + ]); + + $render_input($attributes, 'email', 'email', $contact, null); + + if ($showAddress) { + $render_input($attributes, 'address', 'text', false, null); + + $render_group($attributes, 'city_postal_code', [ + ['name' => 'city', 'type' => 'text', 'required' => false], + ['name' => 'postal_code', 'type' => 'text', 'required' => false], + ]); + } + + if ($showPhone) { + $render_input($attributes, 'phone', 'tel', false, null); + } + ?> +
\ No newline at end of file diff --git a/src/Blocks/contact-form/save.tsx b/src/Blocks/contact-form/save.tsx index 95699e6..af2f1fd 100644 --- a/src/Blocks/contact-form/save.tsx +++ b/src/Blocks/contact-form/save.tsx @@ -1,59 +1,6 @@ -import { useBlockProps } from '@wordpress/block-editor' -import React from 'react' -import { ContactInputContent } from './ContactInputControl.tsx' -import { ContactGroupContent } from './ContactInputGroup.tsx' -import { SaveProps } from '../common/types.ts' - /** - * The save function defines the way in which the different attributes should - * be combined into the final markup, which is then serialized by the block - * editor into `post_content`. + * Dynamic block – rendering happens in PHP (render.php). */ -export default function save({ attributes }: SaveProps): React.JSX.Element { - const { contact, showAddress, showPhone, showLegend, legend } = attributes - - return ( -
- {showLegend && {legend}} - - - {showAddress && ( - <> - - - - )} - {showPhone && } -
- ) +export default function save() { + return null } diff --git a/src/Blocks/donation-amounts/block.json b/src/Blocks/donation-amounts/block.json index b781053..67a4134 100644 --- a/src/Blocks/donation-amounts/block.json +++ b/src/Blocks/donation-amounts/block.json @@ -12,94 +12,46 @@ "attributes": { "settings": { "type": "array", - "source": "query", - "selector": ".donation-amounts", - "query": { - "type": { - "type": "string", - "source": "attribute", - "attribute": "data-type" + "default": [ + { + "type": "single", + "default": true, + "defaultAmount": 10, + "minAmount": 10, + "maxAmount": 10000, + "unit": "€", + "amounts": [{ "value": 10 }, { "value": 20 }, { "value": 30 }] }, - "default": { - "type": "boolean", - "source": "attribute", - "attribute": "data-default" - }, - "defaultAmount": { - "type": "integer", - "source": "attribute", - "attribute": "data-default-amount" - }, - "minAmount": { - "type": "integer", - "source": "attribute", - "attribute": "data-min-amount" - }, - "maxAmount": { - "type": "integer", - "source": "attribute", - "attribute": "data-max-amount" - }, - "unit": { - "type": "string", - "selector": ".donation-amounts__unit", - "source": "text" - }, - "amounts": { - "type": "array", - "source": "query", - "selector": "input[type=\"radio\"]", - "query": { - "value": { - "type": "string", - "source": "attribute", - "attribute": "value" - } - } + { + "type": "recurring", + "default": false, + "defaultAmount": 10, + "minAmount": 10, + "maxAmount": 10000, + "unit": "€", + "amounts": [{ "value": 10 }, { "value": 20 }, { "value": 30 }] } - } + ] }, - "showLegend": { - "type": "boolean", - "source": "attribute", - "selector": ".fame-form__legend:not(.screen-reader-text)", - "attribute": "class", - "default": true - }, - "legend": { - "type": "string", - "source": "text", - "selector": ".fame-form__legend", - "default": "Donation amount" - }, - "other": { - "type": "boolean", - "source": "attribute", - "selector": ".donation-amounts__other", - "attribute": "class", - "default": false - }, - "otherLabel": { - "type": "string", - "source": "text", - "selector": ".donation-amounts__other label", - "default": "Other amount" + "showLegend": { "type": "boolean", "default": true }, + "legend": { "type": "string", "default": "Donation amount" }, + "other": { "type": "boolean", "default": false }, + "otherLabel": { "type": "string", "default": "Other amount" }, + "legendAlign": { "type": "string", "default": "left" }, + "colsAmounts": { + "type": "number", + "default": 3 } }, "supports": { "multiple": false, - "color": { - "background": false, - "text": true - }, + "color": { "background": false, "text": true }, "html": false, - "typography": { - "fontSize": true - } + "typography": { "fontSize": true } }, "icon": "database", "textdomain": "fame_lahjoitukset", "editorScript": "file:./index.js", "editorStyle": "file:./index.css", - "style": "file:./style-index.css" + "render": "file:./render.php" } diff --git a/src/Blocks/donation-amounts/deprecated/block-v1.json b/src/Blocks/donation-amounts/deprecated/block-v1.json deleted file mode 100644 index fa9e9a4..0000000 --- a/src/Blocks/donation-amounts/deprecated/block-v1.json +++ /dev/null @@ -1,69 +0,0 @@ -{ - "attributes": { - "settings": { - "type": "array", - "source": "query", - "selector": ".donation-amounts", - "query": { - "type": { - "type": "string", - "source": "attribute", - "attribute": "data-type" - }, - "default": { - "type": "boolean", - "source": "attribute", - "attribute": "data-default" - }, - "defaultAmount": { - "type": "integer", - "source": "attribute", - "attribute": "data-default-amount" - }, - "unit": { - "type": "string", - "selector": ".donation-amounts__unit", - "source": "text" - }, - "amounts": { - "type": "array", - "source": "query", - "selector": "input[type=\"radio\"]", - "query": { - "value": { - "type": "string", - "source": "attribute", - "attribute": "value" - } - } - } - } - }, - "showLegend": { - "type": "boolean", - "source": "attribute", - "selector": ".fame-form__legend:not(.screen-reader-text)", - "attribute": "class", - "default": true - }, - "legend": { - "type": "string", - "source": "text", - "selector": ".fame-form__legend", - "default": "Donation amount" - }, - "other": { - "type": "boolean", - "source": "attribute", - "selector": ".donation-amounts__other", - "attribute": "class", - "default": false - }, - "otherLabel": { - "type": "string", - "source": "text", - "selector": ".donation-amounts__other label", - "default": "Other amount" - } - } -} diff --git a/src/Blocks/donation-amounts/deprecated/save-v1.tsx b/src/Blocks/donation-amounts/deprecated/save-v1.tsx deleted file mode 100644 index 2056d3f..0000000 --- a/src/Blocks/donation-amounts/deprecated/save-v1.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import { RichText, useBlockProps } from '@wordpress/block-editor' -import React from 'react' -import { SaveProps } from '../../common/types.ts' -import { DEFAULT_AMOUNT, DEFAULT_LEGEND } from '../../common/donation-amount.ts' -import { Attributes } from '../edit.tsx' -import { __ } from '@wordpress/i18n' - -type SaveAttributes = Attributes & { - type?: boolean -} - -/** - * The save function defines the way in which the different attributes should - * be combined into the final markup, which is then serialized by the block - * editor into `post_content`. - */ -export default function save({ attributes }: SaveProps): React.JSX.Element { - const { settings, other, otherLabel, showLegend, legend } = attributes - - // Visible if other amount is shown or amount buttons is not empty. - const visible = other || settings?.some(type => type?.amounts?.length) - const blockProps = useBlockProps.save({ - className: visible - ? 'fame-form__fieldset fame-form__fieldset--amounts' - : 'fame-form__hidden', - }) - - // Amount must be in cents. - const defaultAmount = - parseInt( - (settings?.find(type => type.default)?.defaultAmount || DEFAULT_AMOUNT).toString() - ) * 100 - - if (!visible) { - return ( -
- -
- ) - } - - return ( -
- - - {settings?.map(type => ( -
- {type.amounts?.map(({ value }, idx) => ( -
- -
- ))} - {other && ( -
-
- {type.unit} - -
- -
- )} -
- ))} - - {/* - The server only cares about `name` field. This hidden - input field must be kept up to date with radio buttons - and free amount field with javascript. - */} - -
- ) -} diff --git a/src/Blocks/donation-amounts/edit.css b/src/Blocks/donation-amounts/edit.css index 69047c7..d6d4ad8 100644 --- a/src/Blocks/donation-amounts/edit.css +++ b/src/Blocks/donation-amounts/edit.css @@ -1,34 +1,2 @@ -.donation-amounts { - display: flex; - align-items: stretch; - gap: 16px; -} - -.donation-amounts > * { - flex: 1; -} - -.wp-block-famehelsinki-donation-amounts .donation-amounts__other-label, -.wp-block-famehelsinki-donation-amounts .donation-amounts__minmax { - font-size: 16px !important; - padding-top: 5px; -} - -.wp-block-famehelsinki-donation-amounts .rich-text { - text-wrap: nowrap !important; -} - -.donation-amounts__other__placeholder, -.donation-amounts .fame-form__label { - padding: var(--form-element-padding); - border: var(--form-element-border); - - &.fame-form__label--default { - border-color: #db1414; - } -} - -.donation-amounts__other__placeholder { - align-self: stretch; - width: 10ch; -} +/* Intentionally left empty. + Styles for this block are defined in the main stylesheet. */ diff --git a/src/Blocks/donation-amounts/edit.tsx b/src/Blocks/donation-amounts/edit.tsx index 04bd67c..f81bb00 100644 --- a/src/Blocks/donation-amounts/edit.tsx +++ b/src/Blocks/donation-amounts/edit.tsx @@ -1,7 +1,20 @@ -import React, { useEffect } from 'react' +import React, { CSSProperties, useEffect } from 'react' import { __ } from '@wordpress/i18n' -import { Button, Flex, PanelBody, TextControl, ToggleControl } from '@wordpress/components' -import { InspectorControls, RichText, useBlockProps } from '@wordpress/block-editor' +import { + Button, + Flex, + PanelBody, + RangeControl, + TextControl, + ToggleControl, +} from '@wordpress/components' +import { + InspectorControls, + RichText, + useBlockProps, + AlignmentToolbar, + BlockControls, +} from '@wordpress/block-editor' import { getDonationLabel, useCurrentDonationType } from '../common/donation-type.ts' import { EditProps } from '../common/types.ts' import { @@ -25,6 +38,8 @@ export type Attributes = { legend?: string other?: boolean otherLabel?: string + legendAlign?: string + colsAmounts?: number } /** @@ -80,7 +95,15 @@ export default function Edit({ clientId, }: EditProps): React.JSX.Element { const { 'famehelsinki/donation-types': types } = context - const { settings, other, legend, showLegend, otherLabel } = attributes + const { + settings, + other, + legend, + showLegend, + otherLabel, + legendAlign = 'left', + colsAmounts: colsAmountsAttr, + } = attributes const currentType = useCurrentDonationType(clientId) const current = settings?.find(({ type }) => type === currentType) @@ -132,7 +155,12 @@ export default function Edit({ }) }, [types, currentType, settings, setAttributes]) - const blockProps = useBlockProps({ className: 'fame-form__fieldset--amounts' }) + const colsAmounts = Math.max(1, Math.min(3, colsAmountsAttr ?? 3)) + + const blockProps = useBlockProps({ + className: 'fame-form__fieldset--amounts', + style: { ['--amount-cols' as any]: String(colsAmounts) }, + }) // Use effect hook should ensure that settings will be set to an array. if (!settings) return
Loading...
@@ -142,6 +170,12 @@ export default function Edit({ return ( <> + + setAttributes({ legendAlign: next || 'left' })} + /> + setAttributes({ showLegend: value })} /> - {visible && !showLegend && ( - setAttributes({ legend: value })} - /> - )} + setAttributes({ legend: value })} + /> + setAttributes({ colsAmounts: v ?? 3 })} + /> + + ))} + + {campaigns.length < MAX_CAMPAIGNS ? ( +
+
+ { + if (e.key === 'Enter') { + e.preventDefault() + addCampaign() + } + }} + /> +
+ +
+ ) : ( +

+ {__( + 'Maximum of 10 campaigns reached.', + 'fame_lahjoitukset' + )} +

+ )} + + + )} +
+
+ +
+ {show ? ( +
+ {showLabel && ( + + )} + +
+ ) : ( +
+ {__( + 'The campaign selector is not in use. Use the toggle in the sidebar to enable it.', + 'fame_lahjoitukset' + )} +
+ )} +
+ + ) +} diff --git a/src/Blocks/donation-campaigns/index.ts b/src/Blocks/donation-campaigns/index.ts new file mode 100644 index 0000000..20f6b44 --- /dev/null +++ b/src/Blocks/donation-campaigns/index.ts @@ -0,0 +1,9 @@ +import { registerBlockType } from '@wordpress/blocks' +import Edit from './edit' +import save from './save' +import metadata from './block.json' + +registerBlockType(metadata.name, { + edit: Edit, + save, +} as any) diff --git a/src/Blocks/donation-campaigns/render.php b/src/Blocks/donation-campaigns/render.php new file mode 100644 index 0000000..c567e00 --- /dev/null +++ b/src/Blocks/donation-campaigns/render.php @@ -0,0 +1,58 @@ +|null $attributes */ +$attributes = $attributes ?? []; + +$show = array_key_exists('show', $attributes) ? (bool) $attributes['show'] : false; +$campaigns = isset($attributes['campaigns']) && is_array($attributes['campaigns']) + ? $attributes['campaigns'] + : []; + +if (!$show || empty($campaigns)) { + return; +} + +$showLabel = array_key_exists('showLabel', $attributes) ? (bool) $attributes['showLabel'] : true; +$label = isset($attributes['label']) && trim((string) $attributes['label']) !== '' + ? (string) $attributes['label'] + : __('Campaign', 'fame_lahjoitukset'); + +$wrapper_attrs = get_block_wrapper_attributes([ + 'class' => 'fame-form__group', +]); + +?> +
> + + + + + +
\ No newline at end of file diff --git a/src/Blocks/donation-campaigns/save.tsx b/src/Blocks/donation-campaigns/save.tsx new file mode 100644 index 0000000..25c9d31 --- /dev/null +++ b/src/Blocks/donation-campaigns/save.tsx @@ -0,0 +1,3 @@ +export default function save() { + return null +} diff --git a/src/Blocks/donation-form/__snapshots__/save.test.ts.snap b/src/Blocks/donation-form/__snapshots__/save.test.ts.snap deleted file mode 100644 index 7914e64..0000000 --- a/src/Blocks/donation-form/__snapshots__/save.test.ts.snap +++ /dev/null @@ -1,48 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Gutenberg Block Save Function renders correctly and matches snapshot 1`] = ` -
-
-
-
- - - - -
-
- - Loading - -
-
-
-
-`; diff --git a/src/Blocks/donation-form/block.json b/src/Blocks/donation-form/block.json index 7a4554b..818c581 100644 --- a/src/Blocks/donation-form/block.json +++ b/src/Blocks/donation-form/block.json @@ -7,31 +7,24 @@ "category": "widgets", "description": "Gutenberg block for Fame donation system.", "example": {}, - "allowedBlocks": ["core/paragraph", "core/group"], "providesContext": { "famehelsinki/donation-types": "types" }, "attributes": { "types": { - "type": "array" + "type": "array", + "default": [] + }, + "colsDesktop": { + "type": "number", + "default": 1 }, "token": { "type": "boolean", - "source": "attribute", - "selector": ".fame-form", - "attribute": "data-token" - }, - "campaign": { - "type": "string", - "source": "attribute", - "selector": ".fame-form input[name=\"campaign\"]", - "attribute": "value" + "default": false }, "returnAddress": { "type": "string", - "source": "attribute", - "selector": ".fame-form input[name=\"return_address\"]", - "attribute": "value", "default": "/" }, "primaryColor": { @@ -58,9 +51,9 @@ "type": "string", "default": "1px" }, - "useModernStyle": { - "type": "boolean", - "default": true + "dangerColor": { + "type": "string", + "default": "#dc3545" } }, "supports": { @@ -75,12 +68,15 @@ "html": false, "typography": { "fontSize": true - } + }, + "border": { "radius": true, "width": true } }, + "allowedBlocks": ["core/columns"], "icon": "money", "textdomain": "fame_lahjoitukset", "editorScript": "file:./index.js", "editorStyle": "file:./index.css", "viewScript": "file:./view.js", - "style": "file:./view.css" + "style": "file:./view.css", + "render": "file:./render.php" } diff --git a/src/Blocks/donation-form/edit.css b/src/Blocks/donation-form/edit.css index a412ca5..2767dac 100644 --- a/src/Blocks/donation-form/edit.css +++ b/src/Blocks/donation-form/edit.css @@ -1,66 +1,60 @@ -/* Editor-only base styles for the block */ -.wp-block-famehelsinki-donation-form { - --form-element-border: var(--border-width, 2px) solid var(--primary-color, #000); - --form-element-padding: 4px 16px; +/* Editor-only styles for famehelsinki/donation-form + Keep editor preview aligned with front styles */ + +.editor-styles-wrapper .wp-block-famehelsinki-donation-form { + + /* allow container queries used by inner layouts */ + container-type: inline-size; } -.fame-form__wrapper > * + * { +/* Spacing between inner blocks inside our wrapper (editor markup) */ +.editor-styles-wrapper .wp-block-famehelsinki-donation-form .fame-form__wrapper > * + * { margin-top: 16px; } -/* Gutenberg editor draws button as a div, not a real button */ -.fame-form__controls .wp-element-button { +.editor-styles-wrapper +.wp-block-famehelsinki-donation-form +.fame-form__controls +.wp-element-button { display: inline; } -.fame-form__controls .donation-type__input { - color: var(--primary-color, #000); -} - -.fame-form__row { +.editor-styles-wrapper .wp-block-famehelsinki-donation-form .fame-form__row { display: flex; flex-wrap: wrap; gap: 0 16px; } -.fame-form__row > * { +.editor-styles-wrapper .wp-block-famehelsinki-donation-form .fame-form__row > * { flex: 1; } -.fame-form__help { +.editor-styles-wrapper .wp-block-famehelsinki-donation-form .fame-form__help { font-size: 0.8em; - color: var(--third-color, #333); + color: inherit; } -.fame-form__row > .fame-form__help { +.editor-styles-wrapper .wp-block-famehelsinki-donation-form .fame-form__row > .fame-form__help { flex-basis: 100%; } -.fame-form__group--required .fame-form__label::after { +.editor-styles-wrapper +.wp-block-famehelsinki-donation-form +.fame-form__group--required +.fame-form__label::after { content: "*"; - color: #f00; + color: var(--fame-clr-danger, #f00); } -/* ========================================================= - EDITOR: MODERN STYLES (apply only when class is set) - ========================================================= */ - -.editor-styles-wrapper -.wp-block-famehelsinki-donation-form.has-modern-style -:where( - .wp-block-famehelsinki-donation-type, - .wp-block-famehelsinki-donation-amounts, - .wp-block-famehelsinki-donation-providers -) -input[type="radio"], +/* Inputs: hide radios/checks visually but keep accessible (editor) */ .editor-styles-wrapper -.wp-block-famehelsinki-donation-form.has-modern-style +.wp-block-famehelsinki-donation-form :where( .wp-block-famehelsinki-donation-type, .wp-block-famehelsinki-donation-amounts, .wp-block-famehelsinki-donation-providers ) -.fame-form__check-input { +:where(input[type="radio"], input[type="checkbox"], .fame-form__check-input) { position: absolute; width: 1px; height: 1px; @@ -73,49 +67,88 @@ input[type="radio"], border: 0; } -.editor-styles-wrapper -.wp-block-famehelsinki-donation-form.has-modern-style -:where(.fame-form__legend) { +/* Legend */ +.editor-styles-wrapper .wp-block-famehelsinki-donation-form .fame-form__legend { line-height: 1.3; margin: 0 0 0.5rem 0; } +/* Tabs / amount buttons: shared label look */ .editor-styles-wrapper -.wp-block-famehelsinki-donation-form.has-modern-style -:where(.wp-block-famehelsinki-donation-type) { +.wp-block-famehelsinki-donation-form +:where( + .wp-block-famehelsinki-donation-type .fame-form__label, + .wp-block-famehelsinki-donation-amounts .fame-form__label +) { + display: flex; + width: 100%; + min-width: 0; + box-sizing: border-box; + + justify-content: center; + align-items: center; + + padding: 0.5rem; + margin: 0; /* avoid overflow caused by per-label margins */ + border: var(--border-width, 1px) solid var(--primary-color, #000); + border-radius: var(--border-radius, 0); + + cursor: pointer; + user-select: none; +} + +/* Donation type layout in editor */ +.editor-styles-wrapper .wp-block-famehelsinki-donation-form .wp-block-famehelsinki-donation-type { display: flex; flex-wrap: wrap; gap: 10px; } +/* Amounts grid in editor */ .editor-styles-wrapper -.wp-block-famehelsinki-donation-form.has-modern-style -:where(.donation-amounts__other-edit) { - flex-direction: column-reverse; - align-items: flex-start; +.wp-block-famehelsinki-donation-form +.wp-block-famehelsinki-donation-amounts +.donation-amounts { + display: grid; + grid-template-columns: repeat(var(--amount-cols, 3), minmax(0, 1fr)); + gap: 10px; } +.editor-styles-wrapper +.wp-block-famehelsinki-donation-form +.wp-block-famehelsinki-donation-amounts +.donation-amounts > * { + min-width: 0; +} + +.editor-styles-wrapper +.wp-block-famehelsinki-donation-form +:where( + .wp-block-famehelsinki-donation-type .fame-form__label, + .wp-block-famehelsinki-donation-amounts .fame-form__label, + .wp-block-famehelsinki-donation-providers .provider-type__label +).fame-form__label--default { + background-color: var(--primary-color, #000); + color: var(--secondary-color, #fff); + border-color: var(--primary-color, #000); +} +/* Checked state (works when the input is actually toggled) */ .editor-styles-wrapper -.wp-block-famehelsinki-donation-form.has-modern-style +.wp-block-famehelsinki-donation-form :where( .wp-block-famehelsinki-donation-type .fame-form__label, .wp-block-famehelsinki-donation-amounts .fame-form__label -) { - display: flex; - width: 100%; - justify-content: center; - align-items: center; - padding: 0.5rem; - border: var(--border-width) solid var(--primary-color); - border-radius: var(--border-radius); - cursor: pointer; - user-select: none; +):has(:where(input[type="radio"], input[type="checkbox"], .fame-form__check-input):checked) { + background-color: var(--primary-color, #000); + color: var(--secondary-color, #fff); + border-color: var(--primary-color, #000); } +/* Contact form: labels are not button-like */ .editor-styles-wrapper -.wp-block-famehelsinki-donation-form.has-modern-style -:where(.wp-block-famehelsinki-contact-form) +.wp-block-famehelsinki-donation-form +.wp-block-famehelsinki-contact-form .fame-form__label { display: block; padding: 0; @@ -127,58 +160,62 @@ input[type="radio"], font-weight: 400; } +/* Editor uses fake input wrappers */ .editor-styles-wrapper -.wp-block-famehelsinki-donation-form.has-modern-style -:where(.wp-block-famehelsinki-contact-form) +.wp-block-famehelsinki-donation-form +.wp-block-famehelsinki-contact-form .fame-form__fake-input { - border: var(--border-width) solid var(--primary-color); - border-radius: var(--text-field-border-radius); + border: var(--border-width, 1px) solid var(--third-color, #000); + border-radius: var(--text-field-border-radius, 0); } +/* Other amount (editor): label + input */ .editor-styles-wrapper -.wp-block-famehelsinki-donation-form.has-modern-style -:where(.wp-block-famehelsinki-donation-amounts) -.donation-amounts { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 10px; -} - -.editor-styles-wrapper -.wp-block-famehelsinki-donation-form.has-modern-style -:where(.wp-block-famehelsinki-donation-amounts) +.wp-block-famehelsinki-donation-form +.wp-block-famehelsinki-donation-amounts .donation-amounts__other .fame-form__label { - position: relative; display: block; width: 100%; - margin-bottom: 0.3em; + margin: 0 0 0.3em 0; padding: 0; cursor: default; font-weight: 400; text-align: left; - border: var(--border-width) solid var(--primary-color); - border-radius: var(--border-radius); + border: 0; +} + +/* Make the actual input border match primary (requested) */ +.editor-styles-wrapper +.wp-block-famehelsinki-donation-form +.wp-block-famehelsinki-donation-amounts +.donation-amounts__other +:where(input, .fame-form__input) { + border: var(--border-width, 1px) solid var(--primary-color, #000) !important; + border-radius: var(--text-field-border-radius, 0) !important; + background: transparent; + color: var(--primary-color, #000); } +/* Unit positioning in editor */ .editor-styles-wrapper -.wp-block-famehelsinki-donation-form.has-modern-style -:where(.wp-block-famehelsinki-donation-amounts) +.wp-block-famehelsinki-donation-form +.wp-block-famehelsinki-donation-amounts .donation-amounts__input-wrapper { position: relative; } .editor-styles-wrapper -.wp-block-famehelsinki-donation-form.has-modern-style -:where(.wp-block-famehelsinki-donation-amounts) +.wp-block-famehelsinki-donation-form +.wp-block-famehelsinki-donation-amounts .donation-amounts__input-wrapper input { padding-right: 2em; } .editor-styles-wrapper -.wp-block-famehelsinki-donation-form.has-modern-style -:where(.wp-block-famehelsinki-donation-amounts) +.wp-block-famehelsinki-donation-form +.wp-block-famehelsinki-donation-amounts .donation-amounts__input-wrapper .donation-amounts__unit { position: absolute; @@ -189,87 +226,39 @@ input { font-size: 0.9em; } +/* Fieldsets inside editor: remove UA spacing */ .editor-styles-wrapper -.wp-block-famehelsinki-donation-form.has-modern-style -:where(.wp-block-famehelsinki-donation-amounts .fame-form__fieldset) { +.wp-block-famehelsinki-donation-form +.wp-block-famehelsinki-donation-amounts +.fame-form__fieldset, +.editor-styles-wrapper +.wp-block-famehelsinki-donation-form +.payment-method-selector.fame-form__fieldset { border: 0; padding: 0.01px 0 0; margin: 0 0 16px 0; } .editor-styles-wrapper -.wp-block-famehelsinki-donation-form.has-modern-style -:where(.wp-block-famehelsinki-donation-amounts .donation-amounts > .fame-form__group) { - margin: 0; - padding: 0; -} - -.editor-styles-wrapper -.wp-block-famehelsinki-donation-form.has-modern-style -:where( - .wp-block-famehelsinki-donation-amounts - .donation-amounts - > .fame-form__group - > .fame-form__label -) { - width: 100%; - margin: 0; - box-sizing: border-box; - display: flex; - justify-content: center; - align-items: center; -} - -.editor-styles-wrapper -.wp-block-famehelsinki-donation-form.has-modern-style -:where(.wp-block-famehelsinki-donation-amounts .donation-amounts > *) { - min-width: 0; -} - -.editor-styles-wrapper -.wp-block-famehelsinki-donation-form.has-modern-style -:where(.payment-method-selector.fame-form__fieldset) { - border: 0; - padding: 0.01px 0 0.01px; - margin: 0 0 16px 0; -} - -.editor-styles-wrapper -.wp-block-famehelsinki-donation-form.has-modern-style -:where(.wp-block-famehelsinki-form-controls) { - display: block; - padding-top: 16px; - margin-top: 0; -} - -.editor-styles-wrapper -.wp-block-famehelsinki-donation-form.has-modern-style -:where( - .payment-method-selector.fame-form__fieldset > legend, - .payment-method-selector.fame-form__fieldset > .fame-form__legend -) { +.wp-block-famehelsinki-donation-form +.payment-method-selector.fame-form__fieldset +:where(legend, .fame-form__legend) { padding: 0; margin: 0 0 0.5rem 0; } +/* Providers layout in editor */ .editor-styles-wrapper -.wp-block-famehelsinki-donation-form.has-modern-style -:where(.payment-method-selector.fame-form__fieldset > .fame-form__group) { - margin: 0; - padding: 0; -} - -.editor-styles-wrapper -.wp-block-famehelsinki-donation-form.has-modern-style -:where(.wp-block-famehelsinki-donation-providers) { +.wp-block-famehelsinki-donation-form +.wp-block-famehelsinki-donation-providers { display: flex; flex-wrap: wrap; gap: 10px; } .editor-styles-wrapper -.wp-block-famehelsinki-donation-form.has-modern-style -:where(.wp-block-famehelsinki-donation-providers) +.wp-block-famehelsinki-donation-form +.wp-block-famehelsinki-donation-providers .fame-form__group label { display: block; @@ -279,68 +268,70 @@ label { } .editor-styles-wrapper -.wp-block-famehelsinki-donation-form.has-modern-style -:where(.wp-block-famehelsinki-donation-providers) +.wp-block-famehelsinki-donation-form +.wp-block-famehelsinki-donation-providers .provider-type__label { width: 100%; + min-width: 0; box-sizing: border-box; + display: flex; justify-content: center; align-items: center; + padding: 0.5rem 1rem; - border: var(--border-width) solid var(--primary-color); - border-radius: var(--border-radius); + border: var(--border-width, 1px) solid var(--primary-color, #000); + border-radius: var(--border-radius, 0); + cursor: pointer; +} + +/* Controls spacing in editor */ +.editor-styles-wrapper .wp-block-famehelsinki-donation-form .wp-block-famehelsinki-form-controls { + display: block; + padding-top: 16px; + margin-top: 0; } .editor-styles-wrapper -.wp-block-famehelsinki-donation-form.has-modern-style -:where(.wp-block-famehelsinki-form-controls) +.wp-block-famehelsinki-donation-form +.wp-block-famehelsinki-form-controls .wp-element-button:focus-visible { filter: brightness(0.95); - outline: var(--border-width) solid var(--primary-color, var(--wp--preset--color--primary, #000)); + outline: + var(--border-width, 1px) solid + var(--primary-color, var(--wp--preset--color--primary, #000)); outline-offset: 2px; - color: var(--secondary-color); -} - -.editor-styles-wrapper -.wp-block-famehelsinki-donation-form.has-modern-style -:where(.wp-block-famehelsinki-donation-form) { - padding: 0; - border: none; + color: var(--secondary-color, #fff); } .editor-styles-wrapper -.wp-block-famehelsinki-donation-form.has-modern-style -:where(.donation-amounts__other__placeholder) { - border-color: var(--primary-color); - border-radius: var(--border-radius); - border-width: var(--border-width); - width: unset; +.wp-block-famehelsinki-donation-form +.wp-block-famehelsinki-donation-amounts +.donation-amounts__other__placeholder { + border: var(--border-width, 1px) solid var(--primary-color, #000); + border-radius: var(--border-radius, 0); + padding: 0.6rem 0.75rem; + box-sizing: border-box; } .editor-styles-wrapper -.wp-block-famehelsinki-donation-form.has-modern-style -:where(.contact-form .fame-form__fake-input) { - border-color: var(--primary-color); - border-width: var(--border-width); +.wp-block-famehelsinki-donation-form +:where(.fame-form__label, .provider-type__label) { + font-size: inherit !important; + line-height: inherit !important; } .editor-styles-wrapper -.wp-block-famehelsinki-donation-form.has-modern-style -:where(.fame-form__label.fame-form__label--default) { - background-color: var(--primary-color); - color: var(--secondary-color); +.wp-block-famehelsinki-donation-form +:where(.fame-form__label *, .provider-type__label *) { + font-size: inherit !important; } -.editor-styles-wrapper -.wp-block-famehelsinki-donation-form.has-modern-style { - container-type: inline-size; -} +/* Responsive preview inside editor (container-based) */ -@container (max-width: 700px) { +@container (max-width: 1000px) { - .wp-block-famehelsinki-donation-form.has-modern-style - .is-layout-grid { + .editor-styles-wrapper .wp-block-famehelsinki-donation-form .is-layout-grid { grid-template-columns: 1fr !important; } } diff --git a/src/Blocks/donation-form/edit.tsx b/src/Blocks/donation-form/edit.tsx index e18390f..92e12d0 100644 --- a/src/Blocks/donation-form/edit.tsx +++ b/src/Blocks/donation-form/edit.tsx @@ -1,5 +1,11 @@ +import React, { useEffect, useRef } from 'react' import { __ } from '@wordpress/i18n' -import { InspectorControls, useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor' +import { + InspectorControls, + InnerBlocks, + useBlockProps, + store as blockEditorStore, +} from '@wordpress/block-editor' import { PanelBody, TextControl, @@ -7,45 +13,106 @@ import { CheckboxControl, ColorPicker, BaseControl, + RangeControl, } from '@wordpress/components' -import React, { useEffect } from 'react' +import { useInstanceId } from '@wordpress/compose' +import { useDispatch, useSelect } from '@wordpress/data' +import { createBlock, type BlockInstance } from '@wordpress/blocks' + import { DEFAULT_DONATION_TYPE, getDonationLabel, DONATION_TYPES } from '../common/donation-type.ts' import { EditProps } from '../common/types.ts' -import { useInstanceId } from '@wordpress/compose' -const TEMPLATE_LOCK = { lock: { remove: 'true' } } -const TEMPLATE = [ - 'famehelsinki/donation-type', - 'famehelsinki/donation-amounts', - 'famehelsinki/donation-providers', - 'famehelsinki/form-controls', -].map(block => [block, TEMPLATE_LOCK, []] as const) +const TOP_ALLOWED_BLOCKS = ['core/columns'] as const + +function buildInitialLayout(colsDesktop: 1 | 2 | 3): BlockInstance[] { + const group = (inner: BlockInstance[] = [], attrs: Record = {}) => + createBlock('core/group', attrs, inner) + + const donationType = createBlock('famehelsinki/donation-type') + const donationCampaigns = createBlock('famehelsinki/donation-campaigns') + const donationAmounts = createBlock('famehelsinki/donation-amounts') + const contactForm = createBlock('famehelsinki/contact-form') + const donationProviders = createBlock('famehelsinki/donation-providers') + const formControls = createBlock('famehelsinki/form-controls') + + const g1 = group([donationType, donationCampaigns, donationAmounts]) + + const g2 = group([contactForm]) + + const g3 = group([donationProviders, formControls]) + + const columns = buildColumnsFromGroups(colsDesktop, [g1, g2, g3]) + return [columns] +} + +function buildColumnsFromGroups(colsDesktop: 1 | 2 | 3, groups: BlockInstance[]): BlockInstance { + let columnsContent: BlockInstance[][] + if (colsDesktop === 1) { + columnsContent = [[groups[0], groups[1], groups[2]]] + } else if (colsDesktop === 2) { + columnsContent = [[groups[0], groups[1]], [groups[2]]] + } else { + columnsContent = [[groups[0]], [groups[1]], [groups[2]]] + } -const ALLOWED_BLOCKS = ['core/group', 'core/paragraph'] + return createBlock( + 'core/columns', + {}, + columnsContent.map(inner => createBlock('core/column', {}, inner)) + ) +} /** - * The edit function describes the structure of your block in the context of the - * editor. This represents what the editor will render when the block is used. - * - * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#edit + * Read the 3 core/group blocks inside the top columns, in visual order. + * If not exactly 3 groups, return null (structure unexpected). */ -export default function Edit({ attributes, setAttributes }: EditProps): React.JSX.Element { +function readTopGroups(blocks: BlockInstance[]): BlockInstance[] | null { + const top = blocks?.[0] + if (!top || top.name !== 'core/columns') return null + + const groups: BlockInstance[] = [] + for (const col of top.innerBlocks ?? []) { + for (const child of col.innerBlocks ?? []) { + if (child.name === 'core/group') groups.push(child as BlockInstance) + } + } + return groups.length === 3 ? groups : null +} + +/** + * Repack: rebuild core/columns with desired col count, + * but keep the SAME 3 group blocks. + */ +function repackColumns(colsDesktop: 1 | 2 | 3, currentTop: BlockInstance, groups: BlockInstance[]) { + const next = buildColumnsFromGroups(colsDesktop, groups) + // Preserve top-level columns attributes if you ever add any to columns + return createBlock( + next.name, + { ...(currentTop.attributes as any), ...(next.attributes as any) }, + next.innerBlocks + ) +} + +export default function Edit({ + attributes, + setAttributes, + clientId, +}: EditProps & { clientId: string }): React.JSX.Element { const { types, returnAddress, - campaign, token, primaryColor, secondaryColor, thirdColor, borderRadius, borderWidth, - useModernStyle, textFieldBorderRadius, + colsDesktop, + dangerColor, } = attributes as { types?: string[] returnAddress?: string - campaign?: string primaryColor?: string secondaryColor?: string thirdColor?: string @@ -53,11 +120,12 @@ export default function Edit({ attributes, setAttributes }: EditProps): React.JS borderWidth?: string textFieldBorderRadius?: string token?: boolean - useModernStyle?: boolean + colsDesktop?: number + dangerColor?: string } - // Having a type is always required. Set a default - // value if the list is uninitialized or empty. + const cols = Math.min(3, Math.max(1, colsDesktop ?? 3)) as 1 | 2 | 3 + useEffect(() => { if (!types || types.length === 0) { setAttributes({ types: [DEFAULT_DONATION_TYPE.value] }) @@ -74,7 +142,8 @@ export default function Edit({ attributes, setAttributes }: EditProps): React.JS | '--third-color' | '--border-radius' | '--border-width' - | '--text-field-border-radius', + | '--text-field-border-radius' + | '--fame-clr-danger', string > > @@ -86,16 +155,99 @@ export default function Edit({ attributes, setAttributes }: EditProps): React.JS '--border-radius': borderRadius ?? undefined, '--border-width': borderWidth ?? undefined, '--text-field-border-radius': textFieldBorderRadius ?? undefined, + '--fame-clr-danger': dangerColor ?? undefined, } const primaryColorId = useInstanceId(BaseControl, 'primary-color') const secondaryColorId = useInstanceId(BaseControl, 'secondary-color') const thirdColorId = useInstanceId(BaseControl, 'third-color') + const dangerColorId = useInstanceId(BaseControl, 'fame-clr-danger') + + const innerBlocks = useSelect( + select => select(blockEditorStore).getBlocks(clientId) as BlockInstance[], + [clientId] + ) + const { replaceInnerBlocks } = useDispatch(blockEditorStore) + + // Keep a ref so the effect always reads the latest inner blocks without + // needing them in the dependency array (which would fire on every keystroke). + const innerBlocksRef = useRef(innerBlocks) + innerBlocksRef.current = innerBlocks + + /** + * Single effect that: + * - initializes if empty/broken + * - repacks columns when colsDesktop changes (without losing content) + * + * Intentionally omits `innerBlocks` and `replaceInnerBlocks` from deps: + * - `innerBlocks` is read via ref to avoid running on every child block change. + * - `replaceInnerBlocks` is a stable dispatch reference that never changes. + */ + useEffect(() => { + const currentInnerBlocks = innerBlocksRef.current + const nextInit = buildInitialLayout(cols) + + // Init: empty + if (!currentInnerBlocks || currentInnerBlocks.length === 0) { + replaceInnerBlocks(clientId, nextInit, false) + return + } + + const top = currentInnerBlocks[0] + const hasColumnsTop = currentInnerBlocks.length === 1 && top?.name === 'core/columns' + if (!hasColumnsTop) { + replaceInnerBlocks(clientId, nextInit, false) + return + } + + const groups = readTopGroups(currentInnerBlocks) + if (!groups) { + replaceInnerBlocks(clientId, nextInit, false) + return + } + + // Migration: insert donation-campaigns into g1 if it is missing. + const g1 = groups[0] + const hasCampaigns = g1.innerBlocks?.some(b => b.name === 'famehelsinki/donation-campaigns') + if (!hasCampaigns) { + const campaigns = createBlock('famehelsinki/donation-campaigns') + const donationTypeIdx = + g1.innerBlocks?.findIndex(b => b.name === 'famehelsinki/donation-type') ?? -1 + const insertAt = donationTypeIdx >= 0 ? donationTypeIdx + 1 : 0 + const newG1Inner = [ + ...(g1.innerBlocks?.slice(0, insertAt) ?? []), + campaigns, + ...(g1.innerBlocks?.slice(insertAt) ?? []), + ] + const newG1 = createBlock('core/group', g1.attributes, newG1Inner) + const nextTop = repackColumns(cols, top as BlockInstance, [newG1, groups[1], groups[2]]) + replaceInnerBlocks(clientId, [nextTop], false) + return + } + + // Repack columns only if count differs + const currentColCount = top.innerBlocks?.length ?? 0 + if (currentColCount !== cols) { + const nextTop = repackColumns(cols, top as BlockInstance, groups) + replaceInnerBlocks(clientId, [nextTop], false) + } + // We intentionally read innerBlocks through a ref to avoid rerunning on every block edit. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [cols, clientId]) return ( <> + setAttributes({ colsDesktop: v ?? 3 })} + /> +
t !== type) } + next.sort((a, b) => order.indexOf(a) - order.indexOf(b)) setAttributes({ types: next }) }} @@ -138,24 +291,15 @@ export default function Edit({ attributes, setAttributes }: EditProps): React.JS 'fame_lahjoitukset' )} value={returnAddress ?? ''} - onChange={returnAddress => setAttributes({ returnAddress })} - /> - setAttributes({ campaign })} + onChange={newReturnAddress => + setAttributes({ returnAddress: newReturnAddress }) + } /> + + + + + + + setAttributes({ + dangerColor: + typeof value === 'string' ? value : value?.hex || '', + }) + } + disableAlpha + /> + + setAttributes({ borderRadius: value })} /> + setAttributes({ borderWidth: value })} /> + setAttributes({ textFieldBorderRadius: value })} /> - setAttributes({ useModernStyle: value })} - /> + setAttributes({ token })} + checked={!!token} + onChange={newToken => setAttributes({ token: newToken })} /> +
+ {...useBlockProps({ + className: 'fame-form__wrapper', + style: styleVars, + })} + > + null} + /> +
) } diff --git a/src/Blocks/donation-form/render.php b/src/Blocks/donation-form/render.php new file mode 100644 index 0000000..48d431f --- /dev/null +++ b/src/Blocks/donation-form/render.php @@ -0,0 +1,123 @@ +|null $attributes + * @var string|null $content + * @var WP_Block|null $block + */ + +$attributes = $attributes ?? []; +$content = $content ?? ''; + +if (!function_exists('famehelsinki_sanitize_return_url')) { + /** + * Prevent open redirects: + * - If empty: homepage (absolute) + * - If relative "/path": convert to absolute with home_url() + * - If absolute: allow only same-host + */ + function famehelsinki_sanitize_return_url(string $url): string + { + $url = trim($url); + + $home = home_url('/'); + $site_host = wp_parse_url($home, PHP_URL_HOST); + + if (!$site_host) { + return $home; + } + + if ($url === '') { + return $home; + } + + // Relative path -> absolute + if (str_starts_with($url, '/')) { + return esc_url_raw(home_url($url)); + } + + // Absolute URL -> check host + $parsed = wp_parse_url($url); + if (!$parsed || empty($parsed['host'])) { + return $home; + } + + if ($parsed['host'] !== $site_host) { + return $home; + } + + // Keep user's absolute URL as-is (same host) + return esc_url_raw($url); + } +} + +$attrs = wp_parse_args($attributes, [ + 'returnAddress' => '/', + 'token' => false, + 'primaryColor' => '#000000', + 'secondaryColor' => '#FFFFFF', + 'thirdColor' => '#444', + 'borderRadius' => '0px', + 'borderWidth' => '1px', + 'textFieldBorderRadius' => '0px', + 'dangerColor' => '#dc3545', +]); + +$wrapper_style = sprintf( + '--primary-color:%s;--secondary-color:%s;--third-color:%s;--border-radius:%s;--border-width:%s;--text-field-border-radius:%s;--fame-clr-danger:%s;', + esc_attr($attrs['primaryColor'] ?: 'inherit'), + esc_attr($attrs['secondaryColor'] ?: 'inherit'), + esc_attr($attrs['thirdColor'] ?: 'inherit'), + esc_attr($attrs['borderRadius'] ?: 'inherit'), + esc_attr($attrs['borderWidth'] ?: 'inherit'), + esc_attr($attrs['textFieldBorderRadius'] ?: 'inherit'), + esc_attr($attrs['dangerColor'] ?: '#dc3545') +); + +$block_wrapper_attrs = get_block_wrapper_attributes([ + 'class' => 'fame-form-container fame-form__wrapper', + 'style' => $wrapper_style, +]); + +$token_attr = !empty($attrs['token']) ? ' data-token="1"' : ''; + +/** + * Render inner blocks robustly: + * - Prefer parsed block tree ($block->inner_blocks) + * - Fallback to serialized $content + */ +$inner = ''; + +if ($block instanceof WP_Block && count($block->inner_blocks) > 0) { + foreach ($block->inner_blocks as $inner_block) { + $inner .= $inner_block->render(); + } +} else { + $inner = $content; +} + +$return_address = famehelsinki_sanitize_return_url((string) ($attrs['returnAddress'] ?? '/')); +?> +
> +
novalidate> +
+ +
+ + + + +
+ +
+
+ +
+
+
\ No newline at end of file diff --git a/src/Blocks/donation-form/save.test.ts b/src/Blocks/donation-form/save.test.ts index 21c0d20..25bea08 100644 --- a/src/Blocks/donation-form/save.test.ts +++ b/src/Blocks/donation-form/save.test.ts @@ -1,17 +1,19 @@ -import { describe, it, expect } from '@jest/globals' +import { describe, it, expect, jest } from '@jest/globals' import { render } from '@testing-library/react' import save from './save' -// Mock attributes for testing -const mockAttributes = { - returnAddress: 'https://example.com/return', - campaign: 'test-campaign', - token: true, -} +jest.mock('@wordpress/block-editor', () => { + const React = require('react') + return { + InnerBlocks: { + Content: () => React.createElement('div', { 'data-testid': 'innerblocks-content' }), + }, + } +}) -describe('Gutenberg Block Save Function', () => { - it('renders correctly and matches snapshot', () => { - const { container } = render(save({ attributes: mockAttributes })) - expect(container).toMatchSnapshot() +describe('donation-form save()', () => { + it('renders InnerBlocks.Content so child blocks are serialized', () => { + const { getByTestId } = render(save() as any) + expect(getByTestId('innerblocks-content')).toBeTruthy() }) }) diff --git a/src/Blocks/donation-form/save.tsx b/src/Blocks/donation-form/save.tsx index e67e944..e574ced 100644 --- a/src/Blocks/donation-form/save.tsx +++ b/src/Blocks/donation-form/save.tsx @@ -1,63 +1,15 @@ -import { useBlockProps, useInnerBlocksProps } from '@wordpress/block-editor' import React from 'react' -import { SaveProps } from '../common/types.ts' +import { InnerBlocks } from '@wordpress/block-editor' /** - * The save function defines the way in which the different attributes should - * be combined into the final markup, which is then serialized by the block - * editor into `post_content`. + * This block is rendered dynamically on the server (render.php). * - * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#save + * We still need to output so that child blocks + * are saved into post_content. WordPress uses this serialized structure + * to restore the block tree in the editor. + * + * The actual frontend markup is generated in PHP. */ -export default function save({ attributes }: SaveProps): React.JSX.Element { - const { - returnAddress, - campaign, - token, - primaryColor, - secondaryColor, - thirdColor, - borderRadius, - borderWidth, - textFieldBorderRadius, - useModernStyle, - } = attributes - - const blockProps = useBlockProps.save({ - className: `fame-form-container ${useModernStyle ? 'has-modern-style' : ''}`, - }) - const innerBlockProps = useInnerBlocksProps.save({ - className: 'fame-form__wrapper', - style: { - '--primary-color': primaryColor || 'inherit', - '--secondary-color': secondaryColor || 'inherit', - '--third-color': thirdColor || 'inherit', - '--border-radius': borderRadius || 'inherit', - '--border-width': borderWidth || 'inherit', - '--text-field-border-radius': textFieldBorderRadius || 'inherit', - }, - }) - - return ( -
-
-
- - - - - - {campaign && } - -
-
- Loading -
-
-
- ) +export default function save() { + return } diff --git a/src/Blocks/donation-form/view.css b/src/Blocks/donation-form/view.css index 2f1731e..f712a48 100644 --- a/src/Blocks/donation-form/view.css +++ b/src/Blocks/donation-form/view.css @@ -1,14 +1,14 @@ /** - * @file Basic styling for form so that it looks OK by default. + * Fame donation form – front styles */ :root { --fame-clr-primary: #80bdff; --fame-clr-muted: #6c757d; - --fame-clr-danger: #dc3545; --border-width: 1px; /* fallback */ } +/* Base form styles */ .fame-form-container * { box-sizing: border-box; } @@ -45,6 +45,8 @@ padding: 0.6rem 0.75rem; border: var(--border-width, 1px) solid var(--third-color); border-radius: var(--text-field-border-radius); + background: transparent; + color: var(--primary-color, #000); } .fame-form__input:focus { @@ -62,7 +64,7 @@ display: block; margin-top: 0.25rem; font-size: 16px; - color: var(--third-color); + color: inherit; } .fame-form__row > .fame-form__help { @@ -137,248 +139,311 @@ } } -/* ========================= MODERN STYLE ========================= */ -.wp-block-famehelsinki-donation-form.has-modern-style { +/* MODERN STYLE */ + +.wp-block-famehelsinki-donation-form { --hover-lighten: 16%; +} - .fame-form-container { - max-width: none !important; - } +.wp-block-famehelsinki-donation-form .fame-form__wrapper { + border: 0 !important; + border-style: none !important; + border-width: 0 !important; + outline: 0 !important; +} - .fame-form__input { - background-color: transparent; - color: var(--primary-color, #000); - } +.wp-block-famehelsinki-donation-form .fame-form-container { + max-width: none !important; +} - .fame-form__fieldset { - border: none; - padding: 0; - } +.wp-block-famehelsinki-donation-form .fame-form__input { + background-color: transparent; + color: var(--primary-color, #000); +} - .fame-form__legend { - padding: 0; - margin: 0 0 0.5em 0; - line-height: 1.3; - font-size: clamp(1rem, 4vw + 0.5rem, 1.5rem); - } +.wp-block-famehelsinki-donation-form .fame-form__fieldset { + border: none; + padding: 0; +} - .wp-block-famehelsinki-donation-amounts .fame-form__group, - .wp-block-famehelsinki-donation-type .fame-form__group { - display: block; - width: 100%; - } +.wp-block-famehelsinki-donation-form .fame-form__legend { + padding: 0 0 3px 0; + margin: 0 0 0.5em 0; + line-height: 1.3; + font-size: clamp(1rem, 4vw + 0.5rem, 1.5rem); + display: block; + width: 100%; +} - .wp-block-famehelsinki-donation-type { - display: flex; - flex-wrap: wrap; - gap: 10px; - - /* stylelint-disable-next-line no-descending-specificity */ - .fame-form__label { - display: flex; - width: 100%; - justify-content: center; - align-items: center; - padding: 8px; - border: var(--border-width) solid var(--primary-color); - border-radius: var(--border-radius); - cursor: pointer; - margin-right: 0.5rem; - user-select: none; - - /* stylelint-disable-next-line no-descending-specificity */ - &:has(.fame-form__check-input:checked) { - background-color: var(--primary-color); - color: var(--secondary-color); - border-color: var(--primary-color); - } - - /* stylelint-disable-next-line no-descending-specificity */ - &:hover { - background: color-mix(in srgb, var(--primary-color) 12%, #fff 10%); - } - } - - /* stylelint-disable-next-line no-descending-specificity */ - .fame-form__check-input { - display: none; - } - } +.wp-block-famehelsinki-donation-form .wp-block-famehelsinki-donation-amounts .fame-form__group, +.wp-block-famehelsinki-donation-form .wp-block-famehelsinki-donation-type .fame-form__group { + display: block; + width: 100%; +} - /* stylelint-disable-next-line no-descending-specificity */ - .fame-form__check-input { - display: none; - } +/* Donation type (tabs) */ +.wp-block-famehelsinki-donation-form .wp-block-famehelsinki-donation-type { + display: flex; + flex-wrap: wrap; + gap: 10px; +} - .wp-block-famehelsinki-donation-amounts .donation-amounts { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 10px; - } +.wp-block-famehelsinki-donation-form .wp-block-famehelsinki-donation-type .fame-form__label { + display: flex; + width: 100%; + justify-content: center; + align-items: center; + padding: 8px; + border: var(--border-width) solid var(--primary-color); + border-radius: var(--border-radius); + cursor: pointer; + margin-right: 0.5rem; + user-select: none; +} - .wp-block-famehelsinki-donation-amounts .donation-amounts__other { - position: relative; - width: 100%; - padding-top: 5px; - } +/* Shared “button-like” labels for tabs + amount buttons */ +.wp-block-famehelsinki-donation-form .wp-block-famehelsinki-donation-amounts .fame-form__label, +.wp-block-famehelsinki-donation-form .wp-block-famehelsinki-donation-type .fame-form__label { + display: flex; + width: 100%; + justify-content: center; + align-items: center; + padding: 8px; + margin-right: 0.5rem; + user-select: none; + cursor: pointer; + border: var(--border-width, 2px) solid var(--primary-color); + border-radius: var(--border-radius); + transition: + background 0.15s ease, + color 0.15s ease, + border-color 0.15s ease; +} - .wp-block-famehelsinki-donation-amounts .donation-amounts__input-wrapper { - position: relative; - } +/* Donation type hover (now after base label rules) */ +.wp-block-famehelsinki-donation-form .wp-block-famehelsinki-donation-type .fame-form__label:hover { + background: color-mix(in srgb, var(--primary-color) 12%, #fff 10%); +} - .wp-block-famehelsinki-donation-amounts .donation-amounts__input-wrapper input { - padding-right: 2em; - } +.wp-block-famehelsinki-donation-form .wp-block-famehelsinki-donation-type .fame-form__check-input { + display: none; +} - /* stylelint-disable-next-line no-descending-specificity */ - .wp-block-famehelsinki-donation-amounts .donation-amounts__other .label-wrapper { - display: inline-flex; - font-size: 16px; - } +/* Donation amounts grid */ +.wp-block-famehelsinki-donation-form .wp-block-famehelsinki-donation-amounts .donation-amounts { + display: grid; + grid-template-columns: repeat(var(--amount-cols, 3), minmax(0, 1fr)); + gap: 10px; +} - /* stylelint-disable-next-line no-descending-specificity */ - .wp-block-famehelsinki-donation-amounts .donation-amounts__other .fame-form__label { - position: relative; - display: block; - width: 100%; - padding: 0; - border: none; - cursor: default; - text-align: left; - margin-right: 0; - } +.wp-block-famehelsinki-donation-form +.wp-block-famehelsinki-donation-amounts +.donation-amounts__other { + position: relative; + width: 100%; + padding-top: 5px; +} - .wp-block-famehelsinki-donation-amounts .donation-amounts__other input { - display: block; - width: 100%; - box-sizing: border-box; - border: var(--border-width, 2px) solid var(--primary-color); - border-radius: var(--border-radius); - font-size: inherit; - max-width: none; - color: var(--primary-color); - } +.wp-block-famehelsinki-donation-form +.wp-block-famehelsinki-donation-amounts +.donation-amounts__input-wrapper { + position: relative; +} - .wp-block-famehelsinki-donation-amounts .donation-amounts__other .donation-amounts__minmax { - font-size: 16px; - } +.wp-block-famehelsinki-donation-form +.wp-block-famehelsinki-donation-amounts +.donation-amounts__input-wrapper +input { + padding-right: 2em; +} - /* stylelint-disable-next-line no-descending-specificity */ - .wp-block-famehelsinki-donation-amounts .fame-form__label, - /* stylelint-disable-next-line no-descending-specificity */ - .wp-block-famehelsinki-donation-type .fame-form__label { - display: flex; - width: 100%; - justify-content: center; - align-items: center; - padding: 8px; - margin-right: 0.5rem; - user-select: none; - cursor: pointer; - border: var(--border-width, 2px) solid var(--primary-color); - border-radius: var(--border-radius); - transition: - background 0.15s ease, - color 0.15s ease, - border-color 0.15s ease; - } +.wp-block-famehelsinki-donation-form +.wp-block-famehelsinki-donation-amounts +.donation-amounts__other +.label-wrapper { + display: inline-flex; + font-size: 16px; +} - /* stylelint-disable-next-line no-descending-specificity */ - .wp-block-famehelsinki-donation-amounts .fame-form__label:has(.fame-form__check-input:checked), - .wp-block-famehelsinki-donation-type .fame-form__label:has(.fame-form__check-input:checked) { - background: var(--primary-color); - color: var(--secondary-color); - border-color: var(--primary-color); - } +.wp-block-famehelsinki-donation-form +.wp-block-famehelsinki-donation-amounts +.donation-amounts__other +.fame-form__label { + position: relative; + display: block; + width: 100%; + padding: 0; + border: none; + cursor: default; + text-align: left; + margin-right: 0; +} - /* stylelint-disable-next-line no-descending-specificity */ - .wp-block-famehelsinki-donation-amounts .fame-form__label:hover, - /* stylelint-disable-next-line no-descending-specificity */ - .wp-block-famehelsinki-donation-type .fame-form__label:hover { - background: color-mix(in srgb, var(--primary-color) var(--hover-lighten), transparent calc(100% - var(--hover-lighten))); - } +.wp-block-famehelsinki-donation-form +.wp-block-famehelsinki-donation-amounts +.donation-amounts__other +input { + display: block; + width: 100%; + box-sizing: border-box; + border: var(--border-width, 2px) solid var(--primary-color); + border-radius: var(--border-radius); + font-size: inherit; + max-width: none; + color: inherit; +} - .wp-block-famehelsinki-donation-providers { - display: block; - width: 100%; - } +.wp-block-famehelsinki-donation-form +.wp-block-famehelsinki-donation-amounts +.donation-amounts__other +.donation-amounts__minmax { + font-size: 16px; +} - .payment-method-selector:has(> .fame-form__group:only-child) > .fame-form__group { - display: none; - } +/* Hover before checked (order for no-descending-specificity) */ +.wp-block-famehelsinki-donation-form +.wp-block-famehelsinki-donation-amounts +.fame-form__label:hover, +.wp-block-famehelsinki-donation-form .wp-block-famehelsinki-donation-type .fame-form__label:hover { + background: color-mix(in srgb, var(--primary-color) var(--hover-lighten), transparent calc(100% - var(--hover-lighten))); +} - /* stylelint-disable-next-line no-descending-specificity */ - .wp-block-famehelsinki-donation-providers .fame-form__group label { - display: inline-block; - width: 100%; - margin: 0 0 10px 0; - cursor: pointer; - } +/* Checked state (kept after hover) */ +.wp-block-famehelsinki-donation-form +.wp-block-famehelsinki-donation-amounts +.fame-form__label:has(.fame-form__check-input:checked), +.wp-block-famehelsinki-donation-form +.wp-block-famehelsinki-donation-type +.fame-form__label:has(.fame-form__check-input:checked) { + background: var(--primary-color); + color: var(--secondary-color); + border-color: var(--primary-color); +} - .wp-block-famehelsinki-donation-providers .provider-type__label { - display: flex; - width: 100%; - justify-content: center; - align-items: center; - padding: 8px 16px; - border: var(--border-width, 2px) solid var(--primary-color); - border-radius: var(--border-radius); - background: transparent; - transition: - background 0.2s, - color 0.2s, - border-color 0.2s; - } +/* Providers */ +.wp-block-famehelsinki-donation-form .wp-block-famehelsinki-donation-providers { + display: block; + width: 100%; +} - .wp-block-famehelsinki-donation-providers .provider-type__label:hover { - background: color-mix(in srgb, var(--primary-color) var(--hover-lighten), transparent calc(100% - var(--hover-lighten))); - } +.wp-block-famehelsinki-donation-form +.payment-method-selector:has(> .fame-form__group:only-child) > .fame-form__group { + display: none; +} - .wp-block-famehelsinki-donation-providers - .fame-form__check-input:checked + .provider-type__label { - background: var(--primary-color); - color: var(--secondary-color); - border-color: var(--primary-color); - } +/* Contact form labels */ +.wp-block-famehelsinki-donation-form .wp-block-famehelsinki-contact-form label { + font-size: 16px; +} - .wp-block-famehelsinki-donation-providers - .payment-method-selector--single - .fame-form__check-input:checked + .provider-type__label { - background: none; - color: inherit; - border-color: transparent !important; - box-shadow: none; - justify-content: left; - padding: 8px 0; - } +.wp-block-famehelsinki-donation-form +.wp-block-famehelsinki-donation-providers +.fame-form__group +label { + display: inline-block; + width: 100%; + margin: 0 0 10px 0; + cursor: pointer; +} - /* stylelint-disable-next-line no-descending-specificity */ - .wp-block-famehelsinki-contact-form label { - font-size: 16px; - } +.wp-block-famehelsinki-donation-form +.wp-block-famehelsinki-donation-providers +.provider-type__label { + display: flex; + width: 100%; + justify-content: center; + align-items: center; + padding: 8px 16px; + border: var(--border-width, 2px) solid var(--primary-color); + border-radius: var(--border-radius); + background: transparent; + transition: + background 0.2s, + color 0.2s, + border-color 0.2s; +} - .wp-block-famehelsinki-donation-amounts .donation-amounts__error { - color: #d63638; /* WP:n default error-red */ - font-size: 16px; - margin-top: 0.25rem; - } +.wp-block-famehelsinki-donation-form +.wp-block-famehelsinki-donation-providers +.provider-type__label:hover { + background: color-mix(in srgb, var(--primary-color) var(--hover-lighten), transparent calc(100% - var(--hover-lighten))); +} - .donation-amounts__input-wrapper input[aria-invalid="true"] { - border-color: #d63638; - outline-color: #d63638; - box-shadow: 0 0 0 0.2rem color-mix(in srgb, var(--fame-clr-danger) 35%, transparent); - } +.wp-block-famehelsinki-donation-form +.wp-block-famehelsinki-donation-providers +.fame-form__check-input:checked + .provider-type__label { + background: var(--primary-color); + color: var(--secondary-color); + border-color: var(--primary-color); +} + +.wp-block-famehelsinki-donation-form +.wp-block-famehelsinki-donation-providers +.payment-method-selector--single +.fame-form__check-input:checked + .provider-type__label { + background: none; + color: inherit; + border-color: transparent !important; + box-shadow: none; + justify-content: left; + padding: 8px 0; } +/* Errors */ +.wp-block-famehelsinki-donation-form +.wp-block-famehelsinki-donation-amounts +.donation-amounts__error { + color: var(--fame-clr-danger); + font-size: 16px; + margin-top: 0.25rem; +} + +.wp-block-famehelsinki-donation-form .donation-amounts__input-wrapper input[aria-invalid="true"] { + border-color: var(--fame-clr-danger); + outline-color: var(--fame-clr-danger); + box-shadow: 0 0 0 0.2rem color-mix(in srgb, var(--fame-clr-danger) 35%, transparent); +} + +/* Hidden block spacing fix */ +.wp-block-famehelsinki-donation-form .fame-form__hidden + fieldset { + margin-block-start: 0 !important; +} + +/* Allow editing block spacing from editor (front reads block-gap variable) */ +.wp-block-famehelsinki-donation-form .wp-block-columns { + gap: var(--wp--style--block-gap, var(--wp--preset--spacing--20, 2rem)) !important; + margin-bottom: 0 !important; +} + +/* Hide radio/check inputs inside our "button-like" controls (front) */ +.wp-block-famehelsinki-donation-form +:where( + .wp-block-famehelsinki-donation-type, + .wp-block-famehelsinki-donation-amounts, + .wp-block-famehelsinki-donation-providers +) +:where(input[type="radio"], input[type="checkbox"], .fame-form__check-input) { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0 0 0 0); + clip-path: inset(50%); + white-space: nowrap; + border: 0; +} + +/* Responsive */ @media (max-width: 1000px) { - /* has-modern-style donation form → mobile style → Group grid */ - .wp-block-famehelsinki-donation-form.has-modern-style - .fame-form--donations - .wp-block-group.is-layout-grid:is( - [class^="wp-container-core-group-is-layout-"], - [class*=" wp-container-core-group-is-layout-"] - ) { - grid-template-columns: repeat(1, minmax(0, 1fr)); + .wp-block-famehelsinki-donation-form .wp-block-columns { + flex-wrap: wrap !important; + } + + .wp-block-famehelsinki-donation-form .wp-block-column { + flex: 0 0 100% !important; + max-width: 100% !important; } } diff --git a/src/Blocks/donation-form/view/AmountHandler.ts b/src/Blocks/donation-form/view/AmountHandler.ts index e469eff..3cf7f94 100644 --- a/src/Blocks/donation-form/view/AmountHandler.ts +++ b/src/Blocks/donation-form/view/AmountHandler.ts @@ -325,16 +325,36 @@ export default class AmountHandler { }) const types = form.elements.namedItem('type') + + // RadioNodeList (normal case) if (types instanceof RadioNodeList) { types.forEach(radio => radio.addEventListener('change', onChangeType)) // Ensure that state is up to date. - const selected = Array.prototype.find.call(types, type => type.checked) - if (selected) { + const selected = Array.prototype.find.call(types, (t: any) => t?.checked) as + | HTMLInputElement + | undefined + + if (selected?.value) { this.#updateType(selected.value) + } else { + // Fallback: if nothing checked, still try first value if exists + const first = types[0] as any as HTMLInputElement | undefined + if (first?.value) this.#updateType(first.value) } } + // Hidden input for single-type forms. + else if (types instanceof HTMLInputElement && types.value) { + this.#updateType(types.value) + } + + // No type field at all: if there is exactly one wrapper, initialize it + else if (this.#wrappers.length === 1) { + const selectedAmount = this.#wrappers[0].ensureSelection() + this.#onChangeAmount(selectedAmount) + } + // Extra safety: if submit is attempted while invalid, block it. form.addEventListener( 'submit', diff --git a/src/Blocks/donation-form/view/FormHandler.ts b/src/Blocks/donation-form/view/FormHandler.ts index 440cb71..69029f1 100644 --- a/src/Blocks/donation-form/view/FormHandler.ts +++ b/src/Blocks/donation-form/view/FormHandler.ts @@ -265,11 +265,12 @@ export default class FormHandler { ) } } catch (error) { + // eslint-disable-next-line no-console console.error('Submit failed', error) if (error instanceof Validation) { - Object.entries(error.errors).forEach(([key, error]) => { - this.addError(key, error) + Object.entries(error.errors).forEach(([key, message]) => { + this.addError(key, message) }) return } diff --git a/src/Blocks/donation-providers/block.json b/src/Blocks/donation-providers/block.json index b2ab5ba..e3cf6d2 100644 --- a/src/Blocks/donation-providers/block.json +++ b/src/Blocks/donation-providers/block.json @@ -9,65 +9,25 @@ "parent": ["famehelsinki/donation-form"], "usesContext": ["famehelsinki/donation-types"], "attributes": { - "showLegend": { - "type": "boolean", - "default": true - }, - "showLegendSingle": { - "type": "boolean", - "default": true - }, - "showLegendRecurring": { - "type": "boolean", - "default": true - }, - "legend": { - "type": "string", - "source": "text", - "selector": "legend.fame-form__legend", - "default": "Payment provider" - }, - "providers": { - "type": "array", - "source": "query", - "selector": ".payment-method-selector > div", - "query": { - "value": { - "type": "string", - "source": "attribute", - "selector": "input", - "attribute": "value" - }, - "label": { - "type": "string", - "source": "text", - "selector": ".provider-type__label" - }, - "type": { - "type": "string", - "source": "attribute", - "selector": "input", - "attribute": "data-type" - } - }, - "default": [] - } + "showLegend": { "type": "boolean", "default": true }, + "showLegendSingle": { "type": "boolean", "default": true }, + "showLegendRecurring": { "type": "boolean", "default": true }, + "legend": { "type": "string", "default": "Payment provider" }, + "providers": { "type": "array", "default": [] }, + "legendAlign": { "type": "string", "default": "left" } }, "supports": { "multiple": false, - "color": { - "background": false, - "text": true - }, + "color": { "background": false, "text": true }, "html": false, - "typography": { - "fontSize": true - } + "typography": { "fontSize": true } }, + "allowedBlocks": ["core/paragraph"], "icon": "bank", "textdomain": "fame_lahjoitukset", "editorScript": "file:./index.js", "editorStyle": "file:./index.css", "script": "file:./view.js", - "style": "file:./style-index.css" + "style": "file:./style-index.css", + "render": "file:./render.php" } diff --git a/src/Blocks/donation-providers/deprecated/save-v1.tsx b/src/Blocks/donation-providers/deprecated/save-v1.tsx deleted file mode 100644 index f3c9539..0000000 --- a/src/Blocks/donation-providers/deprecated/save-v1.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import React from 'react' -import { RichText, useBlockProps } from '@wordpress/block-editor' -import { SaveProps } from '../../common/types.ts' - -type Provider = { value: string; label: string; type: string } - -type Attrs = { - providers?: Provider[] - legend?: string - showLegend?: boolean -} - -/** - * The save function defines the way in which the different attributes should - * be combined into the final markup, which is then serialized by the block - * editor into `post_content`. - * - * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#save - */ -export default function Save({ attributes }: SaveProps): React.JSX.Element { - const { providers = [], legend = 'Payment provider', showLegend = true } = attributes - const blockProps = useBlockProps.save() - - const grouped = providers.reduce>((acc, p) => { - ;(acc[p.type] ||= []).push(p) - return acc - }, {}) - - return ( -
- {Object.entries(grouped).map(([type, list]) => { - const single = list.length === 1 - - return ( -
- {showLegend && ( - - )} - - {single && ( - - )} - - {list.map(provider => ( -
- -
- ))} -
- ) - })} -
- ) -} diff --git a/src/Blocks/donation-providers/edit.tsx b/src/Blocks/donation-providers/edit.tsx index 176156e..6734923 100644 --- a/src/Blocks/donation-providers/edit.tsx +++ b/src/Blocks/donation-providers/edit.tsx @@ -1,6 +1,13 @@ -import React, { useMemo, useEffect } from 'react' +import React, { useMemo, useEffect, CSSProperties } from 'react' import { __ } from '@wordpress/i18n' -import { useBlockProps, InspectorControls, RichText } from '@wordpress/block-editor' +import { + useBlockProps, + InspectorControls, + RichText, + AlignmentToolbar, + BlockControls, + InnerBlocks, +} from '@wordpress/block-editor' import { PanelBody, Flex, CheckboxControl, TextControl, ToggleControl } from '@wordpress/components' import { EditProps } from '../common/types.ts' import { PROVIDERS, Provider } from '../common/Providers.ts' @@ -16,6 +23,7 @@ export type Attributes = { showLegend?: boolean showLegendSingle?: boolean showLegendRecurring?: boolean + legendAlign?: string } /** @@ -36,6 +44,7 @@ export default function Edit({ showLegend = true, showLegendSingle, showLegendRecurring, + legendAlign = 'left', } = attributes const donationTypes: string[] = useMemo( @@ -128,7 +137,24 @@ export default function Edit({ return ( <> + + setAttributes({ legendAlign: next || 'left' })} + /> + + + setAttributes({ legend: value })} + help={__( + 'Description for screen readers (for accessibility).', + 'fame_lahjoitukset' + )} + /> + {donationTypes.map(type => { const selected = new Set((grouped[type] ?? []).map(p => p.value)) const showForType = isLegendShownForType(type) @@ -163,14 +189,6 @@ export default function Edit({ ) })} - - - setAttributes({ legend: value })} - /> -
@@ -189,13 +207,18 @@ export default function Edit({ > {showForType && ( setAttributes({ legend: le })} + style={{ + textAlign: legendAlign as CSSProperties['textAlign'], + fontFamily: 'inherit', + }} /> )} @@ -244,6 +267,19 @@ export default function Edit({ ) })} +
) diff --git a/src/Blocks/donation-providers/index.ts b/src/Blocks/donation-providers/index.ts index 52aace5..df77d72 100644 --- a/src/Blocks/donation-providers/index.ts +++ b/src/Blocks/donation-providers/index.ts @@ -12,7 +12,6 @@ import Edit from './edit' import save from './save' import metadata from './block.json' import './edit.css' -import SaveV1 from './deprecated/save-v1' /** * Every block starts by registering a new block type definition. @@ -28,25 +27,4 @@ registerBlockType(metadata.name, { * @see ./save.js */ save, - /** - * Support old saved content - */ - deprecated: [ - { - attributes: { - providers: { type: 'array' }, - legend: { type: 'string' }, - showLegend: { type: 'boolean', default: true }, - }, - - // Migrate old attribute to new ones - migrate: (attrs: any) => ({ - ...attrs, - showLegendSingle: attrs.showLegend, - showLegendRecurring: attrs.showLegend, - }), - - save: SaveV1, - }, - ], } as any) diff --git a/src/Blocks/donation-providers/render.php b/src/Blocks/donation-providers/render.php new file mode 100644 index 0000000..34fb4c0 --- /dev/null +++ b/src/Blocks/donation-providers/render.php @@ -0,0 +1,130 @@ + $attributes + * @var string $content + * @var WP_Block $block + */ + + +/** @var array|null $attributes */ +$attributes = $attributes ?? []; + +$providers = (isset($attributes['providers']) && is_array($attributes['providers'])) + ? $attributes['providers'] + : []; + +$legend = (isset($attributes['legend']) && trim((string) $attributes['legend']) !== '') + ? (string) $attributes['legend'] + : __('Payment provider', 'fame_lahjoitukset'); + +$showLegend = array_key_exists('showLegend', $attributes) ? (bool) $attributes['showLegend'] : true; +$showLegendSingle = array_key_exists('showLegendSingle', $attributes) ? (bool) $attributes['showLegendSingle'] : null; +$showLegendRecurring = array_key_exists('showLegendRecurring', $attributes) ? (bool) $attributes['showLegendRecurring'] : null; + +$grouped = []; +foreach ($providers as $p) { + if (!is_array($p)) { + continue; + } + + $type = isset($p['type']) ? (string) $p['type'] : ''; + $value = isset($p['value']) ? (string) $p['value'] : ''; + $label = isset($p['label']) ? (string) $p['label'] : ''; + + if ($type === '' || $value === '') { + continue; + } + + if (!isset($grouped[$type])) { + $grouped[$type] = []; + } + + $grouped[$type][] = ['type' => $type, 'value' => $value, 'label' => $label]; +} + +$isLegendShownForType = static function (string $type) use ($showLegend, $showLegendSingle, $showLegendRecurring): bool { + if ($type === 'single') { + return $showLegendSingle ?? $showLegend; + } + if ($type === 'recurring') { + return $showLegendRecurring ?? $showLegend; + } + return $showLegend; +}; + +$wrapper_attrs = get_block_wrapper_attributes(); + +?> +
> + $list) : + $single = count($list) === 1; + $showForType = $isLegendShownForType((string) $type); + + $legendAlign_raw = isset($attributes['legendAlign']) ? (string) $attributes['legendAlign'] : 'left'; + $legendAlign = in_array($legendAlign_raw, ['left', 'center', 'right', 'justify'], true) + ? $legendAlign_raw + : 'left'; + $legend_class = 'fame-form__legend' . ($showForType ? '' : ' screen-reader-text'); + $legend_style = 'text-align:' . $legendAlign . ';'; + ?> +
+ + + + + + + + + +
+ +
+ + +
+ + + +
\ No newline at end of file diff --git a/src/Blocks/donation-providers/save.tsx b/src/Blocks/donation-providers/save.tsx index 96a603c..eadb463 100644 --- a/src/Blocks/donation-providers/save.tsx +++ b/src/Blocks/donation-providers/save.tsx @@ -1,112 +1,6 @@ +import { InnerBlocks } from '@wordpress/block-editor' import React from 'react' -import { RichText, useBlockProps } from '@wordpress/block-editor' -import { SaveProps } from '../common/types.ts' -type Provider = { value: string; label: string; type: string } - -type Attrs = { - providers?: Provider[] - legend?: string - // old (fallback) - showLegend?: boolean - showLegendSingle?: boolean - showLegendRecurring?: boolean -} - -/** - * The save function defines the way in which the different attributes should - * be combined into the final markup, which is then serialized by the block - * editor into `post_content`. - * - * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#save - */ -export default function Save({ attributes }: SaveProps): React.JSX.Element { - const { - providers = [], - legend = 'Payment provider', - showLegend = true, - showLegendSingle, - showLegendRecurring, - } = attributes - - const blockProps = useBlockProps.save() - - const grouped = providers.reduce>((acc, p) => { - ;(acc[p.type] ||= []).push(p) - return acc - }, {}) - - const isLegendShownForType = (type: string) => { - if (type === 'single') return showLegendSingle ?? showLegend ?? true - if (type === 'recurring') return showLegendRecurring ?? showLegend ?? true - return showLegend ?? true - } - - return ( -
- {Object.entries(grouped).map(([type, list]) => { - const single = list.length === 1 - const showForType = isLegendShownForType(type) - - return ( -
- - - {single && ( - - )} - - {list.map(provider => { - const hideSingleLabel = single && !showForType - - return ( -
- -
- ) - })} -
- ) - })} -
- ) +export default function save() { + return } diff --git a/src/Blocks/donation-type/DonationTypes.tsx b/src/Blocks/donation-type/DonationTypes.tsx index 7f31683..401fbf6 100644 --- a/src/Blocks/donation-type/DonationTypes.tsx +++ b/src/Blocks/donation-type/DonationTypes.tsx @@ -26,10 +26,10 @@ const Component: FC = ({ types, value: defaultValue, onChange }) => ( className="donation-type__label" aria-label={__('Donation type', 'fame_lahjoitukset')} allowedFormats={[]} - onChange={label => + onChange={newLabel => onChange({ types: types.map(type => - type.value !== value ? type : { value, label } + type.value !== value ? type : { value, label: newLabel } ), }) } diff --git a/src/Blocks/donation-type/block.json b/src/Blocks/donation-type/block.json index 08d71b1..d94286c 100644 --- a/src/Blocks/donation-type/block.json +++ b/src/Blocks/donation-type/block.json @@ -7,61 +7,24 @@ "category": "widgets", "description": "Allows selecting donation type.", "parent": ["famehelsinki/donation-form"], - "example": {}, "attributes": { - "showLegend": { - "type": "boolean", - "source": "attribute", - "selector": ".fame-form__legend:not(.screen-reader-text)", - "attribute": "class", - "default": true - }, - "legend": { - "type": "string", - "source": "text", - "selector": ".fame-form__legend", - "default": "Donation type" - }, - "value": { - "type": "string", - "source": "attribute", - "selector": "input[name=\"type\"]:checked", - "attribute": "value" - }, - "types": { - "type": "array", - "source": "query", - "selector": ".fame-form__group,.fame-form__hidden", - "query": { - "value": { - "type": "string", - "source": "attribute", - "selector": "input[name=\"type\"]", - "attribute": "value" - }, - "label": { - "type": "string", - "source": "text", - "selector": ".fame-form__label" - } - } - } + "showLegend": { "type": "boolean", "default": true }, + "legend": { "type": "string", "default": "Donation type" }, + "value": { "type": "string", "default": "" }, + "types": { "type": "array", "default": [] }, + "legendAlign": { "type": "string", "default": "left" } }, "usesContext": ["famehelsinki/donation-types"], "supports": { "multiple": false, - "color": { - "background": false, - "text": true - }, + "color": { "background": false, "text": true }, "html": false, - "typography": { - "fontSize": true - } + "typography": { "fontSize": true } }, "icon": "edit-page", "textdomain": "fame_lahjoitukset", "editorScript": "file:./index.js", "editorStyle": "file:./index.css", - "style": "file:./style-index.css" + "style": "file:./style-index.css", + "render": "file:./render.php" } diff --git a/src/Blocks/donation-type/edit.css b/src/Blocks/donation-type/edit.css index 27b0843..df865ef 100644 --- a/src/Blocks/donation-type/edit.css +++ b/src/Blocks/donation-type/edit.css @@ -7,3 +7,8 @@ .donation-type__input { width: 1.5em; } + +.fame-form__legend { + display: block; + width: 100%; +} diff --git a/src/Blocks/donation-type/edit.tsx b/src/Blocks/donation-type/edit.tsx index 0d3f466..b34582a 100644 --- a/src/Blocks/donation-type/edit.tsx +++ b/src/Blocks/donation-type/edit.tsx @@ -1,7 +1,13 @@ -import React, { useEffect } from 'react' +import React, { CSSProperties, useEffect } from 'react' import { __ } from '@wordpress/i18n' import { RadioControl, PanelBody, TextControl, ToggleControl } from '@wordpress/components' -import { InspectorControls, RichText, useBlockProps } from '@wordpress/block-editor' +import { + InspectorControls, + RichText, + useBlockProps, + AlignmentToolbar, + BlockControls, +} from '@wordpress/block-editor' import { DEFAULT_DONATION_TYPE, DONATION_TYPES, DonationType } from '../common/donation-type.ts' import { EditProps } from '../common/types.ts' import DonationTypes from './DonationTypes.tsx' @@ -11,6 +17,7 @@ export type Attributes = { types?: DonationType[] value?: string showLegend?: boolean + legendAlign?: string } /** @@ -25,22 +32,29 @@ export default function Edit({ setAttributes, }: EditProps): React.JSX.Element { const { 'famehelsinki/donation-types': enabledTypes } = context - const { types, value } = attributes + const { types, value, legendAlign = 'left' } = attributes useEffect(() => { + const enabled = + Array.isArray(enabledTypes) && enabledTypes.length > 0 + ? enabledTypes + : DONATION_TYPES.map(t => t.value) + // Calculate updated types. // - enabled types might have changed. // - enabled types might have been removed. const update = DONATION_TYPES // Filter all enabled types. - .filter(({ value }) => enabledTypes?.includes(value)) + .filter(({ value: typeValue }) => enabled.includes(typeValue)) // Use existing type from if it exists, otherwise add // new with default label from DONATION_TYPES array. - .map(t => types?.find(({ value }) => t.value === value) ?? t) + .map(t => types?.find(({ value: typeValue }) => t.value === typeValue) ?? t) - // Check if update includes current default value. - // If not, set first element as the new default value. - const defaultValue = update?.find(type => type.value === value)?.value || update?.[0]?.value + // Calculate default value. Use existing if it exists in updated list, otherwise use first from updated list or fallback to default. + const defaultValue = + update?.find(type => type.value === value)?.value ?? + update?.[0]?.value ?? + DEFAULT_DONATION_TYPE.value // Update if the list has changed. Calling setAttributes // without this check leads to infinite recursion. @@ -65,6 +79,12 @@ export default function Edit({ return ( <> + + setAttributes({ legendAlign: next || 'left' })} + /> + {types?.length && types?.length > 1 && ( @@ -76,7 +96,7 @@ export default function Edit({ )} selected={value ?? types?.[0]?.value} options={types} - onChange={value => setAttributes({ value })} + onChange={nextValue => setAttributes({ value: nextValue })} /> )} setAttributes({ showLegend })} /> - {visible && !attributes.showLegend && ( - setAttributes({ legend })} - /> - )} + + setAttributes({ legend })} + />
@@ -105,18 +126,41 @@ export default function Edit({ {attributes.showLegend && ( setAttributes({ legend })} + style={{ + textAlign: legendAlign as CSSProperties['textAlign'], + fontFamily: 'inherit', + }} /> )} ) : ( - `Type: ${types?.[0]?.value ?? DEFAULT_DONATION_TYPE.value} (hidden)` + <> + {attributes.showLegend && ( + setAttributes({ legend })} + style={{ + textAlign: legendAlign as CSSProperties['textAlign'], + fontFamily: 'inherit', + }} + /> + )} + {`Type: ${types?.[0]?.value ?? DEFAULT_DONATION_TYPE.value} (hidden)`} + )}
diff --git a/src/Blocks/donation-type/render.php b/src/Blocks/donation-type/render.php new file mode 100644 index 0000000..fdb07fa --- /dev/null +++ b/src/Blocks/donation-type/render.php @@ -0,0 +1,136 @@ +|null $attributes Block attributes. + * @var string|null $content Inner content (unused here). + * @var WP_Block $block Block instance. + */ + +$attributes = $attributes ?? []; + +$show_legend = array_key_exists('showLegend', $attributes) ? (bool) $attributes['showLegend'] : true; + +$legend = (isset($attributes['legend']) && trim((string) $attributes['legend']) !== '') + ? (string) $attributes['legend'] + : __('Donation type', 'fame_lahjoitukset'); + +$legend_align_raw = isset($attributes['legendAlign']) ? (string) $attributes['legendAlign'] : 'left'; +$legend_align = in_array($legend_align_raw, ['left', 'center', 'right', 'justify'], true) + ? $legend_align_raw + : 'left'; + +$legend_classes = ['fame-form__legend']; + +if (!$show_legend) { + $legend_classes[] = 'screen-reader-text'; +} + +$legend_classes[] = 'has-text-align-' . $legend_align; + +// Ensures alignment works even when legend is rendered as a
in the hidden branch. +$legend_style = 'text-align:' . $legend_align . ';'; + +$saved_types = (isset($attributes['types']) && is_array($attributes['types'])) ? $attributes['types'] : []; +$saved_value = isset($attributes['value']) ? (string) $attributes['value'] : ''; + +// Context: enabled donation types from parent (providesContext). +$enabled_types = []; +if (array_key_exists('famehelsinki/donation-types', $block->context)) { + $raw = $block->context['famehelsinki/donation-types']; + if (is_array($raw)) { + $enabled_types = array_values(array_map('strval', $raw)); + } +} + +// Types comes from attributes (saved) or context (enabled) or fallback (default). +$types = !empty($saved_types) + ? $saved_types + : [ + ['value' => 'single', 'label' => __('Single', 'fame_lahjoitukset')], + ['value' => 'recurring', 'label' => __('Recurring', 'fame_lahjoitukset')], + ]; + +$types = array_values(array_filter( + $types, + static fn($t) => + is_array($t) && isset($t['value']) && (string) $t['value'] !== '' +)); + +if (!empty($enabled_types)) { + $types = array_values(array_filter( + $types, + static fn($t) => in_array((string) $t['value'], $enabled_types, true) + )); +} + +// Fallback +if (empty($types)) { + $types = [ + ['value' => 'single', 'label' => __('Single', 'fame_lahjoitukset')], + ]; +} + +$values = array_map(static fn($t) => (string) ($t['value'] ?? ''), $types); + +$default_value = + ($saved_value !== '' && in_array($saved_value, $values, true)) + ? $saved_value + : (string) ($types[0]['value'] ?? ''); + +$is_hidden = count($types) <= 1; + +$classes = $is_hidden + ? 'fame-form__hidden' + : 'fame-form__fieldset fame-form__fieldset--donation-type'; + +$wrapper_attrs = get_block_wrapper_attributes(['class' => $classes]); + +?> + + +
> +
+ +
+ + +
+ +
> + + + + + + +
+ +
+ +
+ \ No newline at end of file diff --git a/src/Blocks/donation-type/save.tsx b/src/Blocks/donation-type/save.tsx index 4d2aa1f..9e8345e 100644 --- a/src/Blocks/donation-type/save.tsx +++ b/src/Blocks/donation-type/save.tsx @@ -1,59 +1,8 @@ -import { RichText, useBlockProps } from '@wordpress/block-editor' -import React from 'react' -import { DEFAULT_DONATION_TYPE } from '../common/donation-type.ts' -import { SaveProps } from '../common/types.ts' -import { Attributes } from './edit.tsx' - /** - * The save function defines the way in which the different attributes should - * be combined into the final markup, which is then serialized by the block - * editor into `post_content`. - * - * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#save + * Dynamic block – rendering happens in PHP (render.php). + * Nothing is saved to post_content. */ -export default function save({ attributes }: SaveProps): React.JSX.Element { - const { legend, showLegend, types, value: defaultValue } = attributes - const isHidden = !Array.isArray(types) || types.length <= 1 - const blockProps = useBlockProps.save({ - className: isHidden - ? 'fame-form__hidden' - : 'fame-form__fieldset fame-form__fieldset--donation-type', - }) - - if (isHidden) { - return ( -
- -
- ) - } - return ( -
- - {types.map(({ value, label }) => ( -
- -
- ))} -
- ) +export default function save() { + return null } diff --git a/src/Blocks/form-controls/block.json b/src/Blocks/form-controls/block.json index 37a73d2..0a71bb2 100644 --- a/src/Blocks/form-controls/block.json +++ b/src/Blocks/form-controls/block.json @@ -10,12 +10,16 @@ "attributes": { "submitLabel": { "type": "string", - "source": "text", - "selector": ".fame-form__controls [type=\"submit\"]", "default": "Donate" }, - "submitLabelSingle": { "type": "string", "default": "Donate" }, - "submitLabelRecurring": { "type": "string", "default": "Donate" } + "submitLabelSingle": { + "type": "string", + "default": "Donate" + }, + "submitLabelRecurring": { + "type": "string", + "default": "Donate" + } }, "supports": { "multiple": false, @@ -30,5 +34,6 @@ }, "icon": "button", "textdomain": "fame_lahjoitukset", - "editorScript": "file:./index.js" + "editorScript": "file:./index.js", + "render": "file:./render.php" } diff --git a/src/Blocks/form-controls/deprecated/save-v1.tsx b/src/Blocks/form-controls/deprecated/save-v1.tsx deleted file mode 100644 index 8253436..0000000 --- a/src/Blocks/form-controls/deprecated/save-v1.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { __ } from '@wordpress/i18n' -import { RichText, useBlockProps } from '@wordpress/block-editor' -import React from 'react' -import { SaveProps } from '../../common/types.ts' - -type AttributesV1 = { - submitLabel?: string -} - -export default function SaveV1({ - attributes: { submitLabel = __('Donate', 'fame_lahjoitukset') }, -}: SaveProps): React.JSX.Element { - return ( -
- - -
- ) -} diff --git a/src/Blocks/form-controls/edit.tsx b/src/Blocks/form-controls/edit.tsx index 871504e..09e8e97 100644 --- a/src/Blocks/form-controls/edit.tsx +++ b/src/Blocks/form-controls/edit.tsx @@ -75,7 +75,7 @@ export default function Edit({
setLabelForType(currentType, value)} diff --git a/src/Blocks/form-controls/index.ts b/src/Blocks/form-controls/index.ts index c084afc..6cc909e 100644 --- a/src/Blocks/form-controls/index.ts +++ b/src/Blocks/form-controls/index.ts @@ -11,7 +11,6 @@ import { registerBlockType } from '@wordpress/blocks' import Edit from './edit' import save from './save' import metadata from './block.json' -import SaveV1 from './deprecated/save-v1' /** * Every block starts by registering a new block type definition. @@ -28,27 +27,4 @@ registerBlockType(metadata.name, { * @see ./save.js */ save, - - /** - * Support old saved content (before submitLabelSingle/Recurring - * and before switching from RichText.Content to + + +
\ No newline at end of file diff --git a/src/Blocks/form-controls/save.tsx b/src/Blocks/form-controls/save.tsx index 6a5149d..9e8345e 100644 --- a/src/Blocks/form-controls/save.tsx +++ b/src/Blocks/form-controls/save.tsx @@ -1,46 +1,8 @@ -import { __ } from '@wordpress/i18n' -import { useBlockProps } from '@wordpress/block-editor' -import React from 'react' -import { SaveProps } from '../common/types.ts' +/** + * Dynamic block – rendering happens in PHP (render.php). + * Nothing is saved to post_content. + */ -type Attributes = { - submitLabel?: string // legacy - submitLabelSingle?: string - submitLabelRecurring?: string +export default function save() { + return null } - -const Save = ({ - attributes: { - submitLabel = __('Donate', 'fame_lahjoitukset'), - submitLabelSingle, - submitLabelRecurring, - }, -}: SaveProps): React.JSX.Element => { - // What text should be placed directly inside the button? - // if single/recurring same -> ok - // otherwise use legacy and give data attributes to JS for replacement - const resolved = - submitLabelSingle && submitLabelRecurring && submitLabelSingle === submitLabelRecurring - ? submitLabelSingle - : submitLabel - - return ( -
- - - -
- ) -} - -export default Save