diff --git a/config/form.yml b/config/form.yml index c2864f7..1d634eb 100644 --- a/config/form.yml +++ b/config/form.yml @@ -42,6 +42,14 @@ services: tags: - { name: form.type } + websmid.customhtml.form.type.customhtml_slide: + public: true + class: 'Websmid\CustomHtml\Form\Type\CustomHtmlSlideType' + arguments: + - '@translator' + tags: + - { name: form.type } + websmid.customhtml.form.type.customhtml_block: public: true class: 'Websmid\CustomHtml\Form\Type\CustomHtmlBlockType' diff --git a/src/Config/Config.php b/src/Config/Config.php index ab943ea..43c8fdf 100644 --- a/src/Config/Config.php +++ b/src/Config/Config.php @@ -16,8 +16,24 @@ class Config 'banner' => 'Banner', 'alert' => 'Alert', 'usp' => 'USP', + 'slider' => 'Slider', ]; + const SLIDER_DEFAULT_SLIDES_PER_VIEW_MOBILE = 1; + + const SLIDER_DEFAULT_SLIDES_PER_VIEW_TABLET = 2; + + const SLIDER_DEFAULT_SLIDES_PER_VIEW_DESKTOP = 3; + + const SLIDER_MODES = [ + 'always' => 'Always show as a slider', + 'responsive' => 'Slider on mobile/tablet, static grid on desktop', + ]; + + const SLIDER_DEFAULT_MODE = 'always'; + + const SLIDER_RESPONSIVE_BREAKPOINT = 480; + const ALERT_STYLES = [ 'success' => 'Success', 'info' => 'Info', diff --git a/src/Form/DataHandler/CustomHtmlBlockDataHandler.php b/src/Form/DataHandler/CustomHtmlBlockDataHandler.php index 2642335..f22dece 100644 --- a/src/Form/DataHandler/CustomHtmlBlockDataHandler.php +++ b/src/Form/DataHandler/CustomHtmlBlockDataHandler.php @@ -6,6 +6,7 @@ use PrestaShop\PrestaShop\Core\Form\IdentifiableObject\DataHandler\FormDataHandl use Websmid\CustomHtml\Config\Config; use Websmid\CustomHtml\Model\CustomHtmlBlock; use Websmid\CustomHtml\Model\CustomHtmlFaq; +use Websmid\CustomHtml\Model\CustomHtmlSlide; use Symfony\Component\HttpFoundation\File\UploadedFile; use Db; use Exception; @@ -27,8 +28,14 @@ final class CustomHtmlBlockDataHandler implements FormDataHandlerInterface $block->description = (string) ($formData['description'] ?? ''); $block->extra_description = (string) ($formData['extra_description'] ?? ''); $block->link = (string) ($formData['link'] ?? ''); + $block->link_text = (string) ($formData['link_text'] ?? ''); $block->alert_type = (string) ($formData['alert_type'] ?? 'info'); $block->timeout = (int) ($formData['timeout'] ?? 0); + $block->slider_mode = (string) ($formData['slider_mode'] ?? Config::SLIDER_DEFAULT_MODE); + $block->slides_per_view_mobile = (float) ($formData['slides_per_view_mobile'] ?? Config::SLIDER_DEFAULT_SLIDES_PER_VIEW_MOBILE); + $block->slides_per_view_tablet = (float) ($formData['slides_per_view_tablet'] ?? Config::SLIDER_DEFAULT_SLIDES_PER_VIEW_TABLET); + $block->slides_per_view_desktop = (float) ($formData['slides_per_view_desktop'] ?? Config::SLIDER_DEFAULT_SLIDES_PER_VIEW_DESKTOP); + $block->autoplay_delay = (int) ($formData['autoplay_delay'] ?? 0); $block->image = $this->saveUploadedImage($formData['image'] ?? null, ''); $block->active = (int) $formData['active']; $block->ordering = $this->getNextOrdering($idGroup); @@ -38,6 +45,7 @@ final class CustomHtmlBlockDataHandler implements FormDataHandlerInterface } $this->saveFaqs((int) $block->id, $formData['faqs'] ?? []); + $this->saveSlides((int) $block->id, $formData['slides'] ?? []); return $block->id; } @@ -59,8 +67,14 @@ final class CustomHtmlBlockDataHandler implements FormDataHandlerInterface $block->description = (string) ($formData['description'] ?? ''); $block->extra_description = (string) ($formData['extra_description'] ?? ''); $block->link = (string) ($formData['link'] ?? ''); + $block->link_text = (string) ($formData['link_text'] ?? ''); $block->alert_type = (string) ($formData['alert_type'] ?? 'info'); $block->timeout = (int) ($formData['timeout'] ?? 0); + $block->slider_mode = (string) ($formData['slider_mode'] ?? Config::SLIDER_DEFAULT_MODE); + $block->slides_per_view_mobile = (float) ($formData['slides_per_view_mobile'] ?? Config::SLIDER_DEFAULT_SLIDES_PER_VIEW_MOBILE); + $block->slides_per_view_tablet = (float) ($formData['slides_per_view_tablet'] ?? Config::SLIDER_DEFAULT_SLIDES_PER_VIEW_TABLET); + $block->slides_per_view_desktop = (float) ($formData['slides_per_view_desktop'] ?? Config::SLIDER_DEFAULT_SLIDES_PER_VIEW_DESKTOP); + $block->autoplay_delay = (int) ($formData['autoplay_delay'] ?? 0); $block->image = $this->saveUploadedImage($formData['image'] ?? null, $block->image); $block->active = (int) $formData['active']; @@ -69,6 +83,7 @@ final class CustomHtmlBlockDataHandler implements FormDataHandlerInterface } $this->saveFaqs((int) $id, $formData['faqs'] ?? []); + $this->saveSlides((int) $id, $formData['slides'] ?? []); return true; } @@ -143,4 +158,37 @@ final class CustomHtmlBlockDataHandler implements FormDataHandlerInterface $faq->add(); } } + + /** + * Slides are fully replaced on every save, like FAQ items — except a + * slide's image can't round-trip through its file input, so the previous + * filename is looked up by the slide's hidden id and reused whenever no + * new file was uploaded for that row. + */ + private function saveSlides(int $idBlock, array $slides): void + { + $existingImages = []; + + foreach (CustomHtmlSlide::getByBlockId($idBlock) as $existingSlide) { + $existingImages[(int) $existingSlide['id_ws_customhtml_slide']] = $existingSlide['image']; + } + + CustomHtmlSlide::deleteByBlockId($idBlock); + + foreach (array_values($slides) as $position => $slideData) { + $existingId = (int) ($slideData['id'] ?? 0); + $existingImage = $existingImages[$existingId] ?? ''; + + $slide = new CustomHtmlSlide(); + $slide->id_ws_customhtml = $idBlock; + $slide->title = (string) ($slideData['title'] ?? ''); + $slide->description = (string) ($slideData['description'] ?? ''); + $slide->extra_description = (string) ($slideData['extra_description'] ?? ''); + $slide->link = (string) ($slideData['link'] ?? ''); + $slide->link_text = (string) ($slideData['link_text'] ?? ''); + $slide->image = $this->saveUploadedImage($slideData['image'] ?? null, $existingImage); + $slide->ordering = $position; + $slide->add(); + } + } } diff --git a/src/Form/DataProvider/CustomHtmlBlockDataProvider.php b/src/Form/DataProvider/CustomHtmlBlockDataProvider.php index 2337365..f9f4141 100644 --- a/src/Form/DataProvider/CustomHtmlBlockDataProvider.php +++ b/src/Form/DataProvider/CustomHtmlBlockDataProvider.php @@ -3,8 +3,10 @@ namespace Websmid\CustomHtml\Form\DataProvider; use PrestaShop\PrestaShop\Core\Form\IdentifiableObject\DataProvider\FormDataProviderInterface; +use Websmid\CustomHtml\Config\Config; use Websmid\CustomHtml\Model\CustomHtmlBlock; use Websmid\CustomHtml\Model\CustomHtmlFaq; +use Websmid\CustomHtml\Model\CustomHtmlSlide; use Validate; final class CustomHtmlBlockDataProvider implements FormDataProviderInterface @@ -28,9 +30,16 @@ final class CustomHtmlBlockDataProvider implements FormDataProviderInterface 'description' => $block->description, 'extra_description' => $block->extra_description, 'link' => $block->link, + 'link_text' => $block->link_text, 'alert_type' => $block->alert_type, 'timeout' => (int) $block->timeout, + 'slider_mode' => $block->slider_mode, + 'slides_per_view_mobile' => (float) $block->slides_per_view_mobile, + 'slides_per_view_tablet' => (float) $block->slides_per_view_tablet, + 'slides_per_view_desktop' => (float) $block->slides_per_view_desktop, + 'autoplay_delay' => (int) $block->autoplay_delay, 'faqs' => $this->loadFaqs((int) $id), + 'slides' => $this->loadSlides((int) $id), 'active' => (bool) $block->active, ]; } @@ -48,9 +57,16 @@ final class CustomHtmlBlockDataProvider implements FormDataProviderInterface 'description' => '', 'extra_description' => '', 'link' => '', + 'link_text' => '', 'alert_type' => 'info', 'timeout' => 0, + 'slider_mode' => Config::SLIDER_DEFAULT_MODE, + 'slides_per_view_mobile' => Config::SLIDER_DEFAULT_SLIDES_PER_VIEW_MOBILE, + 'slides_per_view_tablet' => Config::SLIDER_DEFAULT_SLIDES_PER_VIEW_TABLET, + 'slides_per_view_desktop' => Config::SLIDER_DEFAULT_SLIDES_PER_VIEW_DESKTOP, + 'autoplay_delay' => 0, 'faqs' => [], + 'slides' => [], 'active' => true, ]; } @@ -64,4 +80,19 @@ final class CustomHtmlBlockDataProvider implements FormDataProviderInterface ]; }, CustomHtmlFaq::getByBlockId($idBlock)); } + + private function loadSlides(int $idBlock): array + { + return array_map(static function (array $slide) { + return [ + 'id' => (int) $slide['id_ws_customhtml_slide'], + 'title' => $slide['title'], + 'description' => $slide['description'], + 'extra_description' => $slide['extra_description'], + 'link' => $slide['link'], + 'link_text' => $slide['link_text'], + 'image' => null, + ]; + }, CustomHtmlSlide::getByBlockId($idBlock)); + } } diff --git a/src/Form/Type/CustomHtmlBlockType.php b/src/Form/Type/CustomHtmlBlockType.php index 6b00201..754d929 100644 --- a/src/Form/Type/CustomHtmlBlockType.php +++ b/src/Form/Type/CustomHtmlBlockType.php @@ -8,6 +8,7 @@ use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\CollectionType; use Symfony\Component\Form\Extension\Core\Type\FileType; use Symfony\Component\Form\Extension\Core\Type\IntegerType; +use Symfony\Component\Form\Extension\Core\Type\NumberType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextareaType; use PrestaShop\PrestaShop\Core\Form\ConfigurableFormChoiceProviderInterface; @@ -70,6 +71,49 @@ class CustomHtmlBlockType extends AbstractType 'prototype' => true, 'required' => false, ]) + ->add('slides', CollectionType::class, [ + 'entry_type' => CustomHtmlSlideType::class, + 'allow_add' => true, + 'allow_delete' => true, + 'by_reference' => false, + 'prototype' => true, + 'required' => false, + ]) + ->add('slider_mode', ChoiceType::class, [ + 'label' => $this->translator->trans('Slider mode', [], 'Modules.Wscustomhtml.Admin'), + 'choices' => array_flip(Config::SLIDER_MODES), + 'required' => false, + 'row_attr' => ['class' => 'customhtmlblock-type-field customhtmlblock-type-slider'], + ]) + ->add('slides_per_view_mobile', NumberType::class, [ + 'label' => $this->translator->trans('Slides visible on mobile', [], 'Modules.Wscustomhtml.Admin'), + 'required' => false, + 'scale' => 2, + 'attr' => ['min' => 0, 'step' => '0.05'], + 'row_attr' => ['class' => 'customhtmlblock-type-field customhtmlblock-type-slider col-4'], + ]) + ->add('slides_per_view_tablet', NumberType::class, [ + 'label' => $this->translator->trans('Slides visible on tablet', [], 'Modules.Wscustomhtml.Admin'), + 'required' => false, + 'scale' => 2, + 'attr' => ['min' => 0, 'step' => '0.05'], + 'row_attr' => ['class' => 'customhtmlblock-type-field customhtmlblock-type-slider col-4'], + ]) + ->add('slides_per_view_desktop', NumberType::class, [ + 'label' => $this->translator->trans('Slides visible on desktop', [], 'Modules.Wscustomhtml.Admin'), + 'required' => false, + 'scale' => 2, + 'attr' => ['min' => 0, 'step' => '0.05'], + 'help' => $this->translator->trans('In "static grid on desktop" mode this is rounded to the nearest whole number of columns.', [], 'Modules.Wscustomhtml.Admin'), + 'row_attr' => ['class' => 'customhtmlblock-type-field customhtmlblock-type-slider col-4'], + ]) + ->add('autoplay_delay', IntegerType::class, [ + 'label' => $this->translator->trans('Autoplay delay (seconds)', [], 'Modules.Wscustomhtml.Admin'), + 'required' => false, + 'attr' => ['min' => 0, 'placeholder' => '0'], + 'help' => $this->translator->trans('0 means the slider never auto-advances; visitors can still swipe or use the arrows.', [], 'Modules.Wscustomhtml.Admin'), + 'row_attr' => ['class' => 'customhtmlblock-type-field customhtmlblock-type-slider'], + ]) ->add('alert_type', ChoiceType::class, [ 'label' => $this->translator->trans('Alert style', [], 'Modules.Wscustomhtml.Admin'), 'choices' => array_flip(Config::ALERT_STYLES), @@ -94,10 +138,16 @@ class CustomHtmlBlockType extends AbstractType 'row_attr' => ['class' => 'customhtmlblock-type-field customhtmlblock-type-banner'], ]) ->add('link', TextType::class, [ - 'label' => $this->translator->trans('Link', [], 'Modules.Wscustomhtml.Admin'), + 'label' => $this->translator->trans('Link URL', [], 'Modules.Wscustomhtml.Admin'), 'required' => false, 'attr' => ['placeholder' => 'https://example.com or /internal-page'], - 'row_attr' => ['class' => 'customhtmlblock-type-field customhtmlblock-type-banner customhtmlblock-type-usp'], + 'row_attr' => ['class' => 'customhtmlblock-type-field customhtmlblock-type-banner customhtmlblock-type-usp customhtmlblock-type-alert col-6'], + ]) + ->add('link_text', TextType::class, [ + 'label' => $this->translator->trans('Link text', [], 'Modules.Wscustomhtml.Admin'), + 'required' => false, + 'attr' => ['placeholder' => 'Learn more'], + 'row_attr' => ['class' => 'customhtmlblock-type-field customhtmlblock-type-alert col-6'], ]) ->add('custom_class', TextType::class, [ 'label' => $this->translator->trans('Custom classes', [], 'Modules.Wscustomhtml.Admin'), diff --git a/src/Form/Type/CustomHtmlSlideType.php b/src/Form/Type/CustomHtmlSlideType.php new file mode 100644 index 0000000..d0763c3 --- /dev/null +++ b/src/Form/Type/CustomHtmlSlideType.php @@ -0,0 +1,55 @@ +translator = $translator; + } + + public function buildForm(FormBuilderInterface $formBuilder, array $options): void + { + $formBuilder + ->add('id', HiddenType::class, [ + 'required' => false, + ]) + ->add('title', TextType::class, [ + 'label' => $this->translator->trans('Title', [], 'Modules.Wscustomhtml.Admin'), + 'required' => false, + ]) + ->add('description', TextareaType::class, [ + 'label' => $this->translator->trans('Description', [], 'Modules.Wscustomhtml.Admin'), + 'required' => false, + ]) + ->add('extra_description', TextareaType::class, [ + 'label' => $this->translator->trans('Extra description', [], 'Modules.Wscustomhtml.Admin'), + 'required' => false, + ]) + ->add('link', TextType::class, [ + 'label' => $this->translator->trans('Link URL', [], 'Modules.Wscustomhtml.Admin'), + 'required' => false, + 'attr' => ['placeholder' => 'https://example.com or /internal-page'], + ]) + ->add('link_text', TextType::class, [ + 'label' => $this->translator->trans('Link text', [], 'Modules.Wscustomhtml.Admin'), + 'required' => false, + 'attr' => ['placeholder' => 'Learn more'], + ]) + ->add('image', FileType::class, [ + 'label' => $this->translator->trans('Image', [], 'Modules.Wscustomhtml.Admin'), + 'required' => false, + ]); + } +} diff --git a/src/Model/CustomHtmlBlock.php b/src/Model/CustomHtmlBlock.php index efa6d10..ce3b9dd 100644 --- a/src/Model/CustomHtmlBlock.php +++ b/src/Model/CustomHtmlBlock.php @@ -34,6 +34,9 @@ class CustomHtmlBlock extends ObjectModel /** @var string */ public $link = ''; + /** @var string */ + public $link_text = ''; + /** @var string */ public $image = ''; @@ -43,6 +46,21 @@ class CustomHtmlBlock extends ObjectModel /** @var int */ public $timeout = 0; + /** @var string */ + public $slider_mode = 'always'; + + /** @var float */ + public $slides_per_view_mobile = 1; + + /** @var float */ + public $slides_per_view_tablet = 2; + + /** @var float */ + public $slides_per_view_desktop = 3; + + /** @var int */ + public $autoplay_delay = 0; + /** @var int */ public $active = 1; @@ -70,9 +88,15 @@ class CustomHtmlBlock extends ObjectModel 'description' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'required' => false, 'size' => 1024], 'extra_description' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'required' => false, 'size' => 1024], 'link' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'required' => false, 'size' => 512], + 'link_text' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'required' => false, 'size' => 128], 'image' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'required' => false, 'size' => 255], 'alert_type' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'required' => false, 'size' => 16], 'timeout' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedInt', 'required' => false], + 'slider_mode' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'required' => false, 'size' => 16], + 'slides_per_view_mobile' => ['type' => self::TYPE_FLOAT, 'validate' => 'isUnsignedFloat', 'required' => false], + 'slides_per_view_tablet' => ['type' => self::TYPE_FLOAT, 'validate' => 'isUnsignedFloat', 'required' => false], + 'slides_per_view_desktop' => ['type' => self::TYPE_FLOAT, 'validate' => 'isUnsignedFloat', 'required' => false], + 'autoplay_delay' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedInt', 'required' => false], 'active' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'], 'show_title' => ['type' => self::TYPE_BOOL, 'validate' => 'isBool'], 'ordering' => ['type' => self::TYPE_INT, 'validate' => 'isInt'], @@ -105,7 +129,9 @@ class CustomHtmlBlock extends ObjectModel $result = Db::getInstance()->executeS( 'SELECT `id_ws_customhtml`, `id_ws_customhtml_group`, `title`, `type`, `content`, `custom_class`, - `description`, `extra_description`, `link`, `image`, `alert_type`, `timeout`, `active`, `show_title`, `ordering` + `description`, `extra_description`, `link`, `link_text`, `image`, `alert_type`, `timeout`, + `slider_mode`, `slides_per_view_mobile`, `slides_per_view_tablet`, `slides_per_view_desktop`, `autoplay_delay`, + `active`, `show_title`, `ordering` FROM `' . _DB_PREFIX_ . 'ws_customhtml` WHERE `id_ws_customhtml_group` IN (' . implode(',', $groupIds) . ') AND `active` = 1 diff --git a/src/Model/CustomHtmlSlide.php b/src/Model/CustomHtmlSlide.php new file mode 100644 index 0000000..e7f32e0 --- /dev/null +++ b/src/Model/CustomHtmlSlide.php @@ -0,0 +1,76 @@ + 'ws_customhtml_slide', + 'primary' => 'id_ws_customhtml_slide', + 'fields' => [ + 'id_ws_customhtml' => ['type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true], + 'title' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'required' => false, 'size' => 256], + 'description' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'required' => false, 'size' => 1024], + 'extra_description' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'required' => false, 'size' => 1024], + 'link' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'required' => false, 'size' => 512], + 'link_text' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'required' => false, 'size' => 128], + 'image' => ['type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'required' => false, 'size' => 255], + '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_slide`, `title`, `description`, `extra_description`, `link`, `link_text`, `image`, `ordering` + FROM `' . _DB_PREFIX_ . 'ws_customhtml_slide` + WHERE `id_ws_customhtml` = ' . (int) $idBlock . ' + ORDER BY `ordering` ASC' + ); + + return $result ?: []; + } + + public static function deleteByBlockId(int $idBlock): void + { + Db::getInstance()->delete('ws_customhtml_slide', '`id_ws_customhtml` = ' . (int) $idBlock); + } +} diff --git a/src/Module/DisplayHooks.php b/src/Module/DisplayHooks.php index f1d3a36..bf5d2b4 100644 --- a/src/Module/DisplayHooks.php +++ b/src/Module/DisplayHooks.php @@ -2,9 +2,11 @@ namespace Websmid\CustomHtml\Module; +use Websmid\CustomHtml\Config\Config; use Websmid\CustomHtml\Model\CustomHtmlBlock; use Websmid\CustomHtml\Model\CustomHtmlFaq; use Websmid\CustomHtml\Model\CustomHtmlGroup; +use Websmid\CustomHtml\Model\CustomHtmlSlide; use Context; trait DisplayHooks @@ -70,6 +72,7 @@ trait DisplayHooks $blocksByGroup = CustomHtmlBlock::getActiveByGroupIds(array_column($groups, 'id_ws_customhtml_group')); $blocksByGroup = $this->attachFaqsToBlocks($blocksByGroup); $blocksByGroup = $this->attachImageUrls($blocksByGroup); + $blocksByGroup = $this->attachSlidesToBlocks($blocksByGroup); $groups = array_map(static function (array $group) use ($blocksByGroup) { $group['blocks'] = $blocksByGroup[(int) $group['id_ws_customhtml_group']] ?? []; @@ -107,4 +110,67 @@ trait DisplayHooks }, $blocks); }, $blocksByGroup); } + + private function attachSlidesToBlocks(array $blocksByGroup): array + { + return array_map(function (array $blocks) { + return array_map(function (array $block) { + if ($block['type'] === 'slider') { + $block['slides'] = $this->loadSlidesForBlock((int) $block['id_ws_customhtml']); + $block['slider_config'] = $this->buildSliderConfig($block); + $block['slider_desktop_column_width'] = $this->buildSliderDesktopColumnWidth($block); + } + + return $block; + }, $blocks); + }, $blocksByGroup); + } + + private function loadSlidesForBlock(int $idBlock): array + { + return array_map(static function (array $slide) { + $slide['image_url'] = CustomHtmlBlock::getBannerImageUrl($slide['image']); + $slide['show_title'] = true; + + return $slide; + }, CustomHtmlSlide::getByBlockId($idBlock)); + } + + private function buildSliderConfig(array $block): string + { + $config = [ + 'slidesPerView' => (float) $block['slides_per_view_mobile'], + 'spaceBetween' => 16, + 'speed' => 500, + 'watchOverflow' => true, + ]; + + if ($block['slider_mode'] === 'responsive') { + $config['breakpoints'] = [ + Config::SLIDER_RESPONSIVE_BREAKPOINT => ['slidesPerView' => (float) $block['slides_per_view_tablet']], + ]; + } else { + $config['breakpoints'] = [ + 768 => ['slidesPerView' => (float) $block['slides_per_view_tablet']], + 992 => ['slidesPerView' => (float) $block['slides_per_view_desktop']], + ]; + } + + if ((int) $block['autoplay_delay'] > 0) { + $config['loop'] = true; + $config['autoplay'] = [ + 'delay' => (int) $block['autoplay_delay'] * 1000, + 'disableOnInteraction' => false, + ]; + } + + return json_encode($config); + } + + private function buildSliderDesktopColumnWidth(array $block): string + { + $columns = max(1, (int) round((float) $block['slides_per_view_desktop'])); + + return round(100 / $columns, 4) . '%'; + } } diff --git a/src/Module/Install.php b/src/Module/Install.php index 1cac55b..42bd874 100644 --- a/src/Module/Install.php +++ b/src/Module/Install.php @@ -69,7 +69,15 @@ trait Install `description` TEXT, `extra_description` TEXT, `link` VARCHAR(512) NOT NULL DEFAULT \'\', + `link_text` VARCHAR(128) NOT NULL DEFAULT \'\', `image` VARCHAR(255) NOT NULL DEFAULT \'\', + `alert_type` VARCHAR(16) NOT NULL DEFAULT \'info\', + `timeout` INT(11) NOT NULL DEFAULT 0, + `slider_mode` VARCHAR(16) NOT NULL DEFAULT \'always\', + `slides_per_view_mobile` DECIMAL(4,2) NOT NULL DEFAULT 1, + `slides_per_view_tablet` DECIMAL(4,2) NOT NULL DEFAULT 2, + `slides_per_view_desktop` DECIMAL(4,2) NOT NULL DEFAULT 3, + `autoplay_delay` INT(11) NOT NULL DEFAULT 0, `active` TINYINT(1) NOT NULL DEFAULT 1, `show_title` TINYINT(1) NOT NULL DEFAULT 0, `ordering` INT(11) NOT NULL DEFAULT 0, @@ -92,6 +100,23 @@ trait Install ); '; + $sqlToExec[] = ' + CREATE TABLE IF NOT EXISTS ' . _DB_PREFIX_ . 'ws_customhtml_slide ( + `id_ws_customhtml_slide` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, + `id_ws_customhtml` INT(11) NOT NULL, + `title` VARCHAR(256) NOT NULL DEFAULT \'\', + `description` TEXT, + `extra_description` TEXT, + `link` VARCHAR(512) NOT NULL DEFAULT \'\', + `link_text` VARCHAR(128) NOT NULL DEFAULT \'\', + `image` VARCHAR(255) NOT NULL DEFAULT \'\', + `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) { Db::getInstance()->execute(trim($sqlBlock)); } diff --git a/upgrade/upgrade-1.15.0.php b/upgrade/upgrade-1.15.0.php new file mode 100644 index 0000000..af84f6b --- /dev/null +++ b/upgrade/upgrade-1.15.0.php @@ -0,0 +1,21 @@ +executeS( + 'SHOW COLUMNS FROM `' . _DB_PREFIX_ . 'ws_customhtml` LIKE \'link_text\'' + ); + + if (!empty($existing)) { + return true; + } + + return Db::getInstance()->execute( + 'ALTER TABLE `' . _DB_PREFIX_ . 'ws_customhtml` + ADD COLUMN `link_text` VARCHAR(128) NOT NULL DEFAULT \'\' AFTER `link`' + ); +} diff --git a/upgrade/upgrade-1.16.0.php b/upgrade/upgrade-1.16.0.php new file mode 100644 index 0000000..f6428be --- /dev/null +++ b/upgrade/upgrade-1.16.0.php @@ -0,0 +1,48 @@ + 'DECIMAL(4,2) NOT NULL DEFAULT 1', + 'slides_per_view_tablet' => 'DECIMAL(4,2) NOT NULL DEFAULT 2', + 'slides_per_view_desktop' => 'DECIMAL(4,2) NOT NULL DEFAULT 3', + 'autoplay_delay' => 'INT(11) NOT NULL DEFAULT 0', + ]; + + foreach ($columnsToAdd as $column => $definition) { + $existing = Db::getInstance()->executeS( + 'SHOW COLUMNS FROM `' . _DB_PREFIX_ . 'ws_customhtml` LIKE \'' . $column . '\'' + ); + + if (!empty($existing)) { + continue; + } + + if (!Db::getInstance()->execute( + 'ALTER TABLE `' . _DB_PREFIX_ . 'ws_customhtml` ADD COLUMN `' . $column . '` ' . $definition + )) { + return false; + } + } + + return Db::getInstance()->execute( + 'CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . 'ws_customhtml_slide` ( + `id_ws_customhtml_slide` INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, + `id_ws_customhtml` INT(11) NOT NULL, + `title` VARCHAR(256) NOT NULL DEFAULT \'\', + `description` TEXT, + `extra_description` TEXT, + `link` VARCHAR(512) NOT NULL DEFAULT \'\', + `link_text` VARCHAR(128) NOT NULL DEFAULT \'\', + `image` VARCHAR(255) NOT NULL DEFAULT \'\', + `ordering` INT(11) NOT NULL DEFAULT 0, + `date_add` DATETIME NOT NULL, + `date_upd` DATETIME NOT NULL, + KEY `id_ws_customhtml` (`id_ws_customhtml`) + )' + ); +} diff --git a/upgrade/upgrade-1.17.0.php b/upgrade/upgrade-1.17.0.php new file mode 100644 index 0000000..f3ef049 --- /dev/null +++ b/upgrade/upgrade-1.17.0.php @@ -0,0 +1,21 @@ +executeS( + 'SHOW COLUMNS FROM `' . _DB_PREFIX_ . 'ws_customhtml` LIKE \'slider_mode\'' + ); + + if (!empty($existing)) { + return true; + } + + return Db::getInstance()->execute( + 'ALTER TABLE `' . _DB_PREFIX_ . 'ws_customhtml` + ADD COLUMN `slider_mode` VARCHAR(16) NOT NULL DEFAULT \'always\' AFTER `timeout`' + ); +} diff --git a/views/css/front/customhtml.css b/views/css/front/customhtml.css index e20ab72..2a22a43 100644 --- a/views/css/front/customhtml.css +++ b/views/css/front/customhtml.css @@ -18,13 +18,13 @@ margin-bottom: 1rem; } -.customhtml-banner-image--gradient { +/* .customhtml-banner-image--gradient { width: 100%; height: 100%; position: absolute; background: transparent linear-gradient(180deg, #27272777 0%, #272727 100%) 0% 0% no-repeat padding-box; -} +} */ /* Alert */ .customhtml-alert { max-height: 500px; @@ -48,6 +48,11 @@ margin-right: 0.5rem; } +.customhtml-alert-btn { + margin-left: 0.75rem; + white-space: nowrap; +} + .customhtml-usp { gap: 0.5rem; color: inherit; @@ -63,6 +68,32 @@ font-weight: 500; } +/* Slider */ +.customhtml-slider-grid-item { + padding-left: 0.75rem; + padding-right: 0.75rem; + margin-bottom: 1rem; +} + +/* Home banners slider (migrated from ws_extrabanner's home-banners section) */ +.displayHomeBannersSlider--container .customhtml-banner-image { + min-height: 220px; + background-color: #f2f0e9; +} + +.displayHomeBannersSlider--container .customhtml-banner-content * { + color: #fff; +} + +.displayHomeBannersSlider--container .customhtml-banner-description, +.displayHomeBannersSlider--container .customhtml-banner-extra-description { + margin-bottom: 0; +} + +.displayHomeBannersSlider--container .customhtml-banner-extra-description { + font-weight: 500; +} + /* FAQ */ .accordion-icon-minus { display: block; diff --git a/views/js/admin/customhtmlblock_form.js b/views/js/admin/customhtmlblock_form.js new file mode 100644 index 0000000..f2ea562 --- /dev/null +++ b/views/js/admin/customhtmlblock_form.js @@ -0,0 +1,48 @@ +$(document).ready(function () { + window.prestashop.component.initComponents([ + 'TinyMCEEditor', + ]); + + function toggleBlockTypeFields() { + var type = $('#custom_html_block_type').val(); + + $('.customhtmlblock-type-html').hide(); + $('.customhtmlblock-type-faq').hide(); + $('.customhtmlblock-type-banner').hide(); + $('.customhtmlblock-type-alert').hide(); + $('.customhtmlblock-type-usp').hide(); + $('.customhtmlblock-type-slider').hide(); + $('.customhtmlblock-type-' + type).show(); + } + + $('#custom_html_block_type').on('change', toggleBlockTypeFields); + toggleBlockTypeFields(); + + function initRepeater(containerSelector, addButtonSelector, itemClass, removeButtonClass) { + var container = $(containerSelector); + var removeLabel = container.data('removeLabel'); + + $(addButtonSelector).on('click', function () { + var index = container.data('index'); + var prototype = container.data('prototype').replace(/__name__/g, index); + + var item = $('
'); + item.find('.card-body') + .append(prototype) + .append( + '' + ); + + container.append(item); + container.data('index', index + 1); + }); + + container.on('click', '.' + removeButtonClass, function () { + $(this).closest('.' + itemClass).remove(); + }); + } + + initRepeater('#customhtmlblock-faq-items', '#customhtmlblock-add-faq', 'customhtmlblock-faq-item', 'customhtmlblock-remove-faq'); + initRepeater('#customhtmlblock-slide-items', '#customhtmlblock-add-slide', 'customhtmlblock-slide-item', 'customhtmlblock-remove-slide'); +}); diff --git a/views/js/admin/customhtmlblock_grid.js b/views/js/admin/customhtmlblock_grid.js new file mode 100644 index 0000000..def3d44 --- /dev/null +++ b/views/js/admin/customhtmlblock_grid.js @@ -0,0 +1,9 @@ +$(document).ready(function () { + const customhtmlblockGrid = new window.prestashop.component.Grid('customhtmlblock'); + + customhtmlblockGrid.addExtension(new window.prestashop.component.GridExtensions.SortingExtension()); + customhtmlblockGrid.addExtension(new window.prestashop.component.GridExtensions.FiltersResetExtension()); + customhtmlblockGrid.addExtension(new window.prestashop.component.GridExtensions.ColumnTogglingExtension()); + customhtmlblockGrid.addExtension(new window.prestashop.component.GridExtensions.SubmitRowActionExtension()); + customhtmlblockGrid.addExtension(new window.prestashop.component.GridExtensions.PositionExtension()); +}); diff --git a/views/js/admin/customhtmlgroup_grid.js b/views/js/admin/customhtmlgroup_grid.js new file mode 100644 index 0000000..7fa0a3c --- /dev/null +++ b/views/js/admin/customhtmlgroup_grid.js @@ -0,0 +1,9 @@ +$(document).ready(function () { + const customhtmlgroupGrid = new window.prestashop.component.Grid('customhtmlgroup'); + + customhtmlgroupGrid.addExtension(new window.prestashop.component.GridExtensions.SortingExtension()); + customhtmlgroupGrid.addExtension(new window.prestashop.component.GridExtensions.FiltersResetExtension()); + customhtmlgroupGrid.addExtension(new window.prestashop.component.GridExtensions.ColumnTogglingExtension()); + customhtmlgroupGrid.addExtension(new window.prestashop.component.GridExtensions.SubmitRowActionExtension()); + customhtmlgroupGrid.addExtension(new window.prestashop.component.GridExtensions.PositionExtension()); +}); diff --git a/views/templates/admin/form/blocks/block_form.html.twig b/views/templates/admin/form/blocks/block_form.html.twig index 481fa0d..aeb08e4 100644 --- a/views/templates/admin/form/blocks/block_form.html.twig +++ b/views/templates/admin/form/blocks/block_form.html.twig @@ -7,6 +7,17 @@ {% endmacro %} {% import _self as faqMacros %} +{% macro slideFields(slideForm) %} + {{ form_widget(slideForm.id) }} + {{ form_row(slideForm.title) }} + {{ form_row(slideForm.description) }} + {{ form_row(slideForm.extra_description) }} + {{ form_row(slideForm.link) }} + {{ form_row(slideForm.link_text) }} + {{ form_row(slideForm.image) }} +{% endmacro %} +{% import _self as slideMacros %} + {% block customhtmlblock_form_block %}
{{ form_start(customHtmlBlockForm) }} @@ -42,6 +53,7 @@ id="customhtmlblock-faq-items" data-prototype="{{ faqMacros.faqFields(customHtmlBlockForm.faqs.vars.prototype)|e('html_attr') }}" data-index="{{ customHtmlBlockForm.faqs|length }}" + data-remove-label="{{ 'Remove'|trans({}, 'Admin.Actions')|e('html_attr') }}" > {% for faqRow in customHtmlBlockForm.faqs %}
@@ -59,11 +71,48 @@
+
+ +
+ {% for slideRow in customHtmlBlockForm.slides %} +
+
+ {{ slideMacros.slideFields(slideRow) }} + +
+
+ {% endfor %} +
+ +
+ + {{ form_row(customHtmlBlockForm.slider_mode) }} +
+ {{ form_row(customHtmlBlockForm.slides_per_view_mobile) }} + {{ form_row(customHtmlBlockForm.slides_per_view_tablet) }} + {{ form_row(customHtmlBlockForm.slides_per_view_desktop) }} +
+ {{ form_row(customHtmlBlockForm.autoplay_delay) }} + {{ form_row(customHtmlBlockForm.alert_type) }} {{ form_row(customHtmlBlockForm.timeout) }} {{ form_row(customHtmlBlockForm.description) }} {{ form_row(customHtmlBlockForm.extra_description) }} - {{ form_row(customHtmlBlockForm.link) }} +
+ {{ form_row(customHtmlBlockForm.link) }} + {{ form_row(customHtmlBlockForm.link_text) }} +
{{ form_row(customHtmlBlockForm.custom_class) }} {{ form_row(customHtmlBlockForm.active) }} diff --git a/views/templates/admin/form/customhtmlblock_create.html.twig b/views/templates/admin/form/customhtmlblock_create.html.twig index 643c9db..fee3435 100644 --- a/views/templates/admin/form/customhtmlblock_create.html.twig +++ b/views/templates/admin/form/customhtmlblock_create.html.twig @@ -8,48 +8,5 @@ {% block javascripts %} {{ parent() }} - + {% endblock %} diff --git a/views/templates/admin/form/customhtmlblock_edit.html.twig b/views/templates/admin/form/customhtmlblock_edit.html.twig index a4289b8..f1aeae3 100644 --- a/views/templates/admin/form/customhtmlblock_edit.html.twig +++ b/views/templates/admin/form/customhtmlblock_edit.html.twig @@ -8,48 +8,5 @@ {% block javascripts %} {{ parent() }} - + {% endblock %} diff --git a/views/templates/admin/grid/customhtmlblock.html.twig b/views/templates/admin/grid/customhtmlblock.html.twig index 5c4a921..010eb20 100644 --- a/views/templates/admin/grid/customhtmlblock.html.twig +++ b/views/templates/admin/grid/customhtmlblock.html.twig @@ -12,15 +12,5 @@ {% block javascripts %} {{ parent() }} - + {% endblock %} diff --git a/views/templates/admin/grid/customhtmlgroup.html.twig b/views/templates/admin/grid/customhtmlgroup.html.twig index b1c6390..3805dd7 100644 --- a/views/templates/admin/grid/customhtmlgroup.html.twig +++ b/views/templates/admin/grid/customhtmlgroup.html.twig @@ -12,15 +12,5 @@ {% block javascripts %} {{ parent() }} - + {% endblock %} diff --git a/views/templates/hook/_alert.tpl b/views/templates/hook/_alert.tpl index 9c77082..7371701 100644 --- a/views/templates/hook/_alert.tpl +++ b/views/templates/hook/_alert.tpl @@ -9,6 +9,11 @@ {$block.title|escape:'html':'UTF-8'} {/if} {$block.description|escape:'html':'UTF-8'|nl2br} + {if !empty($block.link)} + + {if !empty($block.link_text)}{$block.link_text|escape:'html':'UTF-8'}{else}{l s='Learn more' d='Modules.Wscustomhtml.Shop'}{/if} + + {/if} diff --git a/views/templates/hook/_banner.tpl b/views/templates/hook/_banner.tpl index e020af9..c078d13 100644 --- a/views/templates/hook/_banner.tpl +++ b/views/templates/hook/_banner.tpl @@ -3,9 +3,21 @@
{if !empty($block.image_url)} {$block.title|escape:'html':'UTF-8'} + {else} + {$block.title|escape:'html':'UTF-8'} {/if}
- {include file="module:ws_customhtml/views/templates/hook/_banner_content.tpl" block=$block} + {assign var="customhtmlOverridePath" value="{$smarty.const._PS_THEME_DIR_}modules/ws_customhtml/views/templates/hook/{$hookName}/_banner_content.tpl"} + {if file_exists($customhtmlOverridePath)} + {include file="module:ws_customhtml/views/templates/hook/`$hookName`/_banner_content.tpl" block=$block} + {else} + {include file="module:ws_customhtml/views/templates/hook/_banner_content.tpl" block=$block} + {/if}
- + \ No newline at end of file diff --git a/views/templates/hook/_banner_content.tpl b/views/templates/hook/_banner_content.tpl index d1fe5f9..7562439 100644 --- a/views/templates/hook/_banner_content.tpl +++ b/views/templates/hook/_banner_content.tpl @@ -8,5 +8,7 @@
{$block.extra_description|escape:'html':'UTF-8'|nl2br}
{/if} {if !empty($block.link)} -{l s='Learn more' d='Modules.Wscustomhtml.Shop'} + + {if !empty($block.link_text)}{$block.link_text|escape:'html':'UTF-8'}{else}{l s='Learn more' d='Modules.Wscustomhtml.Shop'}{/if} + {/if} diff --git a/views/templates/hook/_slider.tpl b/views/templates/hook/_slider.tpl new file mode 100644 index 0000000..44d7ccd --- /dev/null +++ b/views/templates/hook/_slider.tpl @@ -0,0 +1,54 @@ +{if !empty($block.slides)} + {if $block.slider_mode == 'responsive'} + {block name=slider_responsive} +
+
+
+ {foreach from=$block.slides item="slide" name="slides"} +
+ {assign var="customhtmlOverridePath" value="{$smarty.const._PS_THEME_DIR_}modules/ws_customhtml/views/templates/hook/{$hookName}/banner.tpl"} + {if file_exists($customhtmlOverridePath)} + {include file="module:ws_customhtml/views/templates/hook/`$hookName`/banner.tpl" block=$slide group=$group hookName=$hookName} + {else} + {include file="module:ws_customhtml/views/templates/hook/_banner.tpl" block=$slide} + {/if} +
+ {/foreach} +
+
+
+
+ {foreach from=$block.slides item="slide" name="slides"} +
+ {assign var="customhtmlOverridePath" value="{$smarty.const._PS_THEME_DIR_}modules/ws_customhtml/views/templates/hook/{$hookName}/banner.tpl"} + {if file_exists($customhtmlOverridePath)} + {include file="module:ws_customhtml/views/templates/hook/`$hookName`/banner.tpl" block=$slide group=$group hookName=$hookName} + {else} + {include file="module:ws_customhtml/views/templates/hook/_banner.tpl" block=$slide} + {/if} +
+ {/foreach} +
+ {/block} + {/if} + {if $block.slider_mode != 'responsive'} + {block name=slider_always} +
+
+ {foreach from=$block.slides item="slide" name="slides"} +
+ {assign var="customhtmlOverridePath" value="{$smarty.const._PS_THEME_DIR_}modules/ws_customhtml/views/templates/hook/{$hookName}/banner.tpl"} + {if file_exists($customhtmlOverridePath)} + {include file="module:ws_customhtml/views/templates/hook/`$hookName`/banner.tpl" block=$slide group=$group hookName=$hookName} + {else} + {include file="module:ws_customhtml/views/templates/hook/_banner.tpl" block=$slide} + {/if} +
+ {/foreach} +
+
+ {/block} + {/if} +{/if} + + diff --git a/views/templates/hook/customhtml_block.tpl b/views/templates/hook/customhtml_block.tpl index f73bb30..0e525e2 100644 --- a/views/templates/hook/customhtml_block.tpl +++ b/views/templates/hook/customhtml_block.tpl @@ -19,6 +19,11 @@ {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} + {elseif $block.type == 'slider'} + {if $block.show_title && !empty($block.title)} +
{$block.title|escape:'html':'UTF-8'}
+ {/if} + {include file="module:ws_customhtml/views/templates/hook/_slider.tpl" block=$block} {else} {if $block.show_title && !empty($block.title)}
{$block.title|escape:'html':'UTF-8'}
diff --git a/ws_customhtml.php b/ws_customhtml.php index 331282c..ee51d2e 100644 --- a/ws_customhtml.php +++ b/ws_customhtml.php @@ -29,7 +29,7 @@ class Ws_CustomHtml extends Module implements WidgetInterface public function __construct() { $this->name = 'ws_customhtml'; - $this->version = '1.13.0'; + $this->version = '1.17.0'; $this->author = 'Isabelle Oving-Anno | De Websmid BV'; $this->need_instance = 0; $this->bootstrap = true;