# 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
