/** * PrintHandler - Manages Print to PDF functionality * Converts Cytoscape canvas to static image for printing, * prepares layout for @media print, and restores after printing. * Requirements: 61.1, 61.5, 62.1-62.7, 63.1-63.5, 64.1, 64.2, 65.1 */ window.PrintHandler = (function () { 'use strict'; /** * @param {string} printBtnId - ID of the Print PDF button * @param {string} printHeaderId - ID of the print header element */ function PrintHandler(printBtnId, printHeaderId) { /** @type {HTMLButtonElement|null} */ this._printBtn = document.getElementById(printBtnId) || null; /** @type {HTMLElement|null} */ this._printHeader = document.getElementById(printHeaderId) || null; /** @type {Function|null} Unsubscribe from StateManager */ this._unsubscribeModel = null; /** @type {Function|null} Bound beforeprint handler */ this._boundBeforePrint = null; /** @type {Function|null} Bound afterprint handler */ this._boundAfterPrint = null; /** @type {Function|null} Bound click handler */ this._boundClickHandler = null; this._init(); } // ─── Initialization ─────────────────────────────────────────────────────── /** * Bind click handler, subscribe to state changes, register print events. * @private */ PrintHandler.prototype._init = function () { try { // Bind click handler for Print PDF button if (this._printBtn) { this._boundClickHandler = this.print.bind(this); this._printBtn.addEventListener('click', this._boundClickHandler); } // Subscribe to StateManager.currentModel to enable/disable button if (window.StateManager && typeof window.StateManager.subscribe === 'function') { this._unsubscribeModel = window.StateManager.subscribe('currentModel', function (model) { try { this._setButtonEnabled(!!model); } catch (e) { console.error('[PrintHandler] Error in currentModel subscriber:', e); } }.bind(this)); } // Register beforeprint / afterprint window events this._boundBeforePrint = this._prepareForPrint.bind(this); this._boundAfterPrint = this._restoreAfterPrint.bind(this); window.addEventListener('beforeprint', this._boundBeforePrint); window.addEventListener('afterprint', this._boundAfterPrint); } catch (err) { console.error('[PrintHandler] Initialization error:', err); } }; // ─── Button State ───────────────────────────────────────────────────────── /** * Enable or disable the Print PDF button. * @param {boolean} enabled * @private */ PrintHandler.prototype._setButtonEnabled = function (enabled) { try { if (this._printBtn) { this._printBtn.disabled = !enabled; } } catch (err) { console.error('[PrintHandler] _setButtonEnabled error:', err); } }; // ─── Print Preparation ──────────────────────────────────────────────────── /** * Prepare the page for printing: * - Update print header with current model file name * - Convert Cytoscape canvas → static PNG image * - Hide original canvas, insert static image into graphContainer * - If no graph, show placeholder * @private */ PrintHandler.prototype._prepareForPrint = function () { try { // Update print header with model file name var modelName = this._getCurrentModelFileName(); if (this._printHeader) { this._printHeader.textContent = modelName ? 'ONNX Model Explorer — ' + modelName : 'ONNX Model Explorer'; } // Get Cytoscape instance var cy = this._getCytoscapeInstance(); var graphContainer = document.getElementById('graphContainer'); if (cy && graphContainer) { try { // Convert canvas to PNG data URL var pngDataUrl = cy.png({ full: true, scale: 2, bg: '#ffffff' }); // Hide ALL direct children of graphContainer (Cytoscape wrapper, canvases, etc.) var children = graphContainer.children; for (var i = 0; i < children.length; i++) { children[i].setAttribute('data-print-hidden', 'true'); children[i].style.display = 'none'; } // Override the fixed height so the image can size naturally this._origHeight = graphContainer.style.height; graphContainer.style.height = 'auto'; graphContainer.style.overflow = 'visible'; // Create and insert static image var img = document.createElement('img'); img.id = 'graph-print-image'; img.src = pngDataUrl; img.alt = 'Model Graph'; img.style.maxWidth = '100%'; img.style.height = 'auto'; img.style.display = 'block'; graphContainer.appendChild(img); } catch (canvasErr) { console.error('[PrintHandler] Canvas to PNG conversion error:', canvasErr); this._insertPlaceholder(graphContainer); } } else if (graphContainer) { // No Cytoscape instance — show placeholder this._insertPlaceholder(graphContainer); } } catch (err) { console.error('[PrintHandler] _prepareForPrint error:', err); } }; /** * Insert a "no graph" placeholder into the graph container. * @param {HTMLElement} container * @private */ PrintHandler.prototype._insertPlaceholder = function (container) { try { var placeholder = document.createElement('p'); placeholder.id = 'graph-print-placeholder'; placeholder.textContent = 'Không có đồ thị để hiển thị'; placeholder.style.textAlign = 'center'; placeholder.style.color = '#6c757d'; placeholder.style.padding = '2rem 0'; container.appendChild(placeholder); } catch (err) { console.error('[PrintHandler] _insertPlaceholder error:', err); } }; // ─── Print Restoration ──────────────────────────────────────────────────── /** * Restore the page after printing: * - Remove static print image * - Remove placeholder * - Restore Cytoscape canvas visibility * @private */ PrintHandler.prototype._restoreAfterPrint = function () { try { // Remove static print image var printImage = document.getElementById('graph-print-image'); if (printImage && printImage.parentNode) { printImage.parentNode.removeChild(printImage); } // Remove placeholder var placeholder = document.getElementById('graph-print-placeholder'); if (placeholder && placeholder.parentNode) { placeholder.parentNode.removeChild(placeholder); } // Restore all hidden children of graphContainer var graphContainer = document.getElementById('graphContainer'); if (graphContainer) { var children = graphContainer.querySelectorAll('[data-print-hidden]'); for (var i = 0; i < children.length; i++) { children[i].style.display = ''; children[i].removeAttribute('data-print-hidden'); } // Restore original fixed height if (this._origHeight !== undefined) { graphContainer.style.height = this._origHeight; graphContainer.style.overflow = ''; this._origHeight = undefined; } } } catch (err) { console.error('[PrintHandler] _restoreAfterPrint error:', err); } }; // ─── Helpers ────────────────────────────────────────────────────────────── /** * Get the current model file name from StateManager, SafeTensorsViewer, or TFLiteViewer. * @returns {string|null} * @private */ PrintHandler.prototype._getCurrentModelFileName = function () { try { // 1. Try StateManager (ONNX models) if (window.StateManager && typeof window.StateManager.getCurrentModel === 'function') { var model = window.StateManager.getCurrentModel(); if (model && model.metadata && model.metadata.fileName) { return model.metadata.fileName; } } // 2. Try SafeTensorsViewer if (window._onnxApp && typeof window._onnxApp.getSafeTensorsViewer === 'function') { var stViewer = window._onnxApp.getSafeTensorsViewer(); if (stViewer && stViewer._fileName) { return stViewer._fileName; } } // 3. Try TFLiteViewer if (window._onnxApp && typeof window._onnxApp.getTFLiteViewer === 'function') { var tflViewer = window._onnxApp.getTFLiteViewer(); if (tflViewer && tflViewer._fileName) { return tflViewer._fileName; } } return null; } catch (err) { console.error('[PrintHandler] _getCurrentModelFileName error:', err); return null; } }; /** * Get the Cytoscape instance from GraphVisualizer. * @returns {cytoscape.Core|null} * @private */ PrintHandler.prototype._getCytoscapeInstance = function () { try { if (window._onnxApp && typeof window._onnxApp.getGraphVisualizer === 'function') { var gv = window._onnxApp.getGraphVisualizer(); if (gv && gv._cy) { return gv._cy; } } return null; } catch (err) { console.error('[PrintHandler] _getCytoscapeInstance error:', err); return null; } }; // ─── Public API ─────────────────────────────────────────────────────────── /** * Trigger the browser print dialog. */ PrintHandler.prototype.print = function () { try { window.print(); } catch (err) { console.error('[PrintHandler] print() error:', err); } }; /** * Destroy the PrintHandler: unsubscribe, remove event listeners. */ PrintHandler.prototype.destroy = function () { try { // Unsubscribe from StateManager if (this._unsubscribeModel) { this._unsubscribeModel(); this._unsubscribeModel = null; } // Remove window event listeners if (this._boundBeforePrint) { window.removeEventListener('beforeprint', this._boundBeforePrint); this._boundBeforePrint = null; } if (this._boundAfterPrint) { window.removeEventListener('afterprint', this._boundAfterPrint); this._boundAfterPrint = null; } // Remove click handler if (this._printBtn && this._boundClickHandler) { this._printBtn.removeEventListener('click', this._boundClickHandler); this._boundClickHandler = null; } } catch (err) { console.error('[PrintHandler] destroy() error:', err); } }; return PrintHandler; })();