Spaces:
Running
Running
| /** | |
| * 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 = '<i class="fas fa-bars"></i>'; | |
| 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; | |