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 @@
+
+