feat(main): Introduce USP block type and custom classes for HTML groups

This release adds a new "USP" (Unique Selling Proposition) block type,
allowing for flexible display of image-based selling points with optional links.

Additionally, administrators can now assign custom CSS classes to Custom HTML Groups,
enhancing layout and styling capabilities.

Includes a refinement to the FAQ accordion template to ensure questions without answers
do not display a collapse button, improving usability.

Updates module version to 1.13.0 with a database upgrade script for the new `custom_class` column.
This commit is contained in:
2026-08-20 15:22:15 +02:00
parent a4e479490b
commit 06e98a429b
19 changed files with 156 additions and 66 deletions
+1
View File
@@ -15,6 +15,7 @@ class Config
'faq' => 'FAQ Accordion', 'faq' => 'FAQ Accordion',
'banner' => 'Banner', 'banner' => 'Banner',
'alert' => 'Alert', 'alert' => 'Alert',
'usp' => 'USP',
]; ];
const ALERT_STYLES = [ const ALERT_STYLES = [
@@ -17,6 +17,7 @@ final class CustomHtmlGroupDataHandler implements FormDataHandlerInterface
$group = new CustomHtmlGroup(); $group = new CustomHtmlGroup();
$group->title = (string) $formData['title']; $group->title = (string) $formData['title'];
$group->hook_name = (string) $formData['hook_name']; $group->hook_name = (string) $formData['hook_name'];
$group->custom_class = (string) ($formData['custom_class'] ?? '');
$group->active = (int) $formData['active']; $group->active = (int) $formData['active'];
$group->ordering = $this->getNextOrdering(); $group->ordering = $this->getNextOrdering();
@@ -48,6 +49,7 @@ final class CustomHtmlGroupDataHandler implements FormDataHandlerInterface
$group->title = (string) $formData['title']; $group->title = (string) $formData['title'];
$group->hook_name = (string) $formData['hook_name']; $group->hook_name = (string) $formData['hook_name'];
$group->custom_class = (string) ($formData['custom_class'] ?? '');
$group->active = (int) $formData['active']; $group->active = (int) $formData['active'];
if (!$group->update()) { if (!$group->update()) {
@@ -19,6 +19,7 @@ final class CustomHtmlGroupDataProvider implements FormDataProviderInterface
return [ return [
'title' => $group->title, 'title' => $group->title,
'hook_name' => $group->hook_name, 'hook_name' => $group->hook_name,
'custom_class' => $group->custom_class,
'active' => (bool) $group->active, 'active' => (bool) $group->active,
]; ];
} }
@@ -28,6 +29,7 @@ final class CustomHtmlGroupDataProvider implements FormDataProviderInterface
return [ return [
'title' => '', 'title' => '',
'hook_name' => '', 'hook_name' => '',
'custom_class' => '',
'active' => true, 'active' => true,
]; ];
} }
+3 -3
View File
@@ -58,9 +58,9 @@ class CustomHtmlBlockType extends AbstractType
'row_attr' => ['class' => 'customhtmlblock-type-field customhtmlblock-type-html'], 'row_attr' => ['class' => 'customhtmlblock-type-field customhtmlblock-type-html'],
]) ])
->add('image', FileType::class, [ ->add('image', FileType::class, [
'label' => $this->translator->trans('Banner image', [], 'Modules.Wscustomhtml.Admin'), 'label' => $this->translator->trans('Image', [], 'Modules.Wscustomhtml.Admin'),
'required' => false, 'required' => false,
'row_attr' => ['class' => 'customhtmlblock-type-field customhtmlblock-type-banner'], 'row_attr' => ['class' => 'customhtmlblock-type-field customhtmlblock-type-banner customhtmlblock-type-usp'],
]) ])
->add('faqs', CollectionType::class, [ ->add('faqs', CollectionType::class, [
'entry_type' => CustomHtmlFaqType::class, 'entry_type' => CustomHtmlFaqType::class,
@@ -97,7 +97,7 @@ class CustomHtmlBlockType extends AbstractType
'label' => $this->translator->trans('Link', [], 'Modules.Wscustomhtml.Admin'), 'label' => $this->translator->trans('Link', [], 'Modules.Wscustomhtml.Admin'),
'required' => false, 'required' => false,
'attr' => ['placeholder' => 'https://example.com or /internal-page'], 'attr' => ['placeholder' => 'https://example.com or /internal-page'],
'row_attr' => ['class' => 'customhtmlblock-type-field customhtmlblock-type-banner'], 'row_attr' => ['class' => 'customhtmlblock-type-field customhtmlblock-type-banner customhtmlblock-type-usp'],
]) ])
->add('custom_class', TextType::class, [ ->add('custom_class', TextType::class, [
'label' => $this->translator->trans('Custom classes', [], 'Modules.Wscustomhtml.Admin'), 'label' => $this->translator->trans('Custom classes', [], 'Modules.Wscustomhtml.Admin'),
+5
View File
@@ -28,6 +28,11 @@ class CustomHtmlGroupType extends AbstractType
'label' => $this->translator->trans('Hook name', [], 'Modules.Wscustomhtml.Admin'), 'label' => $this->translator->trans('Hook name', [], 'Modules.Wscustomhtml.Admin'),
'required' => true, 'required' => true,
]) ])
->add('custom_class', TextType::class, [
'label' => $this->translator->trans('Custom classes', [], 'Modules.Wscustomhtml.Admin'),
'required' => false,
'attr' => ['placeholder' => 'col-md-6'],
])
->add('active', SwitchType::class, [ ->add('active', SwitchType::class, [
'label' => $this->translator->trans('Active', [], 'Modules.Wscustomhtml.Admin'), 'label' => $this->translator->trans('Active', [], 'Modules.Wscustomhtml.Admin'),
'required' => false, 'required' => false,
@@ -72,6 +72,11 @@ final class CustomHtmlGroupGridDefinitionFactory extends AbstractGridDefinitionF
->setName($this->trans('Hook', [], 'Modules.Wscustomhtml.Admin')) ->setName($this->trans('Hook', [], 'Modules.Wscustomhtml.Admin'))
->setOptions(['field' => 'hook_name']) ->setOptions(['field' => 'hook_name'])
) )
->add(
(new DataColumn('custom_class'))
->setName($this->trans('Custom classes', [], 'Modules.Wscustomhtml.Admin'))
->setOptions(['field' => 'custom_class'])
)
->add( ->add(
(new DataColumn('blocks_count')) (new DataColumn('blocks_count'))
->setName($this->trans('Blocks', [], 'Modules.Wscustomhtml.Admin')) ->setName($this->trans('Blocks', [], 'Modules.Wscustomhtml.Admin'))
@@ -32,6 +32,7 @@ final class CustomHtmlGroupQueryBuilder extends AbstractDoctrineQueryBuilder
'wchg.`id_ws_customhtml_group`', 'wchg.`id_ws_customhtml_group`',
'wchg.`title`', 'wchg.`title`',
'wchg.`hook_name`', 'wchg.`hook_name`',
'wchg.`custom_class`',
'wchg.`active`', 'wchg.`active`',
'wchg.`ordering`', 'wchg.`ordering`',
'wchg.`date_add`', 'wchg.`date_add`',
+5 -1
View File
@@ -16,6 +16,9 @@ class CustomHtmlGroup extends ObjectModel
/** @var string */ /** @var string */
public $title = ''; public $title = '';
/** @var string */
public $custom_class = '';
/** @var int */ /** @var int */
public $active = 1; public $active = 1;
@@ -34,6 +37,7 @@ class CustomHtmlGroup extends ObjectModel
'fields' => [ 'fields' => [
'hook_name' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'required' => true, 'size' => 64], 'hook_name' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'required' => true, 'size' => 64],
'title' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'required' => true, 'size' => 256], 'title' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'required' => true, 'size' => 256],
'custom_class' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'required' => false, 'size' => 128],
'active' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'], 'active' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
'ordering' => ['type' => self::TYPE_INT, 'validate' => 'isInt'], 'ordering' => ['type' => self::TYPE_INT, 'validate' => 'isInt'],
'date_add' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'], 'date_add' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
@@ -44,7 +48,7 @@ class CustomHtmlGroup extends ObjectModel
public static function getActiveByHookName(string $hookName): array public static function getActiveByHookName(string $hookName): array
{ {
$result = Db::getInstance()->executeS( $result = Db::getInstance()->executeS(
'SELECT `id_ws_customhtml_group`, `title`, `active`, `ordering` 'SELECT `id_ws_customhtml_group`, `title`, `active`, `custom_class`, `ordering`
FROM `' . _DB_PREFIX_ . 'ws_customhtml_group` FROM `' . _DB_PREFIX_ . 'ws_customhtml_group`
WHERE `hook_name` = \'' . pSQL($hookName) . '\' WHERE `hook_name` = \'' . pSQL($hookName) . '\'
AND `active` = 1 AND `active` = 1
+3 -3
View File
@@ -69,7 +69,7 @@ trait DisplayHooks
$blocksByGroup = CustomHtmlBlock::getActiveByGroupIds(array_column($groups, 'id_ws_customhtml_group')); $blocksByGroup = CustomHtmlBlock::getActiveByGroupIds(array_column($groups, 'id_ws_customhtml_group'));
$blocksByGroup = $this->attachFaqsToBlocks($blocksByGroup); $blocksByGroup = $this->attachFaqsToBlocks($blocksByGroup);
$blocksByGroup = $this->attachBannerImageUrls($blocksByGroup); $blocksByGroup = $this->attachImageUrls($blocksByGroup);
$groups = array_map(static function (array $group) use ($blocksByGroup) { $groups = array_map(static function (array $group) use ($blocksByGroup) {
$group['blocks'] = $blocksByGroup[(int) $group['id_ws_customhtml_group']] ?? []; $group['blocks'] = $blocksByGroup[(int) $group['id_ws_customhtml_group']] ?? [];
@@ -95,11 +95,11 @@ trait DisplayHooks
}, $blocksByGroup); }, $blocksByGroup);
} }
private function attachBannerImageUrls(array $blocksByGroup): array private function attachImageUrls(array $blocksByGroup): array
{ {
return array_map(static function (array $blocks) { return array_map(static function (array $blocks) {
return array_map(static function (array $block) { return array_map(static function (array $block) {
if ($block['type'] === 'banner') { if (in_array($block['type'], ['banner', 'usp'], true)) {
$block['image_url'] = CustomHtmlBlock::getBannerImageUrl($block['image']); $block['image_url'] = CustomHtmlBlock::getBannerImageUrl($block['image']);
} }
+1
View File
@@ -50,6 +50,7 @@ trait Install
`id_ws_customhtml_group` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, `id_ws_customhtml_group` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`hook_name` VARCHAR(64) NOT NULL, `hook_name` VARCHAR(64) NOT NULL,
`title` VARCHAR(256) NOT NULL DEFAULT \'\', `title` VARCHAR(256) NOT NULL DEFAULT \'\',
`custom_class` VARCHAR(128) NOT NULL DEFAULT \'\',
`active` TINYINT(1) NOT NULL DEFAULT 1, `active` TINYINT(1) NOT NULL DEFAULT 1,
`ordering` INT(11) NOT NULL DEFAULT 0, `ordering` INT(11) NOT NULL DEFAULT 0,
`date_add` DATETIME NOT NULL, `date_add` DATETIME NOT NULL,
+21
View File
@@ -0,0 +1,21 @@
<?php
if (!defined('_PS_VERSION_')) {
exit;
}
function upgrade_module_1_13_0($module)
{
$existing = Db::getInstance()->executeS(
'SHOW COLUMNS FROM `' . _DB_PREFIX_ . 'ws_customhtml_group` LIKE \'custom_class\''
);
if (!empty($existing)) {
return true;
}
return Db::getInstance()->execute(
'ALTER TABLE `' . _DB_PREFIX_ . 'ws_customhtml_group`
ADD COLUMN `custom_class` VARCHAR(128) NOT NULL DEFAULT \'\' AFTER `title`'
);
}
+15
View File
@@ -48,6 +48,21 @@
margin-right: 0.5rem; margin-right: 0.5rem;
} }
.customhtml-usp {
gap: 0.5rem;
color: inherit;
text-decoration: none;
}
.customhtml-usp-icon img {
max-height: 32px;
width: auto;
}
.customhtml-usp-text {
font-weight: 500;
}
/* FAQ */ /* FAQ */
.accordion-icon-minus { .accordion-icon-minus {
display: block; display: block;
@@ -27,7 +27,7 @@
{{ form_row(customHtmlBlockForm.content) }} {{ form_row(customHtmlBlockForm.content) }}
{% if currentImageUrl is defined and currentImageUrl %} {% if currentImageUrl is defined and currentImageUrl %}
<div class="customhtmlblock-type-field customhtmlblock-type-banner customhtmlblock-current-image"> <div class="customhtmlblock-type-field customhtmlblock-type-banner customhtmlblock-type-usp customhtmlblock-current-image">
<label class="form-control-label">{{ 'Current image'|trans({}, 'Modules.Wscustomhtml.Admin') }}</label> <label class="form-control-label">{{ 'Current image'|trans({}, 'Modules.Wscustomhtml.Admin') }}</label>
<div><img src="{{ currentImageUrl }}" alt="" style="max-width: 200px; max-height: 200px;"></div> <div><img src="{{ currentImageUrl }}" alt="" style="max-width: 200px; max-height: 200px;"></div>
</div> </div>
@@ -21,6 +21,7 @@
$('.customhtmlblock-type-faq').hide(); $('.customhtmlblock-type-faq').hide();
$('.customhtmlblock-type-banner').hide(); $('.customhtmlblock-type-banner').hide();
$('.customhtmlblock-type-alert').hide(); $('.customhtmlblock-type-alert').hide();
$('.customhtmlblock-type-usp').hide();
$('.customhtmlblock-type-' + type).show(); $('.customhtmlblock-type-' + type).show();
} }
@@ -21,6 +21,7 @@
$('.customhtmlblock-type-faq').hide(); $('.customhtmlblock-type-faq').hide();
$('.customhtmlblock-type-banner').hide(); $('.customhtmlblock-type-banner').hide();
$('.customhtmlblock-type-alert').hide(); $('.customhtmlblock-type-alert').hide();
$('.customhtmlblock-type-usp').hide();
$('.customhtmlblock-type-' + type).show(); $('.customhtmlblock-type-' + type).show();
} }
+11 -2
View File
@@ -3,6 +3,8 @@
{foreach from=$block.faqs item="faq" name="customhtmlFaqLoop"} {foreach from=$block.faqs item="faq" name="customhtmlFaqLoop"}
<div class="card"> <div class="card">
<div class="card-header" id="customhtml-faq-heading-{$block.id_ws_customhtml|intval}-{$faq.id_ws_customhtml_faq|intval}"> <div class="card-header" id="customhtml-faq-heading-{$block.id_ws_customhtml|intval}-{$faq.id_ws_customhtml_faq|intval}">
{if !empty($faq.answer)}
<button <button
class="btn btn-link col-12 d-flex text-left{if !$smarty.foreach.customhtmlFaqLoop.first} collapsed{/if}" class="btn btn-link col-12 d-flex text-left{if !$smarty.foreach.customhtmlFaqLoop.first} collapsed{/if}"
type="button" type="button"
@@ -11,10 +13,13 @@
aria-expanded="{if $smarty.foreach.customhtmlFaqLoop.first}true{else}false{/if}" aria-expanded="{if $smarty.foreach.customhtmlFaqLoop.first}true{else}false{/if}"
aria-controls="customhtml-faq-collapse-{$block.id_ws_customhtml|intval}-{$faq.id_ws_customhtml_faq|intval}" aria-controls="customhtml-faq-collapse-{$block.id_ws_customhtml|intval}-{$faq.id_ws_customhtml_faq|intval}"
> >
<div class="col"> {/if}
{$faq.question|escape:'html':'UTF-8'}
<div class="faq-question col pl-0">
{$faq.question|escape:'html':'UTF-8'}
</div> </div>
{if !empty($faq.answer)}
<div class="col-auto d-flex align-items-center"> <div class="col-auto d-flex align-items-center">
<span class="accordion-icons accordion-icon-minus"> <span class="accordion-icons accordion-icon-minus">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="2" viewBox="0 0 28 2"> <svg xmlns="http://www.w3.org/2000/svg" width="28" height="2" viewBox="0 0 28 2">
@@ -31,7 +36,10 @@
</span> </span>
</div> </div>
</button> </button>
{/if}
</div> </div>
{if !empty($faq.answer)}
<div <div
id="customhtml-faq-collapse-{$block.id_ws_customhtml|intval}-{$faq.id_ws_customhtml_faq|intval}" id="customhtml-faq-collapse-{$block.id_ws_customhtml|intval}-{$faq.id_ws_customhtml_faq|intval}"
class="collapse{if $smarty.foreach.customhtmlFaqLoop.first} show{/if}" class="collapse{if $smarty.foreach.customhtmlFaqLoop.first} show{/if}"
@@ -42,6 +50,7 @@
{$faq.answer nofilter} {$faq.answer nofilter}
</div> </div>
</div> </div>
{/if}
</div> </div>
{/foreach} {/foreach}
</div> </div>
+20
View File
@@ -0,0 +1,20 @@
{if !empty($block.image_url) || ($block.show_title && !empty($block.title))}
{if !empty($block.link)}
<a href="{$block.link|escape:'html':'UTF-8'}" class="customhtml-usp d-flex align-items-center">
{else}
<div class="customhtml-usp d-flex align-items-center">
{/if}
{if !empty($block.image_url)}
<span class="customhtml-usp-icon">
<img src="{$block.image_url|escape:'html':'UTF-8'}" alt="">
</span>
{/if}
{if $block.show_title && !empty($block.title)}
<span class="customhtml-usp-text">{$block.title|escape:'html':'UTF-8'}</span>
{/if}
{if !empty($block.link)}
</a>
{else}
</div>
{/if}
{/if}
+3 -1
View File
@@ -1,7 +1,7 @@
{if !empty($groupList)} {if !empty($groupList)}
<div class="customhtml-container {$hookName|escape:'html':'UTF-8'}--container"> <div class="customhtml-container {$hookName|escape:'html':'UTF-8'}--container">
{foreach from=$groupList item="group"} {foreach from=$groupList item="group"}
<div class="customhtml-group customhtml-group-{$group.id_ws_customhtml_group|intval} row"> <div class="customhtml-group customhtml-group-{$group.id_ws_customhtml_group|intval} row{if !empty($group.custom_class)} {$group.custom_class|escape:'html':'UTF-8'}{/if}">
{foreach from=$group.blocks item="block"} {foreach from=$group.blocks item="block"}
<div class="customhtml-block col{if !empty($block.custom_class)} {$block.custom_class|escape:'html':'UTF-8'}{/if}"> <div class="customhtml-block col{if !empty($block.custom_class)} {$block.custom_class|escape:'html':'UTF-8'}{/if}">
{assign var="customhtmlOverridePath" value="{$smarty.const._PS_THEME_DIR_}modules/ws_customhtml/views/templates/hook/{$hookName}/{$block.type}.tpl"} {assign var="customhtmlOverridePath" value="{$smarty.const._PS_THEME_DIR_}modules/ws_customhtml/views/templates/hook/{$hookName}/{$block.type}.tpl"}
@@ -17,6 +17,8 @@
{include file="module:ws_customhtml/views/templates/hook/_banner.tpl" block=$block} {include file="module:ws_customhtml/views/templates/hook/_banner.tpl" block=$block}
{elseif $block.type == 'alert'} {elseif $block.type == 'alert'}
{include file="module:ws_customhtml/views/templates/hook/_alert.tpl" block=$block} {include file="module:ws_customhtml/views/templates/hook/_alert.tpl" block=$block}
{elseif $block.type == 'usp'}
{include file="module:ws_customhtml/views/templates/hook/_usp.tpl" block=$block}
{else} {else}
{if $block.show_title && !empty($block.title)} {if $block.show_title && !empty($block.title)}
<div class="customhtml-title">{$block.title|escape:'html':'UTF-8'}</div> <div class="customhtml-title">{$block.title|escape:'html':'UTF-8'}</div>
+1 -1
View File
@@ -29,7 +29,7 @@ class Ws_CustomHtml extends Module implements WidgetInterface
public function __construct() public function __construct()
{ {
$this->name = 'ws_customhtml'; $this->name = 'ws_customhtml';
$this->version = '1.11.0'; $this->version = '1.13.0';
$this->author = 'Isabelle Oving-Anno | De Websmid BV'; $this->author = 'Isabelle Oving-Anno | De Websmid BV';
$this->need_instance = 0; $this->need_instance = 0;
$this->bootstrap = true; $this->bootstrap = true;