/** * src/static/js/page-dirty.js — v1.5.0 Phase 1 * * Per-page dirty state matrix helpers. * state.pageDirty matrix와 sidebar UI (.nav-item.has-dirty) 동기화. * * v1.4.6.x state.isDirty boolean은 backward-compat로 유지하되 markDirty/clearDirty가 * matrix 갱신 + 합산 boolean 업데이트. */ import { state } from './state.js'; /** * v1.7.0 — dirty 변경 옵저버. * pending.js 가 onDirtyChange(renderBadge) 로 구독한다. * page-dirty 는 pending 을 import 하지 않는다 (순환 import 회피). */ const _subs = []; /** Register a callback invoked after any markDirty/clearDirty/clearAllDirty. * Dedup: a callback already in _subs is not pushed again (guards repeated initPending). */ export function onDirtyChange(cb) { if (typeof cb === 'function' && !_subs.includes(cb)) _subs.push(cb); } function _notify() { _subs.forEach(cb => { try { cb(); } catch (e) { /* observer must never break dirty tracking */ } }); } /** * Mark a page as having unsaved changes. * Sidebar leaf에 dirty dot 표시 + state.isDirty = true. * * @param {string} pageId — state.pageDirty key (home/ssid/io/network/register/can/opcua/modbus/log/firmware) */ export function markDirty(pageId) { if (!(pageId in state.pageDirty)) { if (typeof console !== 'undefined') console.warn('[page-dirty] unknown pageId:', pageId); return; } state.pageDirty[pageId] = true; state.isDirty = true; // v1.4.6.x compat _syncDot(pageId, true); _notify(); } /** * Clear dirty state for a page (after successful save). * * @param {string} pageId */ export function clearDirty(pageId) { if (!(pageId in state.pageDirty)) return; state.pageDirty[pageId] = false; _syncDot(pageId, false); // 합산 isDirty 갱신 state.isDirty = Object.values(state.pageDirty).some(v => v === true); _notify(); } /** True if any page has unsaved changes. */ export function hasDirty() { return Object.values(state.pageDirty).some(v => v === true); } /** Return array of dirty page ids. */ export function getDirtyPages() { return Object.entries(state.pageDirty) .filter(([, dirty]) => dirty) .map(([id]) => id); } /** Clear all dirty flags (after Save All). */ export function clearAllDirty() { Object.keys(state.pageDirty).forEach(k => { state.pageDirty[k] = false; }); state.isDirty = false; document.querySelectorAll('.nav-item.has-dirty').forEach(el => { el.classList.remove('has-dirty'); }); _notify(); } /** Sync sidebar dirty dot for a single page. */ function _syncDot(pageId, dirty) { const navEl = document.querySelector(`.nav-item[data-page="${pageId}"]`); if (!navEl) return; if (dirty) navEl.classList.add('has-dirty'); else navEl.classList.remove('has-dirty'); }