diff --git a/includes/feedzy-rss-feeds.php b/includes/feedzy-rss-feeds.php index 47fa150e..2a2a6e76 100644 --- a/includes/feedzy-rss-feeds.php +++ b/includes/feedzy-rss-feeds.php @@ -285,6 +285,7 @@ function () { function () { if ( function_exists( 'register_block_type' ) ) { Feedzy_Rss_Feeds_Gutenberg_Block::get_instance(); + Feedzy_Rss_Feeds_Tag_Block::get_instance(); Feedzy_Rss_Feeds_Loop_Block::get_instance(); } } diff --git a/includes/gutenberg/feedzy-rss-feeds-feed-template-block.php b/includes/gutenberg/feedzy-rss-feeds-feed-template-block.php new file mode 100644 index 00000000..4062a5f6 --- /dev/null +++ b/includes/gutenberg/feedzy-rss-feeds-feed-template-block.php @@ -0,0 +1,96 @@ + + * + * @since 5.1.0 + */ +class Feedzy_Rss_Feeds_Feed_Template_Block { + + /** + * A reference to an instance of this class. + * + * @var Feedzy_Rss_Feeds_Feed_Template_Block|null The one Feedzy_Rss_Feeds_Loop_Block instance. + */ + private static $instance; + + /** + * Feedzy RSS Feeds plugin version. + * + * @var string $version The current version of the plugin. + */ + protected $version; + + /** + * Returns an instance of this class. + * + * @return Feedzy_Rss_Feeds_Feed_Template_Block The instance of the class. + */ + public static function get_instance() { + if ( null === self::$instance ) { + self::$instance = new Feedzy_Rss_Feeds_Feed_Template_Block(); + } + return self::$instance; + } + + /** + * Initializes the plugin by setting filters and administration functions. + */ + private function __construct() { + $this->version = Feedzy_Rss_Feeds::get_version(); + add_action( 'init', array( $this, 'register_block' ) ); + } + + /** + * Register Block. + * + * @return void + */ + public function register_block() { + $metadata_file = trailingslashit( FEEDZY_ABSPATH ) . '/build/template/block.json'; + register_block_type_from_metadata( + $metadata_file, + array( + 'render_callback' => array( $this, 'render_callback' ), + ) + ); + } + + /** + * Render Callback + * + * @param array $attributes The block attributes. + * @param string $content The block content. + * @param WP_Block $block The block instance. + * @return string The block content. + */ + public function render_callback( $attributes, $content, $block ) { + if ( + ! isset( $attributes['tag'] ) || empty( $attributes['tag'] ) || + empty( $block->context ) || + ! isset( $block->context['feedzy-rss-feeds/feedItem'] ) || empty( $block->context['feedzy-rss-feeds/feedItem'] ) + ) { + return ''; + } + + $feed_item = $block->context['feedzy-rss-feeds/feedItem']; + + if ( ! isset( $feed_item[ $attributes['tag'] ] ) ) { + return ''; + } + + $wrapper_attributes = get_block_wrapper_attributes(); + + return sprintf( + '
%2$s
', + $wrapper_attributes, + wp_kses_post( $feed_item[ $attributes['tag'] ] ) + ); + } +} diff --git a/includes/gutenberg/feedzy-rss-feeds-loop-block.php b/includes/gutenberg/feedzy-rss-feeds-loop-block.php index 4e63baf5..40414b26 100644 --- a/includes/gutenberg/feedzy-rss-feeds-loop-block.php +++ b/includes/gutenberg/feedzy-rss-feeds-loop-block.php @@ -49,7 +49,9 @@ private function __construct() { $this->version = Feedzy_Rss_Feeds::get_version(); $this->admin = Feedzy_Rss_Feeds::instance()->get_admin(); add_action( 'init', array( $this, 'register_block' ) ); - add_filter( 'feedzy_loop_item', array( $this, 'apply_magic_tags' ), 10, 2 ); + add_action( 'init', array( $this, 'register_feed_block_bindings_source' ) ); + add_action( 'rest_api_init', array( $this, 'register_fetch_feed_endpoint' ) ); + add_filter( 'feedzy_loop_item', array( $this, 'apply_magic_tags' ), 10, 3 ); } /** @@ -93,14 +95,51 @@ public function register_block() { /** * Render Callback * - * @param array $attributes The block attributes. - * @param string $content The block content. + * @param array $attributes The block attributes. + * @param string $content The block content. + * @param WP_Block $block Block instance. * @return string The block content. */ - public function render_callback( $attributes, $content ) { - $content = empty( $content ) ? ( $attributes['innerBlocksContent'] ?? '' ) : $content; - $is_preview = isset( $attributes['innerBlocksContent'] ) && ! empty( $attributes['innerBlocksContent'] ); - $feed_urls = array(); + public function render_callback( $attributes, $content, $block ) { + $feed_items = $this->fetch_feed( $attributes ); + + if ( empty( $feed_items ) ) { + return '
' . esc_html__( 'No feeds to display', 'feedzy-rss-feeds' ) . '
'; + } + + if ( is_wp_error( $feed_items ) ) { + return '
' . $feed_items->get_error_message() . '
'; + } + + $content = empty( $content ) ? ( $attributes['innerBlocksContent'] ?? '' ) : $content; + $loop = ''; + $column_count = isset( $attributes['layout'] ) && isset( $attributes['layout']['columnCount'] ) && ! empty( $attributes['layout']['columnCount'] ) ? $attributes['layout']['columnCount'] : 1; + + foreach ( $feed_items as $key => $item ) { + // Reference https://github.com/WordPress/gutenberg/blob/b3cda428abe895a0a97c0a6df0e0cf5c925d9208/packages/block-library/src/post-template/index.php#L113-L125 + $filter_block_context = static function ( $context ) use ( $item ) { + $context['feedzy-rss-feeds/feedItem'] = $item; + return $context; + }; + + add_filter( 'render_block_context', $filter_block_context, 1 ); + $loop .= apply_filters( 'feedzy_loop_item', $content, $item, $block ); + remove_filter( 'render_block_context', $filter_block_context, 1 ); + } + + return sprintf( + '
%2$s
', + get_block_wrapper_attributes( + array( + 'class' => 'feedzy-loop-columns-' . $column_count, + ) + ), + $loop + ); + } + + public function fetch_feed( $attributes ) { + $feed_urls = array(); if ( isset( $attributes['feed']['type'] ) && 'group' === $attributes['feed']['type'] && isset( $attributes['feed']['source'] ) && is_numeric( $attributes['feed']['source'] ) ) { $group = $attributes['feed']['source']; @@ -109,16 +148,17 @@ public function render_callback( $attributes, $content ) { $feed_urls = ! empty( $value ) ? explode( ',', $value ) : array(); } - if ( isset( $attributes['feed']['type'] ) && 'url' === $attributes['feed']['type'] && isset( $attributes['feed']['source'] ) && is_array( $attributes['feed']['source'] ) ) { + if ( + isset( $attributes['feed']['type'] ) && 'url' === $attributes['feed']['type'] && + isset( $attributes['feed']['source'] ) && is_array( $attributes['feed']['source'] ) + ) { $feed_urls = $attributes['feed']['source']; } if ( empty( $feed_urls ) ) { - return '
' . esc_html__( 'No feeds to display', 'feedzy-rss-feeds' ) . '
'; + return array(); } - $column_count = isset( $attributes['layout'] ) && isset( $attributes['layout']['columnCount'] ) && ! empty( $attributes['layout']['columnCount'] ) ? $attributes['layout']['columnCount'] : 1; - $default_query = array( 'max' => 5, 'sort' => 'default', @@ -146,50 +186,145 @@ public function render_callback( $attributes, $content ) { 'filters' => wp_json_encode( $filters ), ); + $feed = $this->admin->fetch_feed( $feed_urls, $query['refresh'], $options ); + $sizes = array( 'width' => 300, 'height' => 300, ); - $feed = $this->admin->fetch_feed( $feed_urls, $query['refresh'], $options ); - if ( isset( $feed->error ) && ! empty( $feed->error ) ) { - return '
' . esc_html__( 'An error occurred while fetching the feed.', 'feedzy-rss-feeds' ) . '
'; + return new \WP_Error( 'invalid_feed', __( 'No feeds to display', 'feedzy-rss-feeds' ) ); } - $feed_items = apply_filters( 'feedzy_get_feed_array', array(), $options, $feed, implode( ',', $feed_urls ), $sizes ); + return apply_filters( 'feedzy_get_feed_array', array(), $options, $feed, implode( ',', $feed_urls ), $sizes ); + } - if ( empty( $feed_items ) ) { - return '
' . esc_html__( 'No items to display.', 'feedzy-rss-feeds' ) . '
'; + public function register_fetch_feed_endpoint() { + register_rest_route( + 'feedzy/v1', + '/loop/feed', + array( + 'methods' => 'GET', + 'callback' => array( $this, 'get_feed_items' ), + 'permission_callback' => function () { + return is_user_logged_in(); + }, + ) + ); + } + + /** + * Get the feed items. + * + * @param WP_REST_Request $request + * @return void + */ + public function get_feed_items( $request ) { + // Get query parameters + $params = $request->get_query_params(); + + ray( $params ); + + // Extract attributes + $attributes = array(); + + // Sanitize feed data + if ( isset( $params['feed'] ) ) { + $attributes['feed'] = array(); + + // Sanitize feed type + if ( isset( $params['feed']['type'] ) ) { + $attributes['feed']['type'] = sanitize_text_field( $params['feed']['type'] ); + } + + // Sanitize feed source + if ( isset( $params['feed']['source'] ) ) { + if ( is_array( $params['feed']['source'] ) ) { + // For URL type - array of URLs + $attributes['feed']['source'] = array(); + foreach ( $params['feed']['source'] as $url ) { + $clean_url = esc_url_raw( urldecode( $url ) ); + if ( $clean_url ) { + $attributes['feed']['source'][] = $clean_url; + } + } + } else { + // For group type - numeric ID + $attributes['feed']['source'] = absint( $params['feed']['source'] ); + } + } } - $loop = ''; + // Sanitize query parameters + if ( isset( $params['query'] ) ) { + $attributes['query'] = array(); - foreach ( $feed_items as $key => $item ) { - $loop .= apply_filters( 'feedzy_loop_item', $content, $item ); + if ( isset( $params['query']['max'] ) ) { + $attributes['query']['max'] = absint( $params['query']['max'] ); + } + + if ( isset( $params['query']['sort'] ) ) { + $attributes['query']['sort'] = sanitize_text_field( $params['query']['sort'] ); + } + + if ( isset( $params['query']['refresh'] ) ) { + $attributes['query']['refresh'] = sanitize_text_field( $params['query']['refresh'] ); + } } - return sprintf( - '
%2$s
', - $wrapper_attributes = get_block_wrapper_attributes( - array( - 'class' => 'feedzy-loop-columns-' . $column_count, - ) - ), - $loop - ); + // Sanitize conditions/filters + if ( isset( $params['conditions'] ) ) { + $attributes['conditions'] = array(); + // Add specific sanitization for conditions based on your needs + foreach ( $params['conditions'] as $key => $condition ) { + $attributes['conditions'][ sanitize_key( $key ) ] = sanitize_text_field( $condition ); + } + } + + // ray( 'attributes', $attributes ); + + $feed_items = $this->fetch_feed( $attributes ); + + if ( is_wp_error( $feed_items ) ) { + wp_send_json_error( $feed_items->get_error_message() ); + } + + foreach ( $feed_items as &$item ) { + if ( ! is_array( $item ) ) { + continue; + } + + foreach ( $item as $key => $value ) { + if ( is_array( $value ) || is_object( $value ) ) { + unset( $item[ $key ] ); + } + } + } + + return $feed_items; } /** * Magic Tags Replacement. * - * @param string $content The content. - * @param array $item The item. + * @param string $content The content. + * @param array $item The feed item. + * @param WP_Block $block Block instance. * - * @return string The content. + * @return string The feed content. */ - public function apply_magic_tags( $content, $item ) { + public function apply_magic_tags( $content, $item, $block ) { $pattern = '/\{\{feedzy_([^}]+)\}\}/'; + + $block_content = ( new WP_Block( $block->parsed_block ) )->render( array( 'dynamic' => false ) ); + + if ( empty( $block_content ) ) { + $content = do_blocks( $block->attributes['innerBlocksContent'] ); + } else { + $content = $block_content; + } + $content = str_replace( array( FEEDZY_ABSURL . 'img/feedzy.svg', @@ -272,4 +407,42 @@ public function get_value( $key, $item ) { return ''; } } + + public function register_feed_block_bindings_source() { + if ( ! function_exists( 'register_block_bindings_source' ) ) { + return; + } + + register_block_bindings_source( + 'feedzy-rss-feeds/feed', + array( + 'label' => __( 'Feed', 'feedzy-rss-feeds' ), + 'get_value_callback' => array( $this, 'get_block_bindings_value' ), + ) + ); + } + + /** + * + * @param mixed[] $source_args + * @param WP_Block $block_instance + * @param string $attribute_name + * @return string + */ + public function get_block_bindings_value( $source_args, $block_instance, $attribute_name ) { + + if ( ! isset( $block_instance->context['feedzy-rss-feeds/feedItem'] ) || ! is_array( $block_instance->context['feedzy-rss-feeds/feedItem'] ) ) { + return null; + } + + $feed_item = $block_instance->context['feedzy-rss-feeds/feedItem']; + ray( 'get_block_bindings_value', $source_args, $block_instance, $attribute_name, $feed_item ); + + if ( isset( $source_args['key'] ) && isset( $feed_item[ $source_args['key'] ] ) ) { + // If the key exists in the feed item, return its value. + return $feed_item[ $source_args['key'] ]; + } + + return null; + } } diff --git a/includes/gutenberg/feedzy-rss-feeds-tag-block.php b/includes/gutenberg/feedzy-rss-feeds-tag-block.php new file mode 100644 index 00000000..ff297801 --- /dev/null +++ b/includes/gutenberg/feedzy-rss-feeds-tag-block.php @@ -0,0 +1,189 @@ + + * + * @since 5.1.0 + */ +class Feedzy_Rss_Feeds_Tag_Block { + + /** + * A reference to an instance of this class. + * + * @var Feedzy_Rss_Feeds_Tag_Block|null The one Feedzy_Rss_Feeds_Loop_Block instance. + */ + private static $instance; + + /** + * Feedzy RSS Feeds plugin version. + * + * @var string $version The current version of the plugin. + */ + protected $version; + + /** + * Returns an instance of this class. + * + * @return Feedzy_Rss_Feeds_Tag_Block The instance of the class. + */ + public static function get_instance() { + if ( null === self::$instance ) { + self::$instance = new Feedzy_Rss_Feeds_Tag_Block(); + } + return self::$instance; + } + + /** + * Initializes the plugin by setting filters and administration functions. + */ + private function __construct() { + $this->version = Feedzy_Rss_Feeds::get_version(); + add_action( 'init', array( $this, 'register_block' ) ); + } + + /** + * Register Block. + * + * @return void + */ + public function register_block() { + $metadata_file = trailingslashit( FEEDZY_ABSPATH ) . '/build/tag/block.json'; + register_block_type_from_metadata( + $metadata_file, + array( + 'render_callback' => array( $this, 'render_callback' ), + ) + ); + } + + /** + * Render Callback + * + * @param array $attributes The block attributes. + * @param string $content The block content. + * @param WP_Block $block The block instance. + * @return string The block content. + */ + public function render_callback( $attributes, $content, $block ) { + if ( + ! isset( $attributes['tag'] ) || empty( $attributes['tag'] ) || + empty( $block->context ) || + ! isset( $block->context['feedzy-rss-feeds/feedItem'] ) || empty( $block->context['feedzy-rss-feeds/feedItem'] ) + ) { + return ''; + } + + $feed_item = $block->context['feedzy-rss-feeds/feedItem']; + + if ( ! isset( $feed_item[ $attributes['tag'] ] ) ) { + return ''; + } + + $wrapper_attributes = get_block_wrapper_attributes(); + + return sprintf( + '
%2$s
', + $wrapper_attributes, + wp_kses_post( $feed_item[ $attributes['tag'] ] ) + ); + } + + /** + * Magic Tags Replacement. + * + * @param string $content The content. + * @param array $item The feed item. + * + * @return string The content. + */ + public function apply_magic_tags( $content, $item ) { + $pattern = '/\{\{feedzy_([^}]+)\}\}/'; + $content = str_replace( + array( + FEEDZY_ABSURL . 'img/feedzy.svg', + 'http://{{feedzy_url}}', + ), + array( + '{{feedzy_image}}', + '{{feedzy_url}}', + ), + $content + ); + + return preg_replace_callback( + $pattern, + function ( $matches ) use ( $item ) { + return $this->get_value( $matches[1], $item ); + }, + $content + ); + } + + /** + * Get Dynamic Value. + * + * @param string $key The key. + * @param array $item Feed item. + * + * @return string The value. + */ + public function get_value( $key, $item ) { + switch ( $key ) { + case 'title': + return isset( $item['item_title'] ) ? $item['item_title'] : ''; + case 'url': + return isset( $item['item_url'] ) ? $item['item_url'] : ''; + case 'date': + $item_date = isset( $item['item_date'] ) ? wp_date( get_option( 'date_format' ), $item['item_date'] ) : ''; + return $item_date; + case 'time': + $item_date = isset( $item['item_date'] ) ? wp_date( get_option( 'time_format' ), $item['item_date'] ) : ''; + return $item_date; + case 'datetime': + $item_date = isset( $item['item_date'] ) ? wp_date( get_option( 'date_format' ), $item['item_date'] ) : ''; + $item_time = isset( $item['item_date'] ) ? wp_date( get_option( 'time_format' ), $item['item_date'] ) : ''; + /* translators: 1: date, 2: time */ + $datetime = sprintf( __( '%1$s at %2$s', 'feedzy-rss-feeds' ), $item_date, $item_time ); + return $datetime; + case 'author': + if ( isset( $item['item_author'] ) && is_string( $item['item_author'] ) ) { + return $item['item_author']; + } elseif ( isset( $item['item_author'] ) && is_object( $item['item_author'] ) ) { + return $item['item_author']->get_name(); + } + return ''; + case 'description': + return isset( $item['item_description'] ) ? $item['item_description'] : ''; + case 'content': + return isset( $item['item_content'] ) ? $item['item_content'] : ''; + case 'meta': + return isset( $item['item_meta'] ) ? $item['item_meta'] : ''; + case 'categories': + return isset( $item['item_categories'] ) ? $item['item_categories'] : ''; + case 'image': + $settings = apply_filters( 'feedzy_get_settings', array() ); + if ( $settings && ! empty( $settings['general']['default-thumbnail-id'] ) ) { + $default_img = wp_get_attachment_image_src( $settings['general']['default-thumbnail-id'], 'full' ); + $default_img = ! empty( $default_img ) ? reset( $default_img ) : ''; + } else { + $default_img = FEEDZY_ABSURL . 'img/feedzy.svg'; + } + + return isset( $item['item_img_path'] ) ? $item['item_img_path'] : $default_img; + case 'media': + return isset( $item['item_media']['src'] ) ? $item['item_media']['src'] : ''; + case 'price': + return isset( $item['item_price'] ) ? $item['item_price'] : ''; + case 'source': + return isset( $item['item_source'] ) ? $item['item_source'] : ''; + default: + return ''; + } + } +} diff --git a/js/FeedzyLoop/edit.js b/js/FeedzyLoop/edit.js index 3a74d77e..207af470 100644 --- a/js/FeedzyLoop/edit.js +++ b/js/FeedzyLoop/edit.js @@ -11,8 +11,11 @@ import { import { store as blockEditorStore, __experimentalBlockVariationPicker as BlockVariationPicker, + __experimentalUseBlockPreview as useBlockPreview, useBlockProps, InnerBlocks, + BlockContextProvider, + useInnerBlocksProps, } from '@wordpress/block-editor'; import { @@ -22,7 +25,11 @@ import { import { useDispatch, useSelect } from '@wordpress/data'; -import { useState } from '@wordpress/element'; +import { useState, useMemo, memo, useEffect } from '@wordpress/element'; + +import apiFetch from '@wordpress/api-fetch'; + +import { addQueryArgs, buildQueryString } from '@wordpress/url'; import ServerSideRender from '@wordpress/server-side-render'; @@ -41,11 +48,128 @@ const LoadingResponsePlaceholder = () => ( ); +// Component for rendering actual editable inner blocks for active feed item +function FeedItemInnerBlocks({ feedItem, classList }) { + const innerBlocksProps = useInnerBlocksProps( + { + className: `wp-block-feedzy-feed-item ${classList}`, + 'data-feed-item-id': feedItem.id, + }, + { + templateLock: false, + __unstableDisableLayoutClassNames: true, + } + ); + return
; +} + +// Component for rendering interactive preview of feed items +function FeedItemBlockPreview({ + blocks, + feedItem, + classList, + isHidden, + setActiveFeedItemId, +}) { + const blockPreviewProps = useBlockPreview({ + blocks, + props: { + className: `wp-block-feedzy-feed-item ${classList}`, + 'data-feed-item-id': feedItem.id, + }, + }); + + const handleOnClick = () => { + setActiveFeedItemId(feedItem.id); + }; + + const handleOnKeyPress = (event) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + setActiveFeedItemId(feedItem.id); + } + }; + + const style = { + display: isHidden ? 'none' : undefined, + cursor: 'pointer', + outline: 'none', + }; + + return ( +
+ ); +} + +const MemoizedFeedItemBlockPreview = memo(FeedItemBlockPreview); + +// Helper function to check if value is in array +function inArray(value, array) { + return Array.isArray(array) && array.includes(value); +} + +// Hook to fetch and parse RSS feed data using the real API +function useFeedData(feedSource, feedType, attributes = {}) { + const [feedData, setFeedData] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + if (!feedSource) { + setFeedData(null); + setIsLoading(false); + setError(null); + return; + } + + const loadFeed = async () => { + setIsLoading(true); + setError(null); + + try { + // Make API call to WordPress REST endpoint + const response = await apiFetch({ + path: `/feedzy/v1/loop/feed?${buildQueryString(attributes)}`, + method: 'GET', + }); + + console.log(response); + + if (response.error) { + throw new Error(response.error); + } + + // Set the transformed feed data + setFeedData(response); + setIsLoading(false); + } catch (err) { + console.error('Feed loading error:', err); + setError(err.message || 'Failed to load feed'); + setIsLoading(false); + setFeedData(null); + } + }; + + loadFeed(); + }, [feedSource, feedType, JSON.stringify(attributes)]); + + return { feedData, isLoading, error }; +} + const Edit = ({ attributes, setAttributes, clientId }) => { const blockProps = useBlockProps(); const [isEditing, setIsEditing] = useState(!attributes?.feed?.source); const [isPreviewing, setIsPreviewing] = useState(false); + const [activeFeedItemId, setActiveFeedItemId] = useState(); const { clearSelectedBlock, replaceInnerBlocks } = useDispatch(blockEditorStore); @@ -66,12 +190,19 @@ const Edit = ({ attributes, setAttributes, clientId }) => { (select) => { const { getBlock } = select(blockEditorStore); const block = getBlock(clientId); - return serialize(block?.innerBlocks) ?? ''; }, [clientId] ); + const blocks = useSelect( + (select) => { + const { getBlocks } = select(blockEditorStore); + return getBlocks(clientId); + }, + [clientId] + ); + const hasInnerBlocks = useSelect( (select) => 0 < select(blockEditorStore).getBlocks(clientId).length, [clientId] @@ -87,6 +218,21 @@ const Edit = ({ attributes, setAttributes, clientId }) => { return getDefaultBlockVariation(name, 'block'); }, []); + // Fetch RSS feed data using real API + const { + feedData, + isLoading: isFeedLoading, + error: feedError, + } = useFeedData(attributes?.feed?.source, attributes?.feed?.type, { + ...attributes, + query: attributes?.query || {}, + layout: attributes?.layout || {}, + conditions: attributes?.conditions || {}, + }); + + // Create block contexts for each feed item + const feedItemContexts = useMemo(() => feedData, [feedData]); + const onSaveFeed = () => { setIsEditing(false); }; @@ -109,6 +255,7 @@ const Edit = ({ attributes, setAttributes, clientId }) => { }); }; + // Editing state - show feed configuration if (isEditing) { return (
@@ -121,7 +268,8 @@ const Edit = ({ attributes, setAttributes, clientId }) => { ); } - if ((!isSelected || isPreviewing) && innerBlocksContent) { + // Server-side preview state + if (isPreviewing && innerBlocksContent) { return ( <> { ); } + // Interactive preview state - show feed items with interactive editing + if (hasInnerBlocks && feedItemContexts && !isEditing && !isPreviewing) { + if (isFeedLoading) { + return ( + <> + +
+ +
+ + ); + } + + if (feedError) { + return ( + <> + +
+
+

+ Error loading feed: {feedError} +

+ +
+
+ + ); + } + + // Interactive preview with real feed data + return ( + <> + + +
+
+ {feedItemContexts.map((feedItemContext) => ( + + {/* Render editable inner blocks for active feed item */} + {feedItemContext.item_id === + (activeFeedItemId || + feedItemContexts[0]?.item_id) ? ( + + ) : null} + + {/* Render interactive preview for inactive feed items */} + + + ))} +
+
+ + ); + } + + // Default state - show block variation picker or inner blocks return ( <> { ).length > 0 ); }); + console.log(props); + + // if (props.context?.['feedzy-rss-feeds/feedItem']) { + // props.attributes.url = + // props.context?.['feedzy-rss-feeds/feedItem']?.item_img_path; + // } return ( <> @@ -43,7 +49,13 @@ const withFeedzyLoopImage = createHigherOrderComponent((BlockEdit) => { { props.setAttributes({ - url: defaultImage, + metadata: { + bindings: { + url: { + source: 'feedzy-rss-feeds/feed', + }, + }, + }, }); }} > @@ -58,3 +70,25 @@ const withFeedzyLoopImage = createHigherOrderComponent((BlockEdit) => { }, 'withMasonryExtension'); addFilter('editor.BlockEdit', 'feedzy-loop/image', withFeedzyLoopImage); + +function addCustomAttributes(settings, name) { + if ('core/image' === name) { + settings.attributes = { + ...settings.attributes, + feedzyTag: { + type: 'string', + }, + }; + const context = new Set(settings?.usesContext ?? []); + context.add('feedzy-rss-feeds/feedItem'); + settings.usesContext = Array.from(context); + } + + return settings; +} + +addFilter( + 'blocks.registerBlockType', + 'feedzy-loop/attributes', + addCustomAttributes +); diff --git a/js/FeedzyLoop/variations.js b/js/FeedzyLoop/variations.js index b06d976c..f819945a 100644 --- a/js/FeedzyLoop/variations.js +++ b/js/FeedzyLoop/variations.js @@ -137,30 +137,31 @@ const variations = [ [ 'core/image', { - url: window.feedzyData.defaultImage, - alt: '{{feedzy_title}}', - href: '{{feedzy_url}}', - }, - ], - [ - 'core/paragraph', - { - content: - '{{feedzy_title}}', - }, - ], - [ - 'core/paragraph', - { - content: '{{feedzy_meta}}', - fontSize: 'medium', + metadata: { + bindings: { + url: { + source: 'feedzy-rss-feeds/feed', + args: { + key: 'item_img_path', + }, + }, + }, + }, }, ], [ 'core/paragraph', { - content: '{{feedzy_description}}', - fontSize: 'small', + metadata: { + bindings: { + content: { + source: 'feedzy-rss-feeds/feed', + args: { + key: 'item_description', + }, + }, + }, + }, }, ], ], diff --git a/js/FeedzyTag/block.json b/js/FeedzyTag/block.json new file mode 100644 index 00000000..e366aca1 --- /dev/null +++ b/js/FeedzyTag/block.json @@ -0,0 +1,185 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "feedzy-rss-feeds/tag", + "version": "1.0.0", + "title": "Feedzy Tag", + "category": "common", + "icon": "rss", + "keywords": [ + "rss", + "feed", + "feedzy", + "text", + "content" + ], + "ancestor": ["feedzy-rss-feeds/loop"], + "description": "Dynamically display the part of the content feed with full text formatting support.", + "attributes": { + "tag": { + "type": "string" + }, + "content": { + "type": "rich-text", + "source": "rich-text", + "selector": "p" + }, + "align": { + "type": "string" + }, + "backgroundColor": { + "type": "string" + }, + "textColor": { + "type": "string" + }, + "gradient": { + "type": "string" + }, + "fontSize": { + "type": "string" + }, + "fontFamily": { + "type": "string" + }, + "fontStyle": { + "type": "string" + }, + "fontWeight": { + "type": "string" + }, + "lineHeight": { + "type": "string" + }, + "letterSpacing": { + "type": "string" + }, + "textTransform": { + "type": "string" + }, + "textDecoration": { + "type": "string" + }, + "writingMode": { + "type": "string" + }, + "direction": { + "type": "string" + }, + "dropCap": { + "type": "boolean", + "default": false + }, + "placeholder": { + "type": "string" + }, + "className": { + "type": "string" + }, + "style": { + "type": "object" + } + }, + "supports": { + "html": false, + "align": ["left", "center", "right", "wide", "full"], + "alignWide": true, + "anchor": true, + "className": true, + "customClassName": true, + "inserter": true, + "multiple": true, + "reusable": true, + "__experimentalSelector": "p", + "__experimentalSlashInserter": true, + "spacing": { + "margin": true, + "padding": true, + "blockGap": true, + "__experimentalDefaultControls": { + "margin": false, + "padding": false + } + }, + "typography": { + "fontSize": true, + "lineHeight": true, + "fontFamily": true, + "fontStyle": true, + "fontWeight": true, + "letterSpacing": true, + "textTransform": true, + "textDecoration": true, + "writingMode": true, + "direction": true, + "__experimentalFontStyle": true, + "__experimentalFontWeight": true, + "__experimentalLetterSpacing": true, + "__experimentalTextTransform": true, + "__experimentalTextDecoration": true, + "__experimentalWritingMode": true, + "__experimentalDefaultControls": { + "fontSize": true, + "fontAppearance": true, + "textTransform": true + } + }, + "color": { + "gradients": true, + "text": true, + "background": true, + "link": true, + "__experimentalDefaultControls": { + "background": true, + "text": true + } + }, + "dimensions": { + "minHeight": true, + "__experimentalDefaultControls": { + "minHeight": false + } + }, + "border": { + "color": true, + "radius": true, + "style": true, + "width": true, + "__experimentalDefaultControls": { + "color": false, + "radius": false, + "style": false, + "width": false + } + }, + "shadow": true, + "__experimentalBorder": { + "color": true, + "radius": true, + "style": true, + "width": true, + "__experimentalDefaultControls": { + "color": false, + "radius": false, + "style": false, + "width": false + } + }, + "interactivity": { + "clientNavigation": true + }, + "__experimentalConnections": true, + "__experimentalSettings": true + }, + "usesContext": [ + "feedzy-rss-feeds/feedItem", + "postId" + ], + "providesContext": { + "feedzy-rss-feeds/tag": "tag" + }, + "editorScript": "file:./index.js", + "editorStyle": "file:./index.css", + "style": "file:./style-index.css", + "textdomain": "feedzy-rss-block" +} \ No newline at end of file diff --git a/js/FeedzyTag/edit.js b/js/FeedzyTag/edit.js new file mode 100644 index 00000000..6150533f --- /dev/null +++ b/js/FeedzyTag/edit.js @@ -0,0 +1,166 @@ +/* eslint-disable @wordpress/no-unsafe-wp-apis */ +/** + * WordPress dependencies. + */ +import { + useBlockProps, + InspectorControls, + BlockControls, +} from '@wordpress/block-editor'; + +import { + PanelBody, + TextControl, + ToolbarButton, + Button, + RadioControl, +} from '@wordpress/components'; + +import { __ } from '@wordpress/i18n'; + +import { edit as editIcon, check, close } from '@wordpress/icons'; + +import { useState } from '@wordpress/element'; + +// Component to render the actual tag content - displays only ONE property +function TagContent({ tag, feedItem }) { + // Get ONLY the specific property for this tag + const rawValue = feedItem[tag] ?? 'No content found!'; + + // Default text rendering for any other tag + return {rawValue}; +} + +function Edit({ attributes, setAttributes, context }) { + const { tag } = attributes; + const [isEditing, setIsEditing] = useState(!tag); + const [tempValue, setTempValue] = useState(tag || ''); + const blockProps = useBlockProps(); + + // Get feed item context for preview + const feedItem = context?.['feedzy-rss-feeds/feedItem'] ?? []; + const hasContext = feedItem && Object.keys(feedItem).length > 0; + + const handleSave = () => { + setAttributes({ tag: tempValue }); + setIsEditing(false); + }; + + const handleCancel = () => { + setTempValue(tag || ''); + setIsEditing(false); + }; + + const handleKeyPress = (event) => { + if (event.key === 'Enter') { + handleSave(); + } else if (event.key === 'Escape') { + handleCancel(); + } + }; + + return ( +
+ + { + setTempValue(tag || ''); + setIsEditing(true); + }} + isPressed={isEditing} + /> + + + + + setAttributes({ tag: value })} + help={__( + 'Enter a custom tag name.', + 'feedzy-rss-feeds' + )} + /> + + {hasContext && ( + setAttributes({ tag: value })} + options={Object.entries(feedItem).map( + ([key, value]) => ({ + label: key, + value: key, + description: `${String(value).substring(0, 80)}${String(value).length > 80 ? '...' : ''}`, + }) + )} + selected={tag || ''} + /> + )} + + + + {isEditing ? ( +
+ setTempValue(e.target.value)} + onKeyDown={handleKeyPress} + placeholder={__('Custom tag…', 'feedzy-rss-feeds')} + style={{ + padding: '4px 8px', + border: '1px solid #ddd', + borderRadius: '2px', + fontSize: '14px', + minWidth: '150px', + }} + /> +
+ ) : ( +
+ {tag ? ( + + ) : ( + + {__('No tag set', 'feedzy-rss-feeds')} + + )} +
+ )} +
+ ); +} + +export default Edit; diff --git a/js/FeedzyTag/editor.scss b/js/FeedzyTag/editor.scss new file mode 100644 index 00000000..e69de29b diff --git a/js/FeedzyTag/index.js b/js/FeedzyTag/index.js new file mode 100644 index 00000000..e651a4b1 --- /dev/null +++ b/js/FeedzyTag/index.js @@ -0,0 +1,22 @@ +/** + * WordPress dependencies + */ +import { registerBlockType } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import './editor.scss'; +import metadata from './block.json'; +import edit from './edit'; + +const { name } = metadata; + +registerBlockType(name, { + ...metadata, + edit, + save() { + // Rendering in PHP + return null; + }, +}); diff --git a/package.json b/package.json index cfbe3f7f..22a10675 100755 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "build": "npm-run-all build:*", "build:block": "wp-scripts build --webpack-src-dir=js/FeedzyBlock --output-path=build/block --output-filename=index.js", "build:loop": "wp-scripts build --webpack-src-dir=js/FeedzyLoop --output-path=build/loop --output-filename=index.js", + "build:tag": "wp-scripts build --webpack-src-dir=js/FeedzyTag --output-path=build/tag --output-filename=index.js", "build:onboarding": "wp-scripts build --webpack-src-dir=js/Onboarding --output-path=build/onboarding --output-filename=index.js", "build:feedback": "wp-scripts build --webpack-src-dir=js/FeedBack --output-path=build/feedback --output-filename=index.js", "build:actions": "wp-scripts build --webpack-src-dir=js/ActionPopup --output-path=build/action-popup --output-filename=index.js", @@ -28,6 +29,7 @@ "dev": "npm-run-all --parallel dev:*", "dev:block": "wp-scripts start --webpack-src-dir=js/FeedzyBlock --output-path=build/block --output-filename=index.js", "dev:loop": "wp-scripts start --webpack-src-dir=js/FeedzyLoop --output-path=build/loop --output-filename=index.js", + "dev:tag": "wp-scripts start --webpack-src-dir=js/FeedzyTag --output-path=build/tag --output-filename=index.js", "dev:onboarding": "wp-scripts start --webpack-src-dir=js/Onboarding --output-path=build/onboarding --output-filename=index.js", "dev:feedback": "wp-scripts start --webpack-src-dir=js/FeedBack --output-path=build/feedback --output-filename=index.js", "dev:actions": "wp-scripts start --webpack-src-dir=js/ActionPopup --output-path=build/action-popup --output-filename=index.js", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index d32b2fff..1e77ba64 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1645,11 +1645,6 @@ parameters: count: 1 path: includes/gutenberg/feedzy-rss-feeds-gutenberg-block.php - - - message: "#^Method Feedzy_Rss_Feeds_Loop_Block\\:\\:apply_magic_tags\\(\\) has parameter \\$item with no value type specified in iterable type array\\.$#" - count: 1 - path: includes/gutenberg/feedzy-rss-feeds-loop-block.php - - message: "#^Method Feedzy_Rss_Feeds_Loop_Block\\:\\:get_instance\\(\\) has no return type specified\\.$#" count: 1