// ---------- STEP4: 統一アップロードAPI バインダ ----------
// 使い方：
// <input type="file" data-upload data-upload-target="input[name='...']">
// - data-upload            : この属性が付いた <input type="file"> を自動監視
// - data-upload-target    : アップロード成功時にURLを書き込むターゲット（同一form内セレクタ推奨）
(function () {
  function postToUploadApi(file, csrfToken, triggerEl) {
    const fd = new FormData();
    fd.append('_csrf', window.tpcmsGetCsrf());
    fd.append('file', file);
    if (csrfToken) fd.append('csrf_token', csrfToken);
// --- 任意リサイズ値を triggerEl の data/hiddens から読む ---
(function () {
  var wrap = null;
  if (triggerEl && triggerEl.closest) {
    // data属性を持つ要素 or 代表的なラッパ（dropzone等）を優先して探索
    // まず data-image-* を持つ祖先を優先して探す（.dropzone で止まらない）
    wrap = triggerEl.closest('[data-image-max-width], [data-image-max-height], [data-image-quality]');
    // dropzone 内なら、ひとつ上の .row（など）に data-image-* がある想定なので登る
    if (!wrap) {
      var dz = triggerEl.closest('.dropzone');
      if (dz && dz.parentElement) {
        wrap = dz.parentElement.closest('[data-image-max-width], [data-image-max-height], [data-image-quality]');
      }
    }
    // それでも無ければ、補助ラッパや row を候補に、それも無ければ triggerEl
    if (!wrap) {
      wrap = triggerEl.closest('.js-file-uploader') || triggerEl.closest('.row') || triggerEl;
    }
  }

  function pickInt(v){ v = parseInt(v, 10); return (Number.isFinite(v) && v > 0) ? v : 0; }

  var maxW = wrap ? pickInt(wrap.dataset && wrap.dataset.imageMaxWidth)  : 0;
  var maxH = wrap ? pickInt(wrap.dataset && wrap.dataset.imageMaxHeight) : 0;
  var qual = wrap ? pickInt(wrap.dataset && wrap.dataset.imageQuality)   : 0;

  // hidden で渡すケースもフォールバック（後続ステップでPHPが出力）
  if (wrap && (!maxW || !maxH || !qual)) {
    var q = function(sel){ return wrap.querySelector ? wrap.querySelector(sel) : null; };
    if (!maxW) maxW = pickInt(q('input[name$="[image_max_width]"]')  && q('input[name$="[image_max_width]"]').value);
    if (!maxH) maxH = pickInt(q('input[name$="[image_max_height]"]') && q('input[name$="[image_max_height]"]').value);
    if (!qual) qual = pickInt(q('input[name$="[image_quality]"]')    && q('input[name$="[image_quality]"]').value);
  }

  if (qual) { if (qual < 1) qual = 1; if (qual > 100) qual = 100; }
  if (maxW) fd.append('image_max_width',  String(maxW));
  if (maxH) fd.append('image_max_height', String(maxH));
  if (qual) fd.append('image_quality',    String(qual));
})();

    // 現在の管理ページ(URLが /admin/配下)からの相対でOK
    return fetch('upload.php', {
      method: 'POST',
      body: fd,
      credentials: 'same-origin',
      headers: { 'Accept': 'application/json' }
    }).then(function (r) { return r.json(); });
  }

  document.addEventListener('change', function (e) {
    const el = e.target;
    if (!el || !el.matches || !el.matches('input[type="file"][data-upload]')) return;
    if (!el.files || !el.files[0]) return;

    const file = el.files[0];
    const form = el.closest('form');
    let csrf = null;
    if (form) {
      const c = form.querySelector('input[name="csrf_token"]');
      if (c) csrf = c.value;
    }

    // UIロック
    const beforeDisabled = el.disabled;
    el.disabled = true;

    postToUploadApi(file, csrf, el).then(function (json) {
      el.disabled = beforeDisabled;
      if (json && json.ok) {
        // 返却ファイル名をターゲット要素へ反映（name優先、無ければurl）
        const sel = el.getAttribute('data-upload-target');
        if (sel) {
          const target = form ? form.querySelector(sel) : document.querySelector(sel);
          if (target) {
            const write = (json.name || json.url || '').toString();
            if ('value' in target) target.value = write;
            else target.textContent = write;
          }
        }
        el.dispatchEvent(new CustomEvent('tpcms:uploaded', { bubbles: true, detail: json }));
      } else {
        const msg = (json && json.error) ? json.error : 'アップロードに失敗しました';
        alert(msg);
        el.dispatchEvent(new CustomEvent('tpcms:upload-error', { bubbles: true, detail: json || {} }));
      }
    }).catch(function (err) {
      el.disabled = beforeDisabled;
      console.error(err);
      alert('アップロードに失敗しました（通信エラー）');
    });
  }, false);
})();


// ---------- STEP4: ドロップゾーン対応（任意の要素に data-upload-drop） ----------
(function () {
  function postToUploadApi(file, csrfToken, triggerEl) {
    const fd = new FormData();
    fd.append('_csrf', window.tpcmsGetCsrf());
    fd.append('file', file);
    if (csrfToken) fd.append('csrf_token', csrfToken);
// --- 任意リサイズ値を triggerEl の data/hiddens から読む ---
(function () {
  var wrap = null;
  if (triggerEl && triggerEl.closest) {
    // data属性を持つ要素 or 代表的なラッパ（dropzone等）を優先して探索
    // まず data-image-* を持つ祖先を優先して探す（.dropzone で止まらない）
    wrap = triggerEl.closest('[data-image-max-width], [data-image-max-height], [data-image-quality]');
    // dropzone 内なら、ひとつ上の .row（など）に data-image-* がある想定なので登る
    if (!wrap) {
      var dz = triggerEl.closest('.dropzone');
      if (dz && dz.parentElement) {
        wrap = dz.parentElement.closest('[data-image-max-width], [data-image-max-height], [data-image-quality]');
      }
    }
    // それでも無ければ、補助ラッパや row を候補に、それも無ければ triggerEl
    if (!wrap) {
      wrap = triggerEl.closest('.js-file-uploader') || triggerEl.closest('.row') || triggerEl;
    }
  }

  function pickInt(v){ v = parseInt(v, 10); return (Number.isFinite(v) && v > 0) ? v : 0; }

  var maxW = wrap ? pickInt(wrap.dataset && wrap.dataset.imageMaxWidth)  : 0;
  var maxH = wrap ? pickInt(wrap.dataset && wrap.dataset.imageMaxHeight) : 0;
  var qual = wrap ? pickInt(wrap.dataset && wrap.dataset.imageQuality)   : 0;

  // hidden で渡すケースもフォールバック（後続ステップでPHPが出力）
  if (wrap && (!maxW || !maxH || !qual)) {
    var q = function(sel){ return wrap.querySelector ? wrap.querySelector(sel) : null; };
    if (!maxW) maxW = pickInt(q('input[name$="[image_max_width]"]')  && q('input[name$="[image_max_width]"]').value);
    if (!maxH) maxH = pickInt(q('input[name$="[image_max_height]"]') && q('input[name$="[image_max_height]"]').value);
    if (!qual) qual = pickInt(q('input[name$="[image_quality]"]')    && q('input[name$="[image_quality]"]').value);
  }

  if (qual) { if (qual < 1) qual = 1; if (qual > 100) qual = 100; }
  if (maxW) fd.append('image_max_width',  String(maxW));
  if (maxH) fd.append('image_max_height', String(maxH));
  if (qual) fd.append('image_quality',    String(qual));
})();

    return fetch('upload.php', {
      method: 'POST',
      body: fd,
      credentials: 'same-origin',
      headers: { 'Accept': 'application/json' }
    }).then(r => r.json());
  }

  function nearestForm(el) {
    return el.closest ? el.closest('form') : null;
  }

  function findTarget(el, form) {
    const sel = el.getAttribute('data-upload-target');
    if (!sel) return null;
    return form ? form.querySelector(sel) : document.querySelector(sel);
  }

  function handleFiles(el, files) {
    if (!files || !files[0]) return;
    const form = nearestForm(el);
    let csrf = null;
    if (form) {
      const c = form.querySelector('input[name="csrf_token"]');
      if (c) csrf = c.value;
    }
    el.classList.add('is-uploading');
    postToUploadApi(files[0], csrf, el).then(json => {
      el.classList.remove('is-uploading');
      if (json && json.ok && json.url) {
        const target = findTarget(el, form);
        if (target) {
          const write = (json.name || json.url || '').toString();
          if ('value' in target) target.value = write;
          else target.textContent = write;
        }
        el.dispatchEvent(new CustomEvent('tpcms:uploaded', { bubbles: true, detail: json }));
      } else {
        alert((json && json.error) || 'アップロードに失敗しました');
        el.dispatchEvent(new CustomEvent('tpcms:upload-error', { bubbles: true, detail: json || {} }));
      }
    }).catch(err => {
      console.error(err);
      el.classList.remove('is-uploading');
      alert('アップロードに失敗しました（通信エラー）');
    });
  }

  document.addEventListener('dragover', function(e){
    const el = e.target.closest && e.target.closest('[data-upload-drop]');
    if (!el) return;
    e.preventDefault();
    el.classList.add('is-dragover');
  }, false);

  document.addEventListener('dragleave', function(e){
    const el = e.target.closest && e.target.closest('[data-upload-drop]');
    if (!el) return;
    el.classList.remove('is-dragover');
  }, false);

  document.addEventListener('drop', function(e){
    const el = e.target.closest && e.target.closest('[data-upload-drop]');
    if (!el) return;
    e.preventDefault();
    el.classList.remove('is-dragover');
    const dt = e.dataTransfer;
    if (!dt || !dt.files || !dt.files[0]) return;
    handleFiles(el, dt.files);
  }, false);

  // クリックでもファイル選択できるようにする（任意）
  document.addEventListener('click', function(e){
    const el = e.target.closest && e.target.closest('[data-upload-drop]');
    if (!el) return;
    const input = document.createElement('input');
    input.type = 'file';
    input.accept = 'image/*,video/mp4,video/webm';
    input.style.display = 'none';
    document.body.appendChild(input);
    input.addEventListener('change', function(){
      if (input.files && input.files[0]) handleFiles(el, input.files);
      document.body.removeChild(input);
    }, { once: true });
    input.click();
  }, false);
})();


/* --- page_blocks：ドラッグ並べ替え（pointerベース・ハンドル限定／スマホ対応） --- */
(function(){
  function ready(fn){ if(document.readyState!=='loading'){ fn(); } else { document.addEventListener('DOMContentLoaded', fn); } }

  ready(function(){
    var list = document.getElementById('blockList');
    if(!list) return;

    var dragging = null;      // 実体: <li>
    var placeholder = null;   // 置換位置表示: <li class="placeholder">

    function onStart(e){
      // ☰ハンドルのみ開始（.handle は既存CSS/HTMLに合わせる）
      var handle = e.target && e.target.closest && e.target.closest('.handle');
      if(!handle || !list.contains(handle)) return;

      var li = handle.closest('li');
      if(!li) return;

      dragging = li;

      // プレースホルダを用意（同じ高さ）
      placeholder = document.createElement('li');
      placeholder.className = 'placeholder';
      placeholder.style.height = li.getBoundingClientRect().height + 'px';
      placeholder.innerHTML = '';

      dragging.classList.add('dragging-soft');

      // プレースホルダを li の直後に置く
      if (dragging.nextSibling) list.insertBefore(placeholder, dragging.nextSibling);
      else list.appendChild(placeholder);

      // Pointer をハンドルへ集約（対応環境のみ）
      if (e.type === 'pointerdown') {
        try { handle.setPointerCapture && handle.setPointerCapture(e.pointerId); } catch(_){}
      }

      addMoveUpListeners();

      // 掴んだ瞬間にスクロール抑止
      if (e && e.cancelable) e.preventDefault();
    }

    function getClientY(ev){
      if (ev.touches && ev.touches[0]) return ev.touches[0].clientY;
      return ev.clientY;
    }

    function onMove(ev){
      if (!dragging || !placeholder) return;

      // 縦スクロールに負けないため常に抑止（cancelable のみ）
      if (ev.cancelable) ev.preventDefault();

      var y = getClientY(ev);
      var after = getAfter(list, y, dragging, placeholder);
      if (after == null) {
        if (list.lastElementChild !== placeholder) list.appendChild(placeholder);
      } else if (after !== placeholder) {
        list.insertBefore(placeholder, after);
      }
    }

    function onEnd(){
      // プレースホルダ位置へ li を確定
      if (placeholder && dragging) list.insertBefore(dragging, placeholder);
      cleanup();
      updateHidden();
    }

    function cleanup(){
      if (dragging) dragging.classList.remove('dragging-soft');
      dragging = null;
      if (placeholder && placeholder.parentNode) placeholder.parentNode.removeChild(placeholder);
      placeholder = null;
      removeMoveUpListeners();
    }

    // 追加：pointer/touch/mouse すべてを監視（スマホ対応の肝）
    function addMoveUpListeners(){
      document.addEventListener('pointermove', onMove, { passive:false });
      document.addEventListener('pointerup', onEnd, { passive:true });
      document.addEventListener('pointercancel', onEnd, { passive:true });

      document.addEventListener('touchmove', onMove, { passive:false });
      document.addEventListener('touchend', onEnd, { passive:true });
      document.addEventListener('touchcancel', onEnd, { passive:true });

      document.addEventListener('mousemove', onMove, { passive:false });
      document.addEventListener('mouseup', onEnd, { passive:true });
    }
    function removeMoveUpListeners(){
      document.removeEventListener('pointermove', onMove);
      document.removeEventListener('pointerup', onEnd);
      document.removeEventListener('pointercancel', onEnd);

      document.removeEventListener('touchmove', onMove);
      document.removeEventListener('touchend', onEnd);
      document.removeEventListener('touchcancel', onEnd);

      document.removeEventListener('mousemove', onMove);
      document.removeEventListener('mouseup', onEnd);
    }

    // 開始イベント：pointer を最優先しつつ互換も残す
    list.addEventListener('pointerdown', onStart, { passive:false });
    list.addEventListener('touchstart', onStart, { passive:false });
    list.addEventListener('mousedown', onStart, { passive:false });

    function updateHidden(){
      var arr = [];
      Array.prototype.forEach.call(list.querySelectorAll('li'), function(li){
        if (li.classList.contains('placeholder')) return;
        arr.push(parseInt(li.getAttribute('data-idx'), 10));
      });
      var hid = document.getElementById('order_json');
      if(hid) hid.value = JSON.stringify(arr);
    }

    // y位置に対して、差し込み先となる<li>を返す（placeholder以外）
    function getAfter(list, y, dragging, placeholder){
      var els = Array.prototype.slice.call(list.querySelectorAll('li'));
      els = els.filter(function(el){ return el !== dragging && !el.classList.contains('placeholder'); });

      var closest = {offset: -Infinity, element: null};
      els.forEach(function(child){
        var box = child.getBoundingClientRect();
        var offset = y - box.top - box.height/2;
        if(offset < 0 && offset > closest.offset){
          closest = {offset: offset, element: child};
        }
      });
      return closest.element;
    }

    // 初期：順序をhiddenへ
    (function init(){ updateHidden(); })();
  });
})();



/* --- [MOBILE FIX] page_blocks DnD: pointer capture + touch-action --- */
(function () {
  var list = document.getElementById('blockList');
  if (!list) return;

  function beginDrag(e, handleEl) {
    // スクロール抑止（ドラッグ中のみ）
    list.style.touchAction = 'none';
    document.documentElement.classList.add('tpcms-dragging');
    // Pointer Events を強制的にこのハンドルに集約
    try { handleEl.setPointerCapture && handleEl.setPointerCapture(e.pointerId); } catch(_){}
    // iOSでの慣性スクロール抑止
    if (e && typeof e.preventDefault === 'function') e.preventDefault();
  }
  function endDrag() {
    list.style.touchAction = '';
    document.documentElement.classList.remove('tpcms-dragging');
  }

  // ハンドルを掴んだ時だけ発火（class名は既存に合わせて幅広くケア）
  list.addEventListener('pointerdown', function (e) {
    var h = e.target && (e.target.closest('.js-dnd-handle, .drag-handle, .handle'));
    if (!h || !list.contains(h)) return;
    beginDrag(e, h);
  }, { passive: false });

  // 指を離した／キャンセル時に後始末
  list.addEventListener('pointerup', endDrag, { passive: true });
  list.addEventListener('pointercancel', endDrag, { passive: true });
})();


// --- page_blocks: 「編集」ボタンでエディタ(<details>)開閉 ---
(function(){
  document.addEventListener('click', function(e){
    var btn = e.target.closest('.js-edit-toggle');
    if(!btn) return;
    var id = btn.getAttribute('data-target');
    var det = document.getElementById(id);
    if(!det) return;
    det.open = !det.open;
    if(det.open){
      try { det.scrollIntoView({behavior:'smooth', block:'nearest'}); } catch(_){}
    }
  });
})();


/* STEP6: file 入力の自動補強（data-file="1" を対象） （※実際に追加したのは、STEP7脱線編）*/
(function () {
  function isVideoName(s) { return /\.(mp4|webm|ogg|mov|m4v)$/i.test(s || ''); }
  function isHttp(s) { return /^https?:\/\//i.test(s || ''); }
  function basename(p) { p = (p || '').toString().replace(/\\/g, '/'); var a = p.split('/'); return a[a.length - 1] || ''; }
  function makePreviewUrl(val) {
    if (!val || isHttp(val)) return null;
    var name = basename(val);
    return name ? ('../uploads/' + name) : null; // 管理画面は ../uploads が正解
  }
  function enhance(input) {
    if (!input || input.dataset.enhancedFileUi === '1') return;
    input.dataset.enhancedFileUi = '1';

    // IDが無ければ生成（ドロップゾーンのターゲット用）
    if (!input.id) input.id = 'f_' + Math.random().toString(36).slice(2);

    // ドロップゾーン
    var dz = document.createElement('div');
    dz.className = 'dropzone';
    dz.setAttribute('data-upload-drop', '');
    dz.setAttribute('data-upload-target', '#' + input.id);
    dz.setAttribute('role', 'button');
    dz.setAttribute('tabindex', '0');
    dz.textContent = 'ここをクリック/ドラッグ';
    input.insertAdjacentElement('afterend', dz);

    // 既存値の簡易プレビュー（画像のみ／動画は文字）
    var val = (input.value || '').trim();
    if (val) {
      if (!isHttp(val) && !isVideoName(val)) {
        var url = makePreviewUrl(val);
        if (url) {
          var pre = document.createElement('div');
          pre.className = 'preview';
          var img = document.createElement('img');
          img.className = 'thumb';
          img.src = url;
          img.alt = '';
          pre.appendChild(img);
          dz.insertAdjacentElement('afterend', pre);
        }
      } else if (isVideoName(val)) {
        var note = document.createElement('div');
        note.className = 'muted small';
        note.textContent = '（動画ファイル：プレビューなし）';
        dz.insertAdjacentElement('afterend', note);
      }
    }

    // 削除チェック（ローカルファイル時のみ）
    if (val && !isHttp(val)) {
      var wrap = document.createElement('div');
      wrap.className = 'muted';
      var label = document.createElement('label');
      label.className = 'inline';
      var cb = document.createElement('input');
      cb.type = 'checkbox';
      cb.value = '1';
      // name の末尾 "]" の直前に __delete を付与
      cb.name = (input.name || '').replace(/\]$/, '__delete]');
      label.appendChild(cb);
      label.appendChild(document.createTextNode(' この画像/動画を削除する'));
      wrap.appendChild(label);
      dz.insertAdjacentElement('afterend', wrap);
    }
  }

  // 初期化：data-file="1" の file 入力すべて
  document.querySelectorAll('input[type="text"][data-file="1"]').forEach(enhance);

  // 念のため、後から表示された場合にも対応（詳細開閉など）
  var obs = 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"][data-file="1"]')) enhance(n);
        n.querySelectorAll && n.querySelectorAll('input[type="text"][data-file="1"]').forEach(enhance);
      });
    });
  });
  obs.observe(document.body, { childList: true, subtree: true });
})();


/* --- ROWS：ドラッグ並べ替え（pointerベース・ハンドル限定｜タッチ対応） --- */
(function(){
  function onReady(fn){ if(document.readyState!=='loading'){ fn(); } else { document.addEventListener('DOMContentLoaded', fn); } }

  onReady(function(){
    var dragging = null, placeholder = null, moveBind = null, upBind = null, container = null, activePointerId = null;

    document.addEventListener('pointerdown', function(e){
      var handle = e.target && e.target.closest ? e.target.closest('.handle') : null;
      if(!handle) return;
      var cont = handle.closest ? handle.closest('.tpcms-rows') : null;
      if(!cont) return; // ROWS 以外は既存ロジックに任せる
      var row = handle.closest('.tpcms-row');
      if(!row || !cont.contains(row)) return;

      // マウスは左クリックのみ
      if(e.pointerType === 'mouse' && e.button !== 0) return;

      e.preventDefault();
      activePointerId = e.pointerId || null;

      // ドラッグ中のネイティブスクロール無効化
      cont.style.touchAction = 'none';

      dragging = row;
      container = cont;

      // プレースホルダ
      placeholder = document.createElement('div');
      placeholder.className = 'placeholder';
      var h = row.getBoundingClientRect().height;
      placeholder.style.height = h + 'px';
      placeholder.style.border = '1px dashed #ccc';
      placeholder.style.background = '#fafafa';

      dragging.classList.add('dragging-soft');

      // ひとまず直後に置く
      if (dragging.nextSibling) container.insertBefore(placeholder, dragging.nextSibling);
      else container.appendChild(placeholder);

      moveBind = function(ev){
        // 他ポインタは無視
        if(activePointerId != null && ev.pointerId != null && ev.pointerId !== activePointerId) return;
        ev.preventDefault();
        var y = ev.clientY || 0;
        var after = getAfter(container, y, dragging, placeholder);
        if (!after) {
          container.appendChild(placeholder);
        } else {
          container.insertBefore(placeholder, after);
        }
      };
      upBind = function(ev){
        if(activePointerId != null && ev.pointerId != null && ev.pointerId !== activePointerId) return;

        // プレースホルダ位置に row を差し込む
        if (placeholder && placeholder.parentNode) {
          container.insertBefore(dragging, placeholder);
        }
        // 連番を振り直し（name="BASE[0][KEY]" → "BASE[i][KEY]"）
        renumber(container);

        // 片付け
        if (placeholder && placeholder.parentNode) placeholder.parentNode.removeChild(placeholder);
        if (dragging) dragging.classList.remove('dragging-soft');
        if (container) container.style.touchAction = '';

        dragging = placeholder = container = null;
        activePointerId = null;

        document.removeEventListener('pointermove', moveBind, { passive: false });
        document.removeEventListener('pointerup',   upBind);
        moveBind = upBind = null;
      };

      document.addEventListener('pointermove', moveBind, { passive: false });
      document.addEventListener('pointerup',   upBind);
    }, { passive: false });

    function getAfter(cont, y, dragging, placeholder){
      var els = Array.prototype.slice.call(cont.querySelectorAll('.tpcms-row'));
      els = els.filter(function(el){ return el !== dragging && el !== placeholder; });
      var closest = { offset: -Infinity, element: null };
      els.forEach(function(child){
        var box = child.getBoundingClientRect();
        var offset = y - (box.top + box.height/2);
        if(offset < 0 && offset > closest.offset){
          closest = { offset: offset, element: child };
        }
      });
      return closest.element;
    }

    function renumber(cont){
      var base = (cont.getAttribute('data-base') || '').trim();
      if (!base) return;
      var esc = base.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
      var re  = new RegExp('^' + esc + '\\[(\\d+)\\]');

      var rows = cont.querySelectorAll('.tpcms-row');
      Array.prototype.forEach.call(rows, function(row, i){
        var fields = row.querySelectorAll('[name]');
        Array.prototype.forEach.call(fields, function(f){
          var nm = f.getAttribute('name') || '';
          if (re.test(nm)) {
            var nn = nm.replace(re, base + '[' + i + ']');
            if (nn !== nm) f.setAttribute('name', nn);
          }
        });
      });
    }
  });
})();


/* --- MENU：ドラッグ並べ替え（pointerベース・ハンドル限定｜タッチ対応｜li/.row両対応） --- */
(function(){
  function onReady(fn){ if(document.readyState!=='loading'){ fn(); } else { document.addEventListener('DOMContentLoaded', fn); } }

  onReady(function(){
    var dragging=null, placeholder=null, moveBind=null, upBind=null, container=null, activePointerId=null;

    document.addEventListener('pointerdown', function(e){
      var handle = e.target && e.target.closest ? e.target.closest('.handle') : null;
      if(!handle) return;

      // ROWS 側（.tpcms-rows）は別モジュールに任せる
      if (handle.closest('.tpcms-rows')) return;

      // メニューの1項目（li または .row など）を特定
      var item = handle.closest('[data-menu-item], .menu-item, li, .row');
      if(!item) return;

      var cont = item.parentElement;
      if(!cont) return;

      // 本当に menu 用か確認：項目内部または親に items[0] 形式の入力がある
      var isMenu = !!(item.querySelector('[name^="items["]') || cont.querySelector('[name^="items["]'));
      if(!isMenu) return;

      // マウスは左クリックのみ
      if(e.pointerType === 'mouse' && e.button !== 0) return;

      e.preventDefault();
      activePointerId = e.pointerId || null;

      // ドラッグ中のスクロール抑止
      cont.style.touchAction = 'none';

      dragging = item; container = cont;

      // プレースホルダは同タグに（li なら li、div系なら div）
      placeholder = document.createElement(dragging.tagName === 'LI' ? 'li' : 'div');
      placeholder.className = 'placeholder';
      var h = dragging.getBoundingClientRect().height;
      placeholder.style.height = h + 'px';
      placeholder.style.border = '1px dashed #ccc';
      placeholder.style.background = '#fafafa';

      dragging.classList.add('dragging-soft');

      // ひとまず直後に置く
      if (dragging.nextSibling) container.insertBefore(placeholder, dragging.nextSibling);
      else container.appendChild(placeholder);

      moveBind = function(ev){
        if(activePointerId != null && ev.pointerId != null && ev.pointerId !== activePointerId) return;
        ev.preventDefault();
        var y = ev.clientY || 0;
        var after = getAfter(container, y, dragging, placeholder);
        if (!after) container.appendChild(placeholder);
        else container.insertBefore(placeholder, after);
      };
      upBind = function(ev){
        if(activePointerId != null && ev.pointerId != null && ev.pointerId !== activePointerId) return;

        // プレースホルダ位置に差し込む
        if (placeholder && placeholder.parentNode) container.insertBefore(dragging, placeholder);

        // name="items[0][slug]" を items[i][slug] に再連番
        renumberItems(container);

        // 片付け
        if (placeholder && placeholder.parentNode) placeholder.parentNode.removeChild(placeholder);
        if (dragging) dragging.classList.remove('dragging-soft');
        if (container) container.style.touchAction = '';

        dragging = placeholder = container = null;
        activePointerId = null;

        document.removeEventListener('pointermove', moveBind, { passive: false });
        document.removeEventListener('pointerup',   upBind);
        moveBind = upBind = null;
      };

      document.addEventListener('pointermove', moveBind, { passive: false });
      document.addEventListener('pointerup',   upBind);
    }, { passive: false });

    function getMenuChildren(cont){
      // 直下の子のうち、items[] フィールドを含む要素だけを並べ替え対象にする
      return Array.prototype.slice.call(cont.children).filter(function(el){
        if (el === null || el.classList && el.classList.contains('placeholder')) return false;
        if (el.tagName !== 'LI' && !el.classList.contains('row') && !el.matches('[data-menu-item], .menu-item')) return false;
        return !!el.querySelector('[name^="items["]');
      });
    }

    function getAfter(cont, y, dragging, placeholder){
      var els = getMenuChildren(cont).filter(function(el){ return el !== dragging && el !== placeholder; });
      var closest = { offset: -Infinity, element: null };
      els.forEach(function(child){
        var box = child.getBoundingClientRect();
        var offset = y - (box.top + box.height/2);
        if(offset < 0 && offset > closest.offset){
          closest = { offset: offset, element: child };
        }
      });
      return closest.element;
    }

    function renumberItems(cont){
      var re = /^items\[(\d+)\]/;
      var rows = getMenuChildren(cont);
      rows.forEach(function(row, i){
        var fields = row.querySelectorAll('[name]');
        Array.prototype.forEach.call(fields, function(f){
          var nm = f.getAttribute('name') || '';
          if (re.test(nm)) {
            var nn = nm.replace(re, 'items['+i+']');
            if (nn !== nm) f.setAttribute('name', nn);
          }
        });
      });
    }
  });
})();


/* ===========================
 * STEP9: 未保存で離脱の警告
 * 対象：data-form 属性を持つフォーム（例：#siteForm, #newsForm など）
 * 解除：フォーム送信時 / .js-unload-ok を押した時 / data-unload="off" を付与
 * =========================== */
(function () {
  var dirty = false;

  function enableGuard() {
    if (dirty) return;
    dirty = true;
    window.onbeforeunload = function (e) {
      e.preventDefault();
      e.returnValue = '';
      return '';
    };
  }
  function disableGuard() {
    dirty = false;
    window.onbeforeunload = null;
  }

  // data-form を持つフォームを監視
  document.querySelectorAll('form[data-form]').forEach(function (form) {
    // 入力・変更で“編集中”フラグON
    form.addEventListener('input', enableGuard, true);
    form.addEventListener('change', enableGuard, true);

    // 送信時は警告OFF
    form.addEventListener('submit', function () {
      disableGuard();
    });
  });

  // 明示的に離脱を許可したいボタン等に .js-unload-ok を付ける
  document.addEventListener('click', function (ev) {
    var el = ev.target.closest('.js-unload-ok');
    if (!el) return;
    disableGuard();
  });

  // ページ側で一時的に無効化したい場合は <body data-unload="off"> を付ける
  var mo = new MutationObserver(function () {
    var off = (document.body.getAttribute('data-unload') || '').toLowerCase() === 'off';
    if (off) disableGuard();
  });
  if (document.body) {
    mo.observe(document.body, { attributes: true, attributeFilter: ['data-unload'] });
  }
})();


/* STEP9: CSRF トークン取得ヘルパ */
window.tpcmsGetCsrf = function () {
  var el = document.querySelector('input[name="_csrf"]');
  return el && typeof el.value === 'string' ? el.value : '';
};


/* --- [MOBILE FIX #2] page_blocks DnD: drag中は画面スクロールを完全禁止 --- */
(function () {
  // ブロック一覧のUL/OLを幅広く拾う（プロジェクト差異に耐える）
  var list = document.querySelector('#blockList, .block-list, .js-block-list');
  if (!list) return;

  var dragging = false;
  var activeHandle = null;

  function start(e) {
    // ☰ハンドルだけを掴んだときに発火（クラス名は既存想定を網羅）
    var h = e.target && e.target.closest('.js-dnd-handle, .drag-handle, .handle');
    if (!h || !list.contains(h)) return;

    dragging = true;
    activeHandle = h;

    // 可能ならPointerをこの要素に集約
    try { h.setPointerCapture && h.setPointerCapture(e.pointerId); } catch (_) {}

    // 掴んだ瞬間に既定動作を止めて慣性スクロール等を封じる
    if (typeof e.preventDefault === 'function') e.preventDefault();
    document.documentElement.classList.add('tpcms-dragging');
  }

  function move(e) {
    if (!dragging) return;
    // drag中は縦スクロールを完全停止（iOS/Android両対応）
    if (typeof e.cancelable !== 'boolean' || e.cancelable) {
      e.preventDefault();
    }
  }

  function end() {
    dragging = false;
    activeHandle = null;
    document.documentElement.classList.remove('tpcms-dragging');
  }

  // Pointer系（passive:false で preventDefault を許可）
  list.addEventListener('pointerdown', start, { passive: false });
  list.addEventListener('pointermove', move, { passive: false });
  list.addEventListener('pointerup', end, { passive: true });
  list.addEventListener('pointercancel', end, { passive: true });

  // 念のためTouch系も直接ケア（古い端末・実装差異対策）
  window.addEventListener('touchmove', move, { passive: false });
  window.addEventListener('touchend', end, { passive: true });
  window.addEventListener('touchcancel', end, { passive: true });

  // ネイティブのdragstart（画像長押しなど）を抑止
  list.addEventListener('dragstart', function (e) { e.preventDefault(); }, { passive: false });
})();
