Wire up the Filtered Image block end-to-end
This commit is contained in:
+26
-3
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user