monorepo/native/wordpress/maple-code-blocks/assets/js/mcb-script.js

647 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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('<div class="mcb-loading"><div class="mcb-spinner"></div><span>Loading files...</span></div>');
// 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('<div class="mcb-error">' + response.data + '</div>');
}
},
error: (xhr, status, error) => {
console.error('MCB: AJAX failed:', status, error);
this.removeRequest(request);
$fileList.html('<div class="mcb-error">Failed to load repository files: ' + error + '</div>');
}
});
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 = $('<div class="mcb-breadcrumb">');
$breadcrumb.append('<span class="mcb-path-label">Path: </span>');
$breadcrumb.append('<span class="mcb-current-path">/' + this.escapeHtml(this.currentPath) + '</span>');
$fileList.append($breadcrumb);
}
// Render files and folders
this.files.forEach(file => {
const $item = $('<div class="mcb-file-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('<span class="mcb-file-icon">' + icon + '</span>');
$item.append('<span class="mcb-file-name">' + this.escapeHtml(file.name) + '</span>');
// Add file size for files only
if (!file.is_folder) {
const sizeStr = this.formatFileSize(file.size);
$item.append('<span class="mcb-file-size">' + sizeStr + '</span>');
}
$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 = $('<div class="mcb-code-container">');
// Header
const $header = $('<div class="mcb-code-header">');
$header.append($('<span class="mcb-filename">').text(filename));
// Copy button - store content in data, not attribute
const $copyBtn = $('<button class="mcb-copy-btn">Copy</button>');
$copyBtn.data('content', content); // Use data() instead of attr()
$header.append($copyBtn);
$container.append($header);
// Code wrapper
const $wrapper = $('<div class="mcb-code-wrapper">');
const language = this.detectLanguage(filename);
// Create pre/code structure
const $pre = $('<pre>').addClass('line-numbers');
const $code = $('<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 = $('<div class="mcb-tab">')
.attr('data-path', filePath);
$tab.append('<span class="mcb-tab-title">' + this.escapeHtml(filename) + '</span>');
$tab.append('<span class="mcb-tab-close">×</span>');
$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 = $('<textarea>');
$temp.val(content);
$('body').append($temp);
$temp.select();
try {
document.execCommand('copy');
$button.text('Copied!').addClass('copied');
setTimeout(() => {
$button.text('Copy').removeClass('copied');
}, 2000);
} catch (err) {
console.error('Failed to copy:', err);
}
$temp.remove();
}
refreshFiles() {
// Abort any pending requests first
this.abortActiveRequests();
// Clear cache
this.fileCache = {};
// Reload files at current path
this.loadRepositoryFiles(this.currentPath);
this.updateStatus('Repository refreshed');
}
toggleFullscreen() {
this.$element.toggleClass('fullscreen');
}
showWelcome() {
const $codeArea = this.$element.find('.mcb-code-area');
const welcome = `
<div class="mcb-welcome">
<svg viewBox="0 0 24 24" width="64" height="64">
<path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"/>
</svg>
<h4>GitHub Code Viewer</h4>
<p>Select a file from the sidebar to view its content</p>
</div>
`;
$codeArea.html(welcome);
}
showError(message) {
const $codeArea = this.$element.find('.mcb-code-area');
$codeArea.html('<div class="mcb-error">' + this.escapeHtml(message) + '</div>');
}
updateStatus(text) {
this.$element.find('.mcb-status-text').text(text);
}
updateFileInfo(file) {
const info = file.name + ' • ' + this.formatFileSize(file.size) + ' • ' + file.type;
this.$element.find('.mcb-file-info').text(info);
}
formatFileSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
detectLanguage(filename) {
const ext = filename.split('.').pop().toLowerCase();
const languageMap = {
'js': 'javascript',
'jsx': 'jsx',
'ts': 'typescript',
'tsx': 'tsx',
'py': 'python',
'rb': 'ruby',
'php': 'php',
'java': 'java',
'c': 'c',
'cpp': 'cpp',
'cs': 'csharp',
'go': 'go',
'rs': 'rust',
'swift': 'swift',
'kt': 'kotlin',
'scala': 'scala',
'r': 'r',
'sql': 'sql',
'sh': 'bash',
'yml': 'yaml',
'json': 'json',
'xml': 'xml',
'html': 'html',
'css': 'css',
'scss': 'scss',
'md': 'markdown'
};
return languageMap[ext] || 'plain';
}
getFileIcon(type) {
const icons = {
'javascript': '📜',
'python': '🐍',
'php': '🐘',
'java': '☕',
'cpp': '⚙️',
'go': '🐹',
'rust': '🦀',
'ruby': '💎',
'html': '🌐',
'css': '🎨',
'json': '📋',
'markdown': '📝',
'default': '📄'
};
return icons[type] || icons.default;
}
escapeHtml(text) {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, m => map[m]);
}
limitCacheSize() {
// Limit cache to 20 files to prevent memory issues
const maxCacheSize = 20;
const cacheKeys = Object.keys(this.fileCache);
if (cacheKeys.length >= maxCacheSize) {
// Remove oldest cached files (FIFO)
const toRemove = cacheKeys.slice(0, cacheKeys.length - maxCacheSize + 1);
toRemove.forEach(key => {
delete this.fileCache[key];
});
}
}
cleanupBeforeUnload() {
// Clean up event listeners and large objects
this.$element.off();
this.abortActiveRequests();
this.fileCache = null;
this.files = null;
this.openTabs = null;
}
abortActiveRequests() {
// Abort all pending AJAX requests
if (this.activeRequests && this.activeRequests.length > 0) {
this.activeRequests.forEach(request => {
if (request && request.abort) {
request.abort();
}
});
this.activeRequests = [];
}
}
removeRequest(request) {
const index = this.activeRequests.indexOf(request);
if (index > -1) {
this.activeRequests.splice(index, 1);
}
}
}
// Initialize all viewers on page
$(document).ready(() => {
console.log('MCB: Document ready, looking for .maple-code-blocks elements');
const viewers = [];
$('.maple-code-blocks').each(function() {
console.log('MCB: Initializing viewer for element:', this);
try {
const viewer = new GitHubCodeViewer(this);
viewers.push(viewer);
console.log('MCB: Viewer initialized successfully');
} catch (error) {
console.error('MCB: Error initializing viewer:', error);
}
});
console.log('MCB: Total viewers initialized:', viewers.length);
// Cleanup on page unload to prevent memory leaks
$(window).on('beforeunload', () => {
viewers.forEach(viewer => {
if (viewer && viewer.cleanupBeforeUnload) {
viewer.cleanupBeforeUnload();
}
});
});
});
})(jQuery);