| Current Path : /var/www/html/reddsis/docs/ |
| Current File : /var/www/html/reddsis/docs/grapesjs-integration.md |
# GrapesJS Visual Builder Integration
## Overview
GrapesJS is integrated as a visual drag-and-drop layout builder for page layouts. The builder replicates the official [GrapesJS Webpage demo](https://grapesjs.com/demo.html) (via the `grapesjs-preset-webpage` plugin and friends) and opens in a **separate browser tab**. CMS components are exposed as **Custom Code** blocks (same component type as the demo's *Custom Code* block) so they can be dragged, dropped, double-clicked to edit, and extracted back into `{!! $code !!}` Blade variables when inserted into the Monaco editor. Communication between tabs uses `localStorage` + `postMessage`.
## Files Involved
| File | Purpose |
|---|---|
| `resources/js/grapesjs-builder.js` | GrapesJS init, demo plugins, CMS Custom Code blocks, insert logic |
| `resources/views/backend/module/frontendSite/layout-builder.blade.php` | Full-screen standalone page that hosts GrapesJS (demo styling) |
| `resources/views/backend/module/frontendSite/page/create.blade.php` | Create form — button + listener |
| `resources/views/backend/module/frontendSite/page/edit.blade.php` | Edit form — button + listener (same as create) |
| `app/Http/Controllers/Backend/FrontendPageController.php` | `layoutBuilder()` method renders the builder view |
| `routes/web.php` | Route `frontend-site.pages.layout-builder` (GET) |
| `vite.config.js` | Entry `resources/js/grapesjs-builder.js` added as Vite input |
| `package.json` | GrapesJS + demo plugins (`grapesjs-preset-webpage`, `grapesjs-custom-code`, etc.) |
| `docs/grapesjs-integration.md` | This documentation |
## How It Works
### Complete Flow
```
┌──────────────────────────┐ ┌─────────────────────────────┐
│ Create/Edit Page │ │ Layout Builder Tab │
│ │ │ │
│ ┌─────────────────┐ │ │ ┌─────────────────────┐ │
│ │ Monaco Editor │ │ │ │ GrapesJS Canvas │ │
│ │ (layout code) │ │ │ │ │ │
│ └─────────────────┘ │ │ │ Drag & drop blocks │ │
│ ▲ │ │ └─────────────────────┘ │
│ │ │ │ │ │
│ 1. Click button ────────┼──────►│ 2. Load layout from │
│ saves Monaco value │ │ localStorage │
│ to localStorage │ │ │ │
│ │ │ │ 3. User edits visually │
│ │ │ │ │ │
│ 6. storage event ───────┼───────│ 4. Click "Insert to │
│ fires, reads HTML │ │ Layout" │
│ from localStorage │ │ │ │
│ │ │ │ 5. Save HTML+CSS to │
│ 7. Monaco.setValue() │ │ localStorage + │
│ ← HTML+CSS inserted │ │ postMessage → close tab│
│ │ │ │ │
│ 8. localStorage cleared │ └─────────────────────────────┘
└──────────────────────────┘
```
### Step-by-step
| Step | Action | Detail |
|---|---|---|
| 1 | User clicks **"Open Visual Builder"** | `openGrapesBuilder()` dipanggil |
| 2 | Save current layout to localStorage | `localStorage.setItem('grapesjs_layout_input', layoutEditor.getValue())` — menyimpan kod dari Monaco editor |
| 3 | Open new tab | `window.open('{{ route('frontend-site.pages.layout-builder') }}', '_blank')` |
| 4 | Builder reads saved layout | `grapesjs-builder.js` — `localStorage.getItem('grapesjs_layout_input')` — terus remove item lepas baca |
| 5 | User builds layout visually | Drag & drop blocks, edit content, style |
| 6 | User clicks **"Insert to Layout"** | GrapesJS export HTML + CSS via `editor.getHtml()` + `editor.getCss()` |
| 7 | Send back to original tab | Dua method: `localStorage.setItem('grapesjs_layout_result', combined)` + `window.opener.postMessage(...)` |
| 8 | Builder tab closes | `window.close()` |
| 9 | Original tab receives data | `storage` event listener atau `message` event listener detect data masuk |
| 10 | Insert into Monaco | `window.layoutEditor.setValue(html)` — kod terus masuk dalam editor layout |
| 11 | Cleanup | localStorage items dibuang lepas digunakan |
## Communication Between Tabs
### Direction: Create/Edit → Builder (sending existing layout to GrapesJS)
Triggered by `openGrapesBuilder()` function:
```js
function openGrapesBuilder() {
if (window.layoutEditor) {
try {
localStorage.setItem('grapesjs_layout_input', window.layoutEditor.getValue());
} catch (e) {}
}
window.open('{{ route('frontend-site.pages.layout-builder') }}', '_blank');
}
```
Pada builder page, `grapesjs-builder.js` baca dari localStorage:
```js
const savedHtml = localStorage.getItem('grapesjs_layout_input');
const initialHtml = savedHtml || '';
localStorage.removeItem('grapesjs_layout_input');
```
### Direction: Builder → Create/Edit (sending result back)
**Method 1 — `localStorage` + `storage` event (primary)**
Builder saves:
```js
localStorage.setItem('grapesjs_layout_result', combined);
localStorage.setItem('grapesjs_layout_ts', Date.now().toString());
```
Create/Edit page listens:
```js
window.addEventListener('storage', function (e) {
if (e.key === 'grapesjs_layout_result' && e.newValue) {
insertGrapesLayout(e.newValue);
localStorage.removeItem('grapesjs_layout_result');
localStorage.removeItem('grapesjs_layout_ts');
}
});
```
**Method 2 — `postMessage` via `window.opener` (fallback)**
Builder sends:
```js
if (window.opener) {
window.opener.postMessage({
type: 'grapesjs-layout',
html: combined
}, '*');
}
```
Create/Edit page listens:
```js
window.addEventListener('message', function (e) {
if (e.data && e.data.type === 'grapesjs-layout') {
insertGrapesLayout(e.data.html);
}
});
```
**Kenapa guna dua method?**
- `localStorage` + `storage` event lebih reliable kerana ia cross-tab communication yang native dan tak bergantung pada `window.opener`
- `postMessage` via `window.opener` as fallback — sesetengah browser/blocker boleh block `window.opener`
### Data Flow Summary
```
localStorage key Direction Description
─────────────────────────────────────────────────────────────────────────
grapesjs_layout_input Create/Edit → Builder Existing layout content
grapesjs_components Create/Edit → Builder Component list [{code, name}]
grapesjs_layout_result Builder → Create/Edit Generated HTML+CSS
grapesjs_layout_ts Builder → Create/Edit Timestamp (prevent stale data)
```
## Route
```
GET /admin/frontend-site/pages/layout-builder
Name: frontend-site.pages.layout-builder
Middleware: auth:admin, permission:frontend-site.create
```
## Available Blocks
Blocks come from the **demo plugin stack** loaded in `resources/js/grapesjs-builder.js`, mirroring <https://grapesjs.com/demo.html>:
### Plugin-provided categories (same as the demo)
| Category | Source plugin | Sample blocks |
|---|---|---|
| Basic | `grapesjs-blocks-basic` | 1/2/3 Columns, 2 Columns 3/7, Text, Link, Image, Video, Map, Link Block, Quote, Text section |
| Forms | `grapesjs-plugin-forms` | Form, Input, Textarea, Select, Button, Label, Checkbox, Radio |
| Extra | `grapesjs-custom-code`, `grapesjs-component-countdown`, `grapesjs-tooltip`, `grapesjs-tabs`, `grapesjs-typed` | **Custom Code**, Countdown, Tooltip, Tabs, Typed |
| (Preset) | `grapesjs-preset-webpage` | Link Block, Quote, Text section (basic), plus Import/Export/Clear commands |
### CMS Components (Dynamic, registered as Custom Code blocks)
Every CMS component that is ticked on the page create/edit form is auto-registered as a **Custom Code** block (same `custom-code` component type as the demo's *Custom Code* block). They are sent to the builder via localStorage `grapesjs_components`:
| Block | What it creates on the canvas |
|---|---|
| (each component) | A `custom-code` component whose code is the Blade variable `{!! $code !!}`, tagged with `data-cms-component="<code>"` |
**How it works:**
1. On the page form, `openGrapesBuilder()` stores `grapesjs_components` to localStorage — array of `{code, name}`
2. The builder reads the array and calls `editor.Blocks.add('cms-<code>', { content: { type: 'custom-code', 'custom-code-plugin__code': '{!! $code !!}', attributes: { 'data-cms-component': code } }, category: 'CMS Components' })`
3. Drop the block → canvas shows a Custom Code component rendering `{!! $code !!}` (highlighted with a dashed indigo outline). Double-click it to open the code editor modal (just like the demo's Custom Code block).
4. When **Insert to Layout** is clicked, every `[data-cms-component]` element is replaced with its `{!! $code !!}` Blade variable (DOM-based extraction via `DOMParser`, with a regex fallback) — the output is clean and ready for `PortalHandler`.
### Loading existing layouts
When a layout that already contains `{!! $code !!}` is opened in the builder, those placeholders are pre-converted into editable `custom-code` components (`<div data-gjs-type="custom-code" data-cms-component="code">`) so the user can continue editing them visually. On insert they are extracted back to `{!! $code !!}`.
## Demo Plugin Stack
`resources/js/grapesjs-builder.js` imports and enables the same plugins as the official demo (loaded via npm, bundled by Vite):
| Plugin | Purpose |
|---|---|
| `grapesjs-preset-webpage` | Toolbar, panels, Style Manager theme, Import/Export/Clear commands |
| `grapesjs-blocks-basic` | Basic blocks (columns, text, image, …) with `flexGrid: true` |
| `grapesjs-plugin-forms` | Form components |
| `grapesjs-component-countdown` | Countdown block (Extra) |
| `grapesjs-plugin-export` | Export template command |
| `grapesjs-tabs` | Tabs block (Extra) |
| `grapesjs-custom-code` | **Custom Code** component type — used for CMS components |
| `grapesjs-touch` | Touch device support |
| `grapesjs-parser-postcss` | PostCSS parser |
| `grapesjs-tooltip` | Tooltip component + button tooltips (style injected via `style` option) |
| `grapesjs-typed` | Typed-text block (Extra) |
| `grapesjs-style-bg` | Background style property |
> `grapesjs-tui-image-editor` from the demo is intentionally omitted (heavy external image-editor deps).
## Adding New Blocks
### Custom static block
Edit `grapesjs-builder.js` and call `editor.Blocks.add(...)` after init:
```js
editor.Blocks.add('my-block', {
id: 'my-block', // unique ID
label: 'My Block', // display name in panel
category: 'My Category', // group in the panel
content: '<div>HTML</div>', // string OR component object
});
```
### New CMS Custom Code block
CMS component blocks are added dynamically from `grapesjs_components` (see [CMS Components](#cms-components-dynamic-registered-as-custom-code-blocks)). To register one manually with the same Custom Code behaviour:
```js
editor.Blocks.add('cms-mycode', {
label: 'My Component',
category: 'CMS Components',
content: {
type: 'custom-code',
'custom-code-plugin__code': '{!! $mycode !!}',
components: '{!! $mycode !!}',
attributes: { 'data-cms-component': 'mycode' },
},
});
```
Then rebuild:
```bash
npm run build
```
## Key Functions
### `openGrapesBuilder()`
Location: `create.blade.php` and `edit.blade.php` — inline script
```js
function openGrapesBuilder() {
if (window.layoutEditor) {
try {
localStorage.setItem('grapesjs_layout_input', window.layoutEditor.getValue());
} catch (e) {}
}
window.open('{{ route('frontend-site.pages.layout-builder') }}', '_blank');
}
```
### `insertGrapesLayout(html)`
Location: `create.blade.php` and `edit.blade.php` — inline script
```js
function insertGrapesLayout(html) {
if (window.layoutEditor) {
window.layoutEditor.setValue(html);
}
}
```
### GrapesJS Init (in `grapesjs-builder.js`)
```js
const editor = grapesjs.init({
container, // DOM element #gjs
fromElement: false,
components: initialHtml, // layout from localStorage (with {!! $code !!} pre-converted)
showOffsets: true,
selectorManager: { componentFirst: true },
canvas: {
styles: ['https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css'],
},
storageManager: false,
undoManager: { trackSelection: false },
styleManager: { sectors: [ /* General, Dimension, Typography, Decorations, Extra, Flex — same as the demo */ ] },
plugins: [
grapesjsBlocksBasic, grapesjsPluginForms, grapesjsComponentCountdown,
grapesjsPluginExport, grapesjsTabs, grapesjsCustomCode,
grapesjsTouch, grapesjsParserPostcss, grapesjsTooltip,
grapesjsTyped, grapesjsStyleBg, grapesjsPresetWebpage,
],
pluginsOpts: { /* flexGrid, tabs category, typed strings, tooltip CSS, import modal */ },
});
```
### Insert Handler (in `grapesjs-builder.js`)
Registered as the `insert-layout` command (triggered by the ✓ button in the options panel, or the legacy `#gjs-insert-btn`):
```js
editor.Commands.add('insert-layout', {
run: function () {
let html = editor.getHtml();
const css = editor.getCss();
// Replace CMS custom-code component wrappers with Blade variables (DOM-based)
const doc = new DOMParser().parseFromString('<body>' + html + '</body>', 'text/html');
doc.querySelectorAll('[data-cms-component]').forEach(function (el) {
const code = el.getAttribute('data-cms-component');
el.replaceWith('{!! $' + code + ' !!}');
});
html = doc.body.innerHTML;
const combined = css ? '<style>\n' + css + '\n</style>\n\n' + html : html;
localStorage.setItem('grapesjs_layout_result', combined);
localStorage.setItem('grapesjs_layout_ts', Date.now().toString());
if (window.opener) window.opener.postMessage({ type: 'grapesjs-layout', html: combined }, '*');
window.close();
},
});
```
> **CMS Component extraction:** Every `[data-cms-component="code"]` element (the Custom Code wrapper) is replaced with `{!! $code !!}` so the output is clean and ready for `PortalHandler`. A regex fallback handles environments without `DOMParser`.
## Canvas Styles
Bootstrap 5.3 CSS from CDN is loaded inside the canvas for preview only (does not affect the builder UI):
```js
canvas: {
styles: ['https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css'],
}
```
CMS Custom Code components are visually highlighted inside the canvas via CSS in `layout-builder.blade.php` (dashed indigo outline, light background) so they read like the demo's Custom Code block.
## Keyboard Shortcuts
| Shortcut | Action |
|---|---|
| ✓ button (options panel) / `#gjs-insert-btn` | Insert generated HTML+CSS back into the page editor |
| ✕ button (options panel) / `#gjs-close-btn` / close tab | Cancel and close the builder |
| Double-click a Custom Code block | Open the code editor modal (edit the `{!! $code !!}`) |
| Ctrl+Z | Undo (GrapesJS default) |
## Output Format
Inserted into the Monaco editor:
```html
<style>
/* CSS generated by GrapesJS */
</style>
<!-- HTML generated by GrapesJS, with CMS Custom Code blocks extracted to {!! $code !!} -->
```
CSS is wrapped in `<style>` tags, HTML follows. If there is no CSS, only HTML is emitted.
## Important Notes
- The builder mirrors the official [GrapesJS Webpage demo](https://grapesjs.com/demo.html) (same plugin stack, Style Manager sectors, Flex sector icons, tooltip styles)
- GrapesJS only loads on the builder page — the create/edit page stays light
- The builder page is a standalone view (no sidebar/header) — full screen for the editor
- localStorage communication works cross-origin, cross-tab
- `window.opener.postMessage` may be blocked by some browser policies — localStorage is the primary channel
- Bootstrap CDN is for canvas preview only — it does not affect the actual page
- Output is always in the format `<style>...</style>\n\n<html>...`
- localStorage items are cleaned (removeItem) after use to avoid stale data
- Works for both create and edit — on edit, the existing layout is loaded and `{!! $code !!}` placeholders become editable Custom Code components
- **CMS Components are Custom Code blocks** — same `custom-code` component type as the demo's *Custom Code* block: drag, drop, double-click to edit the code, and on insert they are extracted to `{!! $code !!}`
- `grapesjs-tui-image-editor` (from the demo) is omitted to avoid pulling in heavy external image-editor dependencies
- After changing `grapesjs-builder.js` or upgrading plugins, run `npm run build` to regenerate the Vite bundle