cvs-proxy/ui/ui.js

296 lines
No EOL
9.9 KiB
JavaScript

/**
* UI Management Module
* Handles DOM manipulation and view switching
*/
class UIManager {
constructor() {
this.currentFile = null;
this.currentHistory = null;
this.initializeElements();
}
initializeElements() {
// Views
this.welcomeView = document.getElementById('welcomeView');
this.fileView = document.getElementById('fileView');
this.historyView = document.getElementById('historyView');
this.diffView = document.getElementById('diffView');
// File view elements
this.fileName = document.getElementById('fileName');
this.filePath = document.getElementById('filePath');
this.fileContent = document.getElementById('fileContent');
this.historyBtn = document.getElementById('historyBtn');
this.diffBtn = document.getElementById('diffBtn');
// History view elements
this.historyContent = document.getElementById('historyContent');
this.backFromHistoryBtn = document.getElementById('backFromHistoryBtn');
// Diff view elements
this.rev1Select = document.getElementById('rev1Select');
this.rev2Select = document.getElementById('rev2Select');
this.generateDiffBtn = document.getElementById('generateDiffBtn');
this.diffContent = document.getElementById('diffContent');
this.backFromDiffBtn = document.getElementById('backFromDiffBtn');
// Tree elements
this.treeContainer = document.getElementById('treeContainer');
this.refreshBtn = document.getElementById('refreshBtn');
// Status
this.status = document.getElementById('status');
}
/**
* Show a specific view and hide others
* @param {HTMLElement} view - View to show
*/
showView(view) {
[this.welcomeView, this.fileView, this.historyView, this.diffView].forEach(v => {
if (v) v.classList.add('hidden');
});
if (view) view.classList.remove('hidden');
}
/**
* Display file content
* @param {string} path - File path
* @param {string} content - File content
*/
displayFile(path, content) {
this.currentFile = path;
this.fileName.textContent = path.split('/').pop();
this.filePath.textContent = path;
// Escape HTML and display content
const escapedContent = this.escapeHtml(content);
this.fileContent.innerHTML = `<pre><code>${escapedContent}</code></pre>`;
this.showView(this.fileView);
}
/**
* Display file history
* @param {Array} history - Array of revision objects
*/
displayHistory(history) {
this.currentHistory = history;
if (!history || history.length === 0) {
this.historyContent.innerHTML = '<div class="loading">No history available</div>';
this.showView(this.historyView);
return;
}
const historyHTML = history.map(revision => `
<div class="history-item" data-revision="${revision.revision}">
<div class="history-item-header">
<span class="history-revision">${revision.revision}</span>
<span class="history-date">${revision.date || 'N/A'}</span>
</div>
<div class="history-author">Author: <strong>${revision.author || 'Unknown'}</strong></div>
<span class="history-state">${revision.state || 'Exp'}</span>
<div class="history-lines">${revision.lines_changed || 'N/A'}</div>
</div>
`).join('');
this.historyContent.innerHTML = historyHTML;
this.showView(this.historyView);
// Add click handlers to history items
this.historyContent.querySelectorAll('.history-item').forEach(item => {
item.addEventListener('click', () => {
const revision = item.dataset.revision;
window.app.loadFileAtRevision(this.currentFile, revision);
});
});
}
/**
* Display diff
* @param {string} diffText - Diff content
*/
displayDiff(diffText) {
if (!diffText) {
this.diffContent.innerHTML = '<div class="loading">No differences found</div>';
this.showView(this.diffView);
return;
}
// Parse and colorize diff
const lines = diffText.split('\n');
const colorizedLines = lines.map(line => {
let className = '';
if (line.startsWith('+++') || line.startsWith('---') || line.startsWith('@@')) {
className = 'diff-line header';
} else if (line.startsWith('+')) {
className = 'diff-line added';
} else if (line.startsWith('-')) {
className = 'diff-line removed';
} else {
className = 'diff-line context';
}
const escapedLine = this.escapeHtml(line);
return `<div class="${className}">${escapedLine}</div>`;
}).join('');
this.diffContent.innerHTML = `<pre><code>${colorizedLines}</code></pre>`;
this.showView(this.diffView);
}
/**
* Populate revision selectors
* @param {Array} history - Array of revision objects
*/
populateRevisionSelectors(history) {
if (!history || history.length === 0) {
this.rev1Select.innerHTML = '<option>No revisions available</option>';
this.rev2Select.innerHTML = '<option>No revisions available</option>';
return;
}
const options = history.map(rev =>
`<option value="${rev.revision}">${rev.revision} - ${rev.date || 'N/A'}</option>`
).join('');
this.rev1Select.innerHTML = options;
this.rev2Select.innerHTML = options;
// Set default selections
if (history.length > 1) {
this.rev1Select.selectedIndex = history.length - 1;
this.rev2Select.selectedIndex = 0;
}
}
/**
* Update status indicator
* @param {string} message - Status message
* @param {string} type - Status type: 'connecting', 'connected', 'error'
*/
updateStatus(message, type = 'connecting') {
this.status.textContent = message;
this.status.className = `status ${type}`;
}
/**
* Show loading state
* @param {string} message - Loading message
*/
showLoading(message = 'Loading...') {
this.fileContent.innerHTML = `<div class="loading">${message}</div>`;
}
/**
* Show error message
* @param {string} message - Error message
*/
showError(message) {
this.fileContent.innerHTML = `<div class="loading" style="color: #991b1b;">${this.escapeHtml(message)}</div>`;
}
/**
* Escape HTML special characters
* @param {string} text - Text to escape
* @returns {string} Escaped text
*/
escapeHtml(text) {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, m => map[m]);
}
/**
* Build tree view from file list
* @param {Array} files - Array of file paths
* @param {Function} onFileClick - Callback when file is clicked
*/
buildTree(files, onFileClick) {
if (!files || files.length === 0) {
this.treeContainer.innerHTML = '<div class="loading">No files found</div>';
return;
}
// Build hierarchical tree structure
const tree = {};
files.forEach(file => {
const parts = file.split('/');
let current = tree;
parts.forEach((part, index) => {
if (!current[part]) {
current[part] = {};
}
current = current[part];
});
});
// Render tree
this.treeContainer.innerHTML = '';
this.renderTreeNode(tree, '', this.treeContainer, onFileClick);
}
/**
* Recursively render tree nodes
* @param {object} node - Tree node
* @param {string} path - Current path
* @param {HTMLElement} container - Container element
* @param {Function} onFileClick - Callback when file is clicked
*/
renderTreeNode(node, path, container, onFileClick) {
const keys = Object.keys(node).sort();
keys.forEach(key => {
const fullPath = path ? `${path}/${key}` : key;
const isFile = Object.keys(node[key]).length === 0;
const item = document.createElement('div');
item.className = 'tree-item';
const icon = document.createElement('span');
icon.className = 'tree-item-icon';
icon.textContent = isFile ? '📄' : '📁';
const name = document.createElement('span');
name.className = 'tree-item-name';
name.textContent = key;
item.appendChild(icon);
item.appendChild(name);
if (isFile) {
item.addEventListener('click', () => {
// Remove active class from all items
container.querySelectorAll('.tree-item').forEach(i => i.classList.remove('active'));
item.classList.add('active');
onFileClick(fullPath);
});
}
container.appendChild(item);
// Recursively render subdirectories
if (!isFile) {
this.renderTreeNode(node[key], fullPath, container, onFileClick);
}
});
}
/**
* Clear active selection in tree
*/
clearTreeSelection() {
this.treeContainer.querySelectorAll('.tree-item').forEach(item => {
item.classList.remove('active');
});
}
}
// Create global UI manager instance
const ui = new UIManager();