diff --git a/CHANGELOG.md b/CHANGELOG.md index ee0b94a..3e54b10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ ## [Unreleased] +# [v1.25.0] - 2024-05-12 ### Added +* add block builder widget * allow to use `window.tinymceModes` to add or override tinymce modes * add border color on tinymce editor ### Fixed diff --git a/src/core/BuilderBlock/Block/Bootstrap/BootstrapBlock.php b/src/core/BuilderBlock/Block/Bootstrap/BootstrapBlock.php new file mode 100644 index 0000000..12fc12e --- /dev/null +++ b/src/core/BuilderBlock/Block/Bootstrap/BootstrapBlock.php @@ -0,0 +1,14 @@ +setCategory('Bootstrap'); + } +} diff --git a/src/core/BuilderBlock/Block/Bootstrap/ColumnBuilderBlock.php b/src/core/BuilderBlock/Block/Bootstrap/ColumnBuilderBlock.php new file mode 100644 index 0000000..5e2d961 --- /dev/null +++ b/src/core/BuilderBlock/Block/Bootstrap/ColumnBuilderBlock.php @@ -0,0 +1,29 @@ +setName('bsColumn') + ->setLabel('Column') + ->setIsContainer(true) + ->setClass('col-12 col-md-2 pr-md-1') + ->setTemplate('@Core/builder_block/bootstrap/column.html.twig') + ->setIcon('') + ->addSetting(name: 'size', label: 'Extra small', type: 'number', attributes: ['min' => 0, 'max' => 12]) + ->addSetting(name: 'sizeSm', label: 'Small', type: 'number', attributes: ['min' => 0, 'max' => 12]) + ->addSetting(name: 'sizeMd', label: 'Medium', type: 'number', attributes: ['min' => 0, 'max' => 12]) + ->addSetting(name: 'sizeLg', label: 'Large', type: 'number', attributes: ['min' => 0, 'max' => 12]) + ->addSetting(name: 'sizeXl', label: 'Extra large', type: 'number', attributes: ['min' => 0, 'max' => 12]) + ; + } +} diff --git a/src/core/BuilderBlock/Block/Bootstrap/ContainerBuilderBlock.php b/src/core/BuilderBlock/Block/Bootstrap/ContainerBuilderBlock.php new file mode 100644 index 0000000..d8aed25 --- /dev/null +++ b/src/core/BuilderBlock/Block/Bootstrap/ContainerBuilderBlock.php @@ -0,0 +1,24 @@ +setName('bsContainer') + ->setLabel('Container') + ->setIsContainer(true) + ->setTemplate('@Core/builder_block/bootstrap/container.html.twig') + ->setIcon('') + ->addSetting(name: 'isFluid', label: 'Fluid', type: 'checkbox') + ; + } +} diff --git a/src/core/BuilderBlock/Block/Bootstrap/RowBuilderBlock.php b/src/core/BuilderBlock/Block/Bootstrap/RowBuilderBlock.php new file mode 100644 index 0000000..0391db5 --- /dev/null +++ b/src/core/BuilderBlock/Block/Bootstrap/RowBuilderBlock.php @@ -0,0 +1,24 @@ +setName('bsRow') + ->setLabel('Row') + ->setIsContainer(true) + ->setIcon('') + ->setTemplate('@Core/builder_block/bootstrap/row.html.twig') + ->addWidget('bsColumn') + ; + } +} diff --git a/src/core/BuilderBlock/Block/Editor/EditorBlock.php b/src/core/BuilderBlock/Block/Editor/EditorBlock.php new file mode 100644 index 0000000..e8e377b --- /dev/null +++ b/src/core/BuilderBlock/Block/Editor/EditorBlock.php @@ -0,0 +1,14 @@ +setCategory('Editor'); + } +} diff --git a/src/core/BuilderBlock/Block/Editor/TinymceBlock.php b/src/core/BuilderBlock/Block/Editor/TinymceBlock.php new file mode 100644 index 0000000..05f140a --- /dev/null +++ b/src/core/BuilderBlock/Block/Editor/TinymceBlock.php @@ -0,0 +1,24 @@ +setName('tinymce') + ->setLabel('TinyMCE') + ->setIsContainer(false) + ->setIcon('') + ->setTemplate('@Core/builder_block/editor/tinymce.html.twig') + ->addSetting(name: 'value', type: 'textarea', attributes: ['data-tinymce' => '']) + ; + } +} diff --git a/src/core/BuilderBlock/BuilderBlock.php b/src/core/BuilderBlock/BuilderBlock.php new file mode 100644 index 0000000..a684989 --- /dev/null +++ b/src/core/BuilderBlock/BuilderBlock.php @@ -0,0 +1,172 @@ +label = $label; + + return $this; + } + + public function getLabel(): string + { + return $this->label; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + public function getName(): string + { + return $this->name; + } + + public function setCategory(?string $category): self + { + $this->category = $category; + + return $this; + } + + public function getCategory(): ?string + { + return $this->category; + } + + public function setIsContainer(bool $isContainer): self + { + $this->isContainer = $isContainer; + + return $this; + } + + public function getIsContainer(): bool + { + return $this->isContainer; + } + + public function setWidgets(array $widgets): self + { + $this->widgets = $widgets; + + return $this; + } + + public function addWidget(string $widget): self + { + if (!in_array($widget, $this->widgets)) { + $this->widgets[] = $widget; + } + + return $this; + } + + public function getWidgets(): array + { + return $this->widgets; + } + + public function setSettings(array $settings): self + { + $this->settings = $settings; + + return $this; + } + + public function addSetting( + string $name, + string $type = 'input', + ?string $label = null, + array $attributes = [], + array $extraOptions = [], + $default = null + ) { + $this->settings[$name] = [ + 'label' => $label, + 'type' => $type, + 'attr' => $attributes, + 'default' => $default, + ]; + + foreach ($extraOptions as $key => $value) { + if (in_array($key, array_keys($this->settings[$name]))) { + $this->settings[$name][$key] = $value; + } + } + + return $this; + } + + public function getSettings(): array + { + return $this->settings; + } + + public function setTemplate(string $template): self + { + $this->template = $template; + + return $this; + } + + public function getTemplate(): string + { + return $this->template; + } + + public function toArray(): array + { + return [ + 'label' => $this->getLabel(), + 'category' => $this->getCategory(), + 'isContainer' => $this->getIsContainer(), + 'widgets' => $this->getWidgets(), + 'settings' => $this->getSettings(), + 'class' => $this->getClass(), + 'icon' => $this->getIcon(), + ]; + } + + public function setClass(?string $class): self + { + $this->class = $class; + + return $this; + } + + public function getClass(): ?string + { + return $this->class; + } + + public function setIcon(?string $icon): self + { + $this->icon = $icon; + + return $this; + } + + public function getIcon(): ?string + { + return $this->icon; + } +} diff --git a/src/core/BuilderBlock/BuilderBlockContainer.php b/src/core/BuilderBlock/BuilderBlockContainer.php new file mode 100644 index 0000000..980c8f3 --- /dev/null +++ b/src/core/BuilderBlock/BuilderBlockContainer.php @@ -0,0 +1,25 @@ +configure(); + + $this->widgets[$widget->getName()] = $widget; + } + + public function getWidgets(): array + { + return $this->widgets; + } + + public function getWidget(string $name): BuilderBlock + { + return $this->widgets[$name]; + } +} diff --git a/src/core/Controller/Editor/BuilderBlockController.php b/src/core/Controller/Editor/BuilderBlockController.php new file mode 100644 index 0000000..9aeb0b4 --- /dev/null +++ b/src/core/Controller/Editor/BuilderBlockController.php @@ -0,0 +1,40 @@ + true])] + public function widgets(BuilderBlockContainer $container): JsonResponse + { + $data = []; + + foreach ($container->getWidgets() as $widget) { + $data[$widget->getName()] = $this->translate($widget->toArray()); + } + + return $this->json($data); + } + + protected function translate(array $data) + { + $data['label'] = $this->translator->trans($data['label']); + + foreach ($data['settings'] as $key => $value) { + $data['settings'][$key]['label'] = $this->translator->trans($data['settings'][$key]['label']); + } + + return $data; + } +} diff --git a/src/core/DependencyInjection/Compiler/BuilderBlockPass.php b/src/core/DependencyInjection/Compiler/BuilderBlockPass.php new file mode 100644 index 0000000..cdf019f --- /dev/null +++ b/src/core/DependencyInjection/Compiler/BuilderBlockPass.php @@ -0,0 +1,25 @@ +has(BuilderBlockContainer::class)) { + return; + } + + $definition = $container->findDefinition(BuilderBlockContainer::class); + $taggedServices = $container->findTaggedServiceIds('builder_block.widget'); + + foreach ($taggedServices as $id => $tags) { + $definition->addMethodCall('addWidget', [new Reference($id)]); + } + } +} diff --git a/src/core/DependencyInjection/Configuration.php b/src/core/DependencyInjection/Configuration.php index 14a721b..cb6b6d1 100644 --- a/src/core/DependencyInjection/Configuration.php +++ b/src/core/DependencyInjection/Configuration.php @@ -46,99 +46,99 @@ class Configuration implements ConfigurationInterface $treeBuilder->getRootNode() ->children() - ->arrayNode('site') - ->children() - ->scalarNode('name') - ->defaultValue('Murph') - ->isRequired() - ->end() - ->scalarNode('logo') - ->defaultValue('build/images/core/logo.svg') - ->isRequired() - ->end() - ->arrayNode('controllers') - ->prototype('array') - ->children() - ->scalarNode('name') - ->cannotBeEmpty() - ->end() - ->scalarNode('action') - ->cannotBeEmpty() - ->end() - ->end() - ->end() - ->end() - ->arrayNode('security') - ->children() - ->arrayNode('roles') - ->prototype('array') - ->children() - ->scalarNode('name') - ->cannotBeEmpty() - ->end() - ->scalarNode('role') - ->cannotBeEmpty() - ->end() - ->end() - ->end() - ->end() - ->end() - ->end() - ->arrayNode('pages') - ->prototype('array') - ->children() - ->scalarNode('name') - ->isRequired() - ->cannotBeEmpty() - ->end() - ->arrayNode('templates') - ->prototype('array') - ->children() - ->scalarNode('name') - ->cannotBeEmpty() - ->end() - ->scalarNode('file') - ->cannotBeEmpty() - ->end() - ->end() - ->end() - ->end() - ->end() - ->end() - ->end() - ->end() - ->end() - ->arrayNode('editor_js') - ->children() - ->arrayNode('blocks') - ->scalarPrototype() - ->end() - ->end() - ->end() - ->end() - ->arrayNode('file_manager') - ->children() - ->arrayNode('mimes') - ->scalarPrototype() - ->end() - ->defaultValue($defaultMimetypes) - ->end() - ->scalarNode('path') - ->defaultValue('%kernel.project_dir%/public/uploads') - ->cannotBeEmpty() - ->end() - ->scalarNode('path_uri') - ->defaultValue('/uploads') - ->cannotBeEmpty() - ->end() - ->arrayNode('path_locked') - ->scalarPrototype() - ->end() - ->defaultValue($defaultLocked) - ->end() - ->end() - ->end() - ->end() + ->arrayNode('site') + ->children() + ->scalarNode('name') + ->defaultValue('Murph') + ->isRequired() + ->end() + ->scalarNode('logo') + ->defaultValue('build/images/core/logo.svg') + ->isRequired() + ->end() + ->arrayNode('controllers') + ->prototype('array') + ->children() + ->scalarNode('name') + ->cannotBeEmpty() + ->end() + ->scalarNode('action') + ->cannotBeEmpty() + ->end() + ->end() + ->end() + ->end() + ->arrayNode('security') + ->children() + ->arrayNode('roles') + ->prototype('array') + ->children() + ->scalarNode('name') + ->cannotBeEmpty() + ->end() + ->scalarNode('role') + ->cannotBeEmpty() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->arrayNode('pages') + ->prototype('array') + ->children() + ->scalarNode('name') + ->isRequired() + ->cannotBeEmpty() + ->end() + ->arrayNode('templates') + ->prototype('array') + ->children() + ->scalarNode('name') + ->cannotBeEmpty() + ->end() + ->scalarNode('file') + ->cannotBeEmpty() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->arrayNode('editor_js') + ->children() + ->arrayNode('blocks') + ->scalarPrototype() + ->end() + ->end() + ->end() + ->end() + ->arrayNode('file_manager') + ->children() + ->arrayNode('mimes') + ->scalarPrototype() + ->end() + ->defaultValue($defaultMimetypes) + ->end() + ->scalarNode('path') + ->defaultValue('%kernel.project_dir%/public/uploads') + ->cannotBeEmpty() + ->end() + ->scalarNode('path_uri') + ->defaultValue('/uploads') + ->cannotBeEmpty() + ->end() + ->arrayNode('path_locked') + ->scalarPrototype() + ->end() + ->defaultValue($defaultLocked) + ->end() + ->end() + ->end() + ->end(); ; return $treeBuilder; diff --git a/src/core/DependencyInjection/CoreExtension.php b/src/core/DependencyInjection/CoreExtension.php index 4708e21..4cac902 100644 --- a/src/core/DependencyInjection/CoreExtension.php +++ b/src/core/DependencyInjection/CoreExtension.php @@ -2,6 +2,7 @@ namespace App\Core\DependencyInjection; +use App\Core\DependencyInjection\Compiler\BuilderBlockPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\Extension; diff --git a/src/core/Entity/Site/Page/BuilderBlock.php b/src/core/Entity/Site/Page/BuilderBlock.php new file mode 100644 index 0000000..878c96f --- /dev/null +++ b/src/core/Entity/Site/Page/BuilderBlock.php @@ -0,0 +1,20 @@ +add( + 'value', + BuilderType::class, + array_merge([ + 'required' => false, + 'label' => false, + ], $options['options']), + ); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => BuilderBlock::class, + 'options' => [], + ]); + } +} diff --git a/src/core/Form/Type/BuilderType.php b/src/core/Form/Type/BuilderType.php new file mode 100644 index 0000000..f817b26 --- /dev/null +++ b/src/core/Form/Type/BuilderType.php @@ -0,0 +1,40 @@ +vars = array_replace($view->vars, [ + ]); + } + + public function configureOptions(OptionsResolver $resolver) + { + parent::configureOptions($resolver); + + $resolver->setDefaults([ + 'allow_add' => true, + 'allow_remove' => true, + 'compound' => false, + ]); + } + + public function getBlockPrefix() + { + return 'builder'; + } +} diff --git a/src/core/Murph.php b/src/core/Murph.php index aa86fda..07a1b39 100644 --- a/src/core/Murph.php +++ b/src/core/Murph.php @@ -3,7 +3,7 @@ namespace App\Core; if (!defined('MURPH_VERSION')) { - define('MURPH_VERSION', 'v1.24.1'); + define('MURPH_VERSION', 'v1.25.0'); } /** diff --git a/src/core/Resources/assets/css/admin.scss b/src/core/Resources/assets/css/admin.scss index d3b2414..854b818 100644 --- a/src/core/Resources/assets/css/admin.scss +++ b/src/core/Resources/assets/css/admin.scss @@ -759,3 +759,46 @@ label.required::after { color: #49555b; font-size: 20px; } + +.builder-widget { + .block { + border: 1px solid rgba(map-get($theme-colors, 'dark-blue'), 0.3); + padding: 10px; + border-radius: 4px; + margin-bottom: 10px; + background: rgba(map-get($theme-colors, 'dark-blue'), 0.02); + } + + .block-header { + .block-header-item { + font-size: 12px; + display: inline-block; + margin-bottom: 10px; + padding: 2px 6px; + border-radius: 4px; + margin-right: 2px; + cursor: pointer; + } + } + + .block-label { + background: map-get($theme-colors, 'dark-blue'); + border: 1px solid map-get($theme-colors, 'dark-blue'); + color: lighten(map-get($theme-colors, 'dark-blue'), 100%); + } + + .block-settings-inverse { + background: none; + border: 1px solid map-get($theme-colors, 'dark-blue'); + color: map-get($theme-colors, 'dark-blue'); + } + + .block-settings { + padding: 4px; + } + + .block-id { + font-size: 12px; + margin-right: 5px; + } +} diff --git a/src/core/Resources/assets/js/admin.js b/src/core/Resources/assets/js/admin.js index 58cb77c..c1c988d 100644 --- a/src/core/Resources/assets/js/admin.js +++ b/src/core/Resources/assets/js/admin.js @@ -29,3 +29,4 @@ require('./modules/file-picker.js')() require('./modules/analytics.js')() require('./modules/page.js')() require('./modules/node.js')() +require('./modules/builder-block.js')() diff --git a/src/core/Resources/assets/js/components/builder-block/BuilderBlock.vue b/src/core/Resources/assets/js/components/builder-block/BuilderBlock.vue new file mode 100644 index 0000000..fd0229f --- /dev/null +++ b/src/core/Resources/assets/js/components/builder-block/BuilderBlock.vue @@ -0,0 +1,118 @@ + + + diff --git a/src/core/Resources/assets/js/components/builder-block/BuilderBlockCreate.vue b/src/core/Resources/assets/js/components/builder-block/BuilderBlockCreate.vue new file mode 100644 index 0000000..5714ad9 --- /dev/null +++ b/src/core/Resources/assets/js/components/builder-block/BuilderBlockCreate.vue @@ -0,0 +1,163 @@ + + + + + diff --git a/src/core/Resources/assets/js/components/builder-block/BuilderBlockItem.vue b/src/core/Resources/assets/js/components/builder-block/BuilderBlockItem.vue new file mode 100644 index 0000000..0947210 --- /dev/null +++ b/src/core/Resources/assets/js/components/builder-block/BuilderBlockItem.vue @@ -0,0 +1,166 @@ + + + diff --git a/src/core/Resources/assets/js/components/builder-block/BuilderBlockSetting.vue b/src/core/Resources/assets/js/components/builder-block/BuilderBlockSetting.vue new file mode 100644 index 0000000..89d12df --- /dev/null +++ b/src/core/Resources/assets/js/components/builder-block/BuilderBlockSetting.vue @@ -0,0 +1,61 @@ + + + diff --git a/src/core/Resources/assets/js/components/file-manager/Files.vue b/src/core/Resources/assets/js/components/file-manager/Files.vue index 4690a75..581170c 100644 --- a/src/core/Resources/assets/js/components/file-manager/Files.vue +++ b/src/core/Resources/assets/js/components/file-manager/Files.vue @@ -201,7 +201,6 @@ import Routing from '../../../../../../../../../friendsofsymfony/jsrouting-bundl import FileIcon from './FileIcon' const axios = require('axios').default -const $ = require('jquery') const routes = require('../../../../../../../../../../public/js/fos_js_routes.json') Routing.setRoutingData(routes) diff --git a/src/core/Resources/assets/js/modules/builder-block.js b/src/core/Resources/assets/js/modules/builder-block.js new file mode 100644 index 0000000..5c87bfc --- /dev/null +++ b/src/core/Resources/assets/js/modules/builder-block.js @@ -0,0 +1,28 @@ +const Vue = require('vue').default + +const BuilderBlock = require('../components/builder-block/BuilderBlock').default + +module.exports = () => { + const wrappers = document.querySelectorAll('.builder-widget') + + wrappers.forEach((wrapper) => { + const component = wrapper.querySelector('.builder-widget-component') + + return new Vue({ + el: component, + template: ``, + data() { + return { + value: JSON.parse(component.getAttribute('data-value')) + } + }, + components: { + BuilderBlock + } + }) + }) +} diff --git a/src/core/Resources/assets/js/modules/tinymce.js b/src/core/Resources/assets/js/modules/tinymce.js index d85ac09..1f6ab2c 100644 --- a/src/core/Resources/assets/js/modules/tinymce.js +++ b/src/core/Resources/assets/js/modules/tinymce.js @@ -74,6 +74,8 @@ const createTinymceConfig = function () { }) editor.on('Change', () => { + editor.save(); + editor.getElement().dispatchEvent(new Event('input')); window.tinymce.triggerSave(false, true) }) } @@ -618,6 +620,13 @@ const doInitEditor = () => { } module.exports = function () { + document.querySelector('body').addEventListener('builder_block.update', () => { + window.setTimeout(() => { + createTinymceConfig() + doInitEditor() + }, 500) + }) + $(() => { createTinymceConfig() doInitEditor() diff --git a/src/core/Resources/translations/messages.fr.yaml b/src/core/Resources/translations/messages.fr.yaml index d7c2b7d..0021fb2 100644 --- a/src/core/Resources/translations/messages.fr.yaml +++ b/src/core/Resources/translations/messages.fr.yaml @@ -224,3 +224,8 @@ "All roles": "Tous les rôles" "Enable A/B Testing": "Activer le test A/B" "Color": "Couleur" +"Extra small": "Très petit" +"Small": "Petit" +"Medium": "Moyen" +"Large": "Large" +"Extra large": "Très large" diff --git a/src/core/Resources/views/builder_block/bootstrap/column.html.twig b/src/core/Resources/views/builder_block/bootstrap/column.html.twig new file mode 100644 index 0000000..b9967f1 --- /dev/null +++ b/src/core/Resources/views/builder_block/bootstrap/column.html.twig @@ -0,0 +1,13 @@ +{% set sizes = { + 'col-': settings.size|default(null), + 'col-xs-': settings.sizeXs|default(null), + 'col-md-': settings.sizeMd|default(null), + 'col-lg-': settings.sizeLg|default(null), + 'col-xl-': settings.sizeXl|default(null), +} %} + +
+ {% for item in children %} + {{ item|block_to_html }} + {% endfor %} +
diff --git a/src/core/Resources/views/builder_block/bootstrap/container.html.twig b/src/core/Resources/views/builder_block/bootstrap/container.html.twig new file mode 100644 index 0000000..9c9fcb5 --- /dev/null +++ b/src/core/Resources/views/builder_block/bootstrap/container.html.twig @@ -0,0 +1,5 @@ +
+ {% for item in children %} + {{ item|block_to_html }} + {% endfor %} +
diff --git a/src/core/Resources/views/builder_block/bootstrap/row.html.twig b/src/core/Resources/views/builder_block/bootstrap/row.html.twig new file mode 100644 index 0000000..6f8bdb0 --- /dev/null +++ b/src/core/Resources/views/builder_block/bootstrap/row.html.twig @@ -0,0 +1,5 @@ +
+ {% for item in children %} + {{ item|block_to_html }} + {% endfor %} +
diff --git a/src/core/Resources/views/builder_block/editor/tinymce.html.twig b/src/core/Resources/views/builder_block/editor/tinymce.html.twig new file mode 100644 index 0000000..23fa2e1 --- /dev/null +++ b/src/core/Resources/views/builder_block/editor/tinymce.html.twig @@ -0,0 +1 @@ +{{ settings.value|default(null)|file_attributes|murph_url|raw }} diff --git a/src/core/Resources/views/form/bootstrap_4_form_theme.html.twig b/src/core/Resources/views/form/bootstrap_4_form_theme.html.twig index 05895ce..fc86bb8 100644 --- a/src/core/Resources/views/form/bootstrap_4_form_theme.html.twig +++ b/src/core/Resources/views/form/bootstrap_4_form_theme.html.twig @@ -1,5 +1,14 @@ {% extends 'bootstrap_4_layout.html.twig' %} +{% block builder_widget %} + {% set row_attr = row_attr|merge({class: 'builder-widget ' ~ (row_attr.class ?? '')}) %} + +
+
+
+
+{% endblock %} + {% block grapesjs_widget %}
diff --git a/src/core/Twig/Extension/BuilderBlockExtension.php b/src/core/Twig/Extension/BuilderBlockExtension.php new file mode 100644 index 0000000..a8ab5f3 --- /dev/null +++ b/src/core/Twig/Extension/BuilderBlockExtension.php @@ -0,0 +1,47 @@ + ['html']]), + ]; + } + + public function buildHtml($data): string + { + if (null === $data) { + return null; + } + + if (isset($data['widget'])) { + return $this->twig->render($this->container->getWidget($data['widget'])->getTemplate(), [ + 'id' => $data['id'], + 'settings' => $data['settings'], + 'children' => $data['children'], + ]); + } + + $render = ''; + foreach ($data as $item) { + $render .= $this->buildHtml($item); + } + + return $render; + } +}