add base of the block builder
This commit is contained in:
parent
7897bafcc2
commit
7fe1acd47d
20
src/core/Entity/Site/Page/BuilderBlock.php
Normal file
20
src/core/Entity/Site/Page/BuilderBlock.php
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
namespace App\Core\Entity\Site\Page;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity]
|
||||
class BuilderBlock extends JsonBlock
|
||||
{
|
||||
public function getValue()
|
||||
{
|
||||
$value = parent::getValue();
|
||||
|
||||
if (is_string($value)) {
|
||||
return json_decode($value);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
|
@ -5,15 +5,6 @@ namespace App\Core\Entity\Site\Page;
|
|||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity]
|
||||
class ChoiceBlock extends Block
|
||||
class ChoiceBlock extends JsonBlock
|
||||
{
|
||||
public function getValue()
|
||||
{
|
||||
return json_decode(parent::getValue(), true);
|
||||
}
|
||||
|
||||
public function setValue($value): self
|
||||
{
|
||||
return parent::setValue(json_encode($value));
|
||||
}
|
||||
}
|
||||
|
|
19
src/core/Entity/Site/Page/JsonBlock.php
Normal file
19
src/core/Entity/Site/Page/JsonBlock.php
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace App\Core\Entity\Site\Page;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity]
|
||||
class JsonBlock extends Block
|
||||
{
|
||||
public function getValue()
|
||||
{
|
||||
return json_decode(parent::getValue(), true);
|
||||
}
|
||||
|
||||
public function setValue($value): self
|
||||
{
|
||||
return parent::setValue(json_encode($value));
|
||||
}
|
||||
}
|
32
src/core/Form/Site/Page/BuilderBlockType.php
Normal file
32
src/core/Form/Site/Page/BuilderBlockType.php
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
namespace App\Core\Form\Site\Page;
|
||||
|
||||
use App\Core\Entity\Site\Page\BuilderBlock;
|
||||
use App\Core\Form\Type\BuilderType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class BuilderBlockType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder->add(
|
||||
'value',
|
||||
BuilderType::class,
|
||||
array_merge([
|
||||
'required' => false,
|
||||
'label' => false,
|
||||
], $options['options']),
|
||||
);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => BuilderBlock::class,
|
||||
'options' => [],
|
||||
]);
|
||||
}
|
||||
}
|
40
src/core/Form/Type/BuilderType.php
Normal file
40
src/core/Form/Type/BuilderType.php
Normal file
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
namespace App\Core\Form\Type;
|
||||
|
||||
use Symfony\Component\Form\Extension\Core\Type\CollectionType as BaseCollectionType;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
|
||||
|
||||
class BuilderType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildView(FormView $view, FormInterface $form, array $options)
|
||||
{
|
||||
parent::buildView($view, $form, $options);
|
||||
|
||||
$view->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';
|
||||
}
|
||||
}
|
|
@ -759,3 +759,51 @@ 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: 3px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 5px;
|
||||
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-toggler {
|
||||
background: none;
|
||||
border: 1px solid map-get($theme-colors, 'dark-blue');
|
||||
color: map-get($theme-colors, 'dark-blue');
|
||||
}
|
||||
|
||||
.block-settings {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.builder-add {
|
||||
text-align: center;
|
||||
margin-bottom: -5px;
|
||||
|
||||
.picker {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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')()
|
||||
|
|
|
@ -0,0 +1,145 @@
|
|||
<template>
|
||||
<div class="block" :key="blockKey">
|
||||
<BuilderBlockItem
|
||||
v-for="(block, key) in value"
|
||||
:item="block"
|
||||
:widgets="widgets"
|
||||
@remove-item="removeBlock(key)"
|
||||
/>
|
||||
<div class="container">
|
||||
<BuilderBlockCreate
|
||||
:container="value"
|
||||
:widgets="widgets"
|
||||
:allowedWidgets="[]"
|
||||
/>
|
||||
</div>
|
||||
<textarea :name="name" class="d-none">{{ toJson(value) }}</textarea>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BuilderBlockItem from './BuilderBlockItem'
|
||||
import BuilderBlockCreate from './BuilderBlockCreate'
|
||||
import BuilderBlockForm from './BuilderBlockForm'
|
||||
|
||||
const widgets = {
|
||||
row: {
|
||||
label: 'Bootstrap row',
|
||||
settings: {
|
||||
},
|
||||
isContainer: true,
|
||||
widgets: ['column'],
|
||||
},
|
||||
column: {
|
||||
label: 'Bootstrap column',
|
||||
settings: {
|
||||
size: {label: 'Size', type: 'input', attr: {type: 'number'}},
|
||||
sizeMd: {label: 'Size MD', type: 'input', attr: {type: 'number'}},
|
||||
},
|
||||
isContainer: true,
|
||||
widgets: [],
|
||||
},
|
||||
tinymce: {
|
||||
label: 'TinyMCE',
|
||||
settings: {
|
||||
value: {label: 'Value', type: 'textarea', attr: {'data-tinymce': ''}},
|
||||
},
|
||||
isContainer: false,
|
||||
widgets: [],
|
||||
}
|
||||
}
|
||||
|
||||
const blocks = [
|
||||
{
|
||||
widget: 'row',
|
||||
settings: {},
|
||||
children: [
|
||||
{
|
||||
widget: 'column',
|
||||
settings: {
|
||||
size: 12,
|
||||
sizeMd: 6,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
widget: 'tinymce',
|
||||
settings: {
|
||||
value: '<p>Hello, world!</p>',
|
||||
},
|
||||
children: null
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
widget: 'column',
|
||||
settings: {
|
||||
size: 12,
|
||||
sizeMd: 6,
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
export default {
|
||||
name: 'BuilderBlock',
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
initialValue: {
|
||||
type: Array,
|
||||
required: false,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
value: this.initialValue,
|
||||
widgets,
|
||||
blockKey: 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toJson(value) {
|
||||
return JSON.stringify(value)
|
||||
},
|
||||
triggerBuilderBlockEvent() {
|
||||
document.querySelector('body').dispatchEvent(new Event('builder_block.update'))
|
||||
},
|
||||
removeBlock(key) {
|
||||
let newValue = []
|
||||
|
||||
this.value.forEach((v, k) => {
|
||||
if (k !== key) {
|
||||
newValue.push(v)
|
||||
}
|
||||
})
|
||||
|
||||
console.log(newValue)
|
||||
|
||||
this.value = newValue
|
||||
++this.blockKey
|
||||
}
|
||||
},
|
||||
components: {
|
||||
BuilderBlockItem,
|
||||
BuilderBlockCreate,
|
||||
BuilderBlockForm,
|
||||
},
|
||||
mounted() {
|
||||
this.triggerBuilderBlockEvent()
|
||||
},
|
||||
created() {
|
||||
this.triggerBuilderBlockEvent()
|
||||
},
|
||||
updated() {
|
||||
this.triggerBuilderBlockEvent()
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,64 @@
|
|||
<template>
|
||||
<div class="builder-add">
|
||||
<span class="btn btn-sm btn-secondary" v-on:click="togglePicker">
|
||||
<span class="fa fa-plus"></span>
|
||||
</span>
|
||||
|
||||
<div class="picker mt-2 list-group" :class="{'d-none': !showPicker}">
|
||||
<div v-for="(widget, name) in widgets" v-if="allowedWidgets.length == 0 || allowedWidgets.includes(name)" class="list-group-item">
|
||||
<div class="float-right">
|
||||
<span class="btn btn-sm btn-primary" v-on:click="add(name, widget)">
|
||||
<span class="fa fa-plus"></span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{{ widget.label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'BuilderBlockCreate',
|
||||
props: {
|
||||
container: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
widgets: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
allowedWidgets: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showPicker: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
add(name, widget) {
|
||||
this.container.push({
|
||||
widget: name,
|
||||
settings: {},
|
||||
children: [],
|
||||
})
|
||||
|
||||
this.$emit('updateContainer', this.container)
|
||||
this.togglePicker()
|
||||
},
|
||||
togglePicker() {
|
||||
this.showPicker = !this.showPicker
|
||||
}
|
||||
},
|
||||
components: {
|
||||
},
|
||||
mounted() {
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,29 @@
|
|||
<template>
|
||||
<div>
|
||||
<div v-if="value !== null && typeof value === 'object'">
|
||||
<BuilderBlockForm v-for="(item, key) in value" :name="name + '[' + key + ']'" :value="item" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<input type="text" :name="name" :value="value">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'BuilderBlockForm',
|
||||
props: {
|
||||
name: {
|
||||
type: name,
|
||||
required: true
|
||||
},
|
||||
value: {
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,108 @@
|
|||
<template>
|
||||
<div class="block" v-if="widget" :key="blockKey">
|
||||
<div class="block-header">
|
||||
<div class="float-right">
|
||||
<div class="block-header-item text-white bg-danger" v-on:click="removeMe(item)">
|
||||
<span class="fa fa-trash"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="block-header-item block-label" data-toggle="tooltip" data-placement="top" :title="item.widget">
|
||||
{{ widget.label }}
|
||||
</div>
|
||||
<div class="block-header-item block-settings-toggler" v-on:click="toggleSettings" v-if="Object.keys(widget.settings).length">
|
||||
<span class="fa fa-cog"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="block-settings" v-if="Object.keys(widget.settings).length" :class="{'d-none': !showSettings}">
|
||||
<div class="row">
|
||||
<div
|
||||
v-for="(params, setting) in widget.settings"
|
||||
class="col-12 form-group"
|
||||
>
|
||||
<label v-text="params.label"></label>
|
||||
|
||||
<input
|
||||
v-if="params.type == 'input'"
|
||||
v-model="item.settings[setting]"
|
||||
v-bind="params.attr"
|
||||
class="form-control"
|
||||
/>
|
||||
<textarea
|
||||
v-if="params.type == 'textarea'"
|
||||
v-model="item.settings[setting]"
|
||||
v-bind="params.attr"
|
||||
class="form-control"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="item.children !== null && item.children.length > 0" v-for="(child, key) in item.children">
|
||||
<BuilderBlockItem
|
||||
:item="child"
|
||||
:widgets="widgets"
|
||||
@remove-item="removeBlock(key)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="widget.isContainer" class="container">
|
||||
<BuilderBlockCreate
|
||||
:container="item.children"
|
||||
:widgets="widgets"
|
||||
:allowedWidgets="widget.widgets"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BuilderBlockCreate from './BuilderBlockCreate'
|
||||
|
||||
export default {
|
||||
name: 'BuilderBlockItem',
|
||||
props: {
|
||||
widgets: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
item: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
widget: null,
|
||||
showSettings: false,
|
||||
blockKey: 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleSettings() {
|
||||
this.showSettings = !this.showSettings
|
||||
},
|
||||
removeMe() {
|
||||
this.$emit('remove-item')
|
||||
},
|
||||
removeBlock(key) {
|
||||
let children = []
|
||||
|
||||
this.item.children.forEach((v, k) => {
|
||||
if (k !== key) {
|
||||
children.push(v)
|
||||
}
|
||||
})
|
||||
|
||||
this.item.children = children
|
||||
++this.blockKey
|
||||
}
|
||||
},
|
||||
components: {
|
||||
BuilderBlockCreate,
|
||||
},
|
||||
mounted() {
|
||||
this.widget = this.widgets[this.item.widget]
|
||||
}
|
||||
}
|
||||
</script>
|
28
src/core/Resources/assets/js/modules/builder-block.js
Normal file
28
src/core/Resources/assets/js/modules/builder-block.js
Normal file
|
@ -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: `<BuilderBlock
|
||||
:initialValue="value"
|
||||
name="${component.getAttribute('data-name')}"
|
||||
id="${component.getAttribute('data-id')}"
|
||||
/>`,
|
||||
data() {
|
||||
return {
|
||||
value: JSON.parse(component.getAttribute('data-value'))
|
||||
}
|
||||
},
|
||||
components: {
|
||||
BuilderBlock
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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 ?? '')}) %}
|
||||
|
||||
<div {% for attr, value in row_attr %}{{ attr }}="{{ value }}" {% endfor %}>
|
||||
<div class="builder-widget-component" data-value="{{ value is iterable ? value|json_encode : value }}" data-name="{{ full_name }}" data-id="{{ id }}">
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block grapesjs_widget %}
|
||||
<div class="gjs"></div>
|
||||
<div class="d-none">
|
||||
|
|
Loading…
Reference in a new issue