Copy content to other page

is there any way to copy text from one page to another. I don’t want to show same content on other pages, but copy content for further editing.
For example: I have a multi language page and added a new block in one language. Now I want to copy this block to other page and translate the content.

Not currently, but this is something that’s high on the list for ContentBlocks 2. It’s not in the current dev build but we have a separate functional proof of concept for it that may end up being part of 2.0 or a separate module.

Hi @ivmedia, unfortunately not yet, but I’m confident in the upcoming 2.0 version.

Up until then, there is this solution by gelstudios:

We’ve made a Dashboard tool, Simple Copy, for this very purpose. Here is the basic chunk and connector.php that operate it. It allows selection of 2 resources and then a drag/drop UI to clone Content Blocks between them. It’s certainly not a shiny finished product, but a great utility style tool that gets the job done.

Chunk:

<style>
  #simple_copy {
    min-height:400px;  
      
    *{
        box-sizing: border-box;
    }
    
    .layout-block-group {
      margin-bottom: 30px;
    }
    .layout-block-group h4 {
      margin: 20px 0 10px;
    }
    .region-blocks {
      margin-bottom: 10px;
    }
    .region-label {
      font-size: 13px;
      font-weight: bold;
      margin-bottom: 5px;
    }
    #save-btn {
      background: #447996;
      color: #fff;
      border: none;
      padding: 5px 10px;
      border-radius: 4px;
      cursor: pointer;
      margin: 0 0 0 auto;
      font-size: 12px;
    }
    #save-btn:hover {
      background: #2c5d78;
    }
    label {
      display: block;
    }
    .autocomplete-wrapper {
      position: relative;
    }
    .autocomplete-list {
      position: absolute;
      top: 100%;
      left: 0;
      right: 0;
      background: #fff;
      border: 1px solid #ccc;
      z-index: 99;
      max-height: 200px;
      overflow-y: auto;
    }
    .autocomplete-list div {
      padding: 5px 10px;
      cursor: pointer;
    }
    .autocomplete-list div:hover {
      background: #eee;
    }
    input.resource-input {
      padding: 8px;
      border: 1px solid #f1f2f3;
      font-size: 12px;
      width: 100%;
    }
    .block-list {
      min-height: 60px;
      border: 1px solid #ccc;
      padding: 10px;
      border-radius: 5px;
      background: #f9f9f9;
    }
    .block-item {
      padding: 8px 10px;
      background: #f0f0f0;
      margin-bottom: 6px;
      border-radius: 4px;
      cursor: grab;
      display: flex;
      align-items: center;
    }
    .drag-handle {
      font-family: monospace;
      font-size: 16px;
      margin-right: 8px;
      cursor: grab;
      color: #888;
      user-select: none;
    }
    .block-disabled {
      background: #ddd !important;
      opacity: 0.6;
      cursor: not-allowed;
    }
    .block-disabled .drag-handle {
      opacity: 0.3;
      cursor: not-allowed;
    }
    .resource-meta {
      font-size: 1rem;
      font-weight: 700;
      color: #666;
      margin: 0.5rem 0;
    }
    #toast {
      position: fixed;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      background: #333;
      color: #fff;
      padding: 10px 20px;
      border-radius: 6px;
      display: none;
    }
  }
</style>

<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>

<div id="simple_copy">
  <div style="display: flex; gap: 40px;">
    <div style="flex: 1;">
      <label>Origin Resource</label>
      <div class="autocomplete-wrapper">
        <input class="resource-input" id="origin-id" placeholder="Type to search origin resource" />
        <div id="origin-suggestions" class="autocomplete-list"></div>
      </div>
      <div class="resource-meta" id="origin-meta"></div>
      <div id="origin-block-lists"></div>
    </div>
    <div style="flex: 1;">
      <label>Target Resource</label>
      <div class="autocomplete-wrapper">
        <input class="resource-input" id="target-id" placeholder="Type to search target resource" />
        <div id="target-suggestions" class="autocomplete-list"></div>
      </div>
      <div class="resource-meta" id="target-meta"></div>
      <div id="target-block-lists"></div>
    </div>
  </div>
  <button id="save-btn" style="margin-top: 20px;">Save</button>
  <div id="toast"></div>
</div>

<script>
let resourceList = [];

function toast(msg) {
  const el = document.getElementById('toast');
  el.innerText = msg;
  el.style.display = 'block';
  setTimeout(() => el.style.display = 'none', 2000);
}

function fetchTitle(id, metaId) {
  fetch(`/assets/components/simplecopy/connector.php?action=resource_title&id=${id}`)
    .then(res => res.json())
    .then(data => {
      const title = data.title || '';
      const link = `/manager/?a=resource/update&id=${id}`;
      document.getElementById(metaId).innerHTML = `${title} <a href="${link}" target="_blank" style="font-size: 0.9rem; margin-left: 8px;"><i class="fa-sharp-duotone fa-regular fa-link"></i></a>`;
    });
}

function createBlockItem(block, targetId = null, layoutId = null) {
  const div = document.createElement('div');
  div.className = 'block-item';
  div.dataset.block = JSON.stringify(block);
  const title = block.field_name || 'Unknown';
  let preview = '';
  if (block.value) {
    preview = block.value.replace(/(<([^>]+)>)/gi, '').substring(0, 60);
  }
  div.innerHTML = `<span class="drag-handle">⠿</span><span><strong>${title}</strong>${preview ? ` — ${preview}` : ''}</span>`;
  const layoutOK = block.allowed_layouts.length === 0 || block.allowed_layouts.includes(layoutId);
  const resourceOK = block.allowed_resources.length === 0 || block.allowed_resources.includes(targetId);
  if (!layoutOK || !resourceOK) {
    div.classList.add('block-disabled');
    div.setAttribute('title', 'Not allowed in this layout/resource');
  }
  return div;
}

function renderBlockLists(layouts, containerId, prefix, pullClone = false, targetId = null) {
  const container = document.getElementById(containerId);
  container.innerHTML = '';
  Object.entries(layouts).forEach(([layoutId, layout]) => {
    const layoutWrap = document.createElement('div');
    layoutWrap.className = 'layout-block-group';
    layoutWrap.innerHTML = `<h4>${layout.title || 'Layout ' + layoutId}</h4>`;
    const regions = layout.regions || {};
    Object.entries(regions).forEach(([region, blocks]) => {
      const regionWrap = document.createElement('div');
      regionWrap.className = 'region-blocks';
      regionWrap.innerHTML = `<div class="region-label">${region}</div><div class="block-list" id="${prefix}-layout-${layoutId}-${region}"></div>`;
      const blockList = regionWrap.querySelector('.block-list');
      blocks.forEach(block => {
        const el = createBlockItem(block, targetId, parseInt(layoutId));
        blockList.appendChild(el);
      });
      new Sortable(blockList, {
        group: {
          name: 'blocks',
          pull: pullClone ? 'clone' : false,
          put: !pullClone
        },
        animation: 150
      });
      layoutWrap.appendChild(regionWrap);
    });
    container.appendChild(layoutWrap);
  });
}

function saveTargetOrder() {
  const targetId = parseInt(document.getElementById('target-id').dataset.selected);
  if (!targetId) return;
  const layoutData = {};
  document.querySelectorAll('[id^="target-layout-"]').forEach(el => {
    const [, , layoutId, region] = el.id.split('-');
    const layoutKey = layoutId;
    layoutData[layoutKey] = layoutData[layoutKey] || { title: '', regions: {} };
    const h4 = el.closest('.layout-block-group')?.querySelector('h4');
    if (h4 && !layoutData[layoutKey].title) {
      layoutData[layoutKey].title = h4.textContent.trim();
    }
    layoutData[layoutKey].regions[region] = Array.from(el.children).map(c => JSON.parse(c.dataset.block));
  });
  fetch('/assets/components/simplecopy/connector.php', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      action: 'save',
      target: targetId,
      layouts: JSON.stringify(layoutData)
    })
  })
  .then(res => res.json())
  .then(resp => {
    toast(resp.success ? 'Saved!' : 'Error saving');
  });
}

function initAutocomplete(inputId, listId, metaId, blockListId, prefix, isOrigin) {
  const input = document.getElementById(inputId);
  const list = document.getElementById(listId);

  input.addEventListener('input', () => {
    const val = input.value.toLowerCase();
    list.innerHTML = '';
    if (!val) return;
    const matches = resourceList.filter(r => r.pagetitle.toLowerCase().includes(val) || r.id.toString().includes(val));
    matches.forEach(match => {
      const div = document.createElement('div');
      div.textContent = `${match.pagetitle} (${match.id})`;
      div.dataset.id = match.id;
      list.appendChild(div);
    });
  });

  list.addEventListener('click', e => {
    if (e.target && e.target.dataset.id) {
      const id = e.target.dataset.id;
      input.value = `${e.target.textContent}`;
      input.dataset.selected = id;
      list.innerHTML = '';
      fetchTitle(id, metaId);
      fetch(`/assets/components/simplecopy/connector.php?action=blocks&id=${id}`)
        .then(res => res.json())
        .then(data => {
          if (!data.layouts) return toast('No layouts found');
          renderBlockLists(data.layouts, blockListId, prefix, isOrigin, parseInt(id));
        });
    }
  });
}

function loadResourcesAndInitAutocomplete() {
  fetch('/assets/components/simplecopy/connector.php?action=resources')
    .then(res => res.json())
    .then(data => {
      resourceList = data.resources;
      initAutocomplete('origin-id', 'origin-suggestions', 'origin-meta', 'origin-block-lists', 'origin', true);
      initAutocomplete('target-id', 'target-suggestions', 'target-meta', 'target-block-lists', 'target', false);
    });
}

document.addEventListener('DOMContentLoaded', () => {
  document.getElementById('save-btn').addEventListener('click', saveTargetOrder);
  loadResourcesAndInitAutocomplete();
});
</script>

assets/components/simplecopy/connector.php:

<?php
header('Content-Type: application/json');

require_once dirname(dirname(dirname(dirname(__FILE__)))) . '/config.core.php';
require_once MODX_CORE_PATH . 'config/' . MODX_CONFIG_KEY . '.inc.php';
require_once MODX_CONNECTORS_PATH . 'index.php';

if (!$modx) {
    http_response_code(500);
    echo json_encode(['error' => 'MODX not initialized']);
    exit;
}

$modx->addPackage(
    'contentblocks',
    MODX_CORE_PATH . 'components/contentblocks/model/',
    null,
    'ContentBlocks\\Model\\',
    [
        'packagePath' => MODX_CORE_PATH . 'components/contentblocks/model/',
        'useTablePrefix' => true,
    ]
);

$action = $_REQUEST['action'] ?? '';

switch ($action) {
    case 'resource_title':
        $id = (int)($_GET['id'] ?? 0);
        $res = $modx->getObject('modResource', $id);
        echo json_encode(['title' => $res ? $res->get('pagetitle') : '']);
        break;

    case 'blocks':
        $id = (int)($_GET['id'] ?? 0);
        $res = $modx->getObject('modResource', $id);
        if (!$res) {
            echo json_encode(['error' => 'Resource not found']);
            exit;
        }

        $props = $res->get('properties');
        $layoutsRaw = json_decode($props['contentblocks']['content'] ?? '', true);
        if (!is_array($layoutsRaw)) $layoutsRaw = [];

        $fieldConfigs = [];
        $fields = $modx->getIterator('cbField');
        foreach ($fields as $field) {
            $fid = $field->get('id');
            $layoutRestrictions = array_filter(array_map('intval', explode(',', $field->get('layouts'))));

            $resourceRestrictions = [];
            $availability = $field->get('availability');
            if ($availability) {
                $rules = json_decode($availability, true);
                foreach ($rules as $rule) {
                    if ($rule['field'] === 'resource') {
                        $resourceRestrictions = array_filter(array_map('intval', explode(',', $rule['value'] ?? '')));
                    }
                }
            }

            $fieldConfigs[$fid] = [
                'name' => $field->get('name'),
                'allowed_layouts' => $layoutRestrictions,
                'allowed_resources' => $resourceRestrictions,
            ];
        }

        $blocksByLayout = [];
        foreach ($layoutsRaw as $layout) {
            $layoutId = $layout['layout'] ?? 0;
            $title = $layout['title'] ?? '';
            foreach ($layout['content'] ?? [] as $region => $blockGroup) {
                foreach ($blockGroup as $block) {
                    $fieldId = $block['field'] ?? 0;
                    $block['field_name'] = $fieldConfigs[$fieldId]['name'] ?? 'Unknown';
                    $block['allowed_layouts'] = $fieldConfigs[$fieldId]['allowed_layouts'] ?? [];
                    $block['allowed_resources'] = $fieldConfigs[$fieldId]['allowed_resources'] ?? [];
                    $block['_region'] = $region;
                    $blocksByLayout[$layoutId]['title'] = $title;
                    $blocksByLayout[$layoutId]['regions'][$region][] = $block;
                }
            }
        }

        echo json_encode(['layouts' => $blocksByLayout]);
        break;

case 'resources':
    $c = $modx->newQuery('modResource');
    $c->select(['id', 'pagetitle']);
    $c->where(['deleted' => 0, 'class_key' => 'modDocument']);
    $c->sortby('pagetitle', 'ASC');
    $results = [];
    foreach ($modx->getIterator('modResource', $c) as $res) {
        $results[] = [
            'id' => $res->get('id'),
            'pagetitle' => $res->get('pagetitle')
        ];
    }
    echo json_encode(['resources' => $results]);
    break;

    case 'save':
        $targetId = (int)($_POST['target'] ?? 0);
        $submitted = json_decode($_POST['layouts'] ?? '', true);

        $target = $modx->getObject('modResource', $targetId);
        if (!$target || !is_array($submitted)) {
            echo json_encode(['success' => false, 'error' => 'Invalid save target or format']);
            exit;
        }

        $layouts = [];
        foreach ($submitted as $layoutId => $layoutData) {
            $regions = [];
            foreach ($layoutData['regions'] as $regionName => $blocks) {
                $regions[$regionName] = $blocks;
            }
            $layouts[] = [
                'layout' => (int)$layoutId,
                'title' => $layoutData['title'] ?? '',
                'content' => $regions,
                'settings' => [],
                'parent' => 0
            ];
        }

        $props = $target->get('properties');
        $props['contentblocks']['content'] = json_encode($layouts);
        $target->set('properties', $props);
        $target->save();

        echo json_encode(['success' => true]);
        break;

    default:
        echo json_encode(['error' => 'Unknown action']);
        break;
}