/** * GitHub Code Viewer - Main JavaScript */ (function($) { 'use strict'; // GitHub Code Viewer Class class GitHubCodeViewer { constructor(element) { this.$element = $(element); this.repo = this.$element.data('repo'); this.theme = this.$element.data('theme'); this.showLineNumbers = this.$element.data('show-line-numbers'); this.initialFile = this.$element.data('initial-file'); this.files = []; this.openTabs = []; this.activeTab = null; this.fileCache = {}; this.activeRequests = []; // Track active AJAX requests this.currentPath = ''; // Track current folder path this.init(); } init() { this.bindEvents(); this.loadRepositoryFiles(); } bindEvents() { // Search functionality with debouncing let searchTimeout; this.$element.on('input', '.mcb-search-input', (e) => { clearTimeout(searchTimeout); searchTimeout = setTimeout(() => { this.filterFiles($(e.target).val()); }, 300); // Debounce for 300ms }); // File/folder selection this.$element.on('click', '.mcb-file-item', (e) => { const $item = $(e.currentTarget); const isFolder = $item.data('is-folder'); const path = $item.data('path'); if (isFolder) { // Navigate to folder this.loadRepositoryFiles(path); } else { // Load file this.loadFile(path); } }); // Tab management this.$element.on('click', '.mcb-tab', (e) => { const $tab = $(e.currentTarget); const filePath = $tab.data('path'); this.switchToTab(filePath); }); this.$element.on('click', '.mcb-tab-close', (e) => { e.stopPropagation(); const $tab = $(e.target).closest('.mcb-tab'); const filePath = $tab.data('path'); this.closeTab(filePath); }); // Copy button this.$element.on('click', '.mcb-copy-btn', (e) => { this.copyCode($(e.target)); }); // Home button - go to root this.$element.on('click', '.mcb-home-btn', () => { this.loadRepositoryFiles(''); // Load root }); // Refresh button this.$element.on('click', '.mcb-refresh-btn', () => { this.refreshFiles(); }); // Fullscreen toggle this.$element.on('click', '.mcb-fullscreen-btn', () => { this.toggleFullscreen(); }); } loadRepositoryFiles(path = '') { console.log('MCB: Loading repository files for:', this.repo, 'path:', path); const $fileList = this.$element.find('.mcb-file-list'); // Update current path this.currentPath = path; // Show loading indicator $fileList.html('
Loading files...
'); // Abort any pending requests this.abortActiveRequests(); console.log('MCB: Making AJAX request to:', mcb_ajax.ajax_url); const request = $.ajax({ url: mcb_ajax.ajax_url, type: 'POST', data: { action: 'mcb_get_repo_files', repo: this.repo, path: path, nonce: mcb_ajax.nonce }, success: (response) => { console.log('MCB: AJAX response:', response); this.removeRequest(request); if (response.success) { console.log('MCB: Files loaded:', response.data); this.files = response.data; this.renderFileList(); // Load initial file if specified if (this.initialFile) { const file = this.files.find(f => f.path === this.initialFile); if (file) { this.loadFile(this.initialFile); } } } else { console.error('MCB: Error:', response.data); $fileList.html('
' + response.data + '
'); } }, error: (xhr, status, error) => { console.error('MCB: AJAX failed:', status, error); this.removeRequest(request); $fileList.html('
Failed to load repository files: ' + error + '
'); } }); this.activeRequests.push(request); } renderFileList() { const $fileList = this.$element.find('.mcb-file-list'); $fileList.empty(); // Add current path breadcrumb if not at root if (this.currentPath) { const $breadcrumb = $('
'); $breadcrumb.append('Path: '); $breadcrumb.append('/' + this.escapeHtml(this.currentPath) + ''); $fileList.append($breadcrumb); } // Render files and folders this.files.forEach(file => { const $item = $('
') .attr('data-path', file.path) .attr('data-name', file.name.toLowerCase()) .attr('data-is-folder', file.is_folder || false); // Add appropriate icon let icon; if (file.type === 'parent') { icon = '⬆'; // Up arrow for parent $item.addClass('mcb-parent-folder'); } else if (file.is_folder) { icon = '📁'; // Folder icon $item.addClass('mcb-folder'); } else { icon = this.getFileIcon(file.type); $item.addClass('mcb-file'); } $item.append('' + icon + ''); $item.append('' + this.escapeHtml(file.name) + ''); // Add file size for files only if (!file.is_folder) { const sizeStr = this.formatFileSize(file.size); $item.append('' + sizeStr + ''); } $fileList.append($item); }); } organizeFileTree(files) { const tree = {}; files.forEach(file => { const parts = file.path.split('/'); let current = tree; parts.forEach((part, index) => { if (index === parts.length - 1) { // It's a file current[part] = file; } else { // It's a directory if (!current[part]) { current[part] = {}; } current = current[part]; } }); }); return tree; } filterFiles(searchTerm) { const $items = this.$element.find('.mcb-file-item'); const term = searchTerm.toLowerCase(); $items.each((index, item) => { const $item = $(item); const fileName = $item.data('name'); if (!term || fileName.includes(term)) { $item.show(); } else { $item.hide(); } }); } loadFile(filePath) { // Limit cache size to prevent memory issues this.limitCacheSize(); // Check if file is already in cache if (this.fileCache[filePath]) { this.displayFile(filePath, this.fileCache[filePath]); return; } // Update status this.updateStatus('Loading file...'); const request = $.ajax({ url: mcb_ajax.ajax_url, type: 'POST', data: { action: 'mcb_load_file', repo: this.repo, file_path: filePath, nonce: mcb_ajax.nonce }, success: (response) => { this.removeRequest(request); if (response.success) { this.fileCache[filePath] = response.data; this.displayFile(filePath, response.data); this.updateStatus('Ready'); } else { this.showError('Failed to load file: ' + response.data); this.updateStatus('Error loading file'); } }, error: () => { this.removeRequest(request); this.showError('Network error while loading file'); this.updateStatus('Network error'); } }); this.activeRequests.push(request); } displayFile(filePath, fileData) { // Store in cache this.fileCache[filePath] = fileData; // Limit number of open tabs to prevent memory issues const maxTabs = 10; // Add to tabs if not already open if (!this.openTabs.includes(filePath)) { // Check tab limit if (this.openTabs.length >= maxTabs) { // Close the oldest tab const oldestTab = this.openTabs[0]; this.closeTab(oldestTab); } this.openTabs.push(filePath); this.addTab(filePath, fileData.filename); } // Only switch to tab if not already active if (this.activeTab !== filePath) { this.switchToTab(filePath); } else { // Just update the content if already active const $codeArea = this.$element.find('.mcb-code-area'); const safeContent = this.createSafeCodeDisplay(fileData.content, fileData.filename); $codeArea.html(safeContent); // Apply syntax highlighting if (typeof Prism !== 'undefined') { Prism.highlightAll(); } // Update status const fileSize = this.formatFileSize(fileData.content.length); const lineCount = fileData.content.split('\n').length; this.$element.find('.mcb-file-info').text(fileData.filename + ' • ' + lineCount + ' lines • ' + fileSize); } // Display content const $codeArea = this.$element.find('.mcb-code-area'); // Create safe HTML content const safeContent = this.createSafeCodeDisplay(fileData.content, fileData.filename); $codeArea.html(safeContent); // Apply syntax highlighting if Prism is loaded if (typeof Prism !== 'undefined') { Prism.highlightAll(); } // Mark file as active in sidebar this.$element.find('.mcb-file-item').removeClass('active'); this.$element.find('.mcb-file-item[data-path="' + filePath + '"]').addClass('active'); // Update file info const file = this.files.find(f => f.path === filePath); if (file) { this.updateFileInfo(file); } } createSafeCodeDisplay(content, filename) { // The content is already escaped by PHP, but we'll double-check const $container = $('
'); // Header const $header = $('
'); $header.append($('').text(filename)); // Copy button - store content in data, not attribute const $copyBtn = $(''); $copyBtn.data('content', content); // Use data() instead of attr() $header.append($copyBtn); $container.append($header); // Code wrapper const $wrapper = $('
'); const language = this.detectLanguage(filename); // Create pre/code structure const $pre = $('
').addClass('line-numbers');
            const $code = $('').addClass('language-' + language);
            
            // CRITICAL: Ensure content is text, not HTML
            $code.text(content);
            
            $pre.append($code);
            $wrapper.append($pre);
            $container.append($wrapper);
            
            return $container;
        }
        
        addTab(filePath, filename) {
            const $tabs = this.$element.find('.mcb-tabs');
            
            const $tab = $('
') .attr('data-path', filePath); $tab.append('' + this.escapeHtml(filename) + ''); $tab.append('×'); $tabs.append($tab); } switchToTab(filePath) { // Prevent switching if already active if (this.activeTab === filePath) { return; } this.activeTab = filePath; // Update tab states this.$element.find('.mcb-tab').removeClass('active'); this.$element.find('.mcb-tab[data-path="' + filePath + '"]').addClass('active'); // Load file content if cached, otherwise just display the tab if (this.fileCache[filePath]) { // Display cached content without calling displayFile const $codeArea = this.$element.find('.mcb-code-area'); const fileData = this.fileCache[filePath]; const safeContent = this.createSafeCodeDisplay(fileData.content, fileData.filename); $codeArea.html(safeContent); // Apply syntax highlighting if Prism is loaded if (typeof Prism !== 'undefined') { Prism.highlightAll(); } // Update status const fileSize = this.formatFileSize(fileData.content.length); const lineCount = fileData.content.split('\n').length; this.$element.find('.mcb-file-info').text(fileData.filename + ' • ' + lineCount + ' lines • ' + fileSize); } } closeTab(filePath) { // Remove from open tabs const index = this.openTabs.indexOf(filePath); if (index > -1) { this.openTabs.splice(index, 1); } // Remove tab element this.$element.find('.mcb-tab[data-path="' + filePath + '"]').remove(); // If this was the active tab, switch to another if (this.activeTab === filePath) { if (this.openTabs.length > 0) { this.switchToTab(this.openTabs[this.openTabs.length - 1]); } else { // Show welcome screen this.showWelcome(); } } } copyCode($button) { const content = $button.data('content'); // Use data() instead of attr() if (!content) { return; } // Create temporary textarea const $temp = $('