Source: 10d-tasks-editors.js

/* ── Today's tasks — per-card editors: comments, notes, checkpoints ── */

/**
 * Binds the per-card status-comment editor handlers (accept/skip/edit input,
 * plus the comment-history expander). Called from `bindPlanEvents()` after
 * each render.
 * @param {Function} qa - Selector helper closed over the rendered column lists.
 * @returns {void}
 */
function bindPlanCommentEvents(qa) {
  // Accept / skip / edit for status comment
  function saveComment(pid) {
    const task = getPlanTasks().find((task) => task.id === pid);
    if (!task) {
      _pendingCommentId = null;
      _pendingCommentText = '';
      renderPlan();
      return;
    }
    if (!task.statusComments) task.statusComments = [];
    const inp = document.getElementById('pc-inp-' + pid);
    const val = inp ? inp.value.trim() : (_pendingCommentText || '').trim();
    const entry = [...task.statusComments]
      .reverse()
      .find((comment) => comment.status === task.status);
    if (entry) {
      if (val) {
        entry.comment = val;
      } else {
        // Empty accept behaves as skip — remove the entry so the row
        // collapses to "+ add reason" rather than reopening the input.
        task.statusComments = task.statusComments.filter((comment) => comment !== entry);
      }
    } else if (val) {
      task.statusComments.push({ status: task.status, comment: val, ts: Date.now() });
    }
    _pendingCommentId = null;
    _pendingCommentText = '';
    savePlan();
    renderPlan();
  }
  qa('.plan-comment-ok').forEach((btn) => {
    btn.addEventListener('click', () => saveComment(btn.dataset.pid));
  });
  qa('.plan-comment-skip').forEach((btn) => {
    btn.addEventListener('click', () => {
      const task = getPlanTasks().find((task) => task.id === btn.dataset.pid);
      if (task && task.statusComments && task.statusComments.length) {
        const last = task.statusComments[task.statusComments.length - 1];
        if (!last.comment) task.statusComments.pop();
      }
      _pendingCommentId = null;
      _pendingCommentText = '';
      savePlan();
      renderPlan();
    });
  });
  qa('.plan-comment-edit').forEach((btn) => {
    btn.addEventListener('click', () => {
      const task = getPlanTasks().find((task) => task.id === btn.dataset.pid);
      _pendingCommentId = btn.dataset.pid;
      if (task && task.statusComments) {
        const ac = [...task.statusComments]
          .reverse()
          .find((comment) => comment.status === task.status);
        _pendingCommentText = ac ? ac.comment || '' : '';
      } else {
        _pendingCommentText = '';
      }
      renderPlan();
    });
  });
  qa('.plan-comment-input').forEach((inp) => {
    // Mirror typed text into the in-flight buffer
    inp.addEventListener('input', () => {
      if (inp.dataset.pid === _pendingCommentId) _pendingCommentText = inp.value;
    });
    inp.addEventListener('keydown', (event) => {
      if (event.key === 'Enter') saveComment(inp.dataset.pid);
      if (event.key === 'Escape') {
        const task = getPlanTasks().find((task) => task.id === inp.dataset.pid);
        if (task && task.statusComments && task.statusComments.length) {
          const last = task.statusComments[task.statusComments.length - 1];
          if (!last.comment) task.statusComments.pop();
        }
        _pendingCommentId = null;
        _pendingCommentText = '';
        savePlan();
        renderPlan();
      }
    });
    // Auto-focus the in-flight input, with cursor at end
    if (inp.dataset.pid === _pendingCommentId) {
      inp.focus();
      const len = inp.value.length;
      try {
        inp.setSelectionRange(len, len);
      } catch (err) {
        // Silently skip — setSelectionRange may fail on certain input types in some browsers
      }
    }
  });

  // History expand/collapse
  qa('.plan-comment-history-toggle').forEach((btn) => {
    btn.addEventListener('click', () => {
      _expandedHistoryId = _expandedHistoryId === btn.dataset.pid ? null : btn.dataset.pid;
      renderPlan();
    });
  });
}

/**
 * Binds the per-card note editor handlers (toggle, display-to-edit, save,
 * delete, cancel, and textarea keyboard shortcuts). Called from
 * `bindPlanEvents()` after each render.
 * @param {Function} qa - Selector helper closed over the rendered column lists.
 * @returns {void}
 */
function bindPlanNoteEvents(qa) {
  /** Focuses the note textarea for the given task ID after the next render. */
  function focusNoteInput(pid) {
    setTimeout(() => {
      const ta = document.querySelector(`.plan-note-input[data-pid="${pid}"]`);
      if (ta) {
        ta.focus();
        ta.setSelectionRange(ta.value.length, ta.value.length);
      }
    }, 0);
  }

  // Note button — toggle textarea open/closed
  qa('.plan-note-btn').forEach((btn) => {
    btn.addEventListener('click', (event) => {
      event.stopPropagation();
      const pid = btn.dataset.pid;
      if (_noteOpenIds.has(pid)) _noteOpenIds.delete(pid);
      else _noteOpenIds.add(pid);
      renderPlan();
      if (_noteOpenIds.has(pid)) focusNoteInput(pid);
    });
  });

  // Note display row — click to open edit
  qa('.plan-note-display').forEach((el) => {
    el.addEventListener('click', () => {
      const pid = el.dataset.pid;
      _noteOpenIds.add(pid);
      renderPlan();
      focusNoteInput(pid);
    });
  });

  // Note save
  qa('.plan-note-save').forEach((btn) => {
    btn.addEventListener('click', () => {
      const pid = btn.dataset.pid;
      const ta = btn.closest('.plan-note-area').querySelector('.plan-note-input');
      const val = ta.value.trim();
      const task = getPlanTasks().find((task) => task.id === pid);
      if (task) {
        if (val) task.note = val;
        else delete task.note;
        savePlan();
      }
      _noteOpenIds.delete(pid);
      renderPlan();
    });
  });

  // Note remove
  qa('.plan-note-del').forEach((btn) => {
    btn.addEventListener('click', () => {
      const pid = btn.dataset.pid;
      const task = getPlanTasks().find((task) => task.id === pid);
      if (task) {
        delete task.note;
        savePlan();
      }
      _noteOpenIds.delete(pid);
      renderPlan();
    });
  });

  // Note cancel
  qa('.plan-note-cancel').forEach((btn) => {
    btn.addEventListener('click', () => {
      _noteOpenIds.delete(btn.dataset.pid);
      renderPlan();
    });
  });

  // Note textarea keyboard shortcuts
  qa('.plan-note-input').forEach((ta) => {
    ta.addEventListener('keydown', (event) => {
      event.stopPropagation();
      if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
        event.preventDefault();
        ta.closest('.plan-note-area').querySelector('.plan-note-save').click();
      }
      if (event.key === 'Escape') {
        ta.closest('.plan-note-area').querySelector('.plan-note-cancel').click();
      }
    });
    ta.addEventListener('click', (event) => event.stopPropagation());
  });
}

/**
 * Binds the per-card checkpoint editor handlers (open/close badge, three-state
 * done toggles, inline edit, delete, add-on-Enter, and drag-to-reorder).
 * Called from `bindPlanEvents()` after each render.
 * @param {Function} qa - Selector helper closed over the rendered column lists.
 * @returns {void}
 */
function bindPlanCheckpointEvents(qa) {
  // Checkpoint: toggle open/closed
  qa('.cp-badge').forEach((btn) => {
    btn.addEventListener('click', (event) => {
      event.stopPropagation();
      const pid = btn.dataset.pid;
      if (_cpOpenIds.has(pid)) _cpOpenIds.delete(pid);
      else _cpOpenIds.add(pid);
      renderPlan();
      // Auto-focus add input when opening
      if (_cpOpenIds.has(pid)) {
        setTimeout(() => {
          const inp = document.querySelector(`.cp-add-input[data-pid="${pid}"]`);
          if (inp) inp.focus();
        }, 0);
      }
    });
  });

  // Checkpoint: toggle done (three-state: false → 'partial' → true → false)
  qa('.cp-check').forEach((el) => {
    el.addEventListener('click', (event) => {
      event.stopPropagation();
      const task = getPlanTasks().find((task) => task.id === el.dataset.pid);
      if (!task || !task.checkpoints) return;
      const idx = parseInt(el.dataset.cpidx);
      // eslint-disable-next-line security/detect-object-injection -- idx is parseInt() of a DOM data attribute, so it is a number (or NaN), never a property name
      const cur = task.checkpoints[idx].done;
      // eslint-disable-next-line security/detect-object-injection -- idx is parseInt() of a DOM data attribute, so it is a number (or NaN), never a property name
      task.checkpoints[idx].done = cur === false ? 'partial' : cur === 'partial' ? true : false;
      savePlan();
      renderPlan();
    });
  });

  // Checkpoint: toggle done via label click; double-click to edit
  qa('.cp-label').forEach((lbl) => {
    lbl.addEventListener('click', (event) => {
      event.stopPropagation();
      const task = getPlanTasks().find((task) => task.id === lbl.dataset.pid);
      if (!task || !task.checkpoints) return;
      const idx = parseInt(lbl.dataset.cpidx);
      // eslint-disable-next-line security/detect-object-injection -- idx is parseInt() of a DOM data attribute, so it is a number (or NaN), never a property name
      const cur = task.checkpoints[idx].done;
      // eslint-disable-next-line security/detect-object-injection -- idx is parseInt() of a DOM data attribute, so it is a number (or NaN), never a property name
      task.checkpoints[idx].done = cur === false ? 'partial' : cur === 'partial' ? true : false;
      savePlan();
      renderPlan();
    });
    lbl.addEventListener('dblclick', (event) => {
      event.stopPropagation();
      _cpEditId = lbl.dataset.pid;
      _cpEditIdx = parseInt(lbl.dataset.cpidx);
      renderPlan();
      setTimeout(() => {
        const inp = document.querySelector(
          '.cp-edit-input[data-pid="' + _cpEditId + '"][data-cpidx="' + _cpEditIdx + '"]'
        );
        if (inp) {
          inp.focus();
          inp.select();
        }
      }, 0);
    });
  });

  // Checkpoint: save/cancel inline edit
  qa('.cp-edit-input').forEach((inp) => {
    const save = () => {
      const val = inp.value.trim();
      const task = getPlanTasks().find((task) => task.id === inp.dataset.pid);
      if (task && task.checkpoints && val) task.checkpoints[parseInt(inp.dataset.cpidx)].text = val;
      _cpEditId = null;
      _cpEditIdx = null;
      savePlan();
      renderPlan();
    };
    inp.addEventListener('keydown', (event) => {
      event.stopPropagation();
      if (event.key === 'Enter') {
        event.preventDefault();
        save();
      }
      if (event.key === 'Escape') {
        _cpEditId = null;
        _cpEditIdx = null;
        renderPlan();
      }
    });
    inp.addEventListener('blur', save);
    inp.addEventListener('click', (event) => event.stopPropagation());
  });

  // Checkpoint: delete
  qa('.cp-del-btn').forEach((btn) => {
    btn.addEventListener('click', (event) => {
      event.stopPropagation();
      const task = getPlanTasks().find((task) => task.id === btn.dataset.pid);
      if (!task || !task.checkpoints) return;
      task.checkpoints.splice(parseInt(btn.dataset.cpidx), 1);
      savePlan();
      renderPlan();
    });
  });

  // Checkpoint: add on Enter
  qa('.cp-add-input').forEach((inp) => {
    inp.addEventListener('keydown', (event) => {
      if (event.key !== 'Enter') return;
      const val = inp.value.trim();
      if (!val) return;
      const task = getPlanTasks().find((task) => task.id === inp.dataset.pid);
      if (!task) return;
      if (!Array.isArray(task.checkpoints)) task.checkpoints = [];
      task.checkpoints.push({
        id: 'cp' + Date.now() + Math.random().toString(36).slice(2),
        text: val,
        done: false,
      });
      savePlan();
      renderPlan();
      // Re-focus add input after render
      setTimeout(() => {
        const next = document.querySelector(`.cp-add-input[data-pid="${inp.dataset.pid}"]`);
        if (next) next.focus();
      }, 0);
    });
    inp.addEventListener('click', (event) => event.stopPropagation());
  });

  // Checkpoint: drag-to-reorder
  let _cpDragPid = null,
    _cpDragIdx = null;
  qa('.cp-row').forEach((row) => {
    row.addEventListener('dragstart', (event) => {
      _cpDragPid = row.dataset.pid;
      _cpDragIdx = parseInt(row.dataset.cpidx);
      event.dataTransfer.effectAllowed = 'move';
    });
    row.addEventListener('dragover', (event) => {
      event.preventDefault();
      event.dataTransfer.dropEffect = 'move';
      document
        .querySelectorAll('.cp-row.cp-drag-over')
        .forEach((row) => row.classList.remove('cp-drag-over'));
      row.classList.add('cp-drag-over');
    });
    row.addEventListener('dragleave', () => row.classList.remove('cp-drag-over'));
    row.addEventListener('drop', (event) => {
      event.preventDefault();
      row.classList.remove('cp-drag-over');
      const targetIdx = parseInt(row.dataset.cpidx);
      if (_cpDragPid !== row.dataset.pid || _cpDragIdx === null || _cpDragIdx === targetIdx) return;
      const task = getPlanTasks().find((task) => task.id === _cpDragPid);
      if (!task || !task.checkpoints) return;
      const moved = task.checkpoints.splice(_cpDragIdx, 1)[0];
      task.checkpoints.splice(targetIdx, 0, moved);
      savePlan();
      renderPlan();
      _cpDragIdx = null;
      _cpDragPid = null;
    });
  });
}