<?php
declare(strict_types=1);

require_once __DIR__ . '/_auth.php';
tpcms_require_admin();

// --- 表示専用：先頭の「NN_」を隠す（例：01_～99_） ---
if (!function_exists('__tpcms_strip_nn_prefix')) {
  function __tpcms_strip_nn_prefix(string $s): string {
    return preg_replace('/^\d{2}_/u', '', $s);
  }
}

// --- list表示用ヘルパ：partialsのlabel取得（キャッシュ付） ---
if (!function_exists('__tpcms_label_for_partial')) {
  function __tpcms_label_for_partial(string $file): string {
    static $cache = null;
    if ($cache === null) {
      $cache = [];
      foreach (glob(__DIR__.'/../themes/*/*/partials/*.html') as $p) {
        $fname = basename($p);
        if (isset($cache[$fname])) continue; // 同名は先勝ち
        $src = @file_get_contents($p);
        if ($src !== false && preg_match('/tpcms-form\s*:\s*.*?"label"\s*:\s*"([^"]*)"/s', $src, $m)) {
          $cache[$fname] = (string)$m[1];
        } else {
          $cache[$fname] = pathinfo($fname, PATHINFO_FILENAME);
        }
      }
    }
    return $cache[$file] ?? pathinfo($file, PATHINFO_FILENAME);
  }
}

// --- list表示用ヘルパ：テキスト抜粋（先頭100文字・全角対応） ---
if (!function_exists('__tpcms_text_snippet')) {
  function __tpcms_text_snippet(array $vars, int $limit = 100): string {
    $pick = '';
    // 1) 直下のTEXT優先
    if (isset($vars['TEXT']) && is_string($vars['TEXT']) && trim($vars['TEXT']) !== '') {
      $pick = $vars['TEXT'];
    }
    // 2) ROWSの先頭行からTEXT→他の文字列を探索
    if ($pick === '' && isset($vars['ROWS']) && is_array($vars['ROWS'])) {
      foreach ($vars['ROWS'] as $r) {
        if (isset($r['TEXT']) && is_string($r['TEXT']) && trim($r['TEXT']) !== '') { $pick = $r['TEXT']; break; }
        foreach ((array)$r as $vv) { if (is_string($vv) && trim($vv) !== '') { $pick = $vv; break; } }
        if ($pick !== '') break;
      }
    }
    // 3) 直下の他キーから最初の文字列
    if ($pick === '') {
      foreach ($vars as $vv) { if (is_string($vv) && trim($vv) !== '') { $pick = $vv; break; } }
    }
    if ($pick === '') return '';

    // 整形：<br>を改行化→タグ除去→実体復号→空白整形→100文字
    $pick = preg_replace('/<\s*br\s*\/?>/i', "\n", $pick);
    $pick = strip_tags($pick);
    $pick = html_entity_decode($pick, ENT_QUOTES, 'UTF-8');
    $pick = preg_replace('/\s+/u', ' ', trim($pick));
    if (mb_strlen($pick, 'UTF-8') > $limit) {
      return mb_substr($pick, 0, $limit, 'UTF-8').'…';
    }
    return $pick;
  }
}

// --- ROWS: 行削除時の物理削除（保存時） ------------------------------
// POSTに rows_deleted_files[] が来ていれば、/uploads 配下の該当ファイルを削除します。
// ※ 外部URL(http/https)や /uploads/ 以外は無視。パストラバーサル対策あり。
// ※ 「行を削除」クリック時にJSが rows_deleted_files[] を積む前提（前ステップで実装済み）。
if ($_SERVER['REQUEST_METHOD'] === 'POST'
    && !empty($_POST['rows_deleted_files'])
    && is_array($_POST['rows_deleted_files'])) {

    // シャットダウン時に実行（保存処理の後でも確実に走る）
    $__rows_deleted_files = array_values(array_filter(array_map(function($v){ return (string)$v; }, $_POST['rows_deleted_files'])));
    register_shutdown_function(function() use ($__rows_deleted_files) {
        $uploadsDir = realpath(__DIR__ . '/../uploads');
        if ($uploadsDir === false) return;

        $seen = [];
        foreach ($__rows_deleted_files as $raw) {
            $v = trim($raw);
            if ($v === '') continue;

            // 外部URLは物理削除対象外
            if (preg_match('~^https?://~i', $v)) continue;

            // /uploads/ から始まる場合はプレフィックス除去
            if (strpos($v, '/uploads/') === 0) {
                $v = substr($v, 9); // '/uploads/' の長さ=9
            }

            // ベース名のみ採用（パストラバーサル防止）
            $bn = basename($v);
            if ($bn === '' || $bn === '.' || $bn === '..') continue;
            if (isset($seen[$bn])) continue;
            $seen[$bn] = true;

            $path = $uploadsDir . DIRECTORY_SEPARATOR . $bn;
            $real = realpath($path);
            if ($real && strpos($real, $uploadsDir) === 0 && is_file($real)) {
                @unlink($real);
            }
        }
    });
}

require_once __DIR__ . '/../config.php';
require_once __DIR__ . '/../app/formdef.php'; // STEP6: tpcms-form 読み込み

/* ---------- 入力：ページslug ---------- */
$slug = isset($_GET['page']) ? trim((string)$_GET['page']) : 'index';
if (!preg_match('~^[a-z0-9][a-z0-9\-_]{0,99}$~', $slug)) {
    http_response_code(400);
    exit('Bad Request: invalid page slug');
}

/* ---------- 使用中のテーマ取得 ---------- */
$active   = tpcms_active_theme(); // ['theme'=>'...', 'color'=>'...', 'base'=>'...']
$themeKey = $active['theme'] ?? 'beginner9';
$color    = $active['color'] ?? 'white';

/* ---------- ページJSONの標準/互換パス ---------- */
$pathStd   = TPCMS_DATA . "/themes/{$themeKey}/pages/{$slug}.json";          // 正式保存先
$pathColor = TPCMS_DATA . "/themes/{$themeKey}/{$color}/pages/{$slug}.json"; // 互換読み込み
$pagePath  = is_file($pathStd) ? $pathStd : (is_file($pathColor) ? $pathColor : $pathStd);

/* ---------- 読み込み（無ければ既定形） ---------- */
$page = tpcms_load_json($pagePath);
if (!isset($page['meta']) || !is_array($page['meta']))     $page['meta']   = ['title' => '', 'description' => ''];
if (!isset($page['blocks']) || !is_array($page['blocks'])) $page['blocks'] = [];

/* ---------- POST：保存（meta）／削除（blocks） ---------- */
$saved = false;
$error = '';
$info  = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {

tpcms_require_post_csrf();
    // CSRF（存在すれば実行。STEP9で本実装予定）
    if (function_exists('tpcms_verify_csrf')) {
        try {
            tpcms_verify_csrf($_POST['csrf'] ?? '');
        } catch (Throwable $e) {
            http_response_code(400);
            exit('Bad Request: invalid CSRF');
        }
    }

    $op = (string)($_POST['op'] ?? '');

    // 既存配列をベースに「未知キー温存」で編集
    $new = $page;
    $__unlinkQueue = []; // 保存成功後に物理削除するキュー
    $__uploadsDir  = dirname(__DIR__) . '/uploads';

    if (strncmp($op, 'delete:', 7) === 0) {
        // ----- ブロック削除 -----
        $idx = (int)substr($op, 7);
        if ($idx >= 0 && $idx < count($new['blocks'])) {

// ---------- 削除するブロックが参照する /uploads のファイルを削除キューに積む（他ブロック未使用のみ） ----------
$__targetData = $new['blocks'][$idx]['data'] ?? [];

// 値をフラット化（配列の中まで走査して文字列を集める）
$__flatVals = [];
$__flatten = function ($v) use (&$__flatten, & $__flatVals) {
    if (is_array($v)) {
        foreach ($v as $vv) $__flatten($vv);
    } else {
        $__flatVals[] = (string)$v;
    }
};

// 同ページ内の「他のブロック」で同じファイル名を使っていないか確認
$__usedElsewhere = function (string $basename, array $blocks, int $skipIdx) {
    $stack = $blocks;
    unset($stack[$skipIdx]); // 自分は除外
    $iter = [$stack];
    while ($iter) {
        $cur = array_pop($iter);
        if (is_array($cur)) {
            foreach ($cur as $c) $iter[] = $c;
        } else {
            $curStr = (string)$cur;
            if ($curStr !== '') {
                $bn = basename(str_replace('\\','/', $curStr));
                if ($bn !== '' && $bn === $basename) return true;
            }
        }
    }
    return false;
};

$__flatten($__targetData);

foreach ($__flatVals as $__val) {
    // 外部URLは対象外・空もスキップ
    if ($__val === '' || preg_match('~^https?://~i', $__val)) continue;

    // /uploads/xxx でも xxx でも等価とみなす
    $__bn = basename(str_replace('\\','/', $__val));
    if ($__bn === '') continue;

    // 他ブロックが同じ basename を使っていれば削除しない（安全側）
    if ($__usedElsewhere($__bn, $new['blocks'] ?? [], $idx)) continue;

    $__full = $__uploadsDir . '/' . $__bn;
    if (is_file($__full)) $__unlinkQueue[] = $__full;
}
// 以降で array_splice()/unset でブロックを除去し、保存成功後（$saved===true）に $__unlinkQueue を実行

            array_splice($new['blocks'], $idx, 1);
            $info = 'ブロックを削除しました。';
        } else {
            $error = '不正なブロック番号です。';
        }

    } elseif ($op === 'add-block') {
        // ----- ブロック追加（パーツ選択→空データ） -----
        $partial = basename((string)($_POST['partial'] ?? ''));
        if (!preg_match('~^[A-Za-z0-9._\-]{1,100}\.html$~', $partial)) {
            $error = '不正なパーツ名です。';
        } else {
            $ppath = TPCMS_THEMES . "/{$themeKey}/{$color}/partials/{$partial}";
            if (!is_file($ppath)) {
                $error = '指定のパーツが存在しません。';
            } else {

$dataInit = [];
if (isset($_POST['use_add_form']) && isset($_POST['add_form']) && is_array($_POST['add_form'])) {
    // table / rows などは配列のまま受け取り
    $dataInit = $_POST['add_form'];
}

// --- checkbox(複数) クレンジング：["__EMPTY__"] を [] に、混在時は "__EMPTY__" だけ除去（空文字は保持） ---
$__is_list = static function(array $arr): bool {
    if ($arr === []) return true;
    $keys = array_keys($arr);
    return $keys === array_keys($keys); // 0..N-1 なら true
};
$__clean_empty = static function (&$node) use (&$__clean_empty, $__is_list): void {
    if (!is_array($node)) return;
    foreach ($node as &$vv) { $__clean_empty($vv); }
    unset($vv);
    if ($__is_list($node)) {
        $node = array_values(array_filter($node, static function ($x) {
            return $x !== '__EMPTY__';
        }));
    }
};
$__clean_empty($dataInit);

                // --- once:true のパーツは1ページ1回まで ---
                $__ok_to_add = true;
                $__def = tpcms_formdef_load_by_name($partial);
                if (!empty($__def['once'])) {
                  foreach (($new['blocks'] ?? []) as $__b) {
                    $__p = basename((string)($__b['partial'] ?? ''));
                    if ($__p === $partial) { $__ok_to_add = false; break; }
                  }
                }

                if ($__ok_to_add) {
                  $new['blocks'][] = [
                    'partial' => $partial,
                    'data'    => $dataInit
                  ];
                  $info = 'ブロックを追加しました。';
                } else {
                  $error = 'このパーツは1ページにつき1回のみ使用できます。';
                }
            }
        }

    } elseif (strncmp($op, 'update:', 7) === 0) {
        // ----- ブロック編集（data を JSON で更新） -----
        $idx = (int)substr($op, 7);
        if ($idx >= 0 && $idx < count($new['blocks'])) {
            // --- STEP6: フォーム経由の保存（配列もそのまま保持）
            if (isset($_POST['use_form'][$idx]) && isset($_POST['data_form'][$idx]) && is_array($_POST['data_form'][$idx])) {
              $form = $_POST['data_form'][$idx];

              // 既存データをベースに上書き（未知キーは温存）
              $data = (array)($new['blocks'][$idx]['data'] ?? []);
              foreach ($form as $k => $v) {
                if (!is_string($k)) continue;
                // 配列は配列のまま保存（ROWS 等）
                $data[$k] = is_array($v) ? $v : (string)$v;
              }
              
              // --- checkbox(複数) クレンジング：["__EMPTY__"] を [] に、混在時は "__EMPTY__" だけ除去（空文字は保持） ---
              $___is_list = static function(array $arr): bool {
                if ($arr === []) return true;
                $keys = array_keys($arr);
                return $keys === array_keys($keys); // 0..N-1 なら true
              };
              $___clean_empty = static function (&$node) use (&$___clean_empty, $___is_list): void {
                if (!is_array($node)) return;
                foreach ($node as &$vv) { $___clean_empty($vv); }
                unset($vv);
                if ($___is_list($node)) {
                  $node = array_values(array_filter($node, static function ($x) {
                    return $x !== '__EMPTY__';
                  }));
                }
              };
              $___clean_empty($data);

              /* ---------- file削除フラグ（__delete）処理（再帰対応・ROWS含む） ---------- */
/* 事前に用意済みの変数：
 *   $__uploadsDir … dirname(__DIR__).'/uploads'
 *   $__unlinkQueue … 保存成功後にunlinkする配列
 * 参照元（旧値）：$new['blocks'][$idx]['data']
 * 変更対象（新値）：$data
 */
$__collectDeletes = function (&$newData, $oldData) use (&$__collectDeletes, &$__unlinkQueue, $__uploadsDir) {
  if (!is_array($newData)) return;

  foreach ($newData as $k => &$v) {
    // ネスト配列は再帰
    if (is_array($v)) {
      $oldSub = (is_array($oldData[$k] ?? null)) ? $oldData[$k] : [];
      $__collectDeletes($v, $oldSub);
      // ネスト配列直下の __delete 残骸を一掃
      foreach (array_keys($v) as $kk) {
        if (substr((string)$kk, -8) === '__delete') unset($v[$kk]);
      }
      continue;
    }

    // __delete=1 を検出
    if (substr((string)$k, -8) === '__delete' && (string)$v === '1') {
      $base = substr($k, 0, -8);

      // 旧値を旧データから取得（同じ階層の兄弟キー）
      $oldVal = '';
      if (is_array($oldData) && array_key_exists($base, $oldData)) {
        $oldVal = (string)$oldData[$base];
      }

      // 新データからは __delete と本体キーを削除（JSON汚染防止）
      unset($newData[$k]);
      unset($newData[$base]);

      // 物理削除キュー（/uploads 直下のみ、外部URL除外）
      if ($oldVal !== '' && !preg_match('~^https?://~i', $oldVal)) {
        $bn = basename(str_replace('\\','/',$oldVal));
        if ($bn !== '') {
          $full = $__uploadsDir . '/' . $bn;
          if (is_file($full)) { $__unlinkQueue[] = $full; }
        }
      }
    }
  }
};

// 旧値を渡して、$data 内の __delete を再帰処理
$__collectDeletes($data, $new['blocks'][$idx]['data'] ?? []);



/* ---------- file置換検知：旧ファイルの削除キュー（配列の並び替えに耐性） ---------- */
/* 事前に存在する可能性のあるヘルパにフォールバックを用意（未定義でも動くように） */
if (!isset($__bn) || !is_callable($__bn)) {
  $__bn = function($v){
    $s = (string)$v;
    if ($s === '' || preg_match('~^https?://~i', $s)) return '';
    if (strpos($s, '/uploads/') === 0) $s = substr($s, 9); // '/uploads/' の除去
    return basename($s);
  };
}
$__usedElsewhereForUpdate = function($basename) use ($new, $idx) {
  $basename = (string)$basename;
  if ($basename === '') return false;

  // このブロック($idx)以外で、同じbasenameを参照していれば true
  foreach (($new['blocks'] ?? []) as $j => $b) {
    if ($j === $idx) continue;
    $stack = [ $b['data'] ?? null ];
    while ($stack) {
      $cur = array_pop($stack);
      if (is_array($cur)) { foreach ($cur as $vv) $stack[] = $vv; }
      else {
        $s = (string)$cur;
        if ($s !== '' && !preg_match('~^https?://~i', $s)) {
          $bn = basename(str_replace('\\','/',$s));
          if ($bn === $basename) return true;
        }
      }
    }
  }
  return false;
};
if (!isset($__uploadsDir) || !is_string($__uploadsDir)) {
  $__uploadsDir = realpath(__DIR__ . '/../uploads') ?: (__DIR__ . '/../uploads');
}
if (!isset($__unlinkQueue) || !is_array($__unlinkQueue)) {
  $__unlinkQueue = [];
}

/* 配列（リスト）かどうか判定（キーが 0..N の数値のみ） */
$__isList = function($arr){
  if (!is_array($arr)) return false;
  foreach (array_keys($arr) as $k) { if (!is_int($k)) return false; }
  return true;
};

/* ノード配下の「ファイル名（basename）」を多重度付きで収集（再帰） */
$__gatherBns = null;
$__gatherBns = function($node, array &$out) use (&$__gatherBns, $__bn){
  if (is_array($node)) { foreach ($node as $v) $__gatherBns($v, $out); return; }
  $s = (string)$node;
  if ($s === '' || preg_match('~^https?://~i', $s)) return;
  $bn = $__bn($s);
  if ($bn === '') return;
  $out[$bn] = ($out[$bn] ?? 0) + 1;
};

/* 旧=>新を比較し、純減したbasenameだけ削除キューへ（順序変更は無視） */
$__collectReplaced = null;
$__collectReplaced = function($newData, $oldData) use (&$__collectReplaced, &$__unlinkQueue, $__uploadsDir, $__bn, $__usedElsewhereForUpdate, $__isList, $__gatherBns) {
  if (!is_array($newData)) return;

  // 両方が数値キーのリストなら、順不同の多重集合で比較
  if (is_array($oldData) && $__isList($newData) && $__isList($oldData)) {
    $oldMap = []; $__gatherBns($oldData, $oldMap);
    $newMap = []; $__gatherBns($newData, $newMap);

    foreach ($oldMap as $bn => $cntOld) {
      $cntNew = $newMap[$bn] ?? 0;
      $diff  = $cntOld - $cntNew; // 旧 > 新 のときだけ削除候補
      if ($diff > 0 && !call_user_func($__usedElsewhereForUpdate, $bn)) {
        $full = rtrim($__uploadsDir, '/\\') . '/' . $bn;
        if (is_file($full)) {
          for ($i=0; $i<$diff; $i++) { $__unlinkQueue[] = $full; }
        }
      }
    }
    return; // 並べ替えのみならここで終了（削除なし）
  }

  // それ以外（連想配列など）は従来どおりキー対応で比較
  foreach ($newData as $k => $v) {
    if (is_array($v)) {
      $oldSub = (is_array($oldData[$k] ?? null)) ? $oldData[$k] : [];
      $__collectReplaced($v, $oldSub);
      continue;
    }
    if (substr((string)$k, -8) === '__delete') continue; // 削除フラグは別処理

    $oldVal = (string)($oldData[$k] ?? '');
    $newVal = (string)$v;

    // 旧値がローカルファイルのときのみ対象（外部URL除外）
    if ($oldVal !== '' && !preg_match('~^https?://~i', $oldVal)) {
      $oldBn = $__bn($oldVal); if ($oldBn === '') continue;

      // 空/外部URL/別basenameに変わった時だけ置換と判定
      $changed = false;
      if ($newVal === '' || preg_match('~^https?://~i', $newVal)) {
        $changed = true;
      } else {
        $newBn = $__bn($newVal);
        if ($newBn !== $oldBn) $changed = true;
      }

      if ($changed && !call_user_func($__usedElsewhereForUpdate, $oldBn)) {
        $full = rtrim($__uploadsDir, '/\\') . '/' . $oldBn;
        if (is_file($full)) { $__unlinkQueue[] = $full; }
      }
    }
  }
};

/* 実行（旧：保存前JSON、新：POST後の $data） */
$__collectReplaced($data, $new['blocks'][$idx]['data'] ?? []);




              $new['blocks'][$idx]['data'] = $data;
              $info = 'ブロックを更新しました。';

            } else {
              // 従来の JSON テキストエディタでの保存
              $raw = (string)($_POST['data_json'][$idx] ?? '');
              $raw = trim($raw);
              if ($raw === '') $raw = '{}';
              $decoded = json_decode($raw, true);
              if (!is_array($decoded)) {
                $error = 'JSONが不正です（連想配列形式のJSONを入力してください）。';
              } else {
                // partial が欠けているケースは触らず、data のみ差し替え
                $new['blocks'][$idx]['data'] = $decoded;
                $info = 'ブロックを更新しました。';
              }
            }
        } else {
            $error = '不正なブロック番号です。';
        }

    } elseif ($op === 'sort') {
        // ----- 並び順の保存（ドラッグ後の順番を適用） -----
        $orderJson = (string)($_POST['order_json'] ?? '');
        $arr = json_decode($orderJson, true);

        if (!is_array($arr)) {
            $error = '並び順データが不正です。';
        } else {
            $n = count($new['blocks']);
            $valid = (count($arr) === $n);
            if ($valid) {
                $seen = array_fill(0, $n, 0);
                foreach ($arr as $v) {
                    if (!is_int($v) || $v < 0 || $v >= $n || $seen[$v]) { $valid = false; break; }
                    $seen[$v] = 1;
                }
            }
            if (!$valid) {
                $error = '並び順データの検証に失敗しました。';
            } else {
                $re = [];
                foreach ($arr as $v) { $re[] = $new['blocks'][$v]; }
                $new['blocks'] = $re;
                $info = '並び順を保存しました。';
            }
        }

    } else {
        // ----- meta の保存（既存処理） -----
        $meta_title = trim((string)($_POST['meta_title'] ?? ''));
        $meta_desc  = trim((string)($_POST['meta_description'] ?? ''));
        $new['meta']['title']       = $meta_title;
        $new['meta']['description'] = $meta_desc;
        $info = '保存しました。';
    }

    if ($error === '') {
        // 保存ディレクトリを確保
        $dirStd = dirname($pathStd);
        if (!is_dir($dirStd)) {
            @mkdir($dirStd, 0775, true);
        }

        // JSON 書き出し（正式パスに保存）
        $json = json_encode($new, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
        if ($json === false) {
            $error = 'JSONエンコードに失敗しました。';
        } else {
            $ok = @file_put_contents($pathStd, $json);
            if ($ok === false) {
                $error = '保存に失敗しました（権限/パスを確認）。';
            } else {
                $page  = $new;  // 画面再描画用に反映
                $saved = true;
				
                // 物理削除は保存確定後に実施
                if (!empty($__unlinkQueue)) { foreach ($__unlinkQueue as $__f) { @unlink($__f); } }

            }
        }
    }
}

/* ---------- メニュー名（パンくず用） ---------- */
$menu = tpcms_load_json(TPCMS_DATA . '/menu.json');
$pageTitle = $slug;
if (isset($menu['items']) && is_array($menu['items'])) {
    foreach ($menu['items'] as $it) {
        if (($it['slug'] ?? '') === $slug) {
            $pageTitle = (string)($it['title'] ?? $slug);
            break;
        }
    }
}

/* ---------- partials 一覧（追加パネル用／今はUI骨組みのみ） ---------- */
$partialsDir  = TPCMS_THEMES . "/{$themeKey}/{$color}/partials";
$partialFiles = [];
if (is_dir($partialsDir)) {
    foreach (scandir($partialsDir) ?: [] as $f) {
        if ($f === '.' || $f === '..') continue;
        if (substr($f, -5) === '.html') $partialFiles[] = $f;
    }
}
sort($partialFiles, SORT_NATURAL);
?><!doctype html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>ページブロック編集｜<?= tpcms_h($pageTitle) ?>（<?= tpcms_h($slug) ?>）</title>
  <link rel="stylesheet" href="./assets/admin.css">
<?php
// ---------- メニュー候補（内部ページ/アンカーのみ）をJSへ渡す ----------
$__candidates = [];
if (isset($menu['items']) && is_array($menu['items'])) {
  foreach ($menu['items'] as $row) {
    if (!is_array($row)) continue;
    $mSlug = isset($row['slug'])  ? (string)$row['slug']  : '';
    $label = isset($row['label']) ? (string)$row['label'] : $mSlug;
    if ($mSlug === '') continue;
    if (preg_match('~^https?://~i', $mSlug)) continue;        // 外部URLは候補外
    if (stripos($label, '<h3') !== false) continue;           // 見出し行は候補外

    // admin向けラベル：タグ除去→実体復号→空白整形
    $label_admin = $label;
    $label_admin = preg_replace('#<[^>]*>#u', ' ', $label_admin);
    $label_admin = html_entity_decode($label_admin, ENT_QUOTES, 'UTF-8');
    $label_admin = str_replace("\xC2\xA0", ' ', $label_admin);
    $label_admin = preg_replace('/\s+/u', ' ', $label_admin);
    $label_admin = trim($label_admin);

    $__candidates[] = ['slug' => $mSlug, 'label' => ($label_admin !== '' ? $label_admin : $mSlug)];
  }
}
?>
<script>
  // 小ポップアップの候補データ（次ステップでUIから参照）
  window.TPCMS_MENU_CANDIDATES = <?= json_encode($__candidates, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
</script>

</head>
<body class="admin page-blocks">

<header>
<div>
<a href="./">ダッシュボード</a>
<a href="../" target="_blank" rel="noopener">公開側トップ</a>
</div>
<div>
<a href="./logout.php">ログアウト</a>
</div>
</header>

<div id="container">

    <?php if ($saved && $error === ''): ?>
      <p class="notice"><?= tpcms_h($info) ?></p>
    <?php elseif ($error !== ''): ?>
      <p class="err"><?= tpcms_h($error) ?></p>
    <?php endif; ?>

    <div class="breadcrumb muted">
      <?php
      // --- preview link path (admin/ から見たフロントの相対URL) ---
      // 管理画面は常に ?page=<slug> で開く想定なので、ここだけを信頼して使う。
      // 例）?page=index → ../   /  ?page=news → ../news
      $__raw = $_GET['page'] ?? 'index';
      if (is_array($__raw)) {
          $__raw = reset($__raw) ?: 'index';
      }
      $__slugForPreview = trim((string)$__raw);

      // スラッグの許可文字：英数・ハイフン・アンダースコア・スラッシュのみ。
      // それ以外が混じる場合や空は index 扱いにする。
      if ($__slugForPreview === '' || !preg_match('~^[A-Za-z0-9/_-]+$~', $__slugForPreview)) {
          $__slugForPreview = 'index';
      }

      // index はサイト直下、それ以外は ../<slug>
      $__previewHref = ($__slugForPreview === 'index') ? '../' : ('../' . $__slugForPreview);

      // 見出し表示用の安全な文字列（配列混入対策）
      $__themeKeyStr  = is_string($themeKey ?? null) ? $themeKey : '';
      $__colorStr     = is_string($color ?? null)    ? $color    : '';
      $__pageTitleStr = is_string($pageTitle ?? null) ? $pageTitle : ($__slugForPreview ?: 'index');
      ?>
      ページ：<?= tpcms_h($__pageTitleStr) ?> / テーマ：<?= tpcms_h($__themeKeyStr) ?>_<?= tpcms_h($__colorStr) ?> / <a href="<?= tpcms_h($__previewHref) ?>" target="_blank" rel="noopener noreferrer">プレビュー</a>
    </div>

    <form method="post" action="./page_blocks.php?page=<?= tpcms_h($slug) ?>">
    <input type="hidden" name="_csrf" value="<?= tpcms_h(tpcms_csrf_token()) ?>">
      <?php if (function_exists('tpcms_csrf_token')): ?>
        <input type="hidden" name="csrf" value="<?= tpcms_h(tpcms_csrf_token()) ?>">
      <?php endif; ?>
	  <input type="hidden" name="order_json" id="order_json" value="">

      <!-- メタ部（保存対象） -->
      <section class="meta-box mb3rem">
        <h2>ページ基本情報</h2>
        <div class="row">
          <label class="label">サイトタイトル（※必ずあなたのサイト情報に入れ替えて下さい）</label>
          <input class="input" type="text" name="meta_title" value="<?= tpcms_h((string)($page['meta']['title'] ?? '')) ?>">
        </div>
        <div class="row">
          <label class="label">サイト説明（※必ずあなたのサイト情報に入れ替えて下さい）</label>
          <input class="input" type="text" name="meta_description" value="<?= tpcms_h((string)($page['meta']['description'] ?? '')) ?>">
        </div>
      <p><button class="btn1" type="submit" name="op" value="save-meta">ページ基本情報を保存</button></p>
      </section>

      <!-- ブロック一覧（削除ボタンが有効） -->
      <section class="blocks">
        <h2>ブロック一覧</h2>
        <?php if (count($page['blocks']) === 0): ?>
          <p class="muted">このページにはまだブロックがありません。</p>
        <?php else: ?>
          <p class="small">編集対象が見当たらない場合は、<a href="site.php">サイト基本設定</a>を見て下さい。</p>
          <ol id="blockList" class="list">
            <?php foreach ($page['blocks'] as $i => $blk): ?>
              <?php
                $pname = (string)($blk['partial'] ?? '');
                $data  = (array)($blk['data'] ?? []);
                $summaryKeys = implode(', ', array_slice(array_keys($data), 0, 3));
              ?>
            <li data-idx="<?= $i ?>">
              <div>
                <span class="handle">☰</span>
                <?php $__label = __tpcms_label_for_partial($pname); $__snip = __tpcms_text_snippet($data, 100); ?>
                <strong><?= tpcms_h(__tpcms_strip_nn_prefix($__label)) ?></strong>
                <?php if ($__snip !== ''): ?>
                  <span class="muted"> — <?= tpcms_h($__snip) ?></span>
                <?php endif; ?>
              </div>

              <!-- 右端：編集／削除 -->
              <div class="actions">
                <button class="btn" type="button" onclick="(function(btn,id){var det=document.getElementById(id);if(!det)return;det.open=!det.open;if(det.open){var p=det.querySelector('.tpcms-edit-pane');if(p)p.style.removeProperty('display');}btn.textContent=det.open?'閉じる':'編集';})(this,'ed-<?= $i ?>');return false;">編集</button>
                <button class="btn" type="submit" name="op" value="delete:<?= $i ?>" onclick="return confirm('このブロックを削除します。よろしいですか？');">削除</button>
              </div>

              <?php
                $jsonData = json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
                if ($jsonData === false) $jsonData = '{}';
              ?>
              <!-- 下段：全幅でエディタ（summaryは隠す。上の「編集」ボタンで開閉） -->
              <details id="ed-<?= $i ?>" class="editor">
                <summary>編集</summary>

                <div class="tpcms-edit-pane">

                  <?php
                    // tpcms-form 定義がある場合のみ、簡易フォームを表示（text/textarea）
                    $__def = tpcms_formdef_load_by_name($pname);
                    if ($__def && ( !empty($__def['fields']) || !empty($__def['repeaters']) )):
                  ?>
                    <div class="row" style="margin-top:.6rem;">
                      <?php if (!empty($__def['help_html'])): ?>
                        <div class="muted" style="margin:.4rem 0;"><?= $__def['help_html'] ?></div>
                      <?php endif; ?>

                      <input type="hidden" name="use_form[<?= $i ?>]" value="1">
<?php

ob_start(); // repeatersのHTMLを一旦キャプチャして後で出力

// --- repeaters（ROWS）編集UI（fieldsが無くても出す） ---
if (!empty($__def['repeaters'])):
  foreach ($__def['repeaters'] as $__rep):
    $__rep   = (array)$__rep;
    $rKey    = (string)($__rep['key'] ?? '');
    if ($rKey === '') continue;
    $rLab    = (string)($__rep['label'] ?? $rKey);
    $rowdefs = isset($__rep['fields']) && is_array($__rep['fields']) ? $__rep['fields'] : [];
    $___rows = isset($data[$rKey]) && is_array($data[$rKey]) ? array_values($data[$rKey]) : [[]];
    $___base = 'data_form['.$i.']['.$rKey.']';
    $___rowIndex = 0;
    ?>
    <div class="row">
      <label class="label"><?= tpcms_h($rLab) ?></label>
      <div class="tpcms-rows" data-base="<?= tpcms_h($___base) ?>">
        <?php foreach ($___rows as $___r): ?>
          <div class="tpcms-row">
          <div class="handle" role="button" aria-label="並べ替え" title="並べ替え" tabindex="0">☰</div>
            <?php foreach ($rowdefs as $___f):
              $___f  = (array)$___f;
              $___t  = strtolower(trim((string)($___f['type'] ?? 'text')));
              $___k  = (string)($___f['key']  ?? '');
              if ($___k==='') continue;
              $___l  = (string)($___f['label'] ?? $___k);
              $___name = $___base.'['.$___rowIndex.']['.$___k.']';
              $___v  = isset($___r[$___k]) && is_scalar($___r[$___k]) ? (string)$___r[$___k] : '';
              $___ph = (string)($___f['placeholder'] ?? '');
              $___mx = (int)($___f['maxlength'] ?? 0);
              ?>
<?php
  // file用に data-image-* を仕込む（他typeでは空）
  $___imw = (int)($___f['image_max_width']  ?? 0);
  $___imh = (int)($___f['image_max_height'] ?? 0);
  $___iq  = (int)($___f['image_quality']    ?? 0);
  $___attrResize = '';
  if ($___imw > 0) $___attrResize .= ' data-image-max-width="'.$___imw.'"';
  if ($___imh > 0) $___attrResize .= ' data-image-max-height="'.$___imh.'"';
  if ($___iq  > 0) { $___iq = max(1, min(100, $___iq)); $___attrResize .= ' data-image-quality="'.$___iq.'"'; }
?>
<div class="row"<?= ($___t === 'file') ? $___attrResize : '' ?>>
  <label class="label"><?= tpcms_h($___l) ?></label>

  <?php if ($___t === 'textarea'): ?>
    <textarea class="input" name="<?= htmlspecialchars($___name, ENT_QUOTES, 'UTF-8') ?>" rows="3"
      <?= $___ph!==''?' placeholder="'.tpcms_h($___ph).'"':'' ?><?= $___mx>0?' maxlength="'.$___mx.'"':'' ?>><?= tpcms_h($___v) ?></textarea>

  <?php elseif ($___t === 'select'): ?>
    <?php $___opts = isset($___f['options']) && is_array($___f['options']) ? $___f['options'] : [];
          $___allow_empty = isset($___f['allow_empty']) ? (bool)$___f['allow_empty'] : true; ?>
    <select class="input" name="<?= htmlspecialchars($___name, ENT_QUOTES, 'UTF-8') ?>">
      <?php if ($___allow_empty): ?>
        <option value=""><?= tpcms_h($___ph!=='' ? $___ph : '未選択') ?></option>
      <?php endif; ?>
      <?php foreach ($___opts as $____opt):
        $____val = (string)($____opt[0] ?? '');
        $____txt = (string)($____opt[1] ?? $____val); ?>
        <option value="<?= tpcms_h($____val) ?>" <?= ($___v===$____val)?'selected':'' ?>><?= tpcms_h($____txt) ?></option>
      <?php endforeach; ?>
    </select>

  <?php elseif ($___t === 'checkbox'): ?>

    <?php $___opts = isset($___f['options']) && is_array($___f['options']) ? $___f['options'] : []; ?>
    <?php if ($___opts): ?>
      <?php
        // 複数選択：配列POST
        $___nameArr = $___name.'[]';
        $___tmp     = $___r[$___k] ?? [];
        if (is_array($___tmp)) {
          $___sel = array_map('strval', array_values($___tmp));
        } elseif (is_string($___tmp) && $___tmp !== '') {
          $___sel = [ (string)$___tmp ];
        } else {
          $___sel = [];
        }
        $____idx    = 0;
      ?>
      <input type="hidden" name="<?= htmlspecialchars($___nameArr, ENT_QUOTES, 'UTF-8') ?>" value="__EMPTY__">
      <?php foreach ($___opts as $____opt):
        $____val = (string)($____opt[0] ?? '');
        $____txt = (string)($____opt[1] ?? $____val);
        $____id  = 'cbrow_'.$i.'_'.$___rowIndex.'_'.preg_replace('/[^a-z0-9_\-]/i','',$___k).'_'.($____idx++);
      ?>
        <label for="<?= htmlspecialchars($____id, ENT_QUOTES, 'UTF-8') ?>" style="margin-right:1rem;">
          <input
            id="<?= htmlspecialchars($____id, ENT_QUOTES, 'UTF-8') ?>"
            type="checkbox"
            name="<?= htmlspecialchars($___nameArr, ENT_QUOTES, 'UTF-8') ?>"
            value="<?= tpcms_h($____val) ?>"
            <?= in_array($____val, $___sel, true) ? 'checked' : '' ?>
          >
          <?= tpcms_h($____txt) ?>
        </label>
      <?php endforeach; ?>
    <?php else: ?>
      <input type="hidden" name="<?= htmlspecialchars($___name, ENT_QUOTES, 'UTF-8') ?>" value="0">
      <input
        type="checkbox"
        name="<?= htmlspecialchars($___name, ENT_QUOTES, 'UTF-8') ?>"
        value="1"
        <?= in_array((string)$___v, ['1','true','on'], true) ? 'checked' : '' ?>
      >
    <?php endif; ?>

  <?php elseif ($___t === 'radio'): ?>
    <?php $___opts = isset($___f['options']) && is_array($___f['options']) ? $___f['options'] : []; $____idx=0; ?>
    <?php foreach ($___opts as $____opt):
      $____val=(string)($____opt[0]??''); $____txt=(string)($____opt[1]??$____val);
      $____id='rrow_'.preg_replace('/[^a-z0-9_\-]/i','',$___k).'_'.($____idx++); ?>
      <label for="<?= htmlspecialchars($____id, ENT_QUOTES, 'UTF-8') ?>" style="margin-right:1rem;">
        <input id="<?= htmlspecialchars($____id, ENT_QUOTES, 'UTF-8') ?>" type="radio"
               name="<?= htmlspecialchars($___name, ENT_QUOTES, 'UTF-8') ?>"
               value="<?= tpcms_h($____val) ?>" <?= (array_key_exists($___k, (array)$___r) ? ($___v === $____val) : ($____idx === 1)) ? 'checked' : '' ?>>
        <?= tpcms_h($____txt) ?>
      </label>
    <?php endforeach; ?>

  <?php elseif ($___t === 'file'): ?>
    <input class="input js-upload-url as-plain-text" type="text"
           name="<?= htmlspecialchars($___name, ENT_QUOTES, 'UTF-8') ?>"
           value="<?= tpcms_h($___v ?? $___val ?? '') ?>"
           data-file="1"
           data-csrf="<?= tpcms_h($_SESSION['_csrf'] ?? '') ?>"
           placeholder=""
           readonly aria-readonly="true" tabindex="-1">

  <?php else: /* text */ ?>
    <input class="input" type="text"
           name="<?= htmlspecialchars($___name, ENT_QUOTES, 'UTF-8') ?>"
           value="<?= tpcms_h($___v) ?>"
           <?= $___ph!==''?' placeholder="'.tpcms_h($___ph).'"':'' ?><?= $___mx>0?' maxlength="'.$___mx.'"':'' ?><?= !empty($___f['link_picker']) ? ' data-link-picker="1"' : '' ?>>
  <?php endif; ?>
</div>

            <?php endforeach; ?>

            <button class="btn js-rows-del delete-color" type="button">− 行を削除</button>
          </div>
          <?php $___rowIndex++; ?>
        <?php endforeach; ?>

        <button class="btn js-rows-add" type="button">＋ 行を追加</button>
      </div>
    </div>
    <?php
  endforeach;
endif;
?>

<?php $__repeaters_html = ob_get_clean(); ?>


                      <?php foreach ($__def['fields'] as $__f):
                        $__type = strtolower(trim((string)($__f['type'] ?? 'text')));
                        $__key  = (string)($__f['key'] ?? '');
                        $__lab  = (string)($__f['label'] ?? $__key);
                        if ($__key === '') continue;
                        $__val  = is_scalar($data[$__key] ?? null) ? (string)$data[$__key] : '';

                        // 拡張：required / placeholder / maxlength
                        $__req = !empty($__f['required']);
                        $__ph  = isset($__f['placeholder']) ? (string)$__f['placeholder'] : '';
                        $__max = isset($__f['maxlength']) && is_numeric($__f['maxlength']) ? (int)$__f['maxlength'] : 0;


                        if ($__type === 'text'): ?>
                          <div class="row">
                            <label class="label">
                              <?= tpcms_h($__lab) ?><?= $__req ? '（必須）' : '' ?>
                            </label>
                            <input class="input" type="text"
                                   name="data_form[<?= $i ?>][<?= tpcms_h($__key) ?>]"
                                   value="<?= tpcms_h($__val) ?>"
                                   <?= $__ph  !== '' ? ' placeholder="'.tpcms_h($__ph).'"' : '' ?>
                                   <?= $__max > 0  ? ' maxlength="'.$__max.'"' : '' ?>
                                   <?= $__req ? ' required' : '' ?><?= !empty($__f['link_picker']) ? ' data-link-picker="1"' : '' ?>>
                          </div>
  
                        <?php elseif ($__type === 'checkbox'): ?>
                          <div class="row">
                            <label class="label"><?= tpcms_h($__lab) ?></label>
                            <?php
                              $__opts = isset($__f['options']) && is_array($__f['options']) ? $__f['options'] : [];
                              if ($__opts) {
                                // 複数選択：既存値は配列想定
                                $___name = 'data_form['.$i.']['.$__key.'][]';
                                $___tmp  = $data[$__key] ?? [];
                                if (is_array($___tmp)) {
                                  $___sel = array_map('strval', array_values($___tmp));
                                } elseif (is_string($___tmp) && $___tmp !== '') {
                                  $___sel = [ (string)$___tmp ];
                                } else {
                                  $___sel = [];
                                }
                                $___idx  = 0;
                                ?>
                                <input type="hidden" name="<?= htmlspecialchars($___name, ENT_QUOTES, 'UTF-8') ?>" value="__EMPTY__">
                                <?php
                                foreach ($__opts as $__opt) {
                                  $ov  = (string)($__opt[0] ?? '');
                                  $ot  = (string)($__opt[1] ?? $ov);
                                  $oid = 'cb_'.$i.'_'.preg_replace('/[^a-z0-9_\-]/i', '', (string)$__key).'_'.($___idx++);
                            ?>
                                  <label for="<?= htmlspecialchars($oid, ENT_QUOTES, 'UTF-8') ?>" style="margin-right:1rem;">
                                    <input
                                      id="<?= htmlspecialchars($oid, ENT_QUOTES, 'UTF-8') ?>"
                                      type="checkbox"
                                      name="<?= htmlspecialchars($___name, ENT_QUOTES, 'UTF-8') ?>"
                                      value="<?= tpcms_h($ov) ?>"
                                      <?= in_array($ov, $___sel, true) ? 'checked' : '' ?>
                                    >
                                    <?= tpcms_h($ot) ?>
                                  </label>
                            <?php
                                }
                              } else {
                                // 従来の単体チェック（0/1）
                            ?>
                                <input type="hidden" name="data_form[<?= $i ?>][<?= tpcms_h($__key) ?>]" value="0">
                                <input
                                  type="checkbox"
                                  name="data_form[<?= $i ?>][<?= tpcms_h($__key) ?>]"
                                  value="1"
                                  <?= in_array((string)$__val, ['1', 'true', 'on'], true) ? 'checked' : '' ?>
                                >
                            <?php } ?>
                          </div>
						  
                        <?php elseif ($__type === 'radio'): ?>
                          <div class="row">
                            <label class="label"><?= tpcms_h($__lab) ?></label>
                            <?php
                              $__opts = isset($__f['options']) && is_array($__f['options']) ? $__f['options'] : [];
                              $___idx = 0;
                              foreach ($__opts as $__opt):
                                $___val = (string)($__opt[0] ?? '');
                                $___txt = (string)($__opt[1] ?? $___val);
                                $___id  = 'rf_' . $i . '_' . preg_replace('/[^a-z0-9_\-]/i','', (string)$__key) . '_' . $___idx++;
                            ?>
                              <label for="<?= htmlspecialchars($___id, ENT_QUOTES, 'UTF-8') ?>" style="margin-right:1rem;">
                                <input
                                  id="<?= htmlspecialchars($___id, ENT_QUOTES, 'UTF-8') ?>"
                                  type="radio"
                                  name="data_form[<?= $i ?>][<?= tpcms_h($__key) ?>]"
                                  value="<?= tpcms_h($___val) ?>"
                                  <?= (array_key_exists($__key, $data) ? ($__val === $___val) : ($___idx === 1)) ? 'checked' : '' ?>
                                >
                                <?= tpcms_h($___txt) ?>
                              </label>
                            <?php endforeach; ?>
                          </div>
						  
                        <?php elseif ($__type === 'textarea'): ?>
                          <div class="row">
                            <label class="label">
                              <?= tpcms_h($__lab) ?><?= $__req ? '（必須）' : '' ?>
                            </label>
                            <textarea
                             name="data_form[<?= $i ?>][<?= tpcms_h($__key) ?>]"
                              rows="3"
                              <?= $__ph  !== '' ? ' placeholder="'.tpcms_h($__ph).'"' : '' ?>
                              <?= $__max > 0  ? ' maxlength="'.$__max.'"' : '' ?>
                              <?= $__req ? ' required' : '' ?>
                            ><?= tpcms_h($__val) ?></textarea>
                          </div>
						  
                        <?php elseif ($__type === 'select'): ?>
                          <div class="row">
                            <label class="label"><?= tpcms_h($__lab) ?><?= $__req ? '（必須）' : '' ?></label>
                            <?php
                              $__opts        = isset($__f['options']) && is_array($__f['options']) ? $__f['options'] : [];
                              $__placeholder = isset($__f['placeholder']) ? (string)$__f['placeholder'] : '';
                              $__allow_empty = isset($__f['allow_empty']) ? (bool)$__f['allow_empty'] : true; // 既定で未選択可
                            ?>
                            <select name="data_form[<?= $i ?>][<?= tpcms_h($__key) ?>]" class="input" <?= $__req ? ' required' : '' ?>>
                              <?php if ($__allow_empty): ?>
                                <option value=""><?= tpcms_h($__placeholder !== '' ? $__placeholder : '未選択') ?></option>
                              <?php endif; ?>
                              <?php foreach ($__opts as $__opt):
                                $___val = (string)($__opt[0] ?? '');
                                $___txt = (string)($__opt[1] ?? $___val);
                              ?>
                                <option value="<?= tpcms_h($___val) ?>" <?= ($__val === $___val) ? 'selected' : '' ?>><?= tpcms_h($___txt) ?></option>
                              <?php endforeach; ?>
                            </select>
                          </div>

                        <?php elseif ($__type === 'file'): ?>
                        <?php
                          // ---- 任意リサイズ指定（tpcms-form の定義値から） ----
                          $__imw = (int)($__f['image_max_width']  ?? 0);
                          $__imh = (int)($__f['image_max_height'] ?? 0);
                          $__iq  = (int)($__f['image_quality']    ?? 0);
                          $__attrResize = '';
                          if ($__imw > 0) $__attrResize .= ' data-image-max-width="'.$__imw.'"';
                          if ($__imh > 0) $__attrResize .= ' data-image-max-height="'.$__imh.'"';
                          if ($__iq  > 0) { $__iq = max(1, min(100, $__iq)); $__attrResize .= ' data-image-quality="'.$__iq.'"'; }
                        ?>
                          <div class="row"<?= $__attrResize ?>>
                            <label class="label"><?= tpcms_h($__lab) ?><?= $__req ? '（必須）' : '' ?></label>
                            <?php
                              // 一意ID（file入力→テキスト欄への反映ターゲット）
                              $___id = 'fk_' . $i . '_' . preg_replace('/[^a-z0-9_\-]/i','', (string)$__key);
                              $___val = trim((string)$__val);
                              // プレビュー用URL解決：ファイル名なら /uploads/ を前置、http(s)ならそのまま
                              $___url = '';
                              if ($___val !== '') {
                                if (preg_match('~^https?://~i', $___val)) { $___url = $___val; }
                                else { $___url = '../uploads/' . $___val; }
                              }
                              $___isVideo = ($___url !== '' && preg_match('~\.(mp4|webm)(?:\?.*)?$~i', $___url));
                            ?>
                            <!-- 外部URLも可：保存は「ファイル名」または「http(s)://…」 -->
                            <input id="<?= tpcms_h($___id) ?>"
                                   class="input js-upload-url as-plain-text"
                                   type="text"
                                   name="data_form[<?= $i ?>][<?= tpcms_h($__key) ?>]"
                                   placeholder="<?= tpcms_h($__ph !== '' ? $__ph : 'ファイル名') ?>"
                                   value="<?= tpcms_h($___val) ?>"
                                   <?= $__req ? ' required' : '' ?>
                                   readonly aria-readonly="true" tabindex="-1">
                            <div class="dropzone" data-upload-drop data-upload-target="#<?= tpcms_h($___id) ?>" role="button" aria-label="ここをクリック/ドラッグしてアップロード" tabindex="0">ここをクリック/ドラッグ</div>

                            <!-- 簡易プレビュー（画像のみ／動画は非表示） -->
                            <?php if ($___url !== ''): ?>
                              <?php if (!$___isVideo): // 画像のみプレビュー、動画はサムネ非表示 ?>
                                <div class="preview"><img src="<?= tpcms_h($___url) ?>" alt="" class="thumb"></div>
                              <?php else: ?>
                                <div class="muted small">（動画ファイル：プレビューなし）</div>
                              <?php endif; ?>
                            <?php endif; ?>
							
                            <?php if ($___val !== '' && !preg_match('~^https?://~i', $___val)): ?>
                              <div class="muted" style="margin-top:.3rem">
                                <label class="inline">
                                  <input type="checkbox" name="data_form[<?= $i ?>][<?= tpcms_h($__key) ?>__delete]" value="1">
                                  この画像/動画を削除する
                                </label>
                              </div>
                            <?php endif; ?>

                          </div>

<?php elseif ($__type === 'table'): ?>
  <div class="row">
    <label class="label"><?= tpcms_h($__lab) ?></label>
    <?php
      // 定義
      $__cols  = isset($__f['cols'])  && is_array($__f['cols'])  ? array_values($__f['cols'])  : [];
      $__thead = isset($__f['thead']) && is_array($__f['thead']) ? $__f['thead'] : [];
      $___top  = !empty($__thead['top']);
      $___left = !empty($__thead['left']);

      // 既存データ or 初期行
      $___tbl  = isset($data[$__key]) && is_array($data[$__key]) ? $data[$__key] : [];
      $___rows = [];
      if (isset($___tbl['rows']) && is_array($___tbl['rows'])) {
        $___rows = array_values($___tbl['rows']);
      } else {
        $init = isset($__f['rows']) && is_numeric($__f['rows']) ? (int)$__f['rows'] : 0;
        for ($ri=0; $ri<$init; $ri++) $___rows[] = [];
      }

      // 保存済み thead を優先
      if (isset($___tbl['thead']) && is_array($___tbl['thead'])) {
        $___top  = !empty($___tbl['thead']['top']);
        $___left = !empty($___tbl['thead']['left']);
      }

      // data側cols > 定義cols > rows先頭推定
      $___colsActive = null;
      if (isset($___tbl['cols']) && is_array($___tbl['cols']) && $___tbl['cols']) {
        $___colsActive = array_values($___tbl['cols']);
      } elseif ($__cols) {
        $___colsActive = $__cols;
      } else {
        $___colsActive = [];
        if (isset($___rows[0]) && is_array($___rows[0])) {
          $___keys = array_keys($___rows[0]);
          foreach ($___keys as $idx => $k) {
            $___colsActive[] = [
              'key'   => (string)$k,
              'label' => '',
              // 'is_th' は廃止
            ];
          }
        }
      }

      $___base = 'data_form['.$i.']['.$__key.']';
    ?>
    <div class="tpcms-table" data-base="<?= tpcms_h($___base) ?>">
      <div style="margin-bottom:.4rem">
        <label><input type="checkbox" name="<?= tpcms_h($___base) ?>[thead][top]"  value="1" <?= $___top  ? 'checked' : '' ?>> 上部見出し</label>
        <input type="hidden" name="<?= tpcms_h($___base) ?>[thead][left]" value="0">
        <label style="margin-left:1rem;"><input type="checkbox" name="<?= tpcms_h($___base) ?>[thead][left]" value="1" <?= $___left ? 'checked' : '' ?>> 左見出し</label>
      </div>

      <?php // ★ 列設定はUIに出さず hidden で往復保存 ?>
      <?php foreach ($___colsActive as $ci => $cc):
        $ck  = (string)($cc['key']   ?? '');
        $cl  = (string)($cc['label'] ?? '');
        // $cth は廃止（is_th は使わない）
      ?>
        <input type="hidden" name="<?= tpcms_h($___base) ?>[cols][<?= $ci ?>][key]"   value="<?= tpcms_h($ck) ?>">
        <input type="hidden" name="<?= tpcms_h($___base) ?>[cols][<?= $ci ?>][label]" value="<?= tpcms_h($cl) ?>">
      <?php endforeach; ?>

      <div class="tpcms-table-grid" style="display:flex;gap:.5rem;flex-wrap:nowrap;overflow-x:auto">
        <?php foreach ($___colsActive as $c):
          $ck   = (string)($c['key'] ?? '');
          $cl   = (string)($c['label'] ?? $ck);
          if ($ck === '') continue; ?>
          <div class="tpcms-table-col" data-col-key="<?= tpcms_h($ck) ?>">
            <div class="muted" style="font-weight:bold;"><?= tpcms_h($cl) ?></div>
            <?php foreach ($___rows as $rIdx => $rVal):
              $cellName   = $___base.'[rows]['.$rIdx.']['.$ck.']';
              $cellValRaw = $rVal[$ck] ?? '';
              $cellVal    = is_scalar($cellValRaw) ? (string)$cellValRaw : ''; ?>
              <input class="input" type="text"
                     name="<?= htmlspecialchars($cellName, ENT_QUOTES, 'UTF-8') ?>"
                     value="<?= tpcms_h($cellVal) ?>"
                     style="margin:.25rem 0;">
            <?php endforeach; ?>
          </div>
        <?php endforeach; ?>
      </div>
            <div style="margin-top:.5rem; display:flex; gap:.5rem; flex-wrap:wrap;">
              <div>
                <button class="btn js-table-add-row mb1rem" type="button">＋ 行を追加</button>
                <button class="btn js-table-del-row delete-color" type="button">− 最終行を削除</button>
              </div>
              <div>
                <button class="btn js-table-add-col mb1rem" type="button">＋ 列を追加</button>
                <button class="btn js-table-del-col delete-color" type="button">− 最終列を削除</button>
              </div>
            </div>
    </div>
  </div>


                        <?php elseif ($__type === 'rows'): ?>
                          <div class="row">
                            <label class="label"><?= tpcms_h($__lab) ?></label>
                            <?php
                              $__rowdefs = isset($__f['fields']) && is_array($__f['fields']) ? $__f['fields'] : [];
                              // 既存の配列値（なければ1行だけ空で出す）
                              $___rows = isset($data[$__key]) && is_array($data[$__key]) ? array_values($data[$__key]) : [[]];
                              $___base = 'data_form['.$i.']['.$__key.']';
                              $___rowIndex = 0;
                            ?>
                            <div class="tpcms-rows" data-base="<?= tpcms_h($___base) ?>">
                              <?php foreach ($___rows as $___r): ?>
                                <div class="tpcms-row" style="margin: .6rem 0; padding: .6rem; border: 1px dashed #ddd;">
                                <div class="handle" role="button" aria-label="並べ替え" title="並べ替え" tabindex="0">☰</div>
                                  <?php foreach ($__rowdefs as $___f):
                                    // text / textarea / select / radio をサポート
                                    $___t = strtolower(trim((string)($___f['type'] ?? 'text')));
                                    $___k = (string)($___f['key'] ?? '');
                                    $___l = (string)($___f['label'] ?? $___k);
                                    if ($___k === '') continue;
                                    $___name = $___base.'['.$___rowIndex.']['.$___k.']';

                                    // 配列混入に備えて安全取得
                                    $___raw = $___r[$___k] ?? '';
                                    $___v   = is_scalar($___raw) ? (string)$___raw : '';
                                  ?>
<?php
  // file用 data-image-*（他typeは空）
  $___imw = (int)($___f['image_max_width']  ?? 0);
  $___imh = (int)($___f['image_max_height'] ?? 0);
  $___iq  = (int)($___f['image_quality']    ?? 0);
  $___attrResize = '';
  if ($___imw > 0) $___attrResize .= ' data-image-max-width="'.$___imw.'"';
  if ($___imh > 0) $___attrResize .= ' data-image-max-height="'.$___imh.'"';
  if ($___iq  > 0) { $___iq = max(1, min(100, $___iq)); $___attrResize .= ' data-image-quality="'.$___iq.'"'; }
?>
<div class="row"<?= ($___t === 'file') ? $___attrResize : '' ?>>
  <label class="label"><?= tpcms_h($___l) ?></label>

  <?php if ($___t === 'textarea'): ?>

    <textarea class="input" name="<?= htmlspecialchars($___name, ENT_QUOTES, 'UTF-8') ?>" rows="2"><?= tpcms_h($___v) ?></textarea>

  <?php elseif ($___t === 'select'): ?>

    <?php
      $___opts        = isset($___f['options']) && is_array($___f['options']) ? $___f['options'] : [];
      $___placeholder = isset($___f['placeholder']) ? (string)$___f['placeholder'] : '';
      $___allow_empty = isset($___f['allow_empty']) ? (bool)$___f['allow_empty'] : true;
    ?>
    <select class="input" name="<?= htmlspecialchars($___name, ENT_QUOTES, 'UTF-8') ?>">
      <?php if ($___allow_empty): ?>
        <option value=""><?= tpcms_h($___placeholder !== '' ? $___placeholder : '未選択') ?></option>
      <?php endif; ?>
      <?php foreach ($___opts as $____opt):
        $____val = (string)($____opt[0] ?? '');
        $____txt = (string)($____opt[1] ?? $____val);
      ?>
        <option value="<?= tpcms_h($____val) ?>" <?= ($___v === $____val) ? 'selected' : '' ?>><?= tpcms_h($____txt) ?></option>
      <?php endforeach; ?>
    </select>

  <?php elseif ($___t === 'checkbox'): ?>

    <?php $___opts = isset($___f['options']) && is_array($___f['options']) ? $___f['options'] : []; ?>
    <?php if ($___opts): ?>
      <?php
        // 複数選択：配列POST
        $___nameArr = $___name.'[]';
        $___tmp     = $___r[$___k] ?? [];
        if (is_array($___tmp)) {
          $___sel = array_map('strval', array_values($___tmp));
        } elseif (is_string($___tmp) && $___tmp !== '') {
          $___sel = [ (string)$___tmp ];
        } else {
          $___sel = [];
        }
        $____idx    = 0;
      ?>
      <input type="hidden" name="<?= htmlspecialchars($___nameArr, ENT_QUOTES, 'UTF-8') ?>" value="__EMPTY__">
      <?php foreach ($___opts as $____opt):
        $____val = (string)($____opt[0] ?? '');
        $____txt = (string)($____opt[1] ?? $____val);
        $____id  = 'cbrow_'.$i.'_'.$___rowIndex.'_'.preg_replace('/[^a-z0-9_\-]/i','',$___k).'_'.($____idx++);
      ?>
        <label for="<?= htmlspecialchars($____id, ENT_QUOTES, 'UTF-8') ?>" style="margin-right:1rem;">
          <input
            id="<?= htmlspecialchars($____id, ENT_QUOTES, 'UTF-8') ?>"
            type="checkbox"
            name="<?= htmlspecialchars($___nameArr, ENT_QUOTES, 'UTF-8') ?>"
            value="<?= tpcms_h($____val) ?>"
            <?= in_array($____val, $___sel, true) ? 'checked' : '' ?>
          >
          <?= tpcms_h($____txt) ?>
        </label>
      <?php endforeach; ?>
    <?php else: ?>
      <input type="hidden" name="<?= htmlspecialchars($___name, ENT_QUOTES, 'UTF-8') ?>" value="0">
      <input
        type="checkbox"
        name="<?= htmlspecialchars($___name, ENT_QUOTES, 'UTF-8') ?>"
        value="1"
        <?= in_array((string)$___v, ['1','true','on'], true) ? 'checked' : '' ?>
      >
    <?php endif; ?>

  <?php elseif ($___t === 'radio'): ?>

    <?php
      $___opts = isset($___f['options']) && is_array($___f['options']) ? $___f['options'] : [];
      $____idx = 0;
    ?>
    <?php foreach ($___opts as $____opt):
      $____val = (string)($____opt[0] ?? '');
      $____txt = (string)($____opt[1] ?? $____val);
      $____id  = 'rrow_' . $i . '_' . $___rowIndex . '_' . preg_replace('/[^a-z0-9_\-]/i', '', $___k) . '_' . ($____idx++);
    ?>
      <label for="<?= htmlspecialchars($____id, ENT_QUOTES, 'UTF-8') ?>" style="margin-right:1rem;">
        <input id="<?= htmlspecialchars($____id, ENT_QUOTES, 'UTF-8') ?>" type="radio"
               name="<?= htmlspecialchars($___name, ENT_QUOTES, 'UTF-8') ?>"
               value="<?= tpcms_h($____val) ?>"
               <?= (array_key_exists($___k, (array)$___r) ? ($___v === $____val) : ($____idx === 1)) ? 'checked' : '' ?>>
        <?= tpcms_h($____txt) ?>
      </label>
    <?php endforeach; ?>

  <?php else: ?>

    <?php if ($___t === 'file'): ?>
      <input class="input" type="text"
             name="<?= htmlspecialchars($___name, ENT_QUOTES, 'UTF-8') ?>"
             value="<?= tpcms_h($___v) ?>"
             data-file="1">
    <?php else: ?>
      <input class="input" type="text"
             name="<?= htmlspecialchars($___name, ENT_QUOTES, 'UTF-8') ?>"
             value="<?= tpcms_h($___v) ?>">
    <?php endif; ?>

  <?php endif; ?>
</div>

                                  <?php endforeach; ?>

                                  <button class="btn js-rows-del mb1rem delete-color" type="button">− 行を削除</button>
                                </div>
                                <?php $___rowIndex++; ?>
                              <?php endforeach; ?>
                              <button class="btn js-rows-add" type="button">＋ 行を追加</button>
                            </div>
                          </div>

                        <?php endif;
                      endforeach; ?>

                    </div>
                  <?php endif; ?>

                  <!--JSONブロックを表示させたい場合は、url末尾に &debug=1 をつける。例：page_blocks.php?page=service&debug=1-->
                  <?= isset($__repeaters_html) ? $__repeaters_html : '' ?>
                  <?php if (isset($_GET['debug']) && $_GET['debug'] === '1'): ?>
                  <div class="row" style="margin-top:.6rem;">
                    <label class="label">data（JSONで編集）</label>
                    <textarea name="data_json[<?= $i ?>]" rows="8" class="input" style="font-family: monospace;"><?= tpcms_h($jsonData) ?></textarea>
                  </div>
                  <?php endif; ?>

                  <p><button class="btn1" type="submit" name="op" value="update:<?= $i ?>">このブロックを保存</button></p>
                </div><!-- /.tpcms-edit-pane -->
              </details>

            </li>

            <?php endforeach; ?>
          </ol>
        <?php endif; ?>
		<p class="inline-block"><button class="btn" type="submit" name="op" value="sort">並び順を保存</button></p>
      </section>

    </form>

<?php
// 追加パネル用：tpcms-form を使って“追加用フォーム”を空状態で描画する関数
if (!function_exists('tpcms_render_add_fields')) {
  function tpcms_render_add_fields(array $def): void {
    $fields = isset($def['fields']) && is_array($def['fields']) ? $def['fields'] : [];
    // ----- 単項目（fields） -----
    foreach ($fields as $__f) {
      $__f    = (array)$__f;
      $__type = strtolower(trim((string)($__f['type'] ?? 'text')));
      $__key  = (string)($__f['key']  ?? '');
      if ($__key === '') continue;
      $__lab  = (string)($__f['label'] ?? $__key);
      $__req  = !empty($__f['required']);
      $__ph   = (string)($__f['placeholder'] ?? '');
      $__max  = (int)($__f['maxlength'] ?? 0);

      if ($__type === 'text') {
        ?>
        <div class="row">
          <label class="label"><?= tpcms_h($__lab) ?><?= $__req ? '（必須）' : '' ?></label>
          <input class="input" type="text" name="add_form[<?= tpcms_h($__key) ?>]"
            <?= $__ph  !== '' ? ' placeholder="'.tpcms_h($__ph).'"' : '' ?>
            <?= $__max > 0  ? ' maxlength="'.$__max.'"' : '' ?>
            <?= $__req ? ' required' : '' ?><?= !empty($__f['link_picker']) ? ' data-link-picker="1"' : '' ?>>
        </div>
        <?php

      } elseif ($__type === 'textarea') {
        ?>
        <div class="row">
          <label class="label"><?= tpcms_h($__lab) ?><?= $__req ? '（必須）' : '' ?></label>
          <textarea class="input" name="add_form[<?= tpcms_h($__key) ?>]" rows="3"
            <?= $__ph  !== '' ? ' placeholder="'.tpcms_h($__ph).'"' : '' ?>
            <?= $__max > 0  ? ' maxlength="'.$__max.'"' : '' ?>
            <?= $__req ? ' required' : '' ?>></textarea>
        </div>
        <?php

      } elseif ($__type === 'checkbox') {
        $opts = isset($__f['options']) && is_array($__f['options']) ? $__f['options'] : [];
        ?>
        <div class="row">
          <label class="label"><?= tpcms_h($__lab) ?></label>
          <?php if ($opts): ?>
            <?php
              $name = 'add_form['.$__key.'][]';
              $idx  = 0;
            ?>
            <?php foreach ($opts as $opt):
              $val = (string)($opt[0] ?? '');
              $txt = (string)($opt[1] ?? $val);
              $id  = 'af_cb_'.preg_replace('/[^a-z0-9_\-]/i', '', (string)$__key).'_'.($idx++);
            ?>
            <input type="hidden" name="<?= tpcms_h($name) ?>" value="__EMPTY__">
              <label for="<?= tpcms_h($id) ?>" style="margin-right:1rem">
                <input id="<?= tpcms_h($id) ?>" type="checkbox" name="<?= tpcms_h($name) ?>" value="<?= tpcms_h($val) ?>">
                <?= tpcms_h(__tpcms_strip_nn_prefix($txt)) ?>
              </label>
            <?php endforeach; ?>
          <?php else: ?>
            <input type="hidden" name="add_form[<?= tpcms_h($__key) ?>]" value="0">
            <input type="checkbox" name="add_form[<?= tpcms_h($__key) ?>]" value="1">
          <?php endif; ?>
        </div>
        <?php

      } elseif ($__type === 'radio') {
        $opts = isset($__f['options']) && is_array($__f['options']) ? $__f['options'] : [];
        $name = 'add_form['.$__key.']';
        ?>
        <div class="row">
          <label class="label"><?= tpcms_h($__lab) ?></label>
          <?php $idx = 0; foreach ($opts as $opt): $val = (string)($opt[0] ?? ''); $txt = (string)($opt[1] ?? $val); ?>
            <label style="margin-right:1rem">
              <input type="radio" name="<?= tpcms_h($name) ?>" value="<?= tpcms_h($val) ?>" <?= $idx === 0 ? 'checked' : '' ?>> <?= tpcms_h($txt) ?>
            </label>
          <?php $idx++; endforeach; ?>
        </div>
        <?php

      } elseif ($__type === 'select') {
        $opts        = isset($__f['options']) && is_array($__f['options']) ? $__f['options'] : [];
        $allowEmpty  = isset($__f['allow_empty']) ? (bool)$__f['allow_empty'] : true;
        ?>
        <div class="row">
          <label class="label"><?= tpcms_h($__lab) ?><?= $__req ? '（必須）' : '' ?></label>
          <select class="input" name="add_form[<?= tpcms_h($__key) ?>]" <?= $__req ? ' required' : '' ?>>
            <?php if ($allowEmpty): ?>
              <option value=""><?= tpcms_h($__ph !== '' ? $__ph : '未選択') ?></option>
            <?php endif; ?>
            <?php foreach ($opts as $opt): $val=(string)($opt[0]??''); $txt=(string)($opt[1]??$val); ?>
              <option value="<?= tpcms_h($val) ?>"><?= tpcms_h(__tpcms_strip_nn_prefix($txt)) ?></option>
            <?php endforeach; ?>
          </select>
        </div>
        <?php

      } elseif ($__type === 'file') {
        $id = 'af_'.mt_rand(1000,9999).'_'.mt_rand(1000,9999);
        // ---- 任意リサイズ指定（tpcms-form の定義値から） ----
        $__imw = (int)($__f['image_max_width']  ?? 0);
        $__imh = (int)($__f['image_max_height'] ?? 0);
        $__iq  = (int)($__f['image_quality']    ?? 0);
        $__attrResize = '';
        if ($__imw > 0) $__attrResize .= ' data-image-max-width="'.$__imw.'"';
        if ($__imh > 0) $__attrResize .= ' data-image-max-height="'.$__imh.'"';
        if ($__iq  > 0) { $__iq = max(1, min(100, $__iq)); $__attrResize .= ' data-image-quality="'.$__iq.'"'; }
        ?>
        <div class="row"<?= $__attrResize ?>>
          <label class="label"><?= tpcms_h($__lab) ?><?= $__req ? '（必須）' : '' ?></label>
          <input id="<?= tpcms_h($id) ?>" class="input js-upload-url as-plain-text" type="text"
                 name="add_form[<?= tpcms_h($__key) ?>]"
                 placeholder="<?= tpcms_h($__ph !== '' ? $__ph : 'ファイル名') ?>"
                 <?= $__req ? ' required' : '' ?>
                 readonly aria-readonly="true" tabindex="-1">
          <div class="dropzone" data-upload-drop data-upload-target="#<?= tpcms_h($id) ?>" role="button" aria-label="ここをクリック/ドラッグしてアップロード" tabindex="0">ここをクリック/ドラッグ</div>
        </div>
        <?php

      } elseif ($__type === 'table') {
        // 定義
        $cols  = isset($__f['cols'])  && is_array($__f['cols'])  ? array_values($__f['cols'])  : [];
        $thead = isset($__f['thead']) && is_array($__f['thead']) ? $__f['thead'] : [];
        $top   = !empty($thead['top']);
        $left  = !empty($thead['left']);
        $base  = 'add_form['.$__key.']';

        // 初期行数（未指定は2行）
        $initRows = isset($__f['rows']) && is_int($__f['rows']) ? max(1, (int)$__f['rows']) : 2;

        // ★ UIに出さないcols（往復保存用）：定義が無ければ2列デフォルト
        $colsActive = $cols ?: [
          [ 'key' => 'COL1', 'label' => '' ],
          [ 'key' => 'COL2', 'label' => '' ],
        ];
        ?>
        <div class="row">
          <label class="label"><?= tpcms_h($__lab) ?></label>
          <div class="tpcms-table" data-base="<?= tpcms_h($base) ?>">

            <div style="margin-bottom:.4rem">
              <label><input type="checkbox" name="<?= tpcms_h($base) ?>[thead][top]"  value="1" <?= $top  ? 'checked' : '' ?>> 上部見出し</label>
              <input type="hidden" name="<?= tpcms_h($base) ?>[thead][left]" value="0">
              <label style="margin-left:1rem;"><input type="checkbox" name="<?= tpcms_h($base) ?>[thead][left]" value="1" <?= $left ? 'checked' : '' ?>> 左見出し</label>
            </div>

            <?php // ★ 列設定は hidden で送る（UIには出さない） ?>
            <?php foreach ($colsActive as $ci => $cc):
              $ck  = (string)($cc['key']   ?? '');
              $cl  = (string)($cc['label'] ?? '');
            ?>
              <input type="hidden" name="<?= tpcms_h($base) ?>[cols][<?= $ci ?>][key]"   value="<?= tpcms_h($ck) ?>">
              <input type="hidden" name="<?= tpcms_h($base) ?>[cols][<?= $ci ?>][label]" value="<?= tpcms_h($cl) ?>">
            <?php endforeach; ?>

            <div class="tpcms-table-grid" style="display:flex;gap:.5rem;flex-wrap:nowrap;overflow-x:auto">
              <?php foreach ($colsActive as $c):
                $ckey=(string)($c['key']??''); $clab=(string)($c['label']??$ckey);
                if ($ckey==='') continue; ?>
                <div class="tpcms-table-col" data-col-key="<?= tpcms_h($ckey) ?>">
                  <div class="muted" style="font-size:.8rem;margin:.25rem 0"><?= tpcms_h($clab) ?></div>
                  <?php for ($r=0; $r<$initRows; $r++): ?>
                    <input class="input" type="text" name="<?= tpcms_h($base) ?>[rows][<?= $r ?>][<?= tpcms_h($ckey) ?>]" value="" style="margin:.25rem 0">
                  <?php endfor; ?>
                </div>
              <?php endforeach; ?>
            </div>
            <div style="margin-top:.5rem; display:flex; gap:.5rem; flex-wrap:wrap;">
              <div>
                <button class="btn js-table-add-row mb1rem" type="button">＋ 行を追加</button>
                <button class="btn js-table-del-row delete-color" type="button">− 最終行を削除</button>
              </div>
              <div>
                <button class="btn js-table-add-col mb1rem" type="button">＋ 列を追加</button>
                <button class="btn js-table-del-col delete-color" type="button">− 最終列を削除</button>
              </div>
            </div>
          </div>
        </div>
        <?php
      }
    }

    // ----- 繰り返し（repeaters：ROWS） -----
    $reps = isset($def['repeaters']) && is_array($def['repeaters']) ? $def['repeaters'] : [];
    foreach ($reps as $__rep) {
      $__rep   = (array)$__rep;
      $rKey    = (string)($__rep['key'] ?? '');
      if ($rKey === '') continue;
      $rLab    = (string)($__rep['label'] ?? $rKey);
      $rowdefs = isset($__rep['fields']) && is_array($__rep['fields']) ? $__rep['fields'] : [];
      $base    = 'add_form['.$rKey.']';
      ?>
      <div class="row">
        <label class="label"><?= tpcms_h($rLab) ?></label>
        <div class="tpcms-rows" data-base="<?= tpcms_h($base) ?>">
          <div class="tpcms-row">
          <div class="handle" role="button" aria-label="並べ替え" title="並べ替え" tabindex="0">☰</div>
            <?php foreach ($rowdefs as $rf):
              $rf  = (array)$rf;
              $rt  = strtolower(trim((string)($rf['type']  ?? 'text')));
              $rk  = (string)($rf['key']  ?? '');
              if ($rk==='') continue;
              $rl  = (string)($rf['label'] ?? $rk);
              $ph  = (string)($rf['placeholder'] ?? '');
              $mx  = (int)($rf['maxlength'] ?? 0);
              $nm  = $base.'[0]['.$rk.']';

              if ($rt === 'textarea') { ?>
                <div class="row"><label class="label"><?= tpcms_h($rl) ?></label>
                  <textarea class="input" name="<?= tpcms_h($nm) ?>" rows="2"<?= $ph!==''?' placeholder="'.tpcms_h($ph).'"':'' ?><?= $mx>0?' maxlength="'.$mx.'"':'' ?>></textarea>
                </div>
              <?php } elseif ($rt === 'select') {
                $opts        = isset($rf['options']) && is_array($rf['options']) ? $rf['options'] : [];
                $allowEmpty  = isset($rf['allow_empty']) ? (bool)$rf['allow_empty'] : true;
                $req         = !empty($rf['required']);
                ?>
                <div class="row"><label class="label"><?= tpcms_h($rl) ?><?= $req ? '（必須）' : '' ?></label>
                  <select class="input" name="<?= tpcms_h($nm) ?>"<?= $req ? ' required' : '' ?>>
                    <?php if ($allowEmpty): ?>
                      <option value=""><?= tpcms_h($ph !== '' ? $ph : '未選択') ?></option>
                    <?php endif; ?>
                    <?php foreach ($opts as $op): $ov=(string)($op[0]??''); $ot=(string)($op[1]??$ov); ?>
                      <option value="<?= tpcms_h($ov) ?>"><?= tpcms_h($ot) ?></option>
                    <?php endforeach; ?>
                  </select>
                </div>
              <?php } elseif ($rt === 'checkbox') { ?>
                <?php
                  $opts = isset($rf['options']) && is_array($rf['options']) ? $rf['options'] : [];
                ?>
                <div class="row"><label class="label"><?= tpcms_h($rl) ?></label>
                  <?php if ($opts): ?>
                    <?php
                      $nmArr = $base.'[0]['.$rk.'][]';
                      $idx2  = 0;
                    ?>
                    <?php foreach ($opts as $op): $ov=(string)($op[0]??''); $ot=(string)($op[1]??$ov); ?>
                      <label style="margin-right:1rem"><input type="checkbox" name="<?= tpcms_h($nmArr) ?>" value="<?= tpcms_h($ov) ?>"> <?= tpcms_h($ot) ?></label>
                      <?php $idx2++; ?>
                    <?php endforeach; ?>
                  <?php else: ?>
                    <input type="hidden" name="<?= tpcms_h($nm) ?>" value="0">
                    <input type="checkbox" name="<?= tpcms_h($nm) ?>" value="1">
                  <?php endif; ?>
                </div>
              <?php } elseif ($rt === 'radio') {
                $opts = isset($rf['options']) && is_array($rf['options']) ? $rf['options'] : []; ?>
                <div class="row"><label class="label"><?= tpcms_h($rl) ?></label>
                  <?php $idx=0; foreach ($opts as $op): $ov=(string)($op[0]??''); $ot=(string)($op[1]??$ov); ?>
                    <label style="margin-right:1rem"><input type="radio" name="<?= tpcms_h($nm) ?>" value="<?= tpcms_h($ov) ?>" <?= $idx===0?'checked':'' ?>> <?= tpcms_h($ot) ?></label>
                    <?php $idx++; ?>
                  <?php endforeach; ?>
                </div>
              <?php } elseif ($rt === 'file') { ?>
                <?php
                  // 任意リサイズ指定（tpcms-form 定義から）
                  $__imw = (int)($rf['image_max_width']  ?? 0);
                  $__imh = (int)($rf['image_max_height'] ?? 0);
                  $__iq  = (int)($rf['image_quality']    ?? 0);
                  $__attrResize = '';
                  if ($__imw > 0) $__attrResize .= ' data-image-max-width="'.$__imw.'"';
                  if ($__imh > 0) $__attrResize .= ' data-image-max-height="'.$__imh.'"';
                  if ($__iq  > 0) { $__iq = max(1, min(100, $__iq)); $__attrResize .= ' data-image-quality="'.$__iq.'"'; }
                ?>
                <div class="row"<?= $__attrResize ?>><label class="label"><?= tpcms_h($rl) ?></label>
                  <input class="input js-upload-url as-plain-text" type="text"
                         name="<?= tpcms_h($nm) ?>"
                         data-file="1"
                         data-csrf="<?= tpcms_h($_SESSION['_csrf'] ?? '') ?>"
                         placeholder="ファイル名"
                         readonly aria-readonly="true" tabindex="-1">
                </div>
              <?php } else { // text
                ?>
                <div class="row"><label class="label"><?= tpcms_h($rl) ?></label>
                  <input class="input" type="text" name="<?= tpcms_h($nm) ?>"<?= $ph!==''?' placeholder="'.tpcms_h($ph).'"':'' ?><?= $mx>0?' maxlength="'.$mx.'"':'' ?><?= !empty($rf['link_picker']) ? ' data-link-picker="1"' : '' ?>>
                </div>
                <?php
              }
            endforeach; ?>
            <div class="row">
              <button class="btn xs js-rows-del mb1rem delete-color" type="button">− 行を削除</button>
            </div>
          </div>
          <button class="btn js-rows-add" type="button">＋ 行を追加</button>
        </div>
      </div>
      <?php
    }
  }
}
?>

<section class="add-panel">
  <details>
    <summary>新しいブロックを追加する</summary>
    <form method="post" action="./page_blocks.php?page=<?= tpcms_h($slug) ?>">
    <input type="hidden" name="_csrf" value="<?= tpcms_h(tpcms_csrf_token()) ?>">
      <?php if (function_exists('tpcms_csrf_token')): ?>
        <input type="hidden" name="csrf" value="<?= tpcms_h(tpcms_csrf_token()) ?>">
      <?php endif; ?>

      <div class="row" style="margin-top:.8rem;">
        <label class="label">パーツ選択</label>
        <?php
          // 除外する partial ファイル（中央集約）
          $excludeFiles = ['credit.html','header.html','footer.html', 'mainimg.html'];

          // --- 1回のみ使用（once:true）を既に使っている場合は除外 ---
          $usedPartials = [];
          foreach (($page['blocks'] ?? []) as $b) {
            $p = basename((string)($b['partial'] ?? ''));
            if ($p !== '') { $usedPartials[$p] = ($usedPartials[$p] ?? 0) + 1; }
          }
          foreach ($partialFiles as $pf) {
            $def = tpcms_formdef_load_by_name($pf);
            if (!empty($def['once']) && !empty($usedPartials[$pf])) {
              $excludeFiles[] = $pf;
            }
          }

          // カテゴリごとに option を構築（label があれば優先表示）
          $byCat = [];
          foreach ($partialFiles as $pf) {
            if (in_array($pf, $excludeFiles, true)) continue;
                    $def = tpcms_formdef_load_by_name($pf);  // label / category / help_html 等
            $cat = $def['category'] ?? '未分類';
            $lab = isset($def['label']) ? trim((string)$def['label']) : '';
            
            //$txt = ($lab !== '') ? "{$lab}（{$pf}）" : $pf; // ← ●●●ファイル名も併記したい時はこの行を生かす
            $txt = ($lab !== '') ? $lab : $pf;                 // ← ●●●いまの運用：ラベルだけ表示

            $byCat[$cat][] = [$pf, $txt];
          }

          // カテゴリ名でソート／カテゴリ内は表示名でソート
          ksort($byCat, SORT_NATURAL);
          foreach ($byCat as &$opts) {
            usort($opts, fn($a,$b) => strnatcasecmp($a[1], $b[1]));
          }
          unset($opts);

          // 「未分類」はプルダウンに出さない
          if (isset($byCat['未分類'])) {
            unset($byCat['未分類']);
          }
        ?>
        <select id="add_partial_select" name="partial" required>
          <?php foreach ($byCat as $cat => $opts): ?>
            <optgroup label="<?= tpcms_h(__tpcms_strip_nn_prefix($cat)) ?>">
              <?php foreach ($opts as [$val, $txt]): ?>
                <option value="<?= tpcms_h($val) ?>"><?= tpcms_h(__tpcms_strip_nn_prefix($txt)) ?></option>
              <?php endforeach; ?>
            </optgroup>
          <?php endforeach; ?>
        </select>
      </div>

      <?php
        $first = $partialFiles[0] ?? '';
        $firstDef = $first ? tpcms_formdef_load_by_name($first) : null;
      ?>
      <div id="add_form_container" class="tpcms-add-form" style="margin-top:.6rem">
        <?php if ($firstDef): ?>
          <?php if (!empty($firstDef['help_html'])): ?>
            <div class="muted" style="margin:.25rem 0 .6rem"><?= $firstDef['help_html'] ?></div>
          <?php endif; ?>
          <?php tpcms_render_add_fields($firstDef); ?>
        <?php else: ?>
          <p class="muted">このパーツには入力フォームはありません。</p>
        <?php endif; ?>
      </div>

      <input type="hidden" name="use_add_form" value="1">
      <div class="row" style="margin-top:.6rem">
        <button class="btn1" type="submit" name="op" value="add-block">このブロックを保存する</button>
      </div>

      <!-- 全パーツのテンプレート（JSで差し替え） -->
      <div id="add_form_templates" hidden>
        <?php foreach ($partialFiles as $pf):
          if (in_array($pf, $excludeFiles, true)) continue;
          $def = tpcms_formdef_load_by_name($pf);
          if (!$def) continue; ?>
          <template data-partial="<?= tpcms_h($pf) ?>">
            <?php if (!empty($def['help_html'])): ?>
              <div class="muted" style="margin:.25rem 0 .6rem"><?= $def['help_html'] ?></div>
            <?php endif; ?>
            <?php tpcms_render_add_fields($def); ?>
          </template>
        <?php endforeach; ?>
      </div>
    </form>
  </details>
</section>

  </div>

  <script src="./assets/admin.js"></script>

<script>
(function(){
  var sel  = document.getElementById('add_partial_select');
  var wrap = document.getElementById('add_form_container');
  function swap(){
    if (!sel || !wrap) return;
    var tpl = document.querySelector('#add_form_templates template[data-partial="'+ sel.value +'"]');
    if (tpl) { wrap.innerHTML = ''; wrap.appendChild(tpl.content.cloneNode(true)); }
    else     { wrap.innerHTML = '<p class="muted">このパーツにはフォーム定義がありません。</p>'; }
  }
  if (sel && wrap) {
    sel.addEventListener('change', swap);
    // 初期化（ページ読み込み時）
    swap();
  }
})();
</script>

<script>
document.addEventListener('click', function (ev) {
  // 追加
  var add = ev.target.closest('.js-rows-add');
  if (add) {
    ev.preventDefault();
    var wrap = add.closest('.tpcms-rows');
    if (!wrap) return;
    var rows = wrap.querySelectorAll('.tpcms-row');
    var last = rows[rows.length - 1];
    if (!last) return;
    var clone = last.cloneNode(true);

    // 値クリア（checkbox/radioも初期化）
    clone.querySelectorAll('input,textarea').forEach(function (el) {
      if (el.type === 'checkbox' || el.type === 'radio') { el.checked = false; }
      else { el.value = ''; }
    });

    // data-file="1" の拡張UIをリセット（ID/ドロップゾーン/プレビュー/削除チェック）
    clone.querySelectorAll('input[type="text"][data-file="1"]').forEach(function (inp) {
      // 旧強化フラグとIDを除去（再強化でユニークIDを再採番）
      inp.removeAttribute('data-enhanced-file-ui');
      try { delete inp.dataset.enhancedFileUi; } catch(_) {}
      inp.removeAttribute('id');
      inp.value = '';

      // 直後に挿入されるUIを掃除（dropzone / preview / muted[削除チェック]）
      var sib = inp.nextElementSibling;
      while (sib && sib.classList && (sib.classList.contains('dropzone') || sib.classList.contains('preview') || sib.classList.contains('muted'))) {
        var next = sib.nextElementSibling;
        sib.remove();
        sib = next;
      }
    });

    // data-link-picker の入力もリセット（ID/ボタン/付与フラグを除去 → 観測器が新ボタンを再付与）
    clone.querySelectorAll('input[type="text"][data-link-picker]').forEach(function (inp) {
      // 旧フラグ/IDを外す（attachButton が新しいIDとボタンを付与）
      inp.removeAttribute('data-linkpicker-attached');
      try { delete inp.dataset.linkpickerAttached; } catch(_){}
      inp.removeAttribute('id');

      // クローンされてきた旧ボタン（直後の .tpcms-link-btn）があれば除去
      var sib = inp.nextElementSibling;
      if (sib && sib.classList && sib.classList.contains('tpcms-link-btn')) {
        sib.remove();
      }
    });

    wrap.insertBefore(clone, add);
    renumberRows(wrap);
    
    // rename後に、clone内の各ラジオグループで未選択なら先頭だけON（既存行は触らない）
    (function(row){
      var seen = new Set();
      row.querySelectorAll('input[type="radio"][name]').forEach(function (el) {
        var nm = el.name;
        if (seen.has(nm)) return;
        var any = row.querySelector('input[type="radio"][name="'+ nm.replace(/"/g, '\\"') +'"]:checked');
        if (!any) el.checked = true;
        seen.add(nm);
      });
    })(clone);

    return;
  }
  
  // 削除
  var del = ev.target.closest('.js-rows-del');
  if (del) {
    ev.preventDefault();
    var row  = del.closest('.tpcms-row');
    var wrap = row && row.closest('.tpcms-rows');
    if (!row || !wrap) return;

    // ★ 最後の1ブロックは削除不可（警告を出して中止）
    var rows = wrap.querySelectorAll('.tpcms-row');
    if (rows.length <= 1) {
      alert('繰り返しブロックには最低でも1ブロック必要です。');
      return;
    }

    row.remove();
    renumberRows(wrap);
  }
});
function renumberRows(wrap) {
  var base = wrap.getAttribute('data-base');
  var esc  = base.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  var re   = new RegExp('^' + esc + '\\[[0-9]+\\]');
  wrap.querySelectorAll('.tpcms-row').forEach(function (row, idx) {
    row.querySelectorAll('input[name], textarea[name], select[name]').forEach(function (el) {
      var name = el.getAttribute('name');
      if (!name) return;
      el.setAttribute('name', name.replace(re, base + '[' + idx + ']'));
    });
  });
}

 // --- table: 行・列の追加/削除 ---
 document.addEventListener('click', function (ev) {
   var btn;

   // ===== 行を追加 =====
   btn = ev.target.closest('.js-table-add-row');
   if (btn) {
     ev.preventDefault();
     var wrap = btn.closest('.tpcms-table'); if (!wrap) return;
     var base = wrap.getAttribute('data-base');

     // 既存列ごとに、次の行インデックスで空セルを1つ追加
     wrap.querySelectorAll('.tpcms-table-col').forEach(function (col) {
       var key = col.getAttribute('data-col-key');
       var inputs = col.querySelectorAll('input.input');
       var next = inputs.length; // 0始まりの次インデックス
       var name = base + '[rows][' + next + '][' + key + ']';
       var inp = document.createElement('input');
       inp.className = 'input';
       inp.type = 'text';
       inp.name = name;
       inp.value = '';
       inp.style.margin = '.25rem 0';
       col.appendChild(inp);
     });
     return;
   }

   // ===== 最終行を削除 =====
   btn = ev.target.closest('.js-table-del-row');
   if (btn) {
     ev.preventDefault();
     var wrap = btn.closest('.tpcms-table'); if (!wrap) return;

     // 各列の最終入力を1つずつ削除（0行未満にはしない）
     var canDel = true;
     wrap.querySelectorAll('.tpcms-table-col').forEach(function (col) {
       if (col.querySelectorAll('input.input').length === 0) canDel = false;
     });
     if (!canDel) return;
     wrap.querySelectorAll('.tpcms-table-col').forEach(function (col) {
       var inputs = col.querySelectorAll('input.input');
       var last = inputs[inputs.length - 1];
       if (last) last.remove();
     });
     return;
   }

   // ===== 列を追加 =====
   btn = ev.target.closest('.js-table-add-col');
   if (btn) {
     ev.preventDefault();
     var wrap = btn.closest('.tpcms-table'); if (!wrap) return;
     var base = wrap.getAttribute('data-base');

     var grid = wrap.querySelector('.tpcms-table-grid');
     if (!grid) return;

     // 既存キー集合
     var keys = Array.prototype.map.call(
       wrap.querySelectorAll('.tpcms-table-col'),
       function(col){ return col.getAttribute('data-col-key') || ''; }
     );

     // 新キーを COL1.. で自動採番（重複回避）
     var n = 1, newKey;
     do { newKey = 'COL' + n++; } while (keys.indexOf(newKey) !== -1);

     // 既存行数（任意の列の入力数）を取得
     var rowsCount = 0;
     var anyCol = wrap.querySelector('.tpcms-table-col');
     if (anyCol) rowsCount = anyCol.querySelectorAll('input.input').length;

     // 表示用ラベル（編集UIでは入力させない方針）：新しい列N
     var label = '新しい列' + (keys.length + 1);

     // --- hidden: cols[][key|label] を末尾に生成（is_th は廃止）
     var idx = keys.length; // 新列は末尾インデックス
     var hKey   = document.createElement('input');
     var hLabel = document.createElement('input');
     hKey.type='hidden';   hLabel.type='hidden';
     hKey.name   = base + '[cols][' + idx + '][key]';
     hLabel.name = base + '[cols][' + idx + '][label]';
     hKey.value   = newKey;
     hLabel.value = label;
     // hiddenは wrap 直下に置けばOK
     wrap.appendChild(hKey); wrap.appendChild(hLabel);

     // --- 見た目の列DOMを生成
     var col = document.createElement('div');
     col.className = 'tpcms-table-col';
     col.setAttribute('data-col-key', newKey);

     // 既存のヘッダ要素のクラス/スタイルをコピー（追加パネル/編集パネルの差異を吸収）
     var head = document.createElement('div');
     var sampleHead = grid.querySelector('.tpcms-table-col > div');
     if (sampleHead) {
       head.className = sampleHead.className || 'muted';
       var st = sampleHead.getAttribute('style');
       if (st) head.setAttribute('style', st);
     } else {
       // 念のためのフォールバック（編集ブロック相当）
       head.className = 'muted';
       head.style.fontWeight = 'bold';
     }
     head.textContent = label;
     col.appendChild(head);

     for (var r=0; r<rowsCount; r++) {
       var inp = document.createElement('input');
       inp.className = 'input';
       inp.type = 'text';
       inp.name = base + '[rows][' + r + '][' + newKey + ']';
       inp.value = '';
       inp.style.margin = '.25rem 0';
       col.appendChild(inp);
     }

     grid.appendChild(col);
     return;
   }

   // ===== 最終列を削除 =====
   btn = ev.target.closest('.js-table-del-col');
   if (btn) {
     ev.preventDefault();
     var wrap = btn.closest('.tpcms-table'); if (!wrap) return;
     var base = wrap.getAttribute('data-base');

     var cols = wrap.querySelectorAll('.tpcms-table-col');
     if (!cols || cols.length <= 1) return; // 最低1列は残す

     var lastCol = cols[cols.length - 1];
     var lastKey = lastCol.getAttribute('data-col-key') || '';

     // hidden の cols[<idx>] を末尾インデックスで削除
     var lastIdx = cols.length - 1;
     var sel = [
       'input[type="hidden"][name="'+ base + '[cols]['+ lastIdx +'][key]"]',
       'input[type="hidden"][name="'+ base + '[cols]['+ lastIdx +'][label]"]'
     ].join(',');
     wrap.querySelectorAll(sel).forEach(function(h){ h.parentNode && h.parentNode.removeChild(h); });

     // 見た目の列DOMを削除（セルも丸ごと消える）
     lastCol.parentNode.removeChild(lastCol);
     return;
   }
 }, false);

</script>

<script>
/* --- ROWS: 行追加後のみ検知して初期化（重複追加なし）＋ file UI フォールバック付き --- */
(function () {
  if (window.__tpcmsRowsObserverInstalled) return;
  window.__tpcmsRowsObserverInstalled = true;

  function renumberRowsLocal(wrap) {
    var base = wrap.getAttribute('data-base'); // 例: data_form[0][ROWS]
    if (!base) return;
    var prefix = base + '[';
    var rows = wrap.querySelectorAll('.tpcms-row');
    rows.forEach(function (row, idx) {
      row.querySelectorAll('input, textarea, select').forEach(function (el) {
        if (!el.name) return;
        var p = el.name.indexOf(prefix);
        if (p === -1) return;
        var head = el.name.slice(0, p + prefix.length);
        var rest = el.name.slice(p + prefix.length).replace(/^\d+/, String(idx));
        el.name = head + rest;
      });
    });
  }

  // --- フォールバック：data-file="1" テキスト入力に、クリック/ドラッグのUIを付与 ---
  function ensureUploadUI(inp) {
    if (!inp || inp.dataset.enhancedFileUi === '1') return;

    // すでに近接に dropzone があるなら何もしない
    var hasDZ = inp.nextElementSibling && inp.nextElementSibling.classList && inp.nextElementSibling.classList.contains('dropzone');
    if (hasDZ) { inp.dataset.enhancedFileUi = '1'; return; }

    inp.dataset.enhancedFileUi = '1';

    var dz = document.createElement('div');
    dz.className = 'dropzone';
    dz.style.marginTop = '.3rem';
    dz.style.padding = '.5rem';
    dz.style.border = '1px dashed #ccc';
    dz.style.textAlign = 'center';
    dz.textContent = 'ここをクリック / ドラッグ';

    // hidden file picker
    var picker = document.createElement('input');
    picker.type = 'file';
    picker.style.display = 'none';

    function showPreview(value, json) {
      var prev = dz.nextElementSibling;
      if (!prev || !prev.classList || !prev.classList.contains('preview')) {
        prev = document.createElement('div');
        prev.className = 'preview';
        prev.style.marginTop = '.3rem';
        dz.insertAdjacentElement('afterend', prev);
      }
      var url = (json && json.url) ? json.url : ((/^https?:/i.test(value) || (value && value[0] === '/')) ? value : '/uploads/' + value);
      prev.innerHTML = '';
      if (/\.(png|jpe?g|gif|webp|svg)$/i.test(url)) {
        var img = new Image();
        img.src = url;
        img.style.maxWidth = '180px';
        img.style.height = 'auto';
        prev.appendChild(img);
      } else if (/\.(mp4|webm|ogg)$/i.test(url)) {
        var v = document.createElement('video');
        v.src = url; v.controls = true; v.playsInline = true;
        v.style.maxWidth = '240px'; v.style.maxHeight = '160px';
        prev.appendChild(v);
      } else {
        prev.textContent = value || '';
      }
    }

function uploadFile(file) {
  var csrf  = inp.getAttribute('data-csrf') || '';
  var form  = inp.closest('form');
  var prev  = (inp.value || '').trim(); // 差替前の値を保持（後で旧ファイル削除に使用）
  var fd    = new FormData();
  fd.append('_csrf', csrf);
  fd.append('file', file);

  // ▼ 追加：リサイズ指定を最近傍のホルダーから拾って付与（data-image-*）
  //   <div class="row" data-image-max-width="800" ...> のような親要素を探索
  var holder = inp.closest('[data-image-max-width],[data-image-max-height],[data-image-quality]');
  if (holder) {
    var imw = holder.getAttribute('data-image-max-width');
    var imh = holder.getAttribute('data-image-max-height');
    var iq  = holder.getAttribute('data-image-quality');
    if (imw && /^\d+$/.test(imw)) fd.append('image_max_width',  imw);
    if (imh && /^\d+$/.test(imh)) fd.append('image_max_height', imh);
    if (iq  && /^\d+$/.test(iq))  fd.append('image_quality',    iq);
  }

  fetch('./upload.php', { method: 'POST', body: fd, credentials: 'same-origin' })
    .then(function (r) { return r.json(); })
    .then(function (json) {
      if (!json || json.ok !== true) throw json || {};
      var name  = json.name || json.filename || json.file || json.basename || '';
      var value = json.url ? json.url : name;

      // 差替前のローカルファイルを削除対象に積む（外部URLはサーバ側で無視）
      if (prev && form) {
        var h = document.createElement('input');
        h.type  = 'hidden';
        h.name  = 'rows_deleted_files[]';
        h.value = prev;
        form.appendChild(h);
      }

      inp.value = value;
      showPreview(value, json);
    })
    .catch(function (err) {
      try { inp.dispatchEvent(new CustomEvent('tpcms:upload-error', { bubbles: true, detail: err || {} })); } catch (_){}
      alert('アップロードに失敗しました');
    });
}

    dz.addEventListener('click', function () { picker.click(); });
    picker.addEventListener('change', function () {
      var f = picker.files && picker.files[0];
      if (f) uploadFile(f);
    });

    dz.addEventListener('dragover', function (ev) { ev.preventDefault(); dz.classList.add('drag'); });
    dz.addEventListener('dragleave', function () { dz.classList.remove('drag'); });
    dz.addEventListener('drop', function (ev) {
      ev.preventDefault(); dz.classList.remove('drag');
      var f = ev.dataTransfer && ev.dataTransfer.files && ev.dataTransfer.files[0];
      if (f) uploadFile(f);
    });

    inp.insertAdjacentElement('afterend', dz);
    // picker は行内に置いておく（フォーム外でもOKだがここに添付）
    dz.appendChild(picker);

    // 既存値がすでにあるときはプレビューも出しておく
    if (inp.value) { showPreview(inp.value, null); }
  }

  function enhanceRow(row) {
    // nameインデックスを整える
    var wrap = row.closest('.tpcms-rows');
    if (wrap) renumberRowsLocal(wrap);

    // 既存実装があればそれを優先（例：admin.js 内の強化関数）
    if (window.tpcmsEnhanceFileInputs) {
      try { window.tpcmsEnhanceFileInputs(row); } catch (_) {}
    }

    // フォールバック：強化されていない data-file="1" を補完
    row.querySelectorAll('input[type="text"][data-file="1"]').forEach(function (inp) {
      var hasDZ = inp.nextElementSibling && inp.nextElementSibling.classList && inp.nextElementSibling.classList.contains('dropzone');
      if (!hasDZ) ensureUploadUI(inp);
    });

    // 互換用イベント（必要なら外部がフック）
    try { row.dispatchEvent(new CustomEvent('tpcms:rows-added', { bubbles: true, detail: { row: row } })); } catch (_){}
  }

  function observeContainer(container) {
    if (!container || container.__rowsObs) return;
    var obs = new MutationObserver(function (muts) {
      muts.forEach(function (m) {
        m.addedNodes && m.addedNodes.forEach(function (n) {
          if (n && n.nodeType === 1 && n.classList && n.classList.contains('tpcms-row')) {
            enhanceRow(n);
          }
        });
      });
    });
    obs.observe(container, { childList: true });
    container.__rowsObs = obs;
  }

  // 初期化：現存行にもUIを保証
  document.addEventListener('DOMContentLoaded', function () {
    document.querySelectorAll('.tpcms-rows').forEach(function (rows) {
      observeContainer(rows);
      // 既存行にもフォールバック適用
      rows.querySelectorAll('.tpcms-row').forEach(enhanceRow);
    });
  });

  // 「＋ 行を追加」クリック時、対象コンテナの監視だけ保証（追加自体は既存処理に任せる）
  document.addEventListener('click', function (e) {
    var btn = e.target && e.target.closest && e.target.closest('.js-rows-add');
    if (!btn) return;
    var container = btn.closest('.tpcms-rows');
    observeContainer(container);
    // 既存実装が初期化しないケースに備え、少し遅延してフォールバック適用
    setTimeout(function () {
      container.querySelectorAll('.tpcms-row:last-of-type input[type="text"][data-file="1"]').forEach(ensureUploadUI);
    }, 0);
  });
})();
</script>

<script>
// --- ROWS: 行削除時に、その行のFILE値を hidden に積む（キャプチャ段階で先取り） ---
document.addEventListener('click', function (e) {
  var btn = e.target && e.target.closest && e.target.closest('.js-rows-del');
  if (!btn) return;

  var row  = btn.closest('.tpcms-row');
  var form = btn.closest('form');
  if (!row || !form) return;

  // ★ 最後の1ブロックは削除不可：隠し入力の積み増しも行わない
  var wrap = row && row.closest('.tpcms-rows');
  if (wrap) {
    var rows = wrap.querySelectorAll('.tpcms-row');
    if (rows.length <= 1) return;
  }

  // 行がDOMから消される前（キャプチャ段階）に拾ってPOSTへ積む
  row.querySelectorAll('input[type="text"][data-file="1"]').forEach(function (inp) {
    var v = (inp.value || '').trim();
    if (!v) return;
    var h = document.createElement('input');
    h.type  = 'hidden';
    h.name  = 'rows_deleted_files[]';
    h.value = v;
    form.appendChild(h);
  });
}, true); // ← 第3引数 true でキャプチャ段階
</script>

<script>
// --- ROWS: 「この画像/動画を削除する」チェックON→ 旧ファイルをPOSTへ積む（保存時に物理削除） ---
document.addEventListener('change', function (e) {
  var cb = e.target;
  if (!cb || cb.type !== 'checkbox') return;
  if (!cb.name || !/__delete$/.test(cb.name)) return; // xxx__delete だけ対象

  var row  = cb.closest('.tpcms-row');
  var form = cb.closest('form');
  if (!row || !form) return;

  // 対応する file 入力名（__delete を外したもの）
  var baseName = cb.name.replace(/__delete$/, '');
  var inp = null;
  row.querySelectorAll('input[type="text"][data-file="1"]').forEach(function (el) {
    if (el.name === baseName) inp = el;
  });
  if (!inp) return;

  var mark = form.querySelector('input[type="hidden"][name="rows_deleted_files[]"][data-del-for="'+ baseName +'"]');
  if (cb.checked) {
    var v = (inp.value || '').trim();
    if (!v) return;
    if (!mark) {
      mark = document.createElement('input');
      mark.type = 'hidden';
      mark.name = 'rows_deleted_files[]';
      mark.setAttribute('data-del-for', baseName);
      form.appendChild(mark);
    }
    mark.value = v; // 直近の値を設定
  } else {
    if (mark) mark.remove(); // 取り消し
  }
});
</script>

<script>
// Enter で保存。ただし IME 変換中は発火させない（ページ基本情報と同等の体感）
document.addEventListener('keydown', function(e){
  if (e.key !== 'Enter') return;

  var t = e.target;
  if (!t || !t.closest) return;

  // 編集パネル内のみ対象（ページ基本情報は対象外のまま）
  var inPane = t.closest('.tpcms-edit-pane');
  if (!inPane) return;

  var tag = (t.tagName || '').toLowerCase();
  if (tag === 'textarea') return; // textarea は改行優先

  // ★ ここが肝：IME中は保存しない
  if (e.isComposing === true || e.keyCode === 229) return;

  // 入力系だけを対象（ボタン/リンク等は無視）
  if (!(t instanceof HTMLInputElement || t instanceof HTMLSelectElement)) return;

  // 既定送信を止めて、このブロックの update:◯ を押す
  var btn = inPane.querySelector('button[type="submit"][name="op"][value^="update:"]');
  if (!btn) return;

  e.preventDefault();
  btn.click();
}, true);
</script>

<!-- ====== Link Picker (menu.jsonベース) ====== -->
<script>
// 現在ページの slug をJSへ
window.TPCMS_CURRENT_SLUG = <?= json_encode($slug, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
</script>
<style>
  .tpcms-linkpicker-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.15);z-index:9998;display:none}
  .tpcms-linkpicker{position:fixed;z-index:9999;inset:auto 0 0 0;max-width:480px;margin:10vh auto;background:#fff;border:1px solid #ddd;border-radius:10px;box-shadow:0 10px 30px rgba(0,0,0,.15);overflow:hidden;display:none}
  .tpcms-linkpicker h3{margin:0;padding:.8rem 1rem;border-bottom:1px solid #eee;font-size:1rem;background:#f7f7f7}
  .tpcms-linkpicker .list{max-height:50vh;overflow:auto;padding:.5rem}
  .tpcms-linkpicker .item{display:block;width:100%;text-align:left;padding:.5rem .75rem;border-radius:6px;border:1px solid transparent;background:#fff;cursor:pointer}
  .tpcms-linkpicker .item:hover{background:#f4f7ff;border-color:#cfe1ff}
  .tpcms-linkpicker .slug{color:#666;font-size:.85em;margin-left:.4rem}
  .tpcms-linkpicker .foot{display:flex;justify-content:flex-end;gap:.5rem;padding:.6rem .8rem;border-top:1px solid #eee;background:#fafafa}
  .tpcms-link-btn{margin-left:.4rem;vertical-align:middle;display:inline-flex;align-items:center;gap:.25rem}
  .tpcms-link-btn i{pointer-events:none}
  @media (max-width:540px){ .tpcms-linkpicker{max-width:94%;} }
</style>
<div class="tpcms-linkpicker-backdrop" id="tpcmsLinkBackdrop"></div>
<div class="tpcms-linkpicker" id="tpcmsLinkPicker" role="dialog" aria-modal="true" aria-labelledby="tpcmsLinkPickerTitle">
  <h3 id="tpcmsLinkPickerTitle">リンク先を選択</h3>
  <div class="list" id="tpcmsLinkPickerList" tabindex="0" aria-label="メニュー候補"></div>
  <div class="foot">
    <button type="button" class="btn" id="tpcmsLinkPickerClose">閉じる</button>
  </div>
</div>
<script>
(function(){
  var CANDS = Array.isArray(window.TPCMS_MENU_CANDIDATES) ? window.TPCMS_MENU_CANDIDATES : [];
  var CUR   = String(window.TPCMS_CURRENT_SLUG || '');
  var lpBtnClass = 'js-linkpicker-btn';
  var picker   = document.getElementById('tpcmsLinkPicker');
  var listBox  = document.getElementById('tpcmsLinkPickerList');
  var closeBtn = document.getElementById('tpcmsLinkPickerClose');
  var backdrop = document.getElementById('tpcmsLinkBackdrop');
  var targetInput = null;

  function normValueForCurrent(slug){
    slug = String(slug || '');
    // 外部URLはそのまま（ここでは候補に外部は含めていない想定）
    if (slug.indexOf('#') === 0) {
      // アンカー：index以外からは ./#xxx にして“トップの#へ戻る”挙動に揃える
      return (CUR === 'index') ? slug : ('./' + slug);
    }
    return slug;
  }

  function openPicker(forInput){
    targetInput = forInput || null;
    if (!picker || !listBox) return;
    // 一覧を構築
    listBox.innerHTML = '';
    if (!CANDS.length) {
      var p = document.createElement('p');
      p.style.padding = '.8rem';
      p.textContent = '候補がありません（メニュー未設定）';
      listBox.appendChild(p);
    } else {
      CANDS.forEach(function(c){
        var b = document.createElement('button');
        b.type = 'button';
        b.className = 'item';
        b.setAttribute('data-slug', c.slug);
        b.innerHTML = '<span class="label">'+ escapeHtml(c.label || c.slug) +'</span>'
                    + '<span class="slug">'+ escapeHtml(c.slug) +'</span>';
        b.addEventListener('click', function(){
          var v = normValueForCurrent(this.getAttribute('data-slug'));
          if (targetInput) {
            targetInput.value = v;
            try { targetInput.dispatchEvent(new Event('input', {bubbles:true})); } catch(_){}
          }
          hidePicker();
        });
        listBox.appendChild(b);
      });
    }
    // 表示
    backdrop.style.display = 'block';
    picker.style.display   = 'block';
    // フォーカス
    setTimeout(function(){ try{ listBox.focus(); }catch(_){} }, 0);
  }
  function hidePicker(){
    picker.style.display   = 'none';
    backdrop.style.display = 'none';
    targetInput = null;
  }
  function escapeHtml(s){
    return String(s).replace(/[&<>"']/g, function(ch){
      return ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[ch]);
    });
  }

  if (closeBtn) closeBtn.addEventListener('click', hidePicker);
  if (backdrop) backdrop.addEventListener('click', hidePicker);
  document.addEventListener('keydown', function(e){
    if (e.key === 'Escape' && picker && picker.style.display === 'block') hidePicker();
  });

  // 対象inputの検出：URL/リンク/slug/href っぽいname（data-file除外）
  var LINKPICKER_REQUIRE_ATTR = true; // 正規表現は廃止し、data-link-picker="1" の有無で判定

  function ensureId(el){
    if (el.id) return el.id;
    el.id = 'lp_' + Math.random().toString(36).slice(2);
    return el.id;
  }
  function attachButton(input){
    if (!input || input.dataset.linkpickerAttached === '1') return;
    if (input.getAttribute('data-file') === '1') return; // ファイルUIは対象外
    if (!input.hasAttribute('data-link-picker')) return; // フォーム定義で明示（data-link-picker="1"）された入力だけ対象
    // ボタン設置
    var id = ensureId(input);
    var btn = document.createElement('button');
    btn.type = 'button';
    btn.className = 'btn tpcms-link-btn ' + lpBtnClass;
    btn.setAttribute('data-for', '#'+id);
    btn.setAttribute('title', 'メニューから選ぶ');
    btn.innerHTML = '<i class="fa fa-link" aria-hidden="true"></i><span class="sr-only">選択</span>';
    // 入力の直後に差し込む
    input.insertAdjacentElement('afterend', btn);
    input.dataset.linkpickerAttached = '1';
  }

  function scanAll(root){
    (root || document).querySelectorAll('input[type="text"]').forEach(attachButton);
  }
  // 初期スキャン（編集ブロック／追加パネルともに拾う）
  document.addEventListener('DOMContentLoaded', function(){ scanAll(document); });

  // 動的追加（“＋行を追加”など）にも対応
  var mo = new MutationObserver(function(muts){
    muts.forEach(function(m){
      m.addedNodes && m.addedNodes.forEach(function(n){
        if (n.nodeType !== 1) return;
        if (n.matches && n.matches('input[type="text"]')) attachButton(n);
        if (n.querySelectorAll) n.querySelectorAll('input[type="text"]').forEach(attachButton);
      });
    });
  });
  mo.observe(document.body, {childList:true, subtree:true});

  // クリックでポップアップ
  document.addEventListener('click', function(e){
    var btn = e.target && e.target.closest('.'+lpBtnClass);
    if (!btn) return;
    var sel = btn.getAttribute('data-for');
    var input = sel ? document.querySelector(sel) : null;
    openPicker(input || null);
  }, false);
})();
</script>

</body>
</html>
