add base of the block builder

This commit is contained in:
Simon Vieille 2024-05-11 17:45:57 +02:00
parent 7897bafcc2
commit 7fe1acd47d
Signed by: deblan
GPG key ID: 579388D585F70417
14 changed files with 553 additions and 10 deletions

View 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 [];
}
}

View file

@ -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));
}
}

View 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));
}
}

View 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' => [],
]);
}
}

View 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';
}
}

View file

@ -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;
}
}
}

View file

@ -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')()

View file

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

View file

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

View file

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

View file

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

View 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
}
})
})
}

View file

@ -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()

View file

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