/** * SidebarToggle - Toggle Ẩn/Hiện Thanh Bên Trái * Cung cấp nút toggle để ẩn/hiện sidebar, mở rộng vùng nội dung chính. * Lưu trạng thái vào localStorage và khôi phục khi tải lại trang. * Requirements: 27.1, 27.2, 27.3, 27.4, 27.5, 27.6 */ const SidebarToggle = (function () { const STORAGE_KEY_DEFAULT = 'onnx_explorer_sidebar_state'; const STATE_VISIBLE = 'visible'; const STATE_HIDDEN = 'hidden'; class SidebarToggle { /** * @param {string} sidebarSelector - CSS selector cho sidebar * @param {string} mainContentSelector - CSS selector cho vùng nội dung chính * @param {string} [storageKey] - Key localStorage */ constructor(sidebarSelector, mainContentSelector, storageKey) { /** @type {string} */ this._sidebarSelector = sidebarSelector; /** @type {string} */ this._mainContentSelector = mainContentSelector; /** @type {string} */ this._storageKey = storageKey || STORAGE_KEY_DEFAULT; /** @type {HTMLElement|null} */ this._sidebarEl = null; /** @type {HTMLElement|null} */ this._mainContentEl = null; /** @type {HTMLButtonElement|null} */ this._btn = null; /** @type {boolean} */ this._hidden = false; /** @type {Function|null} */ this._onTransitionEndBound = null; /** @type {boolean} */ this._initialized = false; } // ─── Public API ───────────────────────────────────────────────────── /** * Khởi tạo: tìm elements, tạo nút toggle, khôi phục trạng thái, gắn events. */ init() { if (this._initialized) return; this._sidebarEl = document.querySelector(this._sidebarSelector); this._mainContentEl = document.querySelector(this._mainContentSelector); if (!this._sidebarEl) { console.warn('[SidebarToggle] Sidebar element not found:', this._sidebarSelector); return; } if (!this._mainContentEl) { console.warn('[SidebarToggle] Main content element not found:', this._mainContentSelector); return; } this._createButton(); this._bindEvents(); this._restoreState(); this._initialized = true; console.log('[SidebarToggle] Initialized'); } /** * Ẩn thanh bên trái. */ hideSidebar() { if (this._hidden) return; this._sidebarEl.classList.add('sidebar-hidden'); this._mainContentEl.classList.add('sidebar-collapsed'); this._hidden = true; this._updateButtonIcon(); this._saveState(); } /** * Hiện thanh bên trái. */ showSidebar() { if (!this._hidden) return; this._sidebarEl.classList.remove('sidebar-hidden'); this._mainContentEl.classList.remove('sidebar-collapsed'); this._hidden = false; this._updateButtonIcon(); this._saveState(); } /** * Toggle trạng thái ẩn/hiện. */ toggle() { if (this._hidden) { this.showSidebar(); } else { this.hideSidebar(); } } /** * Kiểm tra sidebar đang ẩn hay hiện. * @returns {boolean} */ isHidden() { return this._hidden; } /** * Hủy và dọn dẹp event listeners, DOM. */ destroy() { if (this._onTransitionEndBound && this._sidebarEl) { this._sidebarEl.removeEventListener('transitionend', this._onTransitionEndBound); this._onTransitionEndBound = null; } if (this._btn && this._btn.parentNode) { this._btn.parentNode.removeChild(this._btn); this._btn = null; } if (this._sidebarEl) { this._sidebarEl.classList.remove('sidebar-hidden'); } if (this._mainContentEl) { this._mainContentEl.classList.remove('sidebar-collapsed'); } this._sidebarEl = null; this._mainContentEl = null; this._hidden = false; this._initialized = false; } // ─── Private ──────────────────────────────────────────────────────── /** * Create the toggle button and insert it before the logo in the header. */ _createButton() { const navbarBrand = document.querySelector('.navbar-brand'); if (!navbarBrand || !navbarBrand.parentNode) { console.warn('[SidebarToggle] .navbar-brand not found'); return; } const btn = document.createElement('button'); btn.className = 'btn btn-outline-light btn-sm me-2 sidebar-toggle-btn'; btn.id = 'sidebarToggleBtn'; btn.title = 'Toggle Sidebar'; btn.innerHTML = ''; navbarBrand.parentNode.insertBefore(btn, navbarBrand); this._btn = btn; } /** * Bind click and transitionend events. */ _bindEvents() { if (this._btn) { this._btn.addEventListener('click', () => { this.toggle(); }); } this._onTransitionEndBound = this._onTransitionEnd.bind(this); if (this._sidebarEl) { this._sidebarEl.addEventListener('transitionend', this._onTransitionEndBound); } } /** * Handle transitionend: resize the Cytoscape graph to fit new dimensions. * @param {TransitionEvent} e */ _onTransitionEnd(e) { // Only react to transitions on the sidebar element itself if (e.target !== this._sidebarEl) return; this._resizeGraph(); } /** * Update button icon based on current state. * fa-bars when sidebar is visible, fa-chevron-right when hidden. */ _updateButtonIcon() { if (!this._btn) return; const icon = this._btn.querySelector('i'); if (!icon) return; if (this._hidden) { icon.className = 'fas fa-chevron-right'; this._btn.title = 'Show Sidebar'; } else { icon.className = 'fas fa-bars'; this._btn.title = 'Toggle Sidebar'; } } /** * Save current state to localStorage. */ _saveState() { try { localStorage.setItem(this._storageKey, this._hidden ? STATE_HIDDEN : STATE_VISIBLE); } catch (err) { console.warn('[SidebarToggle] Could not save state:', err); } } /** * Restore state from localStorage on init. */ _restoreState() { try { const saved = localStorage.getItem(this._storageKey); if (saved === STATE_HIDDEN) { this.hideSidebar(); } } catch (err) { console.warn('[SidebarToggle] Could not restore state:', err); } } /** * Call cy.resize() on the Cytoscape instance to adjust to new dimensions. */ _resizeGraph() { try { if (window._onnxApp && typeof window._onnxApp.getGraphVisualizer === 'function') { const visualizer = window._onnxApp.getGraphVisualizer(); if (visualizer && visualizer._cy) { visualizer._cy.resize(); visualizer._cy.fit(); } } } catch (err) { console.warn('[SidebarToggle] Could not resize graph:', err); } } } return SidebarToggle; })(); // Export as global for browser usage window.SidebarToggle = SidebarToggle;