Initial commit
This commit is contained in:
@@ -0,0 +1,359 @@
|
||||
<?php
|
||||
|
||||
|
||||
namespace FluentCrm\App\Services\BlockRender;
|
||||
|
||||
|
||||
use FluentCrm\Framework\Support\Arr;
|
||||
|
||||
class BlockEditorHelper
|
||||
{
|
||||
public static function getStyleDefauls()
|
||||
{
|
||||
return [
|
||||
'spacing' => [
|
||||
[
|
||||
'name' => '2X-Small',
|
||||
'slug' => 'fc-xx-small',
|
||||
'size' => '5px',
|
||||
],
|
||||
[
|
||||
'name' => 'X-Small',
|
||||
'slug' => 'fc-x-small',
|
||||
'size' => '10px',
|
||||
],
|
||||
[
|
||||
'name' => 'Small',
|
||||
'slug' => 'fc-small',
|
||||
'size' => '14px',
|
||||
],
|
||||
[
|
||||
'name' => 'Medium',
|
||||
'slug' => 'fc-medium',
|
||||
'size' => '20px',
|
||||
],
|
||||
[
|
||||
'name' => 'Large',
|
||||
'slug' => 'fc-large',
|
||||
'size' => '30px',
|
||||
],
|
||||
[
|
||||
'name' => 'X-Large',
|
||||
'slug' => 'fc-x-large',
|
||||
'size' => '45px',
|
||||
],
|
||||
[
|
||||
'name' => '2X-Large',
|
||||
'slug' => 'fc-xx-large',
|
||||
'size' => '60px',
|
||||
],
|
||||
],
|
||||
'font-family' => [
|
||||
[
|
||||
'name' => 'System UI',
|
||||
'slug' => 'system-ui',
|
||||
'fontFamily' => "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'",
|
||||
],
|
||||
[
|
||||
'name' => 'Arial',
|
||||
'slug' => 'arial',
|
||||
'fontFamily' => "Arial, 'Helvetica Neue', Helvetica, sans-serif",
|
||||
],
|
||||
[
|
||||
'name' => 'Georgia',
|
||||
'slug' => 'georgia',
|
||||
'fontFamily' => "Georgia, Times, 'Times New Roman', serif",
|
||||
],
|
||||
[
|
||||
'name' => 'Helvetica',
|
||||
'slug' => 'helvetica',
|
||||
'fontFamily' => "Helvetica, Arial, Verdana, sans-serif",
|
||||
],
|
||||
[
|
||||
'name' => 'Courier New',
|
||||
'slug' => 'courier-new',
|
||||
'fontFamily' => "'Courier New', Courier, 'Lucida Sans Typewriter', monospace",
|
||||
],
|
||||
[
|
||||
'name' => 'Times New Roman',
|
||||
'slug' => 'times-new-roman',
|
||||
'fontFamily' => "'Times New Roman', Times, Baskerville, Georgia, serif",
|
||||
],
|
||||
[
|
||||
'name' => 'Trebuchet MS',
|
||||
'slug' => 'trebuchet-ms',
|
||||
'fontFamily' => "'Trebuchet MS', 'Lucida Grande', 'Lucida Sans Unicode', Tahoma, sans-serif",
|
||||
],
|
||||
[
|
||||
'name' => 'Verdana',
|
||||
'slug' => 'verdana',
|
||||
'fontFamily' => "Verdana, Geneva, sans-serif",
|
||||
],
|
||||
],
|
||||
'font-size' => [
|
||||
[
|
||||
'name' => 'Small',
|
||||
'slug' => 'fc-small',
|
||||
'size' => '13px',
|
||||
],
|
||||
[
|
||||
'name' => 'Regular',
|
||||
'slug' => 'fc-regular',
|
||||
'size' => '16px',
|
||||
],
|
||||
[
|
||||
'name' => 'Medium',
|
||||
'slug' => 'fc-medium',
|
||||
'size' => '18px',
|
||||
],
|
||||
[
|
||||
'name' => 'Large',
|
||||
'slug' => 'fc-large',
|
||||
'size' => '26px',
|
||||
],
|
||||
[
|
||||
'name' => 'Extra Large',
|
||||
'slug' => 'fc-x-large',
|
||||
'size' => '32px',
|
||||
],
|
||||
],
|
||||
'color' => [
|
||||
[
|
||||
'name' => 'Black',
|
||||
'slug' => 'black',
|
||||
'color' => '#000000'
|
||||
],
|
||||
[
|
||||
'name' => 'Cyan bluish gray',
|
||||
'slug' => 'cyan-bluish-gray',
|
||||
'color' => '#abb8c3'
|
||||
],
|
||||
[
|
||||
'name' => 'White',
|
||||
'slug' => 'white',
|
||||
'color' => '#ffffff'
|
||||
],
|
||||
[
|
||||
'name' => 'Pale pink',
|
||||
'slug' => 'pale-pink',
|
||||
'color' => '#f78da7'
|
||||
],
|
||||
[
|
||||
'name' => 'Vivid red',
|
||||
'slug' => 'vivid-red',
|
||||
'color' => '#cf2e2e'
|
||||
],
|
||||
[
|
||||
'name' => 'Luminous vivid orange',
|
||||
'slug' => 'luminous-vivid-orange',
|
||||
'color' => '#ff6900'
|
||||
],
|
||||
[
|
||||
'name' => 'Luminous vivid amber',
|
||||
'slug' => 'luminous-vivid-amber',
|
||||
'color' => '#fcb900'
|
||||
],
|
||||
[
|
||||
'name' => 'Light green cyan',
|
||||
'slug' => 'light-green-cyan',
|
||||
'color' => '#7bdcb5'
|
||||
],
|
||||
[
|
||||
'name' => 'Vivid green cyan',
|
||||
'slug' => 'vivid-green-cyan',
|
||||
'color' => '#00d084'
|
||||
],
|
||||
[
|
||||
'name' => 'Pale cyan blue',
|
||||
'slug' => 'pale-cyan-blue',
|
||||
'color' => '#8ed1fc'
|
||||
],
|
||||
[
|
||||
'name' => 'Vivid cyan blue',
|
||||
'slug' => 'vivid-cyan-blue',
|
||||
'color' => '#0693e3'
|
||||
],
|
||||
[
|
||||
'name' => 'Vivid purple',
|
||||
'slug' => 'vivid-purple',
|
||||
'color' => '#9b51e0'
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public static function getDefaultPreset($key = '')
|
||||
{
|
||||
$defaults = self::getStyleDefauls();
|
||||
|
||||
if ($key && isset($defaults[$key])) {
|
||||
return $defaults[$key];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
public static function getStyleDefaultPresets()
|
||||
{
|
||||
$defaults = self::getStyleDefauls();
|
||||
|
||||
$cssProps = '';
|
||||
|
||||
foreach ($defaults['spacing'] as $item) {
|
||||
$cssProps .= '--wp--preset--spacing--' . $item['slug'] . ': ' . $item['size'] . ';';
|
||||
}
|
||||
|
||||
// Keep core color preset variables available inside the editor canvas.
|
||||
// Without these, Gutenberg's link color styles can resolve to empty values.
|
||||
foreach ($defaults['color'] as $item) {
|
||||
$cssProps .= '--wp--preset--color--' . $item['slug'] . ': ' . $item['color'] . ';';
|
||||
}
|
||||
|
||||
return $cssProps;
|
||||
}
|
||||
|
||||
public static function replaceStyleSlugsWithValues($css = '')
|
||||
{
|
||||
if (!$css) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$defaults = self::getStyleDefauls();
|
||||
|
||||
$replaces = [
|
||||
'var(--wp--preset--color--white)' => 'white',
|
||||
];
|
||||
|
||||
foreach ($defaults as $key => $value) {
|
||||
if (is_array($value)) {
|
||||
foreach ($value as $item) {
|
||||
if ($key === 'spacing') {
|
||||
$replaces['var(--wp--preset--spacing--' . $item['slug'] . ')'] = $item['size'];
|
||||
} else if ($key === 'font-size') {
|
||||
$replaces['var(--fcom--font--size--' . $item['slug'] . ')'] = $item['size'];
|
||||
$replaces['var(--wp--preset--font-size--' . $item['slug'] . ')'] = $item['size'];
|
||||
} else if ($key === 'color') {
|
||||
$replaces['var(--fcom--color--' . $item['slug'] . ')'] = $item['color'];
|
||||
$replaces['var(--wp--preset--color--' . $item['slug'] . ')'] = $item['color'];
|
||||
} else if ($key === 'font-family') {
|
||||
$replaces['var(--wp--preset--font-family--' . $item['slug'] . ')'] = $item['fontFamily'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Include active theme font size presets too (small, medium, large, etc).
|
||||
$themeFontSizes = \FluentCrm\App\Services\Helper::getThemeFontSizes();
|
||||
foreach ((array)$themeFontSizes as $themeFontSize) {
|
||||
if (!is_array($themeFontSize) || empty($themeFontSize['slug']) || !isset($themeFontSize['size'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$size = $themeFontSize['size'];
|
||||
if (is_numeric($size)) {
|
||||
$size = $size . 'px';
|
||||
}
|
||||
|
||||
$replaces['var(--wp--preset--font-size--' . $themeFontSize['slug'] . ')'] = $size;
|
||||
}
|
||||
|
||||
return str_replace(array_keys($replaces), array_values($replaces), $css);
|
||||
}
|
||||
|
||||
public static function renderAtts($atts = [], $echo = false)
|
||||
{
|
||||
$rendered = '';
|
||||
foreach ($atts as $key => $value) {
|
||||
if (is_array($value)) {
|
||||
$value = implode(' ', $value);
|
||||
}
|
||||
$value = esc_attr($value);
|
||||
$key = esc_attr($key);
|
||||
$rendered .= $key . '="' . $value . '" ';
|
||||
}
|
||||
|
||||
if ($echo) {
|
||||
echo $rendered;
|
||||
}
|
||||
|
||||
return $rendered;
|
||||
}
|
||||
|
||||
public static function renderStyles($styles = [], $echo = false)
|
||||
{
|
||||
$rendered = '';
|
||||
foreach ($styles as $key => $value) {
|
||||
if (!$value) {
|
||||
continue;
|
||||
}
|
||||
$rendered .= $key . ':' . $value . ';';
|
||||
}
|
||||
|
||||
if ($echo) {
|
||||
echo esc_attr($rendered);
|
||||
}
|
||||
|
||||
return esc_attr($rendered);
|
||||
}
|
||||
|
||||
public static function getBorderColor($attrs = [], $default = '')
|
||||
{
|
||||
if ($borderColor = Arr::get($attrs, 'borderColor', '')) {
|
||||
$color = 'var(--fcom--color--' . $borderColor . ')';
|
||||
return self::replaceStyleSlugsWithValues($color);
|
||||
}
|
||||
|
||||
$color = $default;
|
||||
if ($borderConfig = Arr::get($attrs, 'style.border', [])) {
|
||||
$color = Arr::get($borderConfig, 'color', '');
|
||||
}
|
||||
|
||||
return $color;
|
||||
}
|
||||
|
||||
public static function getDynamicCssForEditor()
|
||||
{
|
||||
$fonts = self::getDefaultPreset('font-family');
|
||||
|
||||
$css = '';
|
||||
|
||||
foreach ($fonts as $font) {
|
||||
$css .= '.editor-styles-wrapper .has-' . $font['slug'] . '-font-family { font-family: ' . $font['fontFamily'] . ' !important; }';
|
||||
}
|
||||
|
||||
return $css;
|
||||
|
||||
}
|
||||
|
||||
public static function getDefaultPrefConfig()
|
||||
{
|
||||
return [
|
||||
'content_width' => 700,
|
||||
'content_border_radius' => '0px',
|
||||
'content_padding_top' => '20px', // matched
|
||||
'content_padding_right' => '20px', // matched
|
||||
'content_padding_bottom' => '20px', // matched
|
||||
'content_padding_left' => '20px', // matched
|
||||
'content_margin_top' => '20px', // new
|
||||
'content_margin_bottom' => '20px', // new
|
||||
|
||||
'headings_font_family' => "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", // matched
|
||||
'content_font_family' => "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", // matched
|
||||
|
||||
'body_bg_color' => '#FAFAFA', // matched
|
||||
'content_bg_color' => '#FFFFFF', // matched
|
||||
|
||||
'headings_color' => '#202020', // matched
|
||||
'text_color' => '#202020', // matched
|
||||
'link_color' => '#0693e3',
|
||||
'link_color_hover' => '', // not used now
|
||||
|
||||
'paragraph_font_size' => '16px', // matched
|
||||
'paragraph_line_height' => '1.5', // matched
|
||||
|
||||
'footer_text_color' => '#202020', // matched,
|
||||
|
||||
'design_template' => 'simple'
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Services\BlockRender;
|
||||
|
||||
use FluentCrm\Framework\Support\Arr;
|
||||
|
||||
class CartProduct
|
||||
{
|
||||
private static $productsCache = [];
|
||||
|
||||
public static function renderProduct($buttonHtml, $data)
|
||||
{
|
||||
if (!defined('FLUENTCART_VERSION')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$defaultAtts = [
|
||||
'productId' => null,
|
||||
'showImage' => true,
|
||||
'showDescription' => true,
|
||||
'showPrice' => true,
|
||||
'showButton' => true,
|
||||
'buttonText' => __('Buy Now', 'fluent-crm'),
|
||||
'backgroundColor' => '#fffeeb',
|
||||
'contentColor' => '',
|
||||
'pricingColor' => '',
|
||||
'template' => 'left',
|
||||
];
|
||||
|
||||
$attrs = isset($data['attrs']) && is_array($data['attrs']) ? $data['attrs'] : [];
|
||||
$atts = wp_parse_args($attrs, $defaultAtts);
|
||||
$productId = absint($atts['productId']);
|
||||
if (!$productId) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$product = self::getProductData($productId);
|
||||
if (!$product) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$template = self::normalizeTemplate(Arr::get($atts, 'template', 'left'));
|
||||
$showImage = RenderValueHelper::normalizeBool(Arr::get($atts, 'showImage'), true);
|
||||
$showDescription = RenderValueHelper::normalizeBool(Arr::get($atts, 'showDescription'), true);
|
||||
$showPrice = RenderValueHelper::normalizeBool(Arr::get($atts, 'showPrice'), true);
|
||||
$showButton = RenderValueHelper::normalizeBool(Arr::get($atts, 'showButton'), true);
|
||||
|
||||
$tableStyle = 'border-radius: 5px;';
|
||||
$contentColorStyle = '';
|
||||
|
||||
if ($bgColor = Arr::get($atts, 'backgroundColor')) {
|
||||
$tableStyle .= 'background-color:' . sanitize_text_field($bgColor) . ';';
|
||||
}
|
||||
|
||||
if ($contentColor = Arr::get($atts, 'contentColor')) {
|
||||
$safeContentColor = sanitize_text_field($contentColor);
|
||||
$tableStyle .= 'color:' . $safeContentColor . ';';
|
||||
$contentColorStyle = 'color:' . $safeContentColor . ';';
|
||||
}
|
||||
|
||||
$priceStyle = '';
|
||||
if ($pricingColor = Arr::get($atts, 'pricingColor')) {
|
||||
$priceStyle = 'color:' . sanitize_text_field($pricingColor) . ';';
|
||||
}
|
||||
|
||||
$contentHtml = sprintf(
|
||||
'<h2 style="%1$smargin:0;"><a href="%2$s" target="_blank" style="%1$stext-decoration:none;">%3$s</a></h2>',
|
||||
$contentColorStyle,
|
||||
esc_url($product['permalink']),
|
||||
wp_kses_post($product['name'])
|
||||
);
|
||||
|
||||
if ($showDescription && !empty($product['short_description'])) {
|
||||
$contentHtml .= sprintf(
|
||||
'<div style="%1$smargin-bottom:5px;">%2$s</div>',
|
||||
$contentColorStyle,
|
||||
wp_kses_post($product['short_description'])
|
||||
);
|
||||
}
|
||||
|
||||
if ($showPrice) {
|
||||
$contentHtml .= sprintf(
|
||||
'<div style="%1$smargin:5px 0;">%2$s</div>',
|
||||
$priceStyle,
|
||||
ProductListRenderer::preparePriceHtml($product['price_html'], __('Free', 'fluent-crm'))
|
||||
);
|
||||
}
|
||||
|
||||
$image = '';
|
||||
if ($showImage && $template !== 'none') {
|
||||
$customImage = trim((string)Arr::get($atts, 'customImage'));
|
||||
$image = $customImage ? esc_url_raw($customImage) : $product['image'];
|
||||
}
|
||||
|
||||
if (!$image) {
|
||||
$template = 'none';
|
||||
}
|
||||
|
||||
if ($showButton) {
|
||||
$fallbackButton = '<a href="' . esc_url($product['permalink']) . '" style="text-decoration:none;display:inline-block;padding:8px 18px;border-radius:4px;background:#2a363d;color:#ffffff;">' . esc_html(Arr::get($atts, 'buttonText', __('Buy Now', 'fluent-crm'))) . '</a>';
|
||||
$contentHtml .= '<div style="margin-top:10px;margin-bottom:10px;">' . ($buttonHtml ?: $fallbackButton) . '</div>';
|
||||
}
|
||||
|
||||
$contentStyle = '';
|
||||
if ($template === 'none' || $template === 'top') {
|
||||
$contentStyle = 'text-align:center;';
|
||||
}
|
||||
|
||||
$imageTd = '';
|
||||
if ($image) {
|
||||
if ($template === 'top') {
|
||||
$contentHtml = '<img src="' . esc_url($image) . '" width="auto" alt="' . esc_attr(wp_strip_all_tags($product['name'])) . '" style="border:0;height:auto;outline:none;text-decoration:none;max-width:100%;-ms-interpolation-mode:bicubic;display:block;margin:0 auto 15px;">' . $contentHtml;
|
||||
} elseif ($template === 'left') {
|
||||
$imageContent = '<img src="' . esc_url($image) . '" width="auto" alt="' . esc_attr(wp_strip_all_tags($product['name'])) . '" style="border:0;height:auto;outline:none;text-decoration:none;max-width:100%;-ms-interpolation-mode:bicubic;display:block;margin:0 auto;">';
|
||||
$imageTd = self::getContentTd($imageContent, $template);
|
||||
}
|
||||
}
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<table class="fce_row" border="0" cellpadding="0" cellspacing="0" width="100%" style="table-layout:fixed;border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;margin-bottom:20px;margin-top:20px;<?php echo $tableStyle; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>"><tbody><tr>
|
||||
<?php
|
||||
if ($imageTd) {
|
||||
echo $imageTd; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
echo self::getContentTd($contentHtml, $template, $contentStyle); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
?>
|
||||
</tr></tbody></table>
|
||||
<?php
|
||||
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
private static function getProductData($productId)
|
||||
{
|
||||
if (isset(self::$productsCache[$productId])) {
|
||||
return self::$productsCache[$productId];
|
||||
}
|
||||
self::$productsCache[$productId] = CartProductData::getProductById($productId, 'large');
|
||||
|
||||
return self::$productsCache[$productId];
|
||||
}
|
||||
|
||||
private static function getContentTd($contentHtml, $template, $extraStyle = '')
|
||||
{
|
||||
$width = ($template === 'left') ? '50' : '100';
|
||||
|
||||
return '<td align="center" valign="middle" width="' . $width . '%" class="fce_column"><table border="0" cellpadding="10" cellspacing="0" width="100%"><tr><td class="fc_column_content" style="padding:10px;' . $extraStyle . '">' . $contentHtml . '</td></tr></table></td>';
|
||||
}
|
||||
|
||||
private static function normalizeTemplate($template)
|
||||
{
|
||||
$template = is_string($template) ? trim($template) : 'left';
|
||||
return in_array($template, ['left', 'top', 'none'], true) ? $template : 'left';
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Services\BlockRender;
|
||||
|
||||
class CartProductData
|
||||
{
|
||||
/**
|
||||
* Query FluentCart products and return normalized product payloads.
|
||||
*
|
||||
* @param array $options
|
||||
* @param string $imageSize
|
||||
* @return array
|
||||
*/
|
||||
public static function getProducts($options = [], $imageSize = 'large')
|
||||
{
|
||||
if (!defined('FLUENTCART_VERSION')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$queryArgs = self::buildQueryArgs($options);
|
||||
$query = new \WP_Query($queryArgs);
|
||||
$posts = $query->posts;
|
||||
wp_reset_postdata();
|
||||
|
||||
if (!$posts) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$postIds = wp_list_pluck($posts, 'ID');
|
||||
$detailMap = self::buildDetailMap($postIds);
|
||||
|
||||
$products = [];
|
||||
foreach ($posts as $post) {
|
||||
$products[] = self::mapPostToProduct($post, $detailMap, $imageSize);
|
||||
}
|
||||
|
||||
return $products;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single FluentCart product by ID.
|
||||
*
|
||||
* @param int $productId
|
||||
* @param string $imageSize
|
||||
* @return array|null
|
||||
*/
|
||||
public static function getProductById($productId, $imageSize = 'large')
|
||||
{
|
||||
$productId = absint($productId);
|
||||
if (!$productId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$products = self::getProducts([
|
||||
'productId' => $productId,
|
||||
'perPage' => 1,
|
||||
], $imageSize);
|
||||
|
||||
return isset($products[0]) ? $products[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build query args for FluentCart products.
|
||||
*
|
||||
* @param array $options
|
||||
* @return array
|
||||
*/
|
||||
public static function buildQueryArgs($options = [])
|
||||
{
|
||||
$perPage = max(1, min(20, absint(isset($options['perPage']) ? $options['perPage'] : 3)));
|
||||
$order = strtolower((string)(isset($options['order']) ? $options['order'] : 'desc')) === 'asc' ? 'ASC' : 'DESC';
|
||||
$orderByRaw = sanitize_key((string)(isset($options['orderBy']) ? $options['orderBy'] : 'date'));
|
||||
$productId = absint(isset($options['productId']) ? $options['productId'] : 0);
|
||||
$search = sanitize_text_field((string)(isset($options['search']) ? $options['search'] : ''));
|
||||
$taxType = sanitize_text_field((string)(isset($options['taxType']) ? $options['taxType'] : 'all'));
|
||||
|
||||
$orderByMap = [
|
||||
'date' => 'date',
|
||||
'modified' => 'modified',
|
||||
'title' => 'title',
|
||||
'menu_order' => 'menu_order',
|
||||
'rand' => 'rand',
|
||||
];
|
||||
$orderBy = isset($orderByMap[$orderByRaw]) ? $orderByMap[$orderByRaw] : 'date';
|
||||
|
||||
$queryArgs = [
|
||||
'post_type' => 'fluent-products',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => $perPage,
|
||||
'orderby' => $orderBy,
|
||||
'order' => $order,
|
||||
'no_found_rows' => true,
|
||||
];
|
||||
|
||||
if ($productId) {
|
||||
$queryArgs['post__in'] = [$productId];
|
||||
$queryArgs['posts_per_page'] = 1;
|
||||
$queryArgs['orderby'] = 'post__in';
|
||||
} elseif ($search !== '') {
|
||||
$queryArgs['s'] = $search;
|
||||
}
|
||||
|
||||
if ($taxType && $taxType !== 'all' && is_numeric($taxType)) {
|
||||
$queryArgs['tax_query'] = [[
|
||||
'taxonomy' => 'product-categories',
|
||||
'field' => 'term_id',
|
||||
'terms' => [(int)$taxType],
|
||||
]];
|
||||
}
|
||||
|
||||
return $queryArgs;
|
||||
}
|
||||
|
||||
private static function buildDetailMap($postIds)
|
||||
{
|
||||
$detailMap = [];
|
||||
if (!$postIds || !class_exists('\FluentCart\App\Models\ProductDetail')) {
|
||||
return $detailMap;
|
||||
}
|
||||
|
||||
try {
|
||||
$details = \FluentCart\App\Models\ProductDetail::query()
|
||||
->whereIn('post_id', $postIds)
|
||||
->get();
|
||||
|
||||
foreach ($details as $detail) {
|
||||
$detailMap[(int)$detail->post_id] = $detail;
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$detailMap = [];
|
||||
}
|
||||
|
||||
return $detailMap;
|
||||
}
|
||||
|
||||
private static function mapPostToProduct($post, $detailMap, $imageSize)
|
||||
{
|
||||
$postId = (int)$post->ID;
|
||||
$detail = isset($detailMap[$postId]) ? $detailMap[$postId] : null;
|
||||
|
||||
$priceHtml = ($detail && isset($detail->formatted_min_price) && $detail->formatted_min_price)
|
||||
? (string)$detail->formatted_min_price
|
||||
: __('Free', 'fluent-crm');
|
||||
$priceHtml = ProductListRenderer::preparePriceHtml($priceHtml, __('Free', 'fluent-crm'));
|
||||
|
||||
$priceText = html_entity_decode(
|
||||
wp_strip_all_tags((string)$priceHtml),
|
||||
ENT_QUOTES,
|
||||
get_bloginfo('charset') ?: 'UTF-8'
|
||||
);
|
||||
$priceText = preg_replace('/\s+/', ' ', trim($priceText));
|
||||
if ($priceText === '') {
|
||||
$priceText = __('Free', 'fluent-crm');
|
||||
}
|
||||
|
||||
$categories = wp_get_post_terms($postId, 'product-categories', ['fields' => 'names']);
|
||||
if (is_wp_error($categories) || !is_array($categories)) {
|
||||
$categories = [];
|
||||
}
|
||||
|
||||
$title = get_the_title($postId);
|
||||
$label = '#' . $postId . ' - ' . $title;
|
||||
if ($priceText) {
|
||||
$label .= ' | ' . $priceText;
|
||||
}
|
||||
if ($categories) {
|
||||
$label .= ' | ' . implode(', ', $categories);
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $postId,
|
||||
'name' => $title,
|
||||
'label' => $label,
|
||||
'short_description' => wp_kses_post(get_the_excerpt($postId)),
|
||||
'price_html' => $priceHtml,
|
||||
'price_text' => $priceText,
|
||||
'categories' => $categories,
|
||||
'image' => get_the_post_thumbnail_url($postId, $imageSize) ?: '',
|
||||
'permalink' => get_permalink($postId) ?: '#',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Services\BlockRender;
|
||||
|
||||
class CartProducts
|
||||
{
|
||||
/**
|
||||
* Render FluentCart products list block for email body.
|
||||
*
|
||||
* @param string $content
|
||||
* @param array $data
|
||||
* @return string
|
||||
*/
|
||||
public static function renderProducts($content, $data)
|
||||
{
|
||||
$customRenderer = apply_filters('fluent_crm/cart_products_renderer', null, $content, $data);
|
||||
if (is_callable($customRenderer)) {
|
||||
try {
|
||||
$rendered = call_user_func($customRenderer, $content, $data);
|
||||
return is_string($rendered) ? ProductListRenderer::postProcessRendered($rendered, $data) : '';
|
||||
} catch (\Throwable $e) {
|
||||
// Fall through to built-in renderer.
|
||||
}
|
||||
}
|
||||
|
||||
if (!defined('FLUENTCART_VERSION')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$attrs = isset($data['attrs']) && is_array($data['attrs']) ? $data['attrs'] : [];
|
||||
$atts = ProductListRenderer::mergeDefaultAttributes($attrs);
|
||||
$products = self::getFluentCartProducts($atts);
|
||||
|
||||
if (!$products) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$html = ProductListRenderer::renderProducts($products, $atts, 'fc_woo_products fc_cart_products');
|
||||
|
||||
return ProductListRenderer::postProcessRendered($html, $data);
|
||||
}
|
||||
|
||||
private static function getFluentCartProducts($atts)
|
||||
{
|
||||
return CartProductData::getProducts([
|
||||
'perPage' => isset($atts['selectedPostsPerPage']) ? $atts['selectedPostsPerPage'] : 3,
|
||||
'order' => isset($atts['order']) ? $atts['order'] : 'desc',
|
||||
'orderBy' => isset($atts['orderBy']) ? $atts['orderBy'] : 'date',
|
||||
'taxType' => isset($atts['taxType']) ? $atts['taxType'] : 'all',
|
||||
], 'large');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,442 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Services\BlockRender;
|
||||
|
||||
class ProductListRenderer
|
||||
{
|
||||
/**
|
||||
* Default block attributes for list-style product blocks.
|
||||
*
|
||||
* @param array $attrs
|
||||
* @return array
|
||||
*/
|
||||
public static function mergeDefaultAttributes($attrs = [])
|
||||
{
|
||||
if (!is_array($attrs)) {
|
||||
$attrs = [];
|
||||
}
|
||||
|
||||
return wp_parse_args($attrs, [
|
||||
'selectedLayout' => 'default',
|
||||
'selectedPostsPerPage' => '3',
|
||||
'showDescription' => true,
|
||||
'showImage' => true,
|
||||
'showPrice' => true,
|
||||
'showButton' => true,
|
||||
'buttonText' => 'Buy Now',
|
||||
'titleColor' => '#2a363d',
|
||||
'descriptionColor' => '',
|
||||
'priceColor' => '#37454e',
|
||||
'buttonColor' => '',
|
||||
'buttonBG' => '#2a363d',
|
||||
'taxType' => 'all',
|
||||
'order' => 'desc',
|
||||
'orderBy' => 'date',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert normalized product arrays into email-safe HTML.
|
||||
*
|
||||
* @param array $products
|
||||
* @param array $atts
|
||||
* @param string $tableClass
|
||||
* @return string
|
||||
*/
|
||||
public static function renderProducts($products, $atts, $tableClass = 'fc_woo_products')
|
||||
{
|
||||
if (!is_array($products) || !$products) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$selectedLayout = self::normalizeLayout(isset($atts['selectedLayout']) ? $atts['selectedLayout'] : 'default');
|
||||
$productsHtml = '';
|
||||
|
||||
foreach ($products as $product) {
|
||||
$productsHtml .= self::renderProduct($product, $atts, $selectedLayout);
|
||||
}
|
||||
|
||||
if (!$productsHtml) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ($selectedLayout === 'layout-2') {
|
||||
return $productsHtml;
|
||||
}
|
||||
|
||||
return '<table class="' . esc_attr($tableClass) . '" border="0" cellpadding="0" cellspacing="0" width="100%"><tbody><tr><td class="template-' . esc_attr($selectedLayout) . '">' . $productsHtml . '</td></tr></tbody></table>';
|
||||
}
|
||||
|
||||
public static function postProcessRendered($html, $data)
|
||||
{
|
||||
if (!is_string($html) || $html === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$attrs = isset($data['attrs']) && is_array($data['attrs']) ? $data['attrs'] : [];
|
||||
if (!self::isShowImageEnabled($attrs)) {
|
||||
$html = self::removeProductImagePlaceholders($html);
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private static function renderProduct($product, $atts, $layout)
|
||||
{
|
||||
$showImage = RenderValueHelper::normalizeBool(isset($atts['showImage']) ? $atts['showImage'] : true, true);
|
||||
$showDescription = RenderValueHelper::normalizeBool(isset($atts['showDescription']) ? $atts['showDescription'] : true, true);
|
||||
$showPrice = RenderValueHelper::normalizeBool(isset($atts['showPrice']) ? $atts['showPrice'] : true, true);
|
||||
$showButton = RenderValueHelper::normalizeBool(isset($atts['showButton']) ? $atts['showButton'] : true, true);
|
||||
|
||||
$titleColor = trim((string)(isset($atts['titleColor']) ? $atts['titleColor'] : ''));
|
||||
$descriptionColor = trim((string)(isset($atts['descriptionColor']) ? $atts['descriptionColor'] : ''));
|
||||
$priceColor = trim((string)(isset($atts['priceColor']) ? $atts['priceColor'] : ''));
|
||||
$buttonColor = trim((string)(isset($atts['buttonColor']) ? $atts['buttonColor'] : ''));
|
||||
$buttonBg = trim((string)(isset($atts['buttonBG']) ? $atts['buttonBG'] : ''));
|
||||
|
||||
$title = wp_kses_post(isset($product['name']) ? $product['name'] : '');
|
||||
$description = wp_kses_post(isset($product['short_description']) ? $product['short_description'] : '');
|
||||
$priceHtml = self::preparePriceHtml(
|
||||
isset($product['price_html']) ? $product['price_html'] : '',
|
||||
__('Free', 'fluent-crm')
|
||||
);
|
||||
$image = esc_url(isset($product['image']) ? $product['image'] : '');
|
||||
$permalink = esc_url(isset($product['permalink']) ? $product['permalink'] : '#');
|
||||
$buttonText = esc_html(isset($atts['buttonText']) ? $atts['buttonText'] : __('Buy Now', 'fluent-crm'));
|
||||
|
||||
$titleStyle = $titleColor ? 'color:' . esc_attr($titleColor) . ';' : '';
|
||||
$descriptionStyle = $descriptionColor ? 'color:' . esc_attr($descriptionColor) . ';' : '';
|
||||
$priceStyle = $priceColor ? 'color:' . esc_attr($priceColor) . ';' : '';
|
||||
$buttonStyle = $buttonColor ? 'color:' . esc_attr($buttonColor) . ';' : '';
|
||||
|
||||
if ($layout === 'layout-2' && $buttonBg) {
|
||||
$buttonStyle .= 'background:' . esc_attr($buttonBg) . ';';
|
||||
}
|
||||
|
||||
if ($layout === 'layout-2') {
|
||||
return self::renderLayoutTwoProduct(
|
||||
$title,
|
||||
$description,
|
||||
$priceHtml,
|
||||
$image,
|
||||
$permalink,
|
||||
$buttonText,
|
||||
$titleStyle,
|
||||
$descriptionStyle,
|
||||
$priceStyle,
|
||||
$buttonStyle,
|
||||
$showImage,
|
||||
$showDescription,
|
||||
$showPrice,
|
||||
$showButton
|
||||
);
|
||||
}
|
||||
|
||||
$contentHtml = '<div class="fc_woo_product_info"><div>';
|
||||
$contentHtml .= '<h2 class="title"><a style="' . esc_attr($titleStyle) . '" target="_blank" href="' . $permalink . '">' . $title . '</a></h2>';
|
||||
|
||||
if ($showPrice) {
|
||||
$contentHtml .= '<span class="price" style="' . esc_attr($priceStyle) . '">' . $priceHtml . '</span>';
|
||||
}
|
||||
|
||||
$contentHtml .= '</div>';
|
||||
|
||||
if ($layout !== 'layout-3' && $showButton) {
|
||||
$contentHtml .= '<a href="' . $permalink . '" target="_blank" style="' . esc_attr($buttonStyle) . '" class="add-to-cart-btn">' . $buttonText . '</a>';
|
||||
}
|
||||
|
||||
$contentHtml .= '</div>';
|
||||
|
||||
$imageHtml = '';
|
||||
if ($showImage && $image) {
|
||||
$imageHtml = '<div class="fc_woo_product_img"><img src="' . $image . '" alt="' . esc_attr(wp_strip_all_tags($title)) . '"></div>';
|
||||
}
|
||||
|
||||
return '<table class="fce_row fc_woo_product ' . esc_attr($layout) . '" border="0" cellpadding="0" cellspacing="0" width="40%" style="margin-bottom:35px;"><tbody><tr><td width="100%">' . $imageHtml . $contentHtml . '</td></tr></tbody></table>';
|
||||
}
|
||||
|
||||
private static function renderLayoutTwoProduct($title, $description, $priceHtml, $image, $permalink, $buttonText, $titleStyle, $descriptionStyle, $priceStyle, $buttonStyle, $showImage, $showDescription, $showPrice, $showButton)
|
||||
{
|
||||
$hasImage = $showImage && $image;
|
||||
$imageCell = '';
|
||||
if ($hasImage) {
|
||||
$imageCell = '<td width="45%" class="fc_cart_product_img" style="background:url(\'' . $image . '\') center no-repeat;background-size:cover;"></td>';
|
||||
}
|
||||
|
||||
$contentWidth = $hasImage ? '55%' : '100%';
|
||||
$contentPadding = $hasImage ? 'padding:10px 10px 10px 20px;' : 'padding:10px;';
|
||||
|
||||
$contentHtml = '<div class="fc_woo_product_info"><div>';
|
||||
$contentHtml .= '<h2 class="title"><a style="' . esc_attr($titleStyle) . '" target="_blank" href="' . $permalink . '">' . $title . '</a></h2>';
|
||||
|
||||
if ($showDescription) {
|
||||
$contentHtml .= '<div class="description" style="' . esc_attr($descriptionStyle) . '">' . $description . '</div>';
|
||||
}
|
||||
|
||||
if ($showPrice) {
|
||||
$contentHtml .= '<span class="price" style="' . esc_attr($priceStyle) . '">' . $priceHtml . '</span>';
|
||||
}
|
||||
|
||||
$contentHtml .= '</div>';
|
||||
|
||||
if ($showButton) {
|
||||
$contentHtml .= '<a href="' . $permalink . '" target="_blank" style="' . esc_attr($buttonStyle) . '" class="add-to-cart-btn">' . $buttonText . '</a>';
|
||||
}
|
||||
|
||||
$contentHtml .= '</div>';
|
||||
|
||||
return '<table class="fce_row fc_woo_product layout-2" border="0" cellpadding="0" cellspacing="0" width="100%" style="margin-bottom:35px;"><tbody><tr>' . $imageCell . '<td width="' . esc_attr($contentWidth) . '" style="' . esc_attr($contentPadding) . '">' . $contentHtml . '</td></tr></tbody></table>';
|
||||
}
|
||||
|
||||
private static function normalizeLayout($layout)
|
||||
{
|
||||
$layout = is_string($layout) ? trim($layout) : 'default';
|
||||
|
||||
return in_array($layout, ['default', 'layout-2', 'layout-3'], true) ? $layout : 'default';
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare product price markup for email/block-editor visual output.
|
||||
*
|
||||
* WooCommerce sale price HTML includes screen-reader helper spans. Remove
|
||||
* those snippet-visible labels while keeping visible sale prices readable.
|
||||
*
|
||||
* @param string $priceHtml Raw product price HTML.
|
||||
* @param string $fallback Fallback text when no price is available.
|
||||
* @return string
|
||||
*/
|
||||
public static function preparePriceHtml($priceHtml, $fallback = '')
|
||||
{
|
||||
$priceHtml = wp_kses_post((string)$priceHtml);
|
||||
|
||||
if ($priceHtml === '') {
|
||||
return esc_html($fallback);
|
||||
}
|
||||
|
||||
$priceHtml = self::removeScreenReaderText($priceHtml);
|
||||
$priceHtml = self::removeAttributeFromTag($priceHtml, 'del', 'aria-hidden');
|
||||
$priceHtml = self::removeAttributeFromTag($priceHtml, 'ins', 'aria-hidden');
|
||||
$priceHtml = self::addInlineStyleToTag(
|
||||
$priceHtml,
|
||||
'del',
|
||||
'color:#9aa3aa;text-decoration:line-through;margin-right:8px;'
|
||||
);
|
||||
$priceHtml = self::addInlineStyleToTag(
|
||||
$priceHtml,
|
||||
'ins',
|
||||
'text-decoration:none;font-weight:600;'
|
||||
);
|
||||
|
||||
return trim(wp_kses_post($priceHtml));
|
||||
}
|
||||
|
||||
private static function removeScreenReaderText($html)
|
||||
{
|
||||
return preg_replace('/<span\b(?=[^>]*\bclass=(["\'])[^"\']*\bscreen-reader-text\b[^"\']*\1)[^>]*>.*?<\/span>/is', '', $html);
|
||||
}
|
||||
|
||||
private static function addInlineStyleToTag($html, $tag, $style)
|
||||
{
|
||||
return preg_replace_callback('/<' . preg_quote($tag, '/') . '\b([^>]*)>/i', function ($matches) use ($tag, $style) {
|
||||
$attributes = $matches[1];
|
||||
|
||||
if (preg_match('/\sstyle=(["\'])(.*?)\1/i', $attributes)) {
|
||||
$attributes = preg_replace_callback('/\sstyle=(["\'])(.*?)\1/i', function ($styleMatches) use ($style) {
|
||||
if (strpos($styleMatches[2], rtrim($style, ';')) !== false) {
|
||||
return $styleMatches[0];
|
||||
}
|
||||
|
||||
return ' style=' . $styleMatches[1] . rtrim($styleMatches[2], ';') . ';' . $style . $styleMatches[1];
|
||||
}, $attributes, 1);
|
||||
|
||||
return '<' . $tag . $attributes . '>';
|
||||
}
|
||||
|
||||
return '<' . $tag . ' style="' . $style . '"' . $attributes . '>';
|
||||
}, $html);
|
||||
}
|
||||
|
||||
private static function removeAttributeFromTag($html, $tag, $attribute)
|
||||
{
|
||||
return preg_replace_callback('/<' . preg_quote($tag, '/') . '\b([^>]*)>/i', function ($matches) use ($tag, $attribute) {
|
||||
$attributes = preg_replace('/\s' . preg_quote($attribute, '/') . '\s*=\s*(["\']).*?\1/i', '', $matches[1]);
|
||||
$attributes = preg_replace('/\s' . preg_quote($attribute, '/') . '\s*=\s*[^\s>]*/i', '', $attributes);
|
||||
|
||||
return '<' . $tag . $attributes . '>';
|
||||
}, $html);
|
||||
}
|
||||
|
||||
private static function removeProductImagePlaceholders($html)
|
||||
{
|
||||
// Fast path first; also helps as fallback when DOM extension is unavailable.
|
||||
$html = preg_replace('/<img\b[^>]*>/i', '', $html);
|
||||
$html = preg_replace('/<figure\b[^>]*>\s*<\/figure>/i', '', $html);
|
||||
$html = self::stripImageColumnsWithRegex($html);
|
||||
|
||||
if (!class_exists('\DOMDocument') || trim($html) === '') {
|
||||
return $html;
|
||||
}
|
||||
|
||||
$internalErrors = libxml_use_internal_errors(true);
|
||||
$dom = new \DOMDocument('1.0', 'UTF-8');
|
||||
$loaded = $dom->loadHTML(
|
||||
'<?xml encoding="utf-8" ?><div id="fc_products_root">' . $html . '</div>',
|
||||
LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD
|
||||
);
|
||||
libxml_clear_errors();
|
||||
libxml_use_internal_errors($internalErrors);
|
||||
|
||||
if (!$loaded) {
|
||||
return $html;
|
||||
}
|
||||
|
||||
$xpath = new \DOMXPath($dom);
|
||||
|
||||
$imageClassQueries = [
|
||||
"//*[contains(concat(' ', normalize-space(@class), ' '), ' fc_woo_product_img ')]",
|
||||
"//*[contains(concat(' ', normalize-space(@class), ' '), ' fc_cart_product_img ')]"
|
||||
];
|
||||
|
||||
foreach ($imageClassQueries as $query) {
|
||||
$nodes = $xpath->query($query);
|
||||
if (!$nodes) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for ($i = $nodes->length - 1; $i >= 0; $i--) {
|
||||
self::removeNode($nodes->item($i));
|
||||
}
|
||||
}
|
||||
|
||||
$imageCells = $xpath->query("//td[contains(translate(@style, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'url(')]");
|
||||
if ($imageCells) {
|
||||
for ($i = $imageCells->length - 1; $i >= 0; $i--) {
|
||||
$cell = $imageCells->item($i);
|
||||
if (self::isImageOnlyCell($cell)) {
|
||||
self::removeNode($cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$rows = $xpath->query('//tr[count(td)=1]');
|
||||
if ($rows) {
|
||||
for ($i = 0; $i < $rows->length; $i++) {
|
||||
$row = $rows->item($i);
|
||||
$cells = $row->getElementsByTagName('td');
|
||||
if (!$cells || !$cells->item(0)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$cell = $cells->item(0);
|
||||
$cell->setAttribute('width', '100%');
|
||||
|
||||
$style = self::normalizeCellStyle($cell->getAttribute('style'));
|
||||
if ($style) {
|
||||
$cell->setAttribute('style', $style);
|
||||
} else {
|
||||
$cell->removeAttribute('style');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$root = $dom->getElementById('fc_products_root');
|
||||
if (!$root) {
|
||||
return $html;
|
||||
}
|
||||
|
||||
$output = '';
|
||||
foreach ($root->childNodes as $child) {
|
||||
$output .= $dom->saveHTML($child);
|
||||
}
|
||||
|
||||
return self::stripImageColumnsWithRegex($output ?: $html);
|
||||
}
|
||||
|
||||
private static function stripImageColumnsWithRegex($html)
|
||||
{
|
||||
$html = preg_replace(
|
||||
'/<div\b[^>]*class=(["\'])[^"\']*(?:fc_woo_product_img|fc_cart_product_img)[^"\']*\1[^>]*>.*?<\/div>/is',
|
||||
'',
|
||||
$html
|
||||
);
|
||||
|
||||
$html = preg_replace(
|
||||
'/<td\b[^>]*style=(["\'])[^"\']*url\([^)]+\)[^"\']*\1[^>]*>\s*<\/td>/is',
|
||||
'',
|
||||
$html
|
||||
);
|
||||
|
||||
$html = preg_replace('/\bwidth=(["\'])55%\1/i', 'width="100%"', $html);
|
||||
$html = preg_replace('/\bpadding-left\s*:\s*[^;]+;?/i', '', $html);
|
||||
$html = preg_replace('/\s{2,}/', ' ', $html);
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
private static function normalizeCellStyle($style)
|
||||
{
|
||||
if (!$style) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$style = preg_replace('/\bbackground(?:-image)?\s*:\s*[^;]+;?/i', '', $style);
|
||||
$style = preg_replace('/\bbackground-size\s*:\s*[^;]+;?/i', '', $style);
|
||||
$style = preg_replace('/\bbackground-position\s*:\s*[^;]+;?/i', '', $style);
|
||||
$style = preg_replace('/\bbackground-repeat\s*:\s*[^;]+;?/i', '', $style);
|
||||
$style = preg_replace('/\bpadding-left\s*:\s*[^;]+;?/i', '', $style);
|
||||
$style = preg_replace('/\bwidth\s*:\s*(?:40%|45%|50%);?/i', '', $style);
|
||||
|
||||
$style = preg_replace_callback(
|
||||
'/\bpadding\s*:\s*([0-9.]+[a-z%]+)\s+([0-9.]+[a-z%]+)\s+([0-9.]+[a-z%]+)\s+([0-9.]+[a-z%]+)\s*;?/i',
|
||||
function ($matches) {
|
||||
return 'padding:' . $matches[1] . ' ' . $matches[2] . ' ' . $matches[3] . ';';
|
||||
},
|
||||
$style
|
||||
);
|
||||
|
||||
$style = preg_replace('/;\s*;/', ';', $style);
|
||||
|
||||
return trim($style, " \t\n\r\0\x0B;");
|
||||
}
|
||||
|
||||
private static function removeNode($node)
|
||||
{
|
||||
if ($node && $node->parentNode) {
|
||||
$node->parentNode->removeChild($node);
|
||||
}
|
||||
}
|
||||
|
||||
private static function isImageOnlyCell($cell)
|
||||
{
|
||||
if (!$cell) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$text = trim(str_replace("\xc2\xa0", '', (string)$cell->textContent));
|
||||
if ($text !== '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($cell->childNodes as $child) {
|
||||
if ($child->nodeType !== XML_ELEMENT_NODE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$tag = strtolower($child->nodeName);
|
||||
if (!in_array($tag, ['img', 'figure', 'picture', 'source', 'br'], true)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static function isShowImageEnabled($attrs)
|
||||
{
|
||||
if (!array_key_exists('showImage', $attrs)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return RenderValueHelper::normalizeBool($attrs['showImage'], true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Services\BlockRender;
|
||||
|
||||
class RenderValueHelper
|
||||
{
|
||||
/**
|
||||
* Normalize mixed values coming from block attrs into bool.
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param bool $default
|
||||
* @return bool
|
||||
*/
|
||||
public static function normalizeBool($value, $default = true)
|
||||
{
|
||||
if ($value === null) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
if (is_bool($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (is_numeric($value)) {
|
||||
return ((int)$value) !== 0;
|
||||
}
|
||||
|
||||
if (is_string($value)) {
|
||||
$normalized = strtolower(trim($value));
|
||||
return !in_array($normalized, ['0', 'false', 'no', 'off'], true);
|
||||
}
|
||||
|
||||
return (bool)$value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Services\BlockRender;
|
||||
|
||||
use FluentCrm\Framework\Support\Arr;
|
||||
|
||||
class WooProduct
|
||||
{
|
||||
private static $productsCache = [];
|
||||
|
||||
public static function renderProduct($buttonHtml, $data)
|
||||
{
|
||||
if(!defined('WC_PLUGIN_FILE')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$defaultAtts = [
|
||||
'productId' => null,
|
||||
'showDescription' => true,
|
||||
'showPrice' => true,
|
||||
'customImage' => '',
|
||||
'backgroundColor' => '#fffeeb',
|
||||
'contentColor' => '',
|
||||
'pricingColor' => '',
|
||||
'template' => 'left'
|
||||
];
|
||||
$atts = wp_parse_args($data['attrs'], $defaultAtts);
|
||||
$productId = (int) $atts['productId'];
|
||||
|
||||
if (!$productId) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if(isset(self::$productsCache[$productId])) {
|
||||
$product = self::$productsCache[$productId];
|
||||
} else {
|
||||
self::$productsCache[$productId] = wc_get_product($productId);
|
||||
$product = self::$productsCache[$productId];
|
||||
}
|
||||
|
||||
if (!$product) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$tableStyle = 'border-radius: 5px;';
|
||||
|
||||
if ($bgColor = Arr::get($atts, 'backgroundColor')) {
|
||||
$tableStyle .= 'background-color: ' . $bgColor . ';';
|
||||
}
|
||||
|
||||
$contentColorStyle = '';
|
||||
if ($color = Arr::get($atts, 'contentColor')) {
|
||||
$tableStyle .= 'color: ' . $color . ';';
|
||||
$contentColorStyle = 'color: ' . $color . ';';
|
||||
}
|
||||
|
||||
$pricingStyle = 'display:block;margin:8px 0 10px;font-size:18px;line-height:1.25;';
|
||||
if ($color = Arr::get($atts, 'pricingColor')) {
|
||||
$pricingStyle .= 'color: ' . $color . ';';
|
||||
}
|
||||
|
||||
$contentHtml = sprintf(
|
||||
'<h2 style="%1$s">%2$s</h2>',
|
||||
$contentColorStyle.'margin: 0;',
|
||||
wp_kses_post($product->get_title())
|
||||
);
|
||||
|
||||
if (Arr::get($atts, 'showDescription')) {
|
||||
$contentHtml .= sprintf(
|
||||
'<div style="%1$s">%2$s</div>',
|
||||
$contentColorStyle.'margin-bottom: 5px;',
|
||||
wc_format_content(wp_kses_post($product->get_short_description() ? $product->get_short_description() : wc_trim_string($product->get_description(), 400)))
|
||||
);
|
||||
}
|
||||
|
||||
if (Arr::get($atts, 'showPrice')) {
|
||||
$contentHtml .= sprintf(
|
||||
'<div class="fcw_p_price" style="%1$s">%2$s</div>',
|
||||
$pricingStyle,
|
||||
ProductListRenderer::preparePriceHtml($product->get_price_html(), __('Free', 'fluent-crm'))
|
||||
);
|
||||
}
|
||||
|
||||
$image = '';
|
||||
|
||||
$template = Arr::get($atts, 'template', 'left');
|
||||
|
||||
if($template != 'none') {
|
||||
$image = self::getImage($product);
|
||||
}
|
||||
if (!$image) {
|
||||
$template = 'none';
|
||||
}
|
||||
|
||||
$atts['template'] = $template;
|
||||
|
||||
$contentStyle = '';
|
||||
if ($template == 'none' || $template == 'top') {
|
||||
$contentStyle = 'text-align: center;';
|
||||
}
|
||||
|
||||
$imageTd = '';
|
||||
if($image) {
|
||||
if($template == 'top') {
|
||||
$contentHtml = '<img src="'.$image.'" width="auto" alt="'.$product->get_title().'" style="border: 0; height: auto; outline: none; text-decoration: none; max-width: 100%; -ms-interpolation-mode: bicubic; display: block;margin: 0 auto 15px;">'.$contentHtml;
|
||||
} else if($template == 'left') {
|
||||
$imageContent = '<img src="'.$image.'" width="auto" alt="'.$product->get_title().'" style="border: 0; height: auto; outline: none; text-decoration: none; max-width: 100%; -ms-interpolation-mode: bicubic; display: block;margin: 0 auto;">';
|
||||
$imageTd = self::getContentTd($imageContent, $template);
|
||||
}
|
||||
}
|
||||
|
||||
$buttonHtml = '<div style="margin-top: 10px; margin-bottom: 10px;">'.$buttonHtml.'</div>';
|
||||
|
||||
$contentHtml .= $buttonHtml;
|
||||
$tableClass = 'fce_row';
|
||||
if ($template === 'left' || $template === 'right') {
|
||||
$tableClass .= ' fc_woo_product_stack_mobile';
|
||||
}
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
|
||||
<table class="<?php echo esc_attr($tableClass); ?>" border="0" cellpadding="0" cellspacing="0" width="100%" style="table-layout: fixed; border-collapse: collapse;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-ms-text-size-adjust: 100%;-webkit-text-size-adjust: 100%;margin-bottom: 20px; margin-top: 20px;<?php echo $tableStyle; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>"><tbody><tr>
|
||||
<?php
|
||||
if($imageTd) {
|
||||
echo $imageTd; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
echo self::getContentTd($contentHtml, $template, $contentStyle); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
?>
|
||||
</tr></tbody></table>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
private static function getContentTd($contentHtml, $template, $extraStyle = '')
|
||||
{
|
||||
if(!defined('WC_PLUGIN_FILE')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$width = '100';
|
||||
if($template == 'left') {
|
||||
$width = '50';
|
||||
}
|
||||
return '<td align="center" valign="middle" width="' . $width . '%" class="fce_column"><table border="0" cellpadding="10" cellspacing="0" width="100%"><tr><td class="fc_column_content" style="padding: 10px;'. $extraStyle.'">'.$contentHtml.'</td></tr></table></td>'; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the main product image URL.
|
||||
*
|
||||
* @param \WC_Product $product Product object.
|
||||
* @param string $size Image size, defaults to 'full'.
|
||||
* @return string
|
||||
*/
|
||||
public static function getImage($product, $size = 'full')
|
||||
{
|
||||
if(!defined('WC_PLUGIN_FILE')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
static $imageCache = [];
|
||||
|
||||
if(isset($imageCache[$product->get_id()])) {
|
||||
return $imageCache[$product->get_id()];
|
||||
}
|
||||
|
||||
$image = '';
|
||||
if ($product->get_image_id()) {
|
||||
$image = wp_get_attachment_image_url($product->get_image_id(), $size);
|
||||
} elseif ($product->get_parent_id()) {
|
||||
$parent_product = wc_get_product($product->get_parent_id());
|
||||
if ($parent_product) {
|
||||
$image = wp_get_attachment_image_url($parent_product->get_image_id(), $size);
|
||||
}
|
||||
}
|
||||
|
||||
$imageCache[$product->get_id()] = $image;
|
||||
|
||||
return $imageCache[$product->get_id()] ;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace FluentCrm\App\Services\BlockRender;
|
||||
|
||||
class WooProducts
|
||||
{
|
||||
/**
|
||||
* Render WooCommerce products list block for email body.
|
||||
*
|
||||
* @param string $content
|
||||
* @param array $data
|
||||
* @return string
|
||||
*/
|
||||
public static function renderProducts($content, $data)
|
||||
{
|
||||
$customRenderer = apply_filters('fluent_crm/products_renderer', null, $content, $data);
|
||||
if (is_callable($customRenderer)) {
|
||||
try {
|
||||
$rendered = call_user_func($customRenderer, $content, $data);
|
||||
return is_string($rendered) ? ProductListRenderer::postProcessRendered($rendered, $data) : '';
|
||||
} catch (\Throwable $e) {
|
||||
// Fall through to built-in renderer.
|
||||
}
|
||||
}
|
||||
|
||||
if (!defined('WC_PLUGIN_FILE') || !function_exists('wc_get_products')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$attrs = isset($data['attrs']) && is_array($data['attrs']) ? $data['attrs'] : [];
|
||||
$atts = ProductListRenderer::mergeDefaultAttributes($attrs);
|
||||
$products = self::getWooProducts($atts);
|
||||
|
||||
if (!$products) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$html = ProductListRenderer::renderProducts($products, $atts, 'fc_woo_products');
|
||||
|
||||
return ProductListRenderer::postProcessRendered($html, $data);
|
||||
}
|
||||
|
||||
private static function getWooProducts($atts)
|
||||
{
|
||||
if (!defined('WC_PLUGIN_FILE') || !function_exists('wc_get_products')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$perPage = max(1, min(20, absint(isset($atts['selectedPostsPerPage']) ? $atts['selectedPostsPerPage'] : 3)));
|
||||
$order = strtolower((string)(isset($atts['order']) ? $atts['order'] : 'desc')) === 'asc' ? 'ASC' : 'DESC';
|
||||
$orderByRaw = sanitize_key((string)(isset($atts['orderBy']) ? $atts['orderBy'] : 'date'));
|
||||
|
||||
$orderByMap = [
|
||||
'date' => 'date',
|
||||
'modified' => 'modified',
|
||||
'title' => 'title',
|
||||
'menu_order' => 'menu_order',
|
||||
'rand' => 'rand',
|
||||
];
|
||||
$orderBy = isset($orderByMap[$orderByRaw]) ? $orderByMap[$orderByRaw] : 'date';
|
||||
|
||||
$queryArgs = [
|
||||
'status' => 'publish',
|
||||
'limit' => $perPage,
|
||||
'orderby' => $orderBy,
|
||||
'order' => $order,
|
||||
'return' => 'objects',
|
||||
];
|
||||
|
||||
$taxType = sanitize_text_field((string)(isset($atts['taxType']) ? $atts['taxType'] : 'all'));
|
||||
if ($taxType && $taxType !== 'all' && is_numeric($taxType)) {
|
||||
$term = get_term((int)$taxType, 'product_cat');
|
||||
if ($term && !is_wp_error($term) && !empty($term->slug)) {
|
||||
$queryArgs['category'] = [$term->slug];
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$wooProducts = wc_get_products($queryArgs);
|
||||
} catch (\Throwable $e) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!$wooProducts) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$products = [];
|
||||
foreach ($wooProducts as $product) {
|
||||
if (!$product || !method_exists($product, 'get_id')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$description = $product->get_short_description();
|
||||
if (!$description && method_exists($product, 'get_description')) {
|
||||
$description = wc_trim_string($product->get_description(), 400);
|
||||
}
|
||||
|
||||
$products[] = [
|
||||
'id' => $product->get_id(),
|
||||
'name' => $product->get_name(),
|
||||
'short_description' => wp_kses_post($description),
|
||||
'price_html' => ProductListRenderer::preparePriceHtml($product->get_price_html(), __('Free', 'fluent-crm')),
|
||||
'image' => WooProduct::getImage($product),
|
||||
'permalink' => $product->get_permalink() ?: '#',
|
||||
];
|
||||
}
|
||||
|
||||
return $products;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user