Skip to content

Commit 5cc05c7

Browse files
committed
feat(visual-editor): Add block rendering system for frontend display
Implement BlockRenderer service and Blade templates for rendering visual editor layouts on the frontend. - Add BlockRenderer service with recursive block rendering - Add Blade templates for all block types: - Layout: container, section, grid, column - Content: heading, text, button, image - Utility: spacer, divider - Add VisualEditor facade for easy access - Add helper functions: render_visual_layout(), render_visual_block() - Add Blade directives: @visualLayout(), @VisualBlock() - Support inline styles from block settings - XSS protection with HTML sanitization
1 parent 6943853 commit 5cc05c7

File tree

16 files changed

+1215
-1
lines changed

16 files changed

+1215
-1
lines changed

packages/inspirecms-visual-editor/composer.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@
3636
"pestphp/pest": "^3.0"
3737
},
3838
"autoload": {
39+
"files": [
40+
"src/helpers.php"
41+
],
3942
"psr-4": {
4043
"SolutionForest\\InspireCmsVisualEditor\\": "src/"
4144
}
@@ -58,7 +61,10 @@
5861
"laravel": {
5962
"providers": [
6063
"SolutionForest\\InspireCmsVisualEditor\\VisualEditorServiceProvider"
61-
]
64+
],
65+
"aliases": {
66+
"VisualEditor": "SolutionForest\\InspireCmsVisualEditor\\Facades\\VisualEditor"
67+
}
6268
}
6369
},
6470
"minimum-stability": "stable",
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
{{--
2+
Button Block
3+
4+
Renders a clickable button/link with various styling options.
5+
Can be rendered as <a> or <button> element.
6+
7+
@param array $block - The block data
8+
@param array $attributes - Prepared HTML attributes
9+
@param \Illuminate\Support\HtmlString $children - Rendered child blocks (usually empty)
10+
@param array $settings - Block settings
11+
@param array $styles - Block styles
12+
--}}
13+
@php
14+
$text = $settings['text'] ?? $settings['label'] ?? 'Click here';
15+
$url = $settings['url'] ?? $settings['href'] ?? '#';
16+
$target = $settings['target'] ?? '_self';
17+
$rel = $settings['rel'] ?? ($target === '_blank' ? 'noopener noreferrer' : null);
18+
$variant = $settings['variant'] ?? 'primary'; // primary, secondary, outline, ghost
19+
$size = $settings['size'] ?? 'medium'; // small, medium, large
20+
$fullWidth = $settings['fullWidth'] ?? false;
21+
$icon = $settings['icon'] ?? null;
22+
$iconPosition = $settings['iconPosition'] ?? 'left'; // left, right
23+
$isButton = $settings['isButton'] ?? false;
24+
$buttonType = $settings['buttonType'] ?? 'button';
25+
26+
$tag = $isButton ? 'button' : 'a';
27+
28+
// Build button classes
29+
$buttonClasses = ['ve-button'];
30+
$buttonClasses[] = "ve-button--{$variant}";
31+
$buttonClasses[] = "ve-button--{$size}";
32+
33+
if ($fullWidth) {
34+
$buttonClasses[] = 've-button--full-width';
35+
}
36+
37+
if ($icon) {
38+
$buttonClasses[] = 've-button--has-icon';
39+
$buttonClasses[] = "ve-button--icon-{$iconPosition}";
40+
}
41+
42+
if (!empty($settings['cssClass'])) {
43+
$buttonClasses[] = $settings['cssClass'];
44+
}
45+
46+
// Merge with existing classes
47+
$attributes['class'] = trim(($attributes['class'] ?? '') . ' ' . implode(' ', $buttonClasses));
48+
49+
// Add link-specific attributes
50+
if ($tag === 'a') {
51+
$attributes['href'] = $url;
52+
if ($target !== '_self') {
53+
$attributes['target'] = $target;
54+
}
55+
if ($rel) {
56+
$attributes['rel'] = $rel;
57+
}
58+
} else {
59+
$attributes['type'] = $buttonType;
60+
}
61+
62+
// Add ARIA label if different from text
63+
if (!empty($settings['ariaLabel']) && $settings['ariaLabel'] !== $text) {
64+
$attributes['aria-label'] = $settings['ariaLabel'];
65+
}
66+
67+
// Full width style
68+
if ($fullWidth) {
69+
$currentStyle = $attributes['style'] ?? '';
70+
$attributes['style'] = trim($currentStyle . '; display: block; width: 100%; text-align: center');
71+
}
72+
73+
// Remove id attribute from wrapper to avoid duplicate
74+
unset($attributes['id']);
75+
if (!empty($block['id'])) {
76+
$attributes['data-block-id'] = $block['id'];
77+
}
78+
@endphp
79+
80+
<{{ $tag }}{!! $renderer->buildAttributeString($attributes) !!}>
81+
@if($icon && $iconPosition === 'left')
82+
<span class="ve-button__icon ve-button__icon--left">{!! $icon !!}</span>
83+
@endif
84+
85+
<span class="ve-button__text">{{ $renderer->escape($text) }}</span>
86+
87+
@if($icon && $iconPosition === 'right')
88+
<span class="ve-button__icon ve-button__icon--right">{!! $icon !!}</span>
89+
@endif
90+
</{{ $tag }}>
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
{{--
2+
Column Block
3+
4+
A flexible column container used within Grid blocks.
5+
Supports span, alignment, and responsive options.
6+
7+
@param array $block - The block data
8+
@param array $attributes - Prepared HTML attributes
9+
@param \Illuminate\Support\HtmlString $children - Rendered child blocks
10+
@param array $settings - Block settings
11+
@param array $styles - Block styles
12+
--}}
13+
@php
14+
$span = $settings['span'] ?? 1;
15+
$verticalAlign = $settings['verticalAlign'] ?? 'start'; // start, center, end, stretch
16+
$horizontalAlign = $settings['horizontalAlign'] ?? 'stretch'; // start, center, end, stretch
17+
18+
// Build column classes
19+
$columnClasses = ['ve-column'];
20+
$columnClasses[] = "ve-column--span-{$span}";
21+
$columnClasses[] = "ve-column--v-{$verticalAlign}";
22+
$columnClasses[] = "ve-column--h-{$horizontalAlign}";
23+
24+
if (!empty($settings['cssClass'])) {
25+
$columnClasses[] = $settings['cssClass'];
26+
}
27+
28+
// Merge with existing classes
29+
$attributes['class'] = trim(($attributes['class'] ?? '') . ' ' . implode(' ', $columnClasses));
30+
31+
// Build column styles
32+
$columnStyles = [];
33+
34+
if ($span > 1) {
35+
$columnStyles[] = "grid-column: span {$span}";
36+
}
37+
38+
// Flex properties for content alignment
39+
$columnStyles[] = "display: flex";
40+
$columnStyles[] = "flex-direction: column";
41+
42+
$justifyMap = [
43+
'start' => 'flex-start',
44+
'center' => 'center',
45+
'end' => 'flex-end',
46+
'stretch' => 'stretch',
47+
];
48+
49+
$alignMap = [
50+
'start' => 'flex-start',
51+
'center' => 'center',
52+
'end' => 'flex-end',
53+
'stretch' => 'stretch',
54+
];
55+
56+
$columnStyles[] = "justify-content: " . ($justifyMap[$verticalAlign] ?? 'flex-start');
57+
$columnStyles[] = "align-items: " . ($alignMap[$horizontalAlign] ?? 'stretch');
58+
59+
// Merge with existing styles
60+
$currentStyle = $attributes['style'] ?? '';
61+
$attributes['style'] = trim($currentStyle . '; ' . implode('; ', $columnStyles));
62+
@endphp
63+
64+
<div{!! $renderer->buildAttributeString($attributes) !!}>
65+
{{ $children }}
66+
</div>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{{--
2+
Container Block
3+
4+
A full-width container that can hold other blocks.
5+
Supports max-width constraints and background options.
6+
7+
@param array $block - The block data
8+
@param array $attributes - Prepared HTML attributes
9+
@param \Illuminate\Support\HtmlString $children - Rendered child blocks
10+
@param array $settings - Block settings
11+
@param array $styles - Block styles
12+
--}}
13+
@php
14+
$tag = $settings['htmlTag'] ?? 'div';
15+
$maxWidth = $settings['maxWidth'] ?? null;
16+
$fullWidth = $settings['fullWidth'] ?? true;
17+
18+
// Build container classes
19+
$containerClasses = ['ve-container'];
20+
if ($fullWidth) {
21+
$containerClasses[] = 've-container--full-width';
22+
}
23+
if (!empty($settings['cssClass'])) {
24+
$containerClasses[] = $settings['cssClass'];
25+
}
26+
27+
// Merge with existing classes
28+
$attributes['class'] = trim(($attributes['class'] ?? '') . ' ' . implode(' ', $containerClasses));
29+
30+
// Add max-width style if specified
31+
if ($maxWidth && empty($styles['maxWidth'])) {
32+
$currentStyle = $attributes['style'] ?? '';
33+
$attributes['style'] = trim($currentStyle . "; max-width: {$maxWidth}; margin-left: auto; margin-right: auto");
34+
}
35+
@endphp
36+
37+
<{{ $tag }}{!! $renderer->buildAttributeString($attributes) !!}>
38+
@if(!empty($settings['innerContainer']))
39+
<div class="ve-container__inner" @if($maxWidth) style="max-width: {{ $maxWidth }}; margin: 0 auto;" @endif>
40+
{{ $children }}
41+
</div>
42+
@else
43+
{{ $children }}
44+
@endif
45+
</{{ $tag }}>
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{{--
2+
Divider Block
3+
4+
Creates a horizontal line/divider between content sections.
5+
Supports various styles and widths.
6+
7+
@param array $block - The block data
8+
@param array $attributes - Prepared HTML attributes
9+
@param \Illuminate\Support\HtmlString $children - Rendered child blocks (always empty)
10+
@param array $settings - Block settings
11+
@param array $styles - Block styles
12+
--}}
13+
@php
14+
$width = $settings['width'] ?? '100%';
15+
$thickness = $settings['thickness'] ?? '1px';
16+
$lineStyle = $settings['style'] ?? 'solid'; // solid, dashed, dotted, double
17+
$color = $settings['color'] ?? '#e5e7eb';
18+
$alignment = $settings['alignment'] ?? 'center'; // left, center, right
19+
$verticalSpacing = $settings['verticalSpacing'] ?? '1rem';
20+
21+
// Build divider classes
22+
$dividerClasses = ['ve-divider'];
23+
$dividerClasses[] = "ve-divider--{$lineStyle}";
24+
$dividerClasses[] = "ve-divider--align-{$alignment}";
25+
26+
if (!empty($settings['cssClass'])) {
27+
$dividerClasses[] = $settings['cssClass'];
28+
}
29+
30+
// Merge with existing classes
31+
$attributes['class'] = trim(($attributes['class'] ?? '') . ' ' . implode(' ', $dividerClasses));
32+
33+
// Build divider styles
34+
$dividerStyles = [];
35+
$dividerStyles[] = "border: none";
36+
$dividerStyles[] = "border-top-width: {$thickness}";
37+
$dividerStyles[] = "border-top-style: {$lineStyle}";
38+
$dividerStyles[] = "border-top-color: {$color}";
39+
$dividerStyles[] = "width: {$width}";
40+
$dividerStyles[] = "margin-top: {$verticalSpacing}";
41+
$dividerStyles[] = "margin-bottom: {$verticalSpacing}";
42+
43+
// Alignment
44+
$marginStyle = match($alignment) {
45+
'left' => 'margin-right: auto; margin-left: 0',
46+
'right' => 'margin-left: auto; margin-right: 0',
47+
'center' => 'margin-left: auto; margin-right: auto',
48+
default => '',
49+
};
50+
if ($marginStyle) {
51+
$dividerStyles[] = $marginStyle;
52+
}
53+
54+
// Merge with existing styles (but our styles take precedence for hr reset)
55+
$currentStyle = $attributes['style'] ?? '';
56+
$attributes['style'] = implode('; ', $dividerStyles) . ($currentStyle ? '; ' . $currentStyle : '');
57+
58+
// Remove id from attributes for hr
59+
unset($attributes['id']);
60+
if (!empty($block['id'])) {
61+
$attributes['data-block-id'] = $block['id'];
62+
}
63+
@endphp
64+
65+
<hr{!! $renderer->buildAttributeString($attributes) !!} />
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{{--
2+
Grid Block
3+
4+
A CSS Grid-based layout container for creating multi-column layouts.
5+
Supports responsive column configurations.
6+
7+
@param array $block - The block data
8+
@param array $attributes - Prepared HTML attributes
9+
@param \Illuminate\Support\HtmlString $children - Rendered child blocks
10+
@param array $settings - Block settings
11+
@param array $styles - Block styles
12+
--}}
13+
@php
14+
$columns = $settings['columns'] ?? 2;
15+
$gap = $settings['gap'] ?? '1rem';
16+
$rowGap = $settings['rowGap'] ?? $gap;
17+
$columnGap = $settings['columnGap'] ?? $gap;
18+
$minColumnWidth = $settings['minColumnWidth'] ?? '250px';
19+
$autoFit = $settings['autoFit'] ?? false;
20+
21+
// Build grid classes
22+
$gridClasses = ['ve-grid'];
23+
$gridClasses[] = "ve-grid--cols-{$columns}";
24+
25+
if (!empty($settings['cssClass'])) {
26+
$gridClasses[] = $settings['cssClass'];
27+
}
28+
29+
// Merge with existing classes
30+
$attributes['class'] = trim(($attributes['class'] ?? '') . ' ' . implode(' ', $gridClasses));
31+
32+
// Build grid styles
33+
$gridStyles = [];
34+
35+
if ($autoFit) {
36+
$gridStyles[] = "grid-template-columns: repeat(auto-fit, minmax({$minColumnWidth}, 1fr))";
37+
} else {
38+
$gridStyles[] = "grid-template-columns: repeat({$columns}, 1fr)";
39+
}
40+
41+
$gridStyles[] = "gap: {$rowGap} {$columnGap}";
42+
$gridStyles[] = "display: grid";
43+
44+
// Merge with existing styles
45+
$currentStyle = $attributes['style'] ?? '';
46+
$attributes['style'] = trim($currentStyle . '; ' . implode('; ', $gridStyles));
47+
@endphp
48+
49+
<div{!! $renderer->buildAttributeString($attributes) !!}>
50+
{{ $children }}
51+
</div>
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{{--
2+
Heading Block
3+
4+
Renders heading text with configurable level (h1-h6).
5+
Supports text alignment and styling options.
6+
7+
@param array $block - The block data
8+
@param array $attributes - Prepared HTML attributes
9+
@param \Illuminate\Support\HtmlString $children - Rendered child blocks (usually empty)
10+
@param array $settings - Block settings
11+
@param array $styles - Block styles
12+
--}}
13+
@php
14+
$level = $settings['level'] ?? 2;
15+
$tag = "h{$level}";
16+
$content = $settings['content'] ?? $settings['text'] ?? '';
17+
$textAlign = $settings['textAlign'] ?? 'left';
18+
19+
// Build heading classes
20+
$headingClasses = ['ve-heading'];
21+
$headingClasses[] = "ve-heading--{$tag}";
22+
$headingClasses[] = "ve-heading--align-{$textAlign}";
23+
24+
if (!empty($settings['cssClass'])) {
25+
$headingClasses[] = $settings['cssClass'];
26+
}
27+
28+
// Merge with existing classes
29+
$attributes['class'] = trim(($attributes['class'] ?? '') . ' ' . implode(' ', $headingClasses));
30+
31+
// Add text-align style if not already in styles
32+
if (empty($styles['textAlign'])) {
33+
$currentStyle = $attributes['style'] ?? '';
34+
$attributes['style'] = trim($currentStyle . "; text-align: {$textAlign}");
35+
}
36+
37+
// Remove generic block id if present (heading should use its own)
38+
unset($attributes['id']);
39+
if (!empty($settings['anchorId'])) {
40+
$attributes['id'] = $settings['anchorId'];
41+
}
42+
@endphp
43+
44+
<{{ $tag }}{!! $renderer->buildAttributeString($attributes) !!}>
45+
{!! $renderer->sanitizeHtml($content) !!}
46+
</{{ $tag }}>

0 commit comments

Comments
 (0)