feat(main): Add FAQ accordion

This commit is contained in:
2026-08-11 13:25:06 +02:00
parent 635c13a0ed
commit 1493081602
22 changed files with 459 additions and 4 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

+8
View File
@@ -33,6 +33,14 @@ services:
arguments: arguments:
- '@websmid.customhtml.form.customhtmlgroup_data_handler' - '@websmid.customhtml.form.customhtmlgroup_data_handler'
websmid.customhtml.form.type.customhtml_faq:
public: true
class: 'Websmid\CustomHtml\Form\Type\CustomHtmlFaqType'
arguments:
- '@translator'
tags:
- { name: form.type }
websmid.customhtml.form.type.customhtml_block: websmid.customhtml.form.type.customhtml_block:
public: true public: true
class: 'Websmid\CustomHtml\Form\Type\CustomHtmlBlockType' class: 'Websmid\CustomHtml\Form\Type\CustomHtmlBlockType'
+6
View File
@@ -6,6 +6,12 @@ class Config
{ {
const HOOKS = [ const HOOKS = [
'actionAdminControllerSetMedia', 'actionAdminControllerSetMedia',
'actionCustomhtmlblockGridDataModifier',
];
const BLOCK_TYPES = [
'html' => 'HTML',
'faq' => 'FAQ Accordion',
]; ];
const ADMIN_CONTROLLERS = [ const ADMIN_CONTROLLERS = [
@@ -11,6 +11,7 @@ use Symfony\Component\HttpFoundation\Response;
use Websmid\CustomHtml\Grid\Definition\Factory\CustomHtmlBlockGridDefinitionFactory; use Websmid\CustomHtml\Grid\Definition\Factory\CustomHtmlBlockGridDefinitionFactory;
use Websmid\CustomHtml\Grid\Filters\CustomHtmlBlockFilters; use Websmid\CustomHtml\Grid\Filters\CustomHtmlBlockFilters;
use Websmid\CustomHtml\Model\CustomHtmlBlock; use Websmid\CustomHtml\Model\CustomHtmlBlock;
use Websmid\CustomHtml\Model\CustomHtmlFaq;
use Validate; use Validate;
class CustomHtmlBlockController extends FrameworkBundleAdminController class CustomHtmlBlockController extends FrameworkBundleAdminController
@@ -107,6 +108,7 @@ class CustomHtmlBlockController extends FrameworkBundleAdminController
return $this->redirectToRoute('customhtmlblock_index'); return $this->redirectToRoute('customhtmlblock_index');
} }
CustomHtmlFaq::deleteByBlockId($idCustomHtmlBlock);
$block->delete(); $block->delete();
$this->addFlash('success', $this->trans('Successful deletion.', 'Admin.Notifications.Success')); $this->addFlash('success', $this->trans('Successful deletion.', 'Admin.Notifications.Success'));
@@ -4,6 +4,7 @@ namespace Websmid\CustomHtml\Form\DataHandler;
use PrestaShop\PrestaShop\Core\Form\IdentifiableObject\DataHandler\FormDataHandlerInterface; use PrestaShop\PrestaShop\Core\Form\IdentifiableObject\DataHandler\FormDataHandlerInterface;
use Websmid\CustomHtml\Model\CustomHtmlBlock; use Websmid\CustomHtml\Model\CustomHtmlBlock;
use Websmid\CustomHtml\Model\CustomHtmlFaq;
use Validate; use Validate;
final class CustomHtmlBlockDataHandler implements FormDataHandlerInterface final class CustomHtmlBlockDataHandler implements FormDataHandlerInterface
@@ -13,6 +14,7 @@ final class CustomHtmlBlockDataHandler implements FormDataHandlerInterface
$block = new CustomHtmlBlock(); $block = new CustomHtmlBlock();
$block->id_ws_customhtml_group = (int) $formData['id_ws_customhtml_group']; $block->id_ws_customhtml_group = (int) $formData['id_ws_customhtml_group'];
$block->title = (string) $formData['title']; $block->title = (string) $formData['title'];
$block->type = (string) $formData['type'];
$block->show_title = (int) $formData['show_title']; $block->show_title = (int) $formData['show_title'];
$block->content = (string) $formData['content']; $block->content = (string) $formData['content'];
$block->active = (int) $formData['active']; $block->active = (int) $formData['active'];
@@ -22,6 +24,8 @@ final class CustomHtmlBlockDataHandler implements FormDataHandlerInterface
return false; return false;
} }
$this->saveFaqs((int) $block->id, $formData['faqs'] ?? []);
return $block->id; return $block->id;
} }
@@ -35,10 +39,35 @@ final class CustomHtmlBlockDataHandler implements FormDataHandlerInterface
$block->id_ws_customhtml_group = (int) $formData['id_ws_customhtml_group']; $block->id_ws_customhtml_group = (int) $formData['id_ws_customhtml_group'];
$block->title = (string) $formData['title']; $block->title = (string) $formData['title'];
$block->type = (string) $formData['type'];
$block->show_title = (int) $formData['show_title']; $block->show_title = (int) $formData['show_title'];
$block->content = (string) $formData['content']; $block->content = (string) $formData['content'];
$block->active = (int) $formData['active']; $block->active = (int) $formData['active'];
return (bool) $block->update(); if (!$block->update()) {
return false;
}
$this->saveFaqs((int) $id, $formData['faqs'] ?? []);
return true;
}
private function saveFaqs(int $idBlock, array $faqs): void
{
CustomHtmlFaq::deleteByBlockId($idBlock);
foreach (array_values($faqs) as $position => $faqData) {
if (empty($faqData['question'])) {
continue;
}
$faq = new CustomHtmlFaq();
$faq->id_ws_customhtml = $idBlock;
$faq->question = (string) $faqData['question'];
$faq->answer = (string) ($faqData['answer'] ?? '');
$faq->ordering = $position;
$faq->add();
}
} }
} }
@@ -4,6 +4,7 @@ namespace Websmid\CustomHtml\Form\DataProvider;
use PrestaShop\PrestaShop\Core\Form\IdentifiableObject\DataProvider\FormDataProviderInterface; use PrestaShop\PrestaShop\Core\Form\IdentifiableObject\DataProvider\FormDataProviderInterface;
use Websmid\CustomHtml\Model\CustomHtmlBlock; use Websmid\CustomHtml\Model\CustomHtmlBlock;
use Websmid\CustomHtml\Model\CustomHtmlFaq;
use Validate; use Validate;
final class CustomHtmlBlockDataProvider implements FormDataProviderInterface final class CustomHtmlBlockDataProvider implements FormDataProviderInterface
@@ -19,8 +20,10 @@ final class CustomHtmlBlockDataProvider implements FormDataProviderInterface
return [ return [
'id_ws_customhtml_group' => (int) $block->id_ws_customhtml_group, 'id_ws_customhtml_group' => (int) $block->id_ws_customhtml_group,
'title' => $block->title, 'title' => $block->title,
'type' => $block->type,
'show_title' => (bool) $block->show_title, 'show_title' => (bool) $block->show_title,
'content' => $block->content, 'content' => $block->content,
'faqs' => $this->loadFaqs((int) $id),
'active' => (bool) $block->active, 'active' => (bool) $block->active,
]; ];
} }
@@ -30,9 +33,21 @@ final class CustomHtmlBlockDataProvider implements FormDataProviderInterface
return [ return [
'id_ws_customhtml_group' => 0, 'id_ws_customhtml_group' => 0,
'title' => '', 'title' => '',
'type' => 'html',
'show_title' => false, 'show_title' => false,
'content' => '', 'content' => '',
'faqs' => [],
'active' => true, 'active' => true,
]; ];
} }
private function loadFaqs(int $idBlock): array
{
return array_map(static function (array $faq) {
return [
'question' => $faq['question'],
'answer' => $faq['answer'],
];
}, CustomHtmlFaq::getByBlockId($idBlock));
}
} }
+16
View File
@@ -5,11 +5,13 @@ namespace Websmid\CustomHtml\Form\Type;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use PrestaShop\PrestaShop\Core\Form\ConfigurableFormChoiceProviderInterface; use PrestaShop\PrestaShop\Core\Form\ConfigurableFormChoiceProviderInterface;
use PrestaShopBundle\Form\Admin\Type\SwitchType; use PrestaShopBundle\Form\Admin\Type\SwitchType;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
use Websmid\CustomHtml\Config\Config;
class CustomHtmlBlockType extends AbstractType class CustomHtmlBlockType extends AbstractType
{ {
@@ -37,6 +39,11 @@ class CustomHtmlBlockType extends AbstractType
'label' => $this->translator->trans('Title', [], 'Modules.Wscustomhtml.Admin'), 'label' => $this->translator->trans('Title', [], 'Modules.Wscustomhtml.Admin'),
'required' => true, 'required' => true,
]) ])
->add('type', ChoiceType::class, [
'label' => $this->translator->trans('Block type', [], 'Modules.Wscustomhtml.Admin'),
'choices' => array_flip(Config::BLOCK_TYPES),
'required' => true,
])
->add('show_title', SwitchType::class, [ ->add('show_title', SwitchType::class, [
'label' => $this->translator->trans('Show title', [], 'Modules.Wscustomhtml.Admin'), 'label' => $this->translator->trans('Show title', [], 'Modules.Wscustomhtml.Admin'),
'required' => false, 'required' => false,
@@ -45,6 +52,15 @@ class CustomHtmlBlockType extends AbstractType
'label' => $this->translator->trans('Content', [], 'Modules.Wscustomhtml.Admin'), 'label' => $this->translator->trans('Content', [], 'Modules.Wscustomhtml.Admin'),
'required' => false, 'required' => false,
'attr' => ['class' => 'autoload_rte'], 'attr' => ['class' => 'autoload_rte'],
'row_attr' => ['class' => 'customhtmlblock-type-field customhtmlblock-type-html'],
])
->add('faqs', CollectionType::class, [
'entry_type' => CustomHtmlFaqType::class,
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'prototype' => true,
'required' => false,
]) ])
->add('active', SwitchType::class, [ ->add('active', SwitchType::class, [
'label' => $this->translator->trans('Active', [], 'Modules.Wscustomhtml.Admin'), 'label' => $this->translator->trans('Active', [], 'Modules.Wscustomhtml.Admin'),
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace Websmid\CustomHtml\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Contracts\Translation\TranslatorInterface;
class CustomHtmlFaqType extends AbstractType
{
private $translator;
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
public function buildForm(FormBuilderInterface $formBuilder, array $options): void
{
$formBuilder
->add('question', TextType::class, [
'label' => $this->translator->trans('Question', [], 'Modules.Wscustomhtml.Admin'),
'required' => false,
])
->add('answer', TextareaType::class, [
'label' => $this->translator->trans('Answer', [], 'Modules.Wscustomhtml.Admin'),
'required' => false,
]);
}
}
@@ -15,7 +15,9 @@ use PrestaShop\PrestaShop\Core\Grid\Filter\Filter;
use PrestaShop\PrestaShop\Core\Grid\Filter\FilterCollection; use PrestaShop\PrestaShop\Core\Grid\Filter\FilterCollection;
use PrestaShop\PrestaShop\Core\Hook\HookDispatcherInterface; use PrestaShop\PrestaShop\Core\Hook\HookDispatcherInterface;
use PrestaShopBundle\Form\Admin\Type\SearchAndResetType; use PrestaShopBundle\Form\Admin\Type\SearchAndResetType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
use Websmid\CustomHtml\Config\Config;
final class CustomHtmlBlockGridDefinitionFactory extends AbstractGridDefinitionFactory final class CustomHtmlBlockGridDefinitionFactory extends AbstractGridDefinitionFactory
{ {
@@ -71,6 +73,11 @@ final class CustomHtmlBlockGridDefinitionFactory extends AbstractGridDefinitionF
->setName($this->trans('Group', [], 'Modules.Wscustomhtml.Admin')) ->setName($this->trans('Group', [], 'Modules.Wscustomhtml.Admin'))
->setOptions(['field' => 'group_title']) ->setOptions(['field' => 'group_title'])
) )
->add(
(new DataColumn('type'))
->setName($this->trans('Type', [], 'Modules.Wscustomhtml.Admin'))
->setOptions(['field' => 'type'])
)
->add( ->add(
(new DataColumn('active')) (new DataColumn('active'))
->setName($this->trans('Active', [], 'Admin.Global')) ->setName($this->trans('Active', [], 'Admin.Global'))
@@ -120,6 +127,15 @@ final class CustomHtmlBlockGridDefinitionFactory extends AbstractGridDefinitionF
'attr' => ['placeholder' => $this->trans('Group', [], 'Modules.Wscustomhtml.Admin')], 'attr' => ['placeholder' => $this->trans('Group', [], 'Modules.Wscustomhtml.Admin')],
]) ])
) )
->add(
(new Filter('type', ChoiceType::class))
->setAssociatedColumn('type')
->setTypeOptions([
'required' => false,
'choices' => array_flip(Config::BLOCK_TYPES),
'placeholder' => $this->trans('All', [], 'Admin.Global'),
])
)
->add( ->add(
(new Filter('actions', SearchAndResetType::class)) (new Filter('actions', SearchAndResetType::class))
->setAssociatedColumn('actions') ->setAssociatedColumn('actions')
@@ -45,6 +45,7 @@ final class CustomHtmlBlockQueryBuilder extends AbstractDoctrineQueryBuilder
'wchb.`id_ws_customhtml`', 'wchb.`id_ws_customhtml`',
'wchb.`title`', 'wchb.`title`',
'wchg.`title` AS group_title', 'wchg.`title` AS group_title',
'wchb.`type`',
'wchb.`active`', 'wchb.`active`',
'wchb.`ordering`', 'wchb.`ordering`',
'wchb.`date_add`' 'wchb.`date_add`'
@@ -108,6 +109,12 @@ final class CustomHtmlBlockQueryBuilder extends AbstractDoctrineQueryBuilder
continue; continue;
} }
if ($filterName === 'type') {
$qb->andWhere('wchb.`type` = :type')
->setParameter('type', pSQL($filterValue));
continue;
}
$qb->andWhere('wchb.`' . $filterName . '` LIKE :' . $filterName) $qb->andWhere('wchb.`' . $filterName . '` LIKE :' . $filterName)
->setParameter($filterName, '%' . pSQL($filterValue) . '%'); ->setParameter($filterName, '%' . pSQL($filterValue) . '%');
} }
+5 -1
View File
@@ -16,6 +16,9 @@ class CustomHtmlBlock extends ObjectModel
/** @var string */ /** @var string */
public $title = ''; public $title = '';
/** @var string */
public $type = 'html';
/** @var string */ /** @var string */
public $content = ''; public $content = '';
@@ -40,6 +43,7 @@ class CustomHtmlBlock extends ObjectModel
'fields' => [ 'fields' => [
'id_ws_customhtml_group' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true], 'id_ws_customhtml_group' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'title' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'required' => true, 'size' => 256], 'title' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'required' => true, 'size' => 256],
'type' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'required' => true, 'size' => 32],
'content' => ['type' => self::TYPE_HTML, 'validate' => 'isCleanHtml'], 'content' => ['type' => self::TYPE_HTML, 'validate' => 'isCleanHtml'],
'active' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'], 'active' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
'show_title' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'], 'show_title' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'],
@@ -58,7 +62,7 @@ class CustomHtmlBlock extends ObjectModel
} }
$result = Db::getInstance()->executeS( $result = Db::getInstance()->executeS(
'SELECT `id_ws_customhtml`, `id_ws_customhtml_group`, `title`, `content`, `active`, `show_title`, `ordering` 'SELECT `id_ws_customhtml`, `id_ws_customhtml_group`, `title`, `type`, `content`, `active`, `show_title`, `ordering`
FROM `' . _DB_PREFIX_ . 'ws_customhtml` FROM `' . _DB_PREFIX_ . 'ws_customhtml`
WHERE `id_ws_customhtml_group` IN (' . implode(',', $groupIds) . ') WHERE `id_ws_customhtml_group` IN (' . implode(',', $groupIds) . ')
AND `active` = 1 AND `active` = 1
+60
View File
@@ -0,0 +1,60 @@
<?php
namespace Websmid\CustomHtml\Model;
use ObjectModel;
use Db;
class CustomHtmlFaq extends ObjectModel
{
/** @var int */
public $id;
/** @var int */
public $id_ws_customhtml;
/** @var string */
public $question = '';
/** @var string */
public $answer = '';
/** @var int */
public $ordering = 0;
/** @var string */
public $date_add;
/** @var string */
public $date_upd;
public static $definition = [
'table' => 'ws_customhtml_faq',
'primary' => 'id_ws_customhtml_faq',
'fields' => [
'id_ws_customhtml' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true],
'question' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'required' => true, 'size' => 512],
'answer' => ['type' => self::TYPE_HTML, 'validate' => 'isCleanHtml'],
'ordering' => ['type' => self::TYPE_INT, 'validate' => 'isInt'],
'date_add' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
'date_upd' => ['type' => self::TYPE_DATE, 'validate' => 'isDate'],
],
];
public static function getByBlockId(int $idBlock): array
{
$result = Db::getInstance()->executeS(
'SELECT `id_ws_customhtml_faq`, `question`, `answer`, `ordering`
FROM `' . _DB_PREFIX_ . 'ws_customhtml_faq`
WHERE `id_ws_customhtml` = ' . (int) $idBlock . '
ORDER BY `ordering` ASC'
);
return $result ?: [];
}
public static function deleteByBlockId(int $idBlock): void
{
Db::getInstance()->delete('ws_customhtml_faq', '`id_ws_customhtml` = ' . (int) $idBlock);
}
}
+15
View File
@@ -3,6 +3,8 @@
namespace Websmid\CustomHtml\Module; namespace Websmid\CustomHtml\Module;
use Websmid\CustomHtml\Config\Config; use Websmid\CustomHtml\Config\Config;
use PrestaShop\PrestaShop\Core\Grid\Data\GridData;
use PrestaShop\PrestaShop\Core\Grid\Record\RecordCollection;
use Tools; use Tools;
trait AdminHooks trait AdminHooks
@@ -27,4 +29,17 @@ trait AdminHooks
$this->context->controller->addCSS($this->getPathUri() . 'views/css/admin/grid.css'); $this->context->controller->addCSS($this->getPathUri() . 'views/css/admin/grid.css');
} }
public function hookActionCustomhtmlblockGridDataModifier(array $params): void
{
$gridData = $params['data'];
$records = array_map(static function (array $row) {
$row['type'] = Config::BLOCK_TYPES[$row['type']] ?? $row['type'];
return $row;
}, $gridData->getRecords()->all());
$params['data'] = new GridData(new RecordCollection($records), $gridData->getRecordsTotal(), $gridData->getQuery());
}
} }
+15
View File
@@ -3,6 +3,7 @@
namespace Websmid\CustomHtml\Module; namespace Websmid\CustomHtml\Module;
use Websmid\CustomHtml\Model\CustomHtmlBlock; use Websmid\CustomHtml\Model\CustomHtmlBlock;
use Websmid\CustomHtml\Model\CustomHtmlFaq;
use Websmid\CustomHtml\Model\CustomHtmlGroup; use Websmid\CustomHtml\Model\CustomHtmlGroup;
use Context; use Context;
@@ -54,6 +55,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);
$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']] ?? [];
@@ -65,4 +67,17 @@ trait DisplayHooks
return !empty($group['blocks']); return !empty($group['blocks']);
})); }));
} }
private function attachFaqsToBlocks(array $blocksByGroup): array
{
return array_map(static function (array $blocks) {
return array_map(static function (array $block) {
if ($block['type'] === 'faq') {
$block['faqs'] = CustomHtmlFaq::getByBlockId((int) $block['id_ws_customhtml']);
}
return $block;
}, $blocks);
}, $blocksByGroup);
}
} }
+44
View File
@@ -3,7 +3,9 @@
namespace Websmid\CustomHtml\Module; namespace Websmid\CustomHtml\Module;
use Websmid\CustomHtml\Config\Config; use Websmid\CustomHtml\Config\Config;
use Context;
use Db; use Db;
use Hook;
use Language; use Language;
use Tab; use Tab;
@@ -31,6 +33,11 @@ trait Install
return false; return false;
} }
if (!$this->registerGroupHooks()) {
error_log('[ws_customhtml] registerGroupHooks() failed');
return false;
}
return true; return true;
} }
@@ -55,6 +62,7 @@ trait Install
`id_ws_customhtml` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, `id_ws_customhtml` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`id_ws_customhtml_group` INT(11) NOT NULL, `id_ws_customhtml_group` INT(11) NOT NULL,
`title` VARCHAR(256) NOT NULL DEFAULT \'\', `title` VARCHAR(256) NOT NULL DEFAULT \'\',
`type` VARCHAR(32) NOT NULL DEFAULT \'html\',
`content` LONGTEXT, `content` LONGTEXT,
`active` TINYINT(1) NOT NULL DEFAULT 1, `active` TINYINT(1) NOT NULL DEFAULT 1,
`show_title` TINYINT(1) NOT NULL DEFAULT 0, `show_title` TINYINT(1) NOT NULL DEFAULT 0,
@@ -65,6 +73,19 @@ trait Install
); );
'; ';
$sqlToExec[] = '
CREATE TABLE IF NOT EXISTS ' . _DB_PREFIX_ . 'ws_customhtml_faq (
`id_ws_customhtml_faq` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`id_ws_customhtml` INT(11) NOT NULL,
`question` VARCHAR(512) NOT NULL DEFAULT \'\',
`answer` TEXT,
`ordering` INT(11) NOT NULL DEFAULT 0,
`date_add` DATETIME NOT NULL,
`date_upd` DATETIME NOT NULL,
KEY `id_ws_customhtml` (`id_ws_customhtml`)
);
';
foreach ($sqlToExec as $sqlBlock) { foreach ($sqlToExec as $sqlBlock) {
Db::getInstance()->execute(trim($sqlBlock)); Db::getInstance()->execute(trim($sqlBlock));
} }
@@ -83,6 +104,29 @@ trait Install
return true; return true;
} }
private function registerGroupHooks(): bool
{
$hookNames = Db::getInstance()->executeS(
'SELECT DISTINCT `hook_name` FROM `' . _DB_PREFIX_ . 'ws_customhtml_group`'
);
$idShop = (int) Context::getContext()->shop->id;
foreach ($hookNames as $row) {
$hookName = $row['hook_name'];
if (Hook::isModuleRegisteredOnHook($this, $hookName, $idShop)) {
continue;
}
if (!$this->registerHook($hookName)) {
return false;
}
}
return true;
}
private function registerTabs(array $moduleTabs): bool private function registerTabs(array $moduleTabs): bool
{ {
$languages = Language::getLanguages(false); $languages = Language::getLanguages(false);
+38
View File
@@ -0,0 +1,38 @@
<?php
if (!defined('_PS_VERSION_')) {
exit;
}
function upgrade_module_1_2_0($module)
{
$columns = Db::getInstance()->executeS(
'SHOW COLUMNS FROM `' . _DB_PREFIX_ . 'ws_customhtml` LIKE \'type\''
);
if (empty($columns)) {
if (!Db::getInstance()->execute(
'ALTER TABLE `' . _DB_PREFIX_ . 'ws_customhtml`
ADD COLUMN `type` VARCHAR(32) NOT NULL DEFAULT \'html\' AFTER `title`'
)) {
return false;
}
}
if (!Db::getInstance()->execute(
'CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . 'ws_customhtml_faq` (
`id_ws_customhtml_faq` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`id_ws_customhtml` INT(11) NOT NULL,
`question` VARCHAR(512) NOT NULL DEFAULT \'\',
`answer` TEXT,
`ordering` INT(11) NOT NULL DEFAULT 0,
`date_add` DATETIME NOT NULL,
`date_upd` DATETIME NOT NULL,
KEY `id_ws_customhtml` (`id_ws_customhtml`)
)'
)) {
return false;
}
return $module->registerHook('actionCustomhtmlblockGridDataModifier');
}
@@ -1,6 +1,12 @@
{% import '@PrestaShop/Admin/macros.html.twig' as ps %} {% import '@PrestaShop/Admin/macros.html.twig' as ps %}
{% form_theme customHtmlBlockForm '@PrestaShop/Admin/TwigTemplateForm/prestashop_ui_kit.html.twig' %} {% form_theme customHtmlBlockForm '@PrestaShop/Admin/TwigTemplateForm/prestashop_ui_kit.html.twig' %}
{% macro faqFields(faqForm) %}
{{ form_row(faqForm.question) }}
{{ form_row(faqForm.answer) }}
{% endmacro %}
{% import _self as faqMacros %}
{% block customhtmlblock_form_block %} {% block customhtmlblock_form_block %}
<div class="card"> <div class="card">
{{ form_start(customHtmlBlockForm) }} {{ form_start(customHtmlBlockForm) }}
@@ -10,6 +16,38 @@
</h3> </h3>
</div> </div>
<div class="card-body"> <div class="card-body">
{{ form_row(customHtmlBlockForm.id_ws_customhtml_group) }}
{{ form_row(customHtmlBlockForm.title) }}
{{ form_row(customHtmlBlockForm.type) }}
{{ form_row(customHtmlBlockForm.show_title) }}
{{ form_row(customHtmlBlockForm.content) }}
<div class="customhtmlblock-type-field customhtmlblock-type-faq">
<label class="form-control-label">
{{ 'FAQ items'|trans({}, 'Modules.Wscustomhtml.Admin') }}
</label>
<div
id="customhtmlblock-faq-items"
data-prototype="{{ faqMacros.faqFields(customHtmlBlockForm.faqs.vars.prototype)|e('html_attr') }}"
data-index="{{ customHtmlBlockForm.faqs|length }}"
>
{% for faqRow in customHtmlBlockForm.faqs %}
<div class="customhtmlblock-faq-item card mb-2">
<div class="card-body">
{{ faqMacros.faqFields(faqRow) }}
<button type="button" class="btn btn-outline-danger btn-sm customhtmlblock-remove-faq">
<i class="material-icons">delete</i> {{ 'Remove'|trans({}, 'Admin.Actions') }}
</button>
</div>
</div>
{% endfor %}
</div>
<button type="button" id="customhtmlblock-add-faq" class="btn btn-outline-primary btn-sm">
<i class="material-icons">add</i> {{ 'Add FAQ item'|trans({}, 'Modules.Wscustomhtml.Admin') }}
</button>
</div>
{{ form_row(customHtmlBlockForm.active) }}
{{ form_rest(customHtmlBlockForm) }} {{ form_rest(customHtmlBlockForm) }}
</div> </div>
<div class="card-footer"> <div class="card-footer">
@@ -13,6 +13,44 @@
window.prestashop.component.initComponents([ window.prestashop.component.initComponents([
'TinyMCEEditor', 'TinyMCEEditor',
]); ]);
function toggleBlockTypeFields() {
var type = $('#custom_html_block_type').val();
if (type === 'html') {
$('.customhtmlblock-type-html').show();
$('.customhtmlblock-type-faq').hide();
} else {
$('.customhtmlblock-type-html').hide();
$('.customhtmlblock-type-faq').show();
}
}
$('#custom_html_block_type').on('change', toggleBlockTypeFields);
toggleBlockTypeFields();
var removeFaqLabel = '{{ 'Remove'|trans({}, 'Admin.Actions')|e('js') }}';
var faqContainer = $('#customhtmlblock-faq-items');
$('#customhtmlblock-add-faq').on('click', function () {
var index = faqContainer.data('index');
var prototype = faqContainer.data('prototype').replace(/__name__/g, index);
var item = $('<div class="customhtmlblock-faq-item card mb-2"><div class="card-body"></div></div>');
item.find('.card-body')
.append(prototype)
.append(
'<button type="button" class="btn btn-outline-danger btn-sm customhtmlblock-remove-faq">' +
'<i class="material-icons">delete</i> ' + removeFaqLabel + '</button>'
);
faqContainer.append(item);
faqContainer.data('index', index + 1);
});
faqContainer.on('click', '.customhtmlblock-remove-faq', function () {
$(this).closest('.customhtmlblock-faq-item').remove();
});
}); });
</script> </script>
{% endblock %} {% endblock %}
@@ -13,6 +13,44 @@
window.prestashop.component.initComponents([ window.prestashop.component.initComponents([
'TinyMCEEditor', 'TinyMCEEditor',
]); ]);
function toggleBlockTypeFields() {
var type = $('#custom_html_block_type').val();
if (type === 'html') {
$('.customhtmlblock-type-html').show();
$('.customhtmlblock-type-faq').hide();
} else {
$('.customhtmlblock-type-html').hide();
$('.customhtmlblock-type-faq').show();
}
}
$('#custom_html_block_type').on('change', toggleBlockTypeFields);
toggleBlockTypeFields();
var removeFaqLabel = '{{ 'Remove'|trans({}, 'Admin.Actions')|e('js') }}';
var faqContainer = $('#customhtmlblock-faq-items');
$('#customhtmlblock-add-faq').on('click', function () {
var index = faqContainer.data('index');
var prototype = faqContainer.data('prototype').replace(/__name__/g, index);
var item = $('<div class="customhtmlblock-faq-item card mb-2"><div class="card-body"></div></div>');
item.find('.card-body')
.append(prototype)
.append(
'<button type="button" class="btn btn-outline-danger btn-sm customhtmlblock-remove-faq">' +
'<i class="material-icons">delete</i> ' + removeFaqLabel + '</button>'
);
faqContainer.append(item);
faqContainer.data('index', index + 1);
});
faqContainer.on('click', '.customhtmlblock-remove-faq', function () {
$(this).closest('.customhtmlblock-faq-item').remove();
});
}); });
</script> </script>
{% endblock %} {% endblock %}
+30
View File
@@ -0,0 +1,30 @@
{if !empty($block.faqs)}
<div class="accordion customhtml-faq-accordion" id="customhtml-faq-{$block.id_ws_customhtml|intval}">
{foreach from=$block.faqs item="faq" name="customhtmlFaqLoop"}
<div class="card">
<div class="card-header" id="customhtml-faq-heading-{$block.id_ws_customhtml|intval}-{$faq.id_ws_customhtml_faq|intval}">
<button
class="btn btn-link{if !$smarty.foreach.customhtmlFaqLoop.first} collapsed{/if}"
type="button"
data-toggle="collapse"
data-target="#customhtml-faq-collapse-{$block.id_ws_customhtml|intval}-{$faq.id_ws_customhtml_faq|intval}"
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}"
>
{$faq.question|escape:'html':'UTF-8'}
</button>
</div>
<div
id="customhtml-faq-collapse-{$block.id_ws_customhtml|intval}-{$faq.id_ws_customhtml_faq|intval}"
class="collapse{if $smarty.foreach.customhtmlFaqLoop.first} show{/if}"
aria-labelledby="customhtml-faq-heading-{$block.id_ws_customhtml|intval}-{$faq.id_ws_customhtml_faq|intval}"
data-parent="#customhtml-faq-{$block.id_ws_customhtml|intval}"
>
<div class="card-body">
{$faq.answer nofilter}
</div>
</div>
</div>
{/foreach}
</div>
{/if}
@@ -7,7 +7,11 @@
{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>
{/if} {/if}
{if $block.type == 'faq'}
{include file="module:ws_customhtml/views/templates/hook/_faq_accordion.tpl" block=$block}
{else}
<div class="customhtml-content">{$block.content nofilter}</div> <div class="customhtml-content">{$block.content nofilter}</div>
{/if}
</div> </div>
{/foreach} {/foreach}
</div> </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.1.0'; $this->version = '1.2.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;