diff --git a/.plugin-data b/.plugin-data index 883d19a..783dffd 100644 --- a/.plugin-data +++ b/.plugin-data @@ -1,4 +1,4 @@ { - "version": "1.0.1", + "version": "1.0.2", "slug": "blockparty-modal" } diff --git a/README.md b/README.md index 7e9edc9..49a012f 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Blockparty Modal is a WordPress plugin that lets you add accessible modal dialog - **Modal dialog**: Uses the native `` element for semantics and accessibility - **Configurable content**: Title (with heading level), rich text content, and optional close button - **Close behaviour**: Choose how the modal closes — click outside, close button only, or prevent closing by backdrop -- **Trigger linking**: Link any block (e.g. core/button) to open a specific modal via `linkedModalId` +- **Trigger linking**: Link a block to open a specific modal via `linkedModalId`; by default only the Button block is allowed as a trigger (filterable) - **Stable modal ID**: Each modal can have a unique ID for trigger association - **Layout & styling**: Supports wide and full-width alignment, dimensions, colors, and spacing - **Internationalized**: Multilingual support with translation files (French included) @@ -70,11 +70,27 @@ npm run build - **Close behaviour** — "Any" (click outside or close button), "Close button only", or "None" - **Close button** — show or hide the close button - **Prevent scroll** — lock body scroll when the modal is open -4. To open the modal from a button: - - Add a **Button** block (or another block that supports the modal trigger) - - In the block settings, set **Linked Modal ID** to the same value as the modal’s **Modal ID** +4. To open the modal from a trigger block: + - Add a **Button** block (by default, only the Button block can be a modal trigger) + - In the block sidebar, open **Attached modal** and select the modal to open - On the frontend, clicking that button will open the corresponding modal +### Blocks allowed as modal triggers + +By default, only the **core/button** block can be linked to a modal. To allow other blocks (e.g. paragraph, image, or custom blocks), use the filter `blockparty_modal_trigger_allowed_blocks` in your theme or plugin: + +```php +add_filter( 'blockparty_modal_trigger_allowed_blocks', function ( $blocks ) { + $blocks[] = 'core/paragraph'; + $blocks[] = 'my-plugin/cta'; + return $blocks; +} ); +``` + +- **Filter name:** `blockparty_modal_trigger_allowed_blocks` +- **Parameters:** `array` — List of block names (e.g. `'core/button'`). +- **Default:** `array( 'core/button' )` + ## 🛠️ Development ### Project Structure @@ -231,7 +247,19 @@ This plugin is distributed under the GPL-2.0-or-later license. See the [LICENSE] ## 📝 Changelog -See [readme.txt](readme.txt) for version history. +See [readme.txt](readme.txt) for the full version history. Recent highlights: + +- **1.0.2** + - Filter `blockparty_modal_trigger_allowed_blocks` to control which blocks can be modal triggers; dialog margin and InnerBlocks fixes. + - Crawl Modal blocks from patterns + - Set min required PHP version to 8.0 + - Style issues + +- **1.0.1** + - Filter `blockparty_modal_trigger_allowed_blocks` to control which blocks can be modal triggers; dialog margin and InnerBlocks fixes. + +- **1.0.0** + - Initial release (Modal block, trigger linking, close behaviour, i18n). --- diff --git a/blockparty-modal.php b/blockparty-modal.php index 39edb15..b028c3b 100644 --- a/blockparty-modal.php +++ b/blockparty-modal.php @@ -2,9 +2,9 @@ /** * Plugin Name: Blockparty Modal * Description: Modal block for WordPress editor. - * Version: 1.0.1 + * Version: 1.0.2 * Requires at least: 6.8 - * Requires PHP: 7.4 + * Requires PHP: 8.0 * Author: Be API Technical Team * License: GPL-2.0-or-later * License URI: https://www.gnu.org/licenses/gpl-2.0.html @@ -19,10 +19,17 @@ exit; // Exit if accessed directly. } -define( 'BLOCKPARTY_MODAL_VERSION', '1.0.1' ); +define( 'BLOCKPARTY_MODAL_VERSION', '1.0.2' ); define( 'BLOCKPARTY_MODAL_URL', plugin_dir_url( __FILE__ ) ); define( 'BLOCKPARTY_MODAL_DIR', plugin_dir_path( __FILE__ ) ); + +// Require vendor +if ( file_exists( BLOCKPARTY_MODAL_DIR . '/vendor/autoload.php' ) ) { + /** @psalm-suppress UnresolvableInclude */ + require BLOCKPARTY_MODAL_DIR . '/vendor/autoload.php'; +} + /** * Registers the block(s) metadata from the `blocks-manifest.php` and registers the block type(s) * based on the registered block metadata. Behind the scenes, it registers also all assets so they can be enqueued @@ -36,18 +43,51 @@ function init(): void { wp_register_block_types_from_metadata_collection( __DIR__ . '/build', __DIR__ . '/build/blocks-manifest.php' ); wp_set_script_translations( 'blockparty-modal-editor-script', 'blockparty-modal', BLOCKPARTY_MODAL_DIR . '/languages' ); } + add_action( 'init', __NAMESPACE__ . '\\init', 10, 0 ); +/** + * Passes the list of blocks allowed as modal triggers to the block editor settings + * so the "Attached modal" panel is only shown for those blocks. + * + * @param array $settings Block editor settings. + * @param \WP_Block_Editor_Context $_context Block editor context (unused). + * @return array Modified settings. + */ +function block_editor_settings_modal_trigger_blocks( array $settings, \WP_Block_Editor_Context $_context ): array { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed -- Required by block_editor_settings_all filter signature. + /** @psalm-suppress MixedAssignment */ + $raw = apply_filters( + 'blockparty_modal_trigger_allowed_blocks', + get_default_modal_trigger_allowed_blocks() + ); + $settings['blockpartyModalTriggerAllowedBlocks'] = array_values( + array_filter( is_array( $raw ) ? $raw : [], 'is_string' ) + ); + return $settings; +} + +add_filter( 'block_editor_settings_all', __NAMESPACE__ . '\\block_editor_settings_modal_trigger_blocks', 10, 2 ); + +/** + * Default list of block names allowed to be used as modal triggers (linkedModalId). + * + * @return string[] Block names (e.g. 'core/button'). + */ +function get_default_modal_trigger_allowed_blocks(): array { + return [ 'core/button' ]; +} + /** * Wraps block output with a trigger wrapper when linkedModalId is set, * so the view script can open the modal on click. + * Only blocks in the allowed list (filterable) get this behavior; by default only core/button. * For core/button, the inner link or button is turned into the trigger (no wrapper). * - * @param string $block_content The block content. - * @param array $block The full block, including attributes. + * @param string $block_content The block content. + * @param array $block The full block, including attributes. * @return string Filtered block content. */ -function render_block_add_modal_trigger( $block_content, $block ) { +function render_block_add_modal_trigger( $block_content, array $block ) { $linked_modal_id = isset( $block['attrs']['linkedModalId'] ) ? $block['attrs']['linkedModalId'] : ''; @@ -56,9 +96,24 @@ function render_block_add_modal_trigger( $block_content, $block ) { return $block_content; } + $block_name = (string) ( $block['blockName'] ?? '' ); + /** @psalm-suppress MixedAssignment */ + $raw_blocks = apply_filters( + 'blockparty_modal_trigger_allowed_blocks', + get_default_modal_trigger_allowed_blocks() + ); + $allowed_blocks = array_filter( + is_array( $raw_blocks ) ? $raw_blocks : array(), + 'is_string' + ); + + if ( '' === $block_name || ! in_array( $block_name, $allowed_blocks, true ) ) { + return $block_content; + } + $dialog_id = 'modal-' . $linked_modal_id; - if ( isset( $block['blockName'] ) && 'core/button' === $block['blockName'] ) { + if ( 'core/button' === $block_name ) { $modified = modify_button_block_for_modal_trigger( $block_content, $linked_modal_id, $dialog_id ); if ( null !== $modified ) { return $modified; diff --git a/package.json b/package.json index 863b883..0da2287 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blockparty-modal", - "version": "1.0.1", + "version": "1.0.2", "description": "Add a modal block to the WordPress editor.", "author": "Be API", "license": "GPL-2.0-or-later", diff --git a/readme.txt b/readme.txt index 6cf5e8e..1a7766d 100644 --- a/readme.txt +++ b/readme.txt @@ -45,6 +45,12 @@ directory take precedence. For example, `/assets/screenshot-1.png` would win ove == Changelog == += 1.0.2 = +* Filter `blockparty_modal_trigger_allowed_blocks` to control which blocks can be modal triggers; dialog margin and InnerBlocks fixes. +* Crawl Modal blocks from patterns +* Set min required PHP version to 8.0 +* Style issues + = 1.0.1 = * Fix margin style for dialog element; set to auto by default instead of 0. * Remove dupplicated InnerBlocks.Content diff --git a/src/blockparty-modal/block.json b/src/blockparty-modal/block.json index 2b6b430..9ce4cad 100644 --- a/src/blockparty-modal/block.json +++ b/src/blockparty-modal/block.json @@ -2,7 +2,7 @@ "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 3, "name": "blockparty/modal", - "version": "1.0.1", + "version": "1.0.2", "title": "Modal", "category": "widgets", "description": "Insert a modal dialog that opens on trigger. Configure content and behaviour in the editor; the modal is displayed on the frontend when activated.", diff --git a/src/blockparty-modal/index.js b/src/blockparty-modal/index.js index 2889384..f0040b3 100644 --- a/src/blockparty-modal/index.js +++ b/src/blockparty-modal/index.js @@ -23,7 +23,7 @@ import metadata from './block.json'; import { MODAL_BLOCK_NAME, LINKED_MODAL_ATTR, - getModalOptionsFromBlocks, + getModalOptionsFromEditor, addLinkedModalAttribute, } from './utils'; @@ -52,7 +52,7 @@ blockTypes.forEach( ( blockType ) => { } } ); -// Add "Open modal on click" panel with Combobox to all blocks except the modal itself. +// Add "Attached modal" panel with Combobox only to blocks allowed as modal triggers (see filter blockparty_modal_trigger_allowed_blocks). addFilter( 'editor.BlockEdit', 'blockparty-modal/with-modal-trigger-control', @@ -63,9 +63,18 @@ addFilter( return ; } + const allowedBlocks = useSelect( ( select ) => { + const settings = select( 'core/block-editor' ).getSettings(); + const list = settings?.blockpartyModalTriggerAllowedBlocks; + return Array.isArray( list ) ? list : [ 'core/button' ]; + }, [] ); + + if ( ! allowedBlocks.includes( name ) ) { + return ; + } + const modalOptions = useSelect( ( select ) => { - const blocks = select( 'core/block-editor' ).getBlocks(); - return getModalOptionsFromBlocks( blocks ); + return getModalOptionsFromEditor( select ); }, [] ); const options = [ diff --git a/src/blockparty-modal/style.scss b/src/blockparty-modal/style.scss index 1d126c0..2894156 100644 --- a/src/blockparty-modal/style.scss +++ b/src/blockparty-modal/style.scss @@ -27,6 +27,11 @@ &:not(.block-editor-block-list__block) { margin: var(--wp-block-blockparty-modal-margin); + + &:first-child, + &:last-child { + margin-block: var(--wp-block-blockparty-modal-margin); + } } &::backdrop { diff --git a/src/blockparty-modal/utils.js b/src/blockparty-modal/utils.js index c851942..eef2875 100644 --- a/src/blockparty-modal/utils.js +++ b/src/blockparty-modal/utils.js @@ -26,34 +26,33 @@ export function generateStableModalId() { } /** - * Recursively collect blockparty/modal blocks with modalId and title. + * Collect modal options from the block editor store using getClientIdsWithDescendants + * and getBlock, so modals inside reusable blocks (core/block) and patterns are included. * - * @param {Object[]} blocks - Block list. + * @param {Function} select - The wp.data select function (e.g. from useSelect). * @return {Object[]} Options for ComboboxControl. */ -export function getModalOptionsFromBlocks( blocks ) { +export function getModalOptionsFromEditor( select ) { + const blockEditor = select( 'core/block-editor' ); + const clientIds = blockEditor.getClientIdsWithDescendants(); + if ( ! Array.isArray( clientIds ) ) { + return []; + } const options = []; - function traverse( blockList ) { - if ( ! blockList || ! blockList.length ) { - return; - } - for ( const block of blockList ) { - if ( block.name === MODAL_BLOCK_NAME ) { - const modalId = block.attributes?.modalId || block.clientId; - const title = - block.attributes?.title?.trim() || - __( 'Modal', 'blockparty-modal' ); - options.push( { - value: modalId, - label: title || `#${ modalId.slice( 0, 8 ) }`, - } ); - } - if ( block.innerBlocks?.length ) { - traverse( block.innerBlocks ); - } + for ( const clientId of clientIds ) { + const block = blockEditor.getBlock( clientId ); + if ( ! block || block.name !== MODAL_BLOCK_NAME ) { + continue; } + const modalId = block.attributes?.modalId || block.clientId; + const title = + block.attributes?.title?.trim() || + __( 'Modal', 'blockparty-modal' ); + options.push( { + value: modalId, + label: title || `#${ String( modalId ).slice( 0, 8 ) }`, + } ); } - traverse( blocks ); return options; }