# Live Copy Feature — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Enable a "Live Copy" button on demo site sections that copies layout JSON to clipboard, and a VB paste handler that imports the layout using Divi's own `copyModuleFromLibrary` API.

**Architecture:** Section Advanced tab gets a Live Copy toggle + demo JSON path field via `addFilter` hooks. Frontend JS shows a floating button on hover for enabled sections, fetches JSON via AJAX, copies to clipboard. VB paste handler detects `et_builder_layouts` JSON and dispatches `divi/edit-post.copyModuleFromLibrary()` which uses Divi's internal block parser — no custom parsing needed.

**Tech Stack:** TypeScript (VB), vanilla JS (frontend), PHP (REST endpoint + section extension), CSS

---

## File Structure

| Action | File | Responsibility |
|--------|------|----------------|
| Create | `includes/Extensions/live-copy/live-copy.php` | PHP: REST endpoint, frontend asset enqueue, section attribute registration |
| Create | `assets/js/dnxte-live-copy-settings.js` | VB: Add Live Copy settings to Section > Advanced via addFilter |
| Create | `assets/js/dnxte-frontend-live-copy.js` | Frontend: Hover button, AJAX fetch, clipboard write |
| Create | `assets/css/dnxte-frontend-live-copy.css` | Frontend: Live Copy button styling |
| Modify | `includes/Extensions/extensions.php` | Register the live-copy extension |
| Modify | `divi-5/visual-builder/src/extensions/cross-site-clipboard/clipboard-bridge.ts` | Fix paste handler to use copyModuleFromLibrary |
| Delete | `divi-5/visual-builder/src/extensions/cross-site-clipboard/layout-parser.ts` | No longer needed — Divi parses blocks internally |

---

### Task 1: Fix VB Paste Handler — Use copyModuleFromLibrary

**Files:**
- Modify: `divi-5/visual-builder/src/extensions/cross-site-clipboard/clipboard-bridge.ts`
- Delete: `divi-5/visual-builder/src/extensions/cross-site-clipboard/layout-parser.ts`

- [ ] **Step 1: Remove layout-parser import and add isLayoutExport inline**

In `clipboard-bridge.ts`, remove the import line:
```typescript
import { isLayoutExport, parseLayoutExport, extractLayoutMediaUrls } from './layout-parser';
```

Add this simple inline function after the `replaceMediaUrls` function:

```typescript
// ─── Layout Export Detection ────────────────────────────────────────

function isLayoutExport(text: string): boolean {
  if (!text || text.length < 30) return false;
  const trimmed = text.trimStart();
  if (!trimmed.startsWith('{')) return false;
  try {
    const data = JSON.parse(trimmed);
    return data?.context === 'et_builder_layouts' && typeof data?.data === 'object';
  } catch {
    return false;
  }
}

function extractLayoutMediaUrls(text: string): string[] {
  const urls = new Set<string>();
  const regex = /https?:\/\/[^\s"'<>\\]+?\/wp-content\/uploads\/[^\s"'<>\\]+/g;
  let match: RegExpExecArray | null;
  while ((match = regex.exec(text)) !== null) {
    urls.add(match[0]);
  }
  return Array.from(urls);
}
```

- [ ] **Step 2: Replace the entire paste handler section**

Replace everything from `// ─── Paste Handler` up to `// ─── Keyboard Listeners` with:

```typescript
// ─── Paste Handler ──────────────────────────────────────────────────

async function handlePaste(e: KeyboardEvent): Promise<void> {
  if (isPasting) return;

  const clipboard = getClipboard();
  if (!clipboard) return;

  let text: string;
  try {
    text = await clipboard.readText();
  } catch {
    return;
  }

  // Path 1: Our own cross-site format
  const pkg = deserialize(text);
  if (pkg && pkg.sourceUrl !== window.location.origin) {
    await executeCrossSitePaste(pkg);
    return;
  }

  // Path 2: et_builder_layouts JSON (Live Copy)
  if (isLayoutExport(text)) {
    await executeLayoutPaste(text);
    return;
  }
}

async function executeCrossSitePaste(pkg: CrossSiteClipboardPackage): Promise<void> {
  const confirmed = confirm(
    `Paste content from ${pkg.sourceUrl}?\n\n` +
      `${pkg.items.length} item(s)` +
      (pkg.mediaUrls.length > 0 ? `, ${pkg.mediaUrls.length} media file(s) will be imported` : '')
  );
  if (!confirmed) return;

  isPasting = true;
  try {
    let processedItems = pkg.items;

    if (pkg.mediaUrls.length > 0) {
      try {
        const mediaResult = await importMedia(pkg.mediaUrls);
        if (Object.keys(mediaResult.mapping).length > 0) {
          processedItems = replaceMediaUrls(pkg.items, mediaResult.mapping);
        }
      } catch (err) {
        console.warn('[CrossSiteClipboard] Media import failed:', err);
      }
    }

    const clipboardDispatch = dispatch('divi/clipboard');
    const eventsStore = select('divi/events');
    const hoveredModule = eventsStore?.getHoveredModule?.();

    if (!hoveredModule?.id) {
      alert('Please hover over a section, row, or module where you want to paste.');
      return;
    }

    const diviClipboard = (window as any)?.divi?.clipboard;
    if (typeof diviClipboard?.pasteModuleFromClipboard !== 'function') return;

    for (const item of processedItems) {
      clipboardDispatch?.addItem?.(item);
      diviClipboard.pasteModuleFromClipboard(hoveredModule.id);
    }
  } catch (err) {
    console.error('[CrossSiteClipboard] Paste failed:', err);
  } finally {
    isPasting = false;
  }
}

async function executeLayoutPaste(text: string): Promise<void> {
  let data: any;
  try {
    data = JSON.parse(text.trim());
  } catch {
    return;
  }

  const layouts = Object.values(data.data || {}) as any[];
  if (layouts.length === 0) return;

  const layoutTitles = layouts.map((l: any) => l.post_title || 'Untitled').join(', ');
  const confirmed = confirm(
    `Paste layout from Live Copy?\n\n${layouts.length} layout(s): ${layoutTitles}`
  );
  if (!confirmed) return;

  isPasting = true;
  try {
    // Import remote media
    const mediaUrls = extractLayoutMediaUrls(text);
    let mediaMapping: Record<string, string> = {};

    if (mediaUrls.length > 0) {
      try {
        const mediaResult = await importMedia(mediaUrls);
        mediaMapping = mediaResult.mapping || {};
      } catch (err) {
        console.warn('[CrossSiteClipboard] Media import failed:', err);
      }
    }

    // Get paste target
    const eventsStore = select('divi/events');
    const hoveredModule = eventsStore?.getHoveredModule?.();

    if (!hoveredModule?.id) {
      alert('Please hover over a section, row, or module where you want to paste.');
      return;
    }

    const editPostDispatch = dispatch('divi/edit-post');
    if (typeof editPostDispatch?.copyModuleFromLibrary !== 'function') {
      alert('Paste failed: Divi import API not available.');
      return;
    }

    // Paste each layout using Divi's own import mechanism
    for (const layout of layouts) {
      let content = layout.post_content || '';

      // Replace remote media URLs with imported local URLs
      for (const [oldUrl, newUrl] of Object.entries(mediaMapping)) {
        if (oldUrl !== newUrl) {
          content = content.split(oldUrl).join(newUrl);
        }
      }

      const payload = { ...layout, content };
      editPostDispatch.copyModuleFromLibrary(payload, 'after', hoveredModule.id);
    }
  } catch (err) {
    console.error('[CrossSiteClipboard] Layout paste failed:', err);
    alert('Paste failed: ' + (err as Error).message);
  } finally {
    isPasting = false;
  }
}
```

- [ ] **Step 3: Delete layout-parser.ts**

Remove `divi-5/visual-builder/src/extensions/cross-site-clipboard/layout-parser.ts`.

- [ ] **Step 4: Remove debug code and build**

Remove all `alert('[DEBUG]` lines and extra debug `console.log` lines from `clipboard-bridge.ts`. Then run:

```
npm run build:divi5
```

- [ ] **Step 5: Commit**

```
feat(cross-clipboard): use Divi copyModuleFromLibrary for layout paste

Replaces custom block parser with Divi's own import API.
Removes layout-parser.ts — Divi parses blocks internally.
```

---

### Task 2: Create Live Copy PHP Extension

**Files:**
- Create: `includes/Extensions/live-copy/live-copy.php`
- Modify: `includes/Extensions/extensions.php`

- [ ] **Step 1: Create the PHP extension class**

Create `includes/Extensions/live-copy/live-copy.php` with:
- REST endpoint: `POST /dnxte/v1/live-copy/get-json` — reads JSON from `demos/` directory by sanitized filename
- Section attribute registration via `block_type_metadata_settings` filter — adds `dnxte_live_copy` attribute with `enable` toggle and `jsonPath` field
- Frontend wrapper render filter — injects `data-dnxte-livecopy` attribute on enabled sections
- VB script registration via `PackageBuildManager`
- Frontend asset enqueue via `wp_enqueue_scripts`

Full code in the implementation — follow `RowSectionExtension.php` and `cross-site-clipboard.php` patterns.

- [ ] **Step 2: Register in extensions.php**

Add `'live-copy' => 'dnxte-live-copy-extension'` to the `$modules` array.

- [ ] **Step 3: Create demos directory**

Create `demos/` directory in the plugin root for JSON files.

- [ ] **Step 4: Commit**

```
feat(live-copy): add PHP extension with REST endpoint and section attributes
```

---

### Task 3: Create VB Settings Script (Section > Advanced)

**Files:**
- Create: `assets/js/dnxte-live-copy-settings.js`

- [ ] **Step 1: Create the settings script**

Vanilla JS IIFE that uses `window.vendor.wp.hooks.addFilter` to:
1. Add `dnxteLiveCopy` group to `divi.moduleLibrary.moduleSettings.groups` with `panel: 'advanced'`
2. Add `dnxte_live_copy` attribute to `divi.moduleLibrary.moduleAttributes` with toggle (`enable`) and text field (`jsonPath`) with `show_if` conditional

Target only `divi/section`. Follow `dnxte-row-section-extension.js` pattern.

- [ ] **Step 2: Commit**

```
feat(live-copy): add Section > Advanced settings for Live Copy
```

---

### Task 4: Create Frontend Live Copy Button (JS + CSS)

**Files:**
- Create: `assets/js/dnxte-frontend-live-copy.js`
- Create: `assets/css/dnxte-frontend-live-copy.css`

- [ ] **Step 1: Create frontend JS**

Vanilla JS IIFE that:
1. Reads `window.dnxteLiveCopy.restUrl` and `.nonce` (set by `wp_localize_script`)
2. Queries all `[data-dnxte-livecopy]` sections
3. Appends a floating button (copy icon + "Live Copy" text) to each
4. On click: POST to REST endpoint with demo ID, write response to `navigator.clipboard.writeText()`, show "Copied!" feedback

- [ ] **Step 2: Create frontend CSS**

Button positioned `absolute` top-right of section, hidden by default, shown on section hover with fade transition. States: default (purple), loading (dimmed), success (green), error (red).

- [ ] **Step 3: Commit**

```
feat(live-copy): add frontend Live Copy button with AJAX and clipboard
```

---

### Task 5: Build and Integration Test

- [ ] **Step 1: Build**

```
npm run build:divi5
```

- [ ] **Step 2: Test VB paste with Divi 5 JSON**

Copy `et_builder_layouts` JSON to clipboard, open VB, hover section, Cmd+V. Layout should paste.

- [ ] **Step 3: Test Live Copy button**

Place demo JSON in `demos/`, enable Live Copy on a section in VB, view frontend, hover, click Live Copy, verify clipboard contains JSON, paste in another site's VB.

- [ ] **Step 4: Test existing cross-site clipboard**

Copy module in VB with Cmd+C, paste on another site with Cmd+V. Verify DNXTE_CROSSCLIP_V1 format still works.
