Wire up the Filtered Image block end-to-end

This commit is contained in:
Keith Solomon
2026-08-05 16:48:03 -05:00
parent c2a0ae2d8c
commit d21d3c9ac2
5 changed files with 285 additions and 3 deletions
+26 -3
View File
@@ -26,16 +26,39 @@ define( 'IMAGE_FILTERS_URL', plugin_dir_url( __FILE__ ) );
/**
* Bootstrap the plugin.
*
* Block registration is wired in a follow-up task. This function exists so the
* activation hook can be added without needing a global-side-effect pattern.
* Registers the Filtered Image block from the build directory. The registered
* block type is static (its markup is produced by the JS save function).
*
* @return void
*/
function image_filters_bootstrap(): void {
// Block registration will be added in Task 5.
if ( ! function_exists( 'register_block_type' ) ) {
return;
}
register_block_type(
IMAGE_FILTERS_DIR . 'build',
array(
'render_callback' => 'image_filters_render_block',
)
);
}
add_action( 'plugins_loaded', 'image_filters_bootstrap' );
/**
* Render callback for the Filtered Image block.
*
* Static blocks render their own markup via save.js. The render_callback
* is a placeholder for future server-side logic (e.g. dynamic alt text).
*
* @param array $attributes Block attributes.
* @param string $content Saved block content.
* @return string
*/
function image_filters_render_block( array $attributes, string $content = '' ): string {
return $content;
}
/**
* Load plugin textdomain for translations.
*
+65
View File
@@ -0,0 +1,65 @@
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "ksolo/image-filter",
"version": "0.1.0",
"title": "Filtered Image",
"category": "media",
"icon": "image-filter",
"description": "An image with an Instagram-style filter applied.",
"keywords": [ "filter", "instagram", "image" ],
"textdomain": "image-filters",
"supports": {
"align": [ "left", "center", "right", "wide", "full" ],
"anchor": true,
"html": false,
"spacing": { "margin": true }
},
"attributes": {
"filter": {
"type": "string",
"default": "normal"
},
"intensity": {
"type": "number",
"default": 100
},
"imageId": {
"type": "number",
"default": 0
},
"imageUrl": {
"type": "string",
"default": "",
"source": "attribute",
"selector": "img",
"attribute": "src"
},
"imageAlt": {
"type": "string",
"default": "",
"source": "attribute",
"selector": "img",
"attribute": "alt"
},
"width": {
"type": "number"
},
"height": {
"type": "number"
},
"linkUrl": {
"type": "string",
"default": ""
},
"caption": {
"type": "string",
"default": "",
"source": "html",
"selector": "figcaption"
}
},
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"style": "file:./style-index.css"
}
+80
View File
@@ -0,0 +1,80 @@
/**
* Editor component for the Filtered Image block.
*
* The MediaPlaceholder is the same UX as the core Image block. After an
* image is selected, the block renders the image with the filter class
* and intensity CSS custom property. The Inspector panel is mounted
* separately.
*
* @package ImageFilters
*/
import { __ } from '@wordpress/i18n';
import { useBlockProps, MediaPlaceholder, BlockControls } from '@wordpress/block-editor';
import { Button, ToolbarGroup, ToolbarItem } from '@wordpress/components';
import { useState } from '@wordpress/element';
export default function Edit( { attributes, setAttributes } ) {
const { filter, intensity, imageId, imageUrl, imageAlt, width, height } = attributes;
const blockProps = useBlockProps( {
className: `wp-block-ksolo-image-filter has-filter-${ filter }`,
style: { '--filter-intensity': String( intensity / 100 ) },
} );
const [ isEditing, setIsEditing ] = useState( ! imageUrl );
if ( isEditing || ! imageUrl ) {
return (
<div { ...blockProps }>
<MediaPlaceholder
onSelect={ ( media ) => {
setAttributes( {
imageId: media.id,
imageUrl: media.url,
imageAlt: media.alt || '',
width: media.width,
height: media.height,
} );
setIsEditing( false );
} }
allowedTypes={ [ 'image' ] }
multiple={ false }
labels={ {
title: __( 'Filtered Image', 'image-filters' ),
instructions: __(
'Upload or select an image to apply a filter.',
'image-filters'
),
} }
/>
</div>
);
}
return (
<figure { ...blockProps }>
<BlockControls>
<ToolbarGroup>
<ToolbarItem>
{ () => (
<Button
onClick={ () => setIsEditing( true ) }
variant="secondary"
label={ __( 'Replace image', 'image-filters' ) }
>
{ __( 'Replace', 'image-filters' ) }
</Button>
) }
</ToolbarItem>
</ToolbarGroup>
</BlockControls>
<img
src={ imageUrl }
alt={ imageAlt }
width={ width }
height={ height }
className={ `ksolo-image-filter-img has-filter-${ filter }` }
/>
</figure>
);
}
+77
View File
@@ -0,0 +1,77 @@
/**
* Block registration entry point.
*
* @package ImageFilters
*/
import { registerBlockType } from '@wordpress/blocks';
import { addFilter } from '@wordpress/hooks';
import './styles/style.scss';
import './styles/editor.scss';
import metadata from './block.json';
import edit from './edit';
import save from './save';
import { PRESETS, DEFAULT_PRESET, isValidPresetSlug } from './presets';
import { FilterPanel } from './inspector';
registerBlockType( metadata.name, {
...metadata,
edit,
save,
} );
/**
* Add the filter panel to the Image block's inspector when the active
* block is the Filtered Image block. We use the editor.BlockEdit filter
* so we don't have to re-implement the entire MediaPlaceholder UX.
*
* @param {Function} BlockEdit
* @return {Function}
*/
function withFilterPanel( BlockEdit ) {
return ( props ) => {
if ( props.name !== 'ksolo/image-filter' ) {
return <BlockEdit { ...props } />;
}
return (
<>
<BlockEdit { ...props } />
<FilterPanel
attributes={ props.attributes }
setAttributes={ props.setAttributes }
/>
</>
);
};
}
addFilter( 'editor.BlockEdit', 'image-filters/with-filter-panel', withFilterPanel );
/**
* Normalise the filter attribute on save. If the saved slug is unknown
* (e.g. the post was edited by hand), fall back to DEFAULT_PRESET so the
* block still renders without errors.
*/
function ensureValidFilter( settings, name ) {
if ( name !== 'ksolo/image-filter' ) {
return settings;
}
const originalSave = settings.save;
settings.save = ( props ) => {
if ( ! isValidPresetSlug( props.attributes.filter ) ) {
props.attributes.filter = DEFAULT_PRESET;
}
return originalSave( props );
};
return settings;
}
addFilter(
'blocks.registerBlockType',
'image-filters/ensure-valid-filter',
ensureValidFilter
);
// Re-export PRESETS so the consuming downstream code (e.g. a future
// design-tool integration) can grab them from this module.
export { PRESETS };
+37
View File
@@ -0,0 +1,37 @@
/**
* Static markup for the Filtered Image block.
*
* Renders a normal <img> with the filter class and an inline CSS custom
* property for intensity. The CSS in style.scss renders the filter; the
* saved HTML is fully static and no client-side JS is needed to display
* it.
*
* @package ImageFilters
*/
import { useBlockProps } from '@wordpress/block-editor';
export default function save( { attributes } ) {
const { filter, intensity, imageUrl, imageAlt, width, height, caption } = attributes;
const blockProps = useBlockProps.save( {
className: `wp-block-ksolo-image-filter has-filter-${ filter }`,
style: { '--filter-intensity': String( intensity / 100 ) },
} );
if ( ! imageUrl ) {
return null;
}
return (
<figure { ...blockProps }>
<img
src={ imageUrl }
alt={ imageAlt }
width={ width }
height={ height }
className={ `ksolo-image-filter-img has-filter-${ filter }` }
/>
{ caption && <figcaption>{ caption }</figcaption> }
</figure>
);
}