Skip to content

Clipboard JSON Support

Enable clipboard functionality with support for JSON and advanced objects. Easily copy and paste complex data structures within your grid and between applications.

Source code
TypeScript ts
import { defineCustomElements } from '@revolist/revogrid/loader';
import { ClipboardJsonPlugin, ColumnStretchPlugin, RowOddPlugin } from '@revolist/revogrid-pro';
import { useRandomData, currentTheme } from '../composables/useRandomData';

const { createRandomData } = useRandomData();
const { isDark } = currentTheme();

defineCustomElements();

const CLIPBOARD_JSON_MARKER = '$rv-parse_';

type DemoRow = {
  id: number;
  name: string;
  price: number;
  bool: boolean;
  json: Record<string, any>;
  new: Record<string, any> | null;
  notes: string;
};

function formatJson(value: any) {
  if (value == null) {
    return '';
  }
  if (typeof value !== 'object') {
    return String(value);
  }
  return JSON.stringify(value, null, 2);
}

function formatPastedValue(value: any) {
  if (!value) {
    return 'Paste JSON here';
  }
  if (typeof value === 'object') {
    return [value.name, value.emoji].filter(Boolean).join(' ') || 'Object pasted';
  }
  return String(value);
}

function formatClipboardJson(value: any) {
  return `${CLIPBOARD_JSON_MARKER}${JSON.stringify(value)}`;
}

function getNextId(rows: DemoRow[]) {
  return rows.reduce((maxId, row) => Math.max(maxId, row.id), 0) + 1;
}

async function writeClipboardText(text: string) {
  try {
    await navigator.clipboard.writeText(text);
    return true;
  } catch (_error) {
    const textarea = document.createElement('textarea');
    textarea.value = text;
    textarea.setAttribute('readonly', 'true');
    textarea.style.position = 'fixed';
    textarea.style.left = '-9999px';
    document.body.appendChild(textarea);
    textarea.select();
    const copied = document.execCommand('copy');
    textarea.remove();
    return copied;
  }
}

function buildSeedRows() {
  return createRandomData(30, { json: true }).map((row: any, index) => ({
    ...row,
    id: index + 1,
    new: null,
    notes: 'Ready',
  })) as DemoRow[];
}

function buildComplexRow(nextId: number): DemoRow {
  return {
    id: nextId,
    name: 'Dragonfruit πŸŽ‰',
    price: 32.5,
    bool: true,
    json: {
      name: 'Dragonfruit',
      emoji: 'πŸŽ‰',
      meta: {
        batch: 'interactive',
        labels: ['rich', 'tropical', 'complex'],
        nutrition: {
          sugar: 23,
          vitaminC: true,
        },
      },
    },
    new: null,
    notes: 'Pasted JSON will appear here',
  };
}

const root = document.getElementById('demo-wrapper');
if (root) {
  const container = document.createElement('section');
  container.style.display = 'grid';
  container.style.gap = '10px';
  container.style.padding = '4px 0';
  container.style.fontFamily = 'Arial, sans-serif';

  const toolbar = document.createElement('div');
  toolbar.style.display = 'flex';
  toolbar.style.gap = '8px';
  toolbar.style.flexWrap = 'wrap';

  const summary = document.createElement('p');
  summary.style.margin = '0';
  summary.style.padding = '8px 10px';
  summary.style.border = '1px solid #d8e1ec';
  summary.style.borderRadius = '6px';
  summary.style.background = '#f4f7ff';
  summary.style.fontSize = '12px';
  summary.style.color = '#0f172a';

  const copyStatus = document.createElement('p');
  copyStatus.style.margin = '0';
  copyStatus.style.padding = '8px 10px';
  copyStatus.style.border = '1px solid #d8e1ec';
  copyStatus.style.borderRadius = '6px';
  copyStatus.style.background = '#f2fbf4';
  copyStatus.style.fontSize = '12px';
  copyStatus.style.color = '#064e3b';

  const pasteStatus = document.createElement('p');
  pasteStatus.style.margin = '0';
  pasteStatus.style.padding = '8px 10px';
  pasteStatus.style.border = '1px solid #d8e1ec';
  pasteStatus.style.borderRadius = '6px';
  pasteStatus.style.background = '#fff7ed';
  pasteStatus.style.fontSize = '12px';
  pasteStatus.style.color = '#7c2d12';

  const createButton = (label: string, onClick: () => void) => {
    const button = document.createElement('button');
    button.type = 'button';
    button.textContent = label;
    button.style.padding = '6px 12px';
    button.style.border = '1px solid #cbd5e1';
    button.style.borderRadius = '6px';
    button.style.background = '#fff';
    button.style.cursor = 'pointer';
    button.style.fontSize = '12px';
    button.style.fontFamily = 'inherit';
    button.addEventListener('click', onClick);
    return button;
  };

  const grid = document.createElement('revo-grid');
  grid.style.minHeight = '480px';
  grid.plugins = [ClipboardJsonPlugin, ColumnStretchPlugin, RowOddPlugin];
  grid.stretch = 'all';
  grid.theme = isDark() ? 'darkMaterial' : 'material';
  grid.hideAttribution = true;

  let sourceRows = buildSeedRows();

  const refreshGridSource = (nextRows: DemoRow[]) => {
    sourceRows = nextRows;
    grid.source = sourceRows;
    summary.textContent = `Rows: ${sourceRows.length} | Try copying JSON cells and pasting back into the 'Paste JSON here' column.`;
  };

  const syncCopyStatus = (message: string) => {
    copyStatus.textContent = `Copy: ${message}`;
  };

  const syncPasteStatus = (message: string) => {
    pasteStatus.textContent = `Paste: ${message}`;
  };

  const summarizeMatrix = (matrix: any[][]) => {
    if (!Array.isArray(matrix) || matrix.length === 0) {
      return 'No clipboard cells.';
    }
    return `${matrix.length} rows Γ— ${matrix[0]?.length ?? 0} columns`;
  };

  const copyRowJson = async (value: any, rowId: number) => {
    const copied = await writeClipboardText(formatClipboardJson(value));
    if (copied) {
      syncCopyStatus(`Copied row ${rowId}. Paste it into the target column to restore the object.`);
    } else {
      syncCopyStatus('Could not write to clipboard. Use keyboard copy in supported browsers.');
    }
  };

  const columns = [
    {
      name: 'ID',
      prop: 'id',
      size: 80,
    },
    {
      name: 'Source JSON (copy-ready)',
      prop: 'json',
      size: 360,
      cellParser: (model: any, column: any) => JSON.stringify(model[column.prop]),
      cellTemplate: (h: any, { value: jsonValue }: any) => {
        return h('pre', { class: 'bg-gray-200 border border-slate-300 rounded p-1 py-1 text-xs text-center inline-block' }, [
          h('code', { class: 'text-gray-900' }, formatJson(jsonValue)),
        ]);
      },
    },
    {
      name: 'Paste JSON here',
      prop: 'new',
      cellTemplate: (h: any, { value }: any) => {
        return h(
          'pre',
          { class: 'bg-slate-100 border border-slate-300 rounded p-1 py-1 mt-2 text-xs inline-block text-center' },
          [h('code', { class: 'text-slate-900' }, formatPastedValue(value))],
        );
      },
    },
    {
      name: 'State',
      prop: 'notes',
      size: 240,
    },
  ];

  grid.columns = columns;

  grid.addEventListener('beforecopyapply', (event: any) => {
    syncCopyStatus(summarizeMatrix(event.detail?.data));
  });

  grid.addEventListener('beforepasteapply', (event: any) => {
    syncPasteStatus(`Pasted ${summarizeMatrix(event.detail?.parsed)}.`);
  });

  toolbar.append(
    createButton('Refresh source', () => refreshGridSource(buildSeedRows())),
    createButton('Add complex row', () =>
      refreshGridSource([
        buildComplexRow(getNextId(sourceRows)),
        ...sourceRows,
      ]),
    ),
    createButton('Copy first JSON', () => {
      const firstRow = sourceRows[0];
      if (firstRow) {
        void copyRowJson(firstRow.json, firstRow.id);
      }
    }),
    createButton('Clear pasted values', () =>
      refreshGridSource(
        sourceRows.map((row) => ({
          ...row,
          new: null,
          notes: 'Paste cell emptied',
        })),
      ),
    ),
  );

  container.append(toolbar, summary, copyStatus, pasteStatus, grid);
  root.appendChild(container);

  refreshGridSource(sourceRows);
  syncCopyStatus('Copy a JSON cell with Ctrl/Cmd+C or use "Copy first JSON".');
  syncPasteStatus('Paste into the target column to restore objects.');
}
Vue vue
<script setup lang="ts">
import { computed, ref } from 'vue';
import { VGrid } from '@revolist/vue3-datagrid';
import { ClipboardJsonPlugin, ColumnStretchPlugin, RowOddPlugin } from '@revolist/revogrid-pro';
import { useRandomData, currentThemeVue } from '../composables/useRandomData';

const { createRandomData } = useRandomData();
const { isDark } = currentThemeVue();

type DemoRow = {
  id: number;
  name: string;
  price: number;
  bool: boolean;
  json: Record<string, any>;
  new: Record<string, any> | null;
  notes: string;
};

const theme = computed(() => isDark.value ? 'darkMaterial' : 'material');
const copyStatus = ref('Copy: Use Ctrl/Cmd+C on JSON cells to inspect copy payload size.');
const pasteStatus = ref('Paste: Paste into "Paste JSON here" cells to see parsed objects.');
const clipboardJsonMarker = '$rv-parse_';

const formatJson = (value: any) => {
  if (value == null) {
    return '';
  }
  if (typeof value !== 'object') {
    return String(value);
  }
  return JSON.stringify(value, null, 2);
};

const formatPastedValue = (value: any) => {
  if (!value) {
    return 'Paste JSON here';
  }
  if (typeof value === 'object') {
    return [value.name, value.emoji].filter(Boolean).join(' ') || 'Object pasted';
  }
  return String(value);
};

const formatClipboardJson = (value: any) => {
  return `${clipboardJsonMarker}${JSON.stringify(value)}`;
};

const writeClipboardText = async (text: string) => {
  try {
    await navigator.clipboard.writeText(text);
    return true;
  } catch (_error) {
    const textarea = document.createElement('textarea');
    textarea.value = text;
    textarea.setAttribute('readonly', 'true');
    textarea.style.position = 'fixed';
    textarea.style.left = '-9999px';
    document.body.appendChild(textarea);
    textarea.select();
    const copied = document.execCommand('copy');
    textarea.remove();
    return copied;
  }
};

const seedRows = () => {
  return createRandomData(30, { json: true }).map((row: any, index: number) => ({
    ...row,
    id: index + 1,
    new: null,
    notes: 'Ready',
  })) as DemoRow[];
};

const buildComplexRow = (nextId: number): DemoRow => ({
  id: nextId,
  name: 'Dragonfruit πŸŽ‰',
  price: 32.5,
  bool: true,
  json: {
    name: 'Dragonfruit',
    emoji: 'πŸŽ‰',
    meta: {
      batch: 'interactive',
      labels: ['rich', 'tropical', 'complex'],
      nutrition: {
        sugar: 23,
        vitaminC: true,
      },
    },
  },
  new: null,
  notes: 'Pasted JSON will appear here',
});

const rows = ref<DemoRow[]>(seedRows());

const summarizeMatrix = (matrix: any[][]) => {
  if (!Array.isArray(matrix) || matrix.length === 0) {
    return 'No clipboard cells.';
  }
  return `${matrix.length} rows Γ— ${matrix[0]?.length ?? 0} columns`;
};

const copyToClipboard = async (value: any, rowId: number) => {
  const copied = await writeClipboardText(formatClipboardJson(value));
  if (copied) {
    copyStatus.value = `Copied row ${rowId}. Paste it into the target column to restore the object.`;
  } else {
    copyStatus.value = 'Could not write to clipboard in this environment.';
  }
};

const copyJsonColumn = {
  name: 'Source JSON (copy-ready)',
  prop: 'json',
  size: 360,
  cellParser: (model: any, column: any) => JSON.stringify(model[column.prop]),
  cellTemplate: (h: any, { value: jsonValue }: any) => {
    return h('pre', { class: 'bg-gray-200 border border-slate-300 rounded p-1 py-1 mt-2 text-xs inline-block text-center' }, [
      h('code', { class: 'text-gray-900' }, formatJson(jsonValue)),
    ]);
  },
};

const columns = [
  {
    name: 'ID',
    prop: 'id',
    size: 80,
  },
  copyJsonColumn,
  {
    name: 'Paste JSON here',
    prop: 'new',
    cellTemplate: (h: any, { value }: any) =>
      h('pre', { class: 'bg-slate-100 border border-slate-300 rounded p-1 py-1 mt-2 text-xs inline-block text-center' }, [
        h('code', { class: 'text-slate-900' }, formatPastedValue(value)),
      ]),
  },
  {
    name: 'State',
    prop: 'notes',
    size: 240,
  },
];

const onCopy = (event: CustomEvent<any>) => {
  copyStatus.value = `Copy: ${summarizeMatrix(event.detail?.data)}`;
};

const onPaste = (event: CustomEvent<any>) => {
  pasteStatus.value = `Paste: ${summarizeMatrix(event.detail?.parsed)}.`;
};

const refreshData = () => {
  rows.value = seedRows();
};

const addComplexRow = () => {
  const nextId = rows.value.reduce((maxId, row) => Math.max(maxId, row.id), 0) + 1;
  rows.value = [buildComplexRow(nextId), ...rows.value];
};

const copyFirstJson = () => {
  const firstRow = rows.value[0];
  if (firstRow) {
    void copyToClipboard(firstRow.json, firstRow.id);
  }
};

const clearPasted = () => {
  rows.value = rows.value.map((row) => ({
    ...row,
    new: null,
    notes: 'Paste cell emptied',
  }));
};

const infoStyle = 'margin: 0; padding: 8px 10px; border: 1px solid #d8e1ec; border-radius: 6px; background: #f4f7ff; font-size: 12px; color: #0f172a;';
const plugins = [ClipboardJsonPlugin, ColumnStretchPlugin, RowOddPlugin];
const buttonStyle =
  'padding: 6px 12px; border: 1px solid #cbd5e1; border-radius: 6px; background: #fff; cursor: pointer; font-size: 12px; font-family: inherit;';
</script>

<template>
  <section style="display: grid; gap: 10px; padding: 4px 0;">
    <div style="display: flex; gap: 8px; flex-wrap: wrap;">
      <button type="button" :style="buttonStyle" @click="refreshData">Refresh source</button>
      <button type="button" :style="buttonStyle" @click="addComplexRow">Add complex row</button>
      <button type="button" :style="buttonStyle" @click="copyFirstJson">Copy first JSON</button>
      <button type="button" :style="buttonStyle" @click="clearPasted">Clear pasted values</button>
    </div>
    <p :style="infoStyle">Rows: {{ rows.length }} | Try copying JSON cells and pasting back into the "Paste JSON here" column.</p>
    <p style="margin: 0; padding: 8px 10px; border: 1px solid #d8e1ec; border-radius: 6px; background: #f2fbf4; font-size: 12px; color: #064e3b;">
      {{ copyStatus }}
    </p>
    <p style="margin: 0; padding: 8px 10px; border: 1px solid #d8e1ec; border-radius: 6px; background: #fff7ed; font-size: 12px; color: #7c2d12;">
      {{ pasteStatus }}
    </p>
    <v-grid
      :theme="theme"
      :source="rows"
      :columns="columns"
      :plugins="plugins"
      stretch="all"
      :hideAttribution="true"
      @beforecopyapply="onCopy"
      @beforepasteapply="onPaste"
      style="min-height: 440px;"
    />
  </section>
</template>
React tsx
import React, { useCallback, useMemo, useState } from 'react';
import { RevoGrid } from '@revolist/react-datagrid';
import { ClipboardJsonPlugin, ColumnStretchPlugin, RowOddPlugin } from '@revolist/revogrid-pro';
import { useRandomData, currentTheme } from '../composables/useRandomData';

const ClipboardJsonDemo: React.FC = () => {
  const { createRandomData } = useRandomData();
  const theme = currentTheme().isDark() ? 'darkMaterial' : 'material';
  const [copyStatus, setCopyStatus] = useState('Copy: Use Ctrl/Cmd+C on JSON cells to inspect copy payload size.');
  const [pasteStatus, setPasteStatus] = useState('Paste: Paste into "Paste JSON here" cells to see parsed objects.');
  const clipboardJsonMarker = '$rv-parse_';

  type DemoRow = {
    id: number;
    name: string;
    price: number;
    bool: boolean;
    json: Record<string, any>;
    new: Record<string, any> | null;
    notes: string;
  };

  const formatJson = useCallback((value: any) => {
    if (value == null) {
      return '';
    }
    if (typeof value !== 'object') {
      return String(value);
    }
    return JSON.stringify(value, null, 2);
  }, []);

  const formatPastedValue = useCallback((value: any) => {
    if (!value) {
      return 'Paste JSON here';
    }
    if (typeof value === 'object') {
      return [value.name, value.emoji].filter(Boolean).join(' ') || 'Object pasted';
    }
    return String(value);
  }, []);

  const formatClipboardJson = useCallback((value: any) => {
    return `${clipboardJsonMarker}${JSON.stringify(value)}`;
  }, [clipboardJsonMarker]);

  const writeClipboardText = useCallback(async (text: string) => {
    try {
      await navigator.clipboard.writeText(text);
      return true;
    } catch (_error) {
      const textarea = document.createElement('textarea');
      textarea.value = text;
      textarea.setAttribute('readonly', 'true');
      textarea.style.position = 'fixed';
      textarea.style.left = '-9999px';
      document.body.appendChild(textarea);
      textarea.select();
      const copied = document.execCommand('copy');
      textarea.remove();
      return copied;
    }
  }, []);

  const seedRows = useCallback(() => {
    return createRandomData(30, { json: true }).map((row: any, index: number) => ({
      ...row,
      id: index + 1,
      new: null,
      notes: 'Ready',
    })) as DemoRow[];
  }, [createRandomData]);

  const buildComplexRow = useCallback((nextId: number): DemoRow => ({
    id: nextId,
    name: 'Dragonfruit πŸŽ‰',
    price: 32.5,
    bool: true,
    json: {
      name: 'Dragonfruit',
      emoji: 'πŸŽ‰',
      meta: {
        batch: 'interactive',
        labels: ['rich', 'tropical', 'complex'],
        nutrition: {
          sugar: 23,
          vitaminC: true,
        },
      },
    },
    new: null,
    notes: 'Pasted JSON will appear here',
  }), []);

  const [source, setSource] = useState<DemoRow[]>(() => seedRows());
  const plugins = useMemo(() => [ClipboardJsonPlugin, ColumnStretchPlugin, RowOddPlugin], []);

  const copyToClipboard = useCallback(async (value: any, rowId: number) => {
    const copied = await writeClipboardText(formatClipboardJson(value));
    if (copied) {
      setCopyStatus(`Copied row ${rowId}. Paste it into the target column to restore the object.`);
    } else {
      setCopyStatus('Could not write to clipboard in this environment.');
    }
  }, [formatClipboardJson, writeClipboardText]);

  const summarizeMatrix = useCallback((matrix: any[][]) => {
    if (!Array.isArray(matrix) || matrix.length === 0) {
      return 'No clipboard cells.';
    }
    return `${matrix.length} rows Γ— ${matrix[0]?.length ?? 0} columns`;
  }, []);

  const handleCopy = useCallback((event: any) => {
    setCopyStatus(`Copy: ${summarizeMatrix(event.detail?.data)}`);
  }, [summarizeMatrix]);

  const handlePaste = useCallback((event: any) => {
    setPasteStatus(`Paste: ${summarizeMatrix(event.detail?.parsed)}.`);
  }, [summarizeMatrix]);

  const columns = useMemo(() => [
    {
      name: 'ID',
      prop: 'id',
      size: 80,
    },
    {
      name: 'Source JSON (copy-ready)',
      prop: 'json',
      size: 360,
      cellParser: (model: any, column: any) => JSON.stringify(model[column.prop]),
      cellTemplate: (h: any, { value: jsonValue }: any) => {
        return h(
          'pre',
          { style: { margin: 0 } },
          [h('code', { className: 'text-xs' }, formatJson(jsonValue))],
        );
      },
    },
    {
      name: 'Paste JSON here',
      prop: 'new',
      cellTemplate: (h: any, { value }: any) => {
        return h(
          'pre',
          { style: { margin: 0 } },
          [h('code', { className: 'text-xs' }, formatPastedValue(value))],
        );
      },
    },
    {
      name: 'State',
      prop: 'notes',
      size: 240,
    },
  ], [formatJson, formatPastedValue]);

  const refreshData = useCallback(() => {
    setSource(seedRows());
  }, [seedRows]);

  const addComplexRow = useCallback(() => {
    setSource((current) => {
      const nextId = current.reduce((maxId, row) => Math.max(maxId, row.id), 0) + 1;
      return [buildComplexRow(nextId), ...current];
    });
  }, [buildComplexRow]);

  const copyFirstJson = useCallback(() => {
    const firstRow = source[0];
    if (firstRow) {
      void copyToClipboard(firstRow.json, firstRow.id);
    }
  }, [copyToClipboard, source]);

  const clearPasted = useCallback(() => {
    setSource((current) =>
      current.map((row) => ({
        ...row,
        new: null,
        notes: 'Paste cell emptied',
      })),
    );
  }, []);

  const buttonStyles = {
    padding: '6px 12px',
    border: '1px solid #cbd5e1',
    borderRadius: '6px',
    background: '#fff',
    cursor: 'pointer',
    fontSize: '12px',
  } as React.CSSProperties;

  const infoStyle = {
    margin: 0,
    padding: '8px 10px',
    border: '1px solid #d8e1ec',
    borderRadius: '6px',
    background: '#f4f7ff',
    fontSize: '12px',
    color: '#0f172a',
  } as React.CSSProperties;

  return (
    <div style={{ display: 'grid', gap: '10px', padding: '4px 0' }}>
      <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
        <button style={buttonStyles} type="button" onClick={refreshData}>Refresh source</button>
        <button style={buttonStyles} type="button" onClick={addComplexRow}>Add complex row</button>
        <button style={buttonStyles} type="button" onClick={copyFirstJson}>Copy first JSON</button>
        <button style={buttonStyles} type="button" onClick={clearPasted}>Clear pasted values</button>
      </div>
      <p style={infoStyle}>Rows: {source.length} | Try copying JSON cells and pasting back into the "Paste JSON here" column.</p>
      <p style={{ ...infoStyle, background: '#f2fbf4', color: '#064e3b' }}> {copyStatus}</p>
      <p style={{ ...infoStyle, background: '#fff7ed', color: '#7c2d12' }}>{pasteStatus}</p>
      <RevoGrid
        theme={theme}
        source={source}
        columns={columns}
        plugins={plugins}
        stretch="all"
        hideAttribution
        onBeforecopyapply={handleCopy}
        onBeforepasteapply={handlePaste}
        style={{ minHeight: '440px' }}
      />
    </div>
  );
};

export default ClipboardJsonDemo;
Angular ts
import { Component } from '@angular/core';
import { RevoGrid } from '@revolist/angular-datagrid';
import { ClipboardJsonPlugin, ColumnStretchPlugin, RowOddPlugin } from '@revolist/revogrid-pro';
import { useRandomData, currentTheme } from '../composables/useRandomData';

type DemoRow = {
  id: number;
  name: string;
  price: number;
  bool: boolean;
  json: Record<string, any>;
  new: Record<string, any> | null;
  notes: string;
};

@Component({
  selector: 'clipboard-json-grid',
  standalone: true,
  imports: [RevoGrid],
  template: `
    <section style="display: grid; gap: 10px; padding: 4px 0;">
      <div style="display: flex; gap: 8px; flex-wrap: wrap;">
        <button type="button" (click)="refreshData()" style="padding: 6px 12px; border: 1px solid #cbd5e1; border-radius: 6px; background: #fff; cursor: pointer; font-size: 12px; font-family: inherit;">Refresh source</button>
        <button type="button" (click)="addComplexRow()" style="padding: 6px 12px; border: 1px solid #cbd5e1; border-radius: 6px; background: #fff; cursor: pointer; font-size: 12px; font-family: inherit;">Add complex row</button>
        <button type="button" (click)="copyFirstJson()" style="padding: 6px 12px; border: 1px solid #cbd5e1; border-radius: 6px; background: #fff; cursor: pointer; font-size: 12px; font-family: inherit;">Copy first JSON</button>
        <button type="button" (click)="clearPasted()" style="padding: 6px 12px; border: 1px solid #cbd5e1; border-radius: 6px; background: #fff; cursor: pointer; font-size: 12px; font-family: inherit;">Clear pasted values</button>
      </div>
      <p style="margin: 0; padding: 8px 10px; border: 1px solid #d8e1ec; border-radius: 6px; background: #f4f7ff; font-size: 12px; color: #0f172a;">
        Rows: {{ source.length }} | Try copying JSON cells and pasting back into the 'Paste JSON here' column.
      </p>
      <p style="margin: 0; padding: 8px 10px; border: 1px solid #d8e1ec; border-radius: 6px; background: #f2fbf4; font-size: 12px; color: #064e3b;">
        {{ copyStatus }}
      </p>
      <p style="margin: 0; padding: 8px 10px; border: 1px solid #d8e1ec; border-radius: 6px; background: #fff7ed; font-size: 12px; color: #7c2d12;">
        {{ pasteStatus }}
      </p>
      <revo-grid
        [theme]="theme"
        [source]="source"
        [columns]="columns"
        [plugins]="plugins"
        stretch="all"
        [hideAttribution]="true"
        (beforecopyapply)="onCopy($event)"
        (beforepasteapply)="onPaste($event)"
        style="min-height: 440px;"
      ></revo-grid>
    </section>
  `,
})
export class ClipboardJsonGridComponent {
  theme = currentTheme().isDark() ? 'darkMaterial' : 'material';
  copyStatus = 'Copy: Use Ctrl/Cmd+C on JSON cells to inspect copy payload size.';
  pasteStatus = 'Paste: Paste into "Paste JSON here" cells to see parsed objects.';
  source: DemoRow[] = this.createSeedRows();
  private clipboardJsonMarker = '$rv-parse_';

  plugins = [ClipboardJsonPlugin, ColumnStretchPlugin, RowOddPlugin];

  columns = [
    {
      name: 'ID',
      prop: 'id',
      size: 80,
    },
    {
      name: 'Source JSON (copy-ready)',
      prop: 'json',
      size: 360,
      cellParser: (model: any, column: any) => JSON.stringify(model[column.prop]),
      cellTemplate: (h: any, { value: jsonValue }: any) => {
        return h('pre', { class: 'bg-gray-200 border border-slate-300 rounded p-1 py-1 mt-2 text-xs inline-block text-center' }, [
          h('code', { class: 'text-gray-900' }, this.formatJson(jsonValue)),
        ]);
      },
    },
    {
      name: 'Paste JSON here',
      prop: 'new',
      cellTemplate: (h: any, data: any) => {
        return h('pre', { class: 'bg-slate-100 border border-slate-300 rounded p-1 py-1 mt-2 text-xs inline-block text-center' }, [
          h('code', { class: 'text-slate-900' }, this.formatPastedValue(data.value)),
        ]);
      },
    },
    {
      name: 'State',
      prop: 'notes',
      size: 240,
    },
  ];

  private createSeedRows() {
    return useRandomData().createRandomData(30, { json: true }).map((row: any, index: number) => ({
      ...row,
      id: index + 1,
      new: null,
      notes: 'Ready',
    })) as DemoRow[];
  }

  private createComplexRow(nextId: number): DemoRow {
    return {
      id: nextId,
      name: 'Dragonfruit πŸŽ‰',
      price: 32.5,
      bool: true,
      json: {
        name: 'Dragonfruit',
        emoji: 'πŸŽ‰',
        meta: {
          batch: 'interactive',
          labels: ['rich', 'tropical', 'complex'],
          nutrition: {
            sugar: 23,
            vitaminC: true,
          },
        },
      },
      new: null,
      notes: 'Pasted JSON will appear here',
    };
  }

  private formatJson(value: any) {
    if (value == null) {
      return '';
    }
    if (typeof value !== 'object') {
      return String(value);
    }
    return JSON.stringify(value, null, 2);
  }

  private formatPastedValue(value: any) {
    if (!value) {
      return 'Paste JSON here';
    }
    if (typeof value === 'object') {
      return [value.name, value.emoji].filter(Boolean).join(' ') || 'Object pasted';
    }
    return String(value);
  }

  private formatClipboardJson(value: any) {
    return `${this.clipboardJsonMarker}${JSON.stringify(value)}`;
  }

  private async writeClipboardText(text: string) {
    try {
      await navigator.clipboard.writeText(text);
      return true;
    } catch (_error) {
      const textarea = document.createElement('textarea');
      textarea.value = text;
      textarea.setAttribute('readonly', 'true');
      textarea.style.position = 'fixed';
      textarea.style.left = '-9999px';
      document.body.appendChild(textarea);
      textarea.select();
      const copied = document.execCommand('copy');
      textarea.remove();
      return copied;
    }
  }

  private summarizeMatrix(matrix: any[][]) {
    if (!Array.isArray(matrix) || matrix.length === 0) {
      return 'No clipboard cells.';
    }
    return `${matrix.length} rows Γ— ${matrix[0]?.length ?? 0} columns`;
  }

  onCopy(event: CustomEvent<any>) {
    this.copyStatus = `Copy: ${this.summarizeMatrix(event.detail?.data)}`;
  }

  onPaste(event: CustomEvent<any>) {
    this.pasteStatus = `Paste: ${this.summarizeMatrix(event.detail?.parsed)}.`;
  }

  async copyRowJson(value: any, rowId: number) {
    const copied = await this.writeClipboardText(this.formatClipboardJson(value));
    if (copied) {
      this.copyStatus = `Copied row ${rowId}. Paste it into the target column to restore the object.`;
    } else {
      this.copyStatus = 'Could not write to clipboard in this environment.';
    }
  }

  refreshData() {
    this.source = this.createSeedRows();
  }

  addComplexRow() {
    const nextId = this.source.reduce((maxId, row) => Math.max(maxId, row.id), 0) + 1;
    this.source = [this.createComplexRow(nextId), ...this.source];
  }

  copyFirstJson() {
    const firstRow = this.source[0];
    if (firstRow) {
      void this.copyRowJson(firstRow.json, firstRow.id);
    }
  }

  clearPasted() {
    this.source = this.source.map((row) => ({
      ...row,
      new: null,
      notes: 'Paste cell emptied',
    }));
  }
}