/**
* Tab Manager - Handles all tab operations including creation, switching, and closing.
*
* This manager provides a complete tab interface similar to modern code editors,
* allowing users to work with multiple files simultaneously. It manages tab state,
* coordinates with the editor manager, and handles file modifications tracking.
*
* @class TabManager
* @author CTrace GUI Team
* @version 1.0.0
*
* @example
* const tabManager = new TabManager(editorManager, notificationManager);
* const tabId = tabManager.createTab('example.js', '/path/to/example.js', 'console.log("Hello")');
*/
class TabManager {
/**
* Creates an instance of TabManager.
*
* @constructor
* @memberof TabManager
* @param {EditorManager} editorManager - Editor manager instance for content management
* @param {NotificationManager} notificationManager - Notification manager for user feedback
*/
constructor(editorManager, notificationManager) {
/**
* Editor manager instance
* @type {EditorManager}
* @private
*/
this.editorManager = editorManager;
/**
* Notification manager instance
* @type {NotificationManager}
* @private
*/
this.notificationManager = notificationManager;
/**
* Counter for generating unique tab IDs
* @type {number}
* @private
*/
this.tabIdCounter = 0;
/**
* Currently active tab ID
* @type {string|null}
*/
this.activeTabId = null;
/**
* Map of open tabs with their data
* @type {Map<string, Object>}
* @private
*/
this.openTabs = new Map();
/**
* DOM element containing the tabs
* @type {HTMLElement}
* @private
*/
this.tabsContainer = document.getElementById('tabs-container');
/**
* Welcome screen DOM element
* @type {HTMLElement}
* @private
*/
this.welcomeScreen = document.getElementById('welcome-screen');
/**
* Editor area DOM element
* @type {HTMLElement}
* @private
*/
this.editorArea = document.getElementById('editor-area');
}
/**
* Create a new tab
* @param {string} fileName - Tab file name
* @param {string} filePath - File path (optional)
* @param {string} content - File content
* @param {Object} fileInfo - File metadata
* @returns {string} - Tab ID
*/
createTab(fileName, filePath = null, content = '', fileInfo = {}) {
// Show editor area if this is the first tab
if (this.openTabs.size === 0) {
this.showEditor();
}
const tabId = 'tab_' + (++this.tabIdCounter);
// Create tab data
this.openTabs.set(tabId, {
filePath: filePath,
content: content,
modified: false,
fileName: fileName,
fileInfo: fileInfo // Store file metadata
});
// Create tab element
const tabElement = document.createElement('div');
tabElement.className = 'tab';
tabElement.setAttribute('data-tab-id', tabId);
tabElement.setAttribute('data-file-path', filePath || '');
// Add warning indicator if file has encoding issues or is partial
const warningIndicator = (fileInfo.encodingWarning || fileInfo.isPartial) ?
`<span class="tab-warning" title="${fileInfo.encodingWarning ? 'Encoding Warning' : ''}${fileInfo.isPartial ? 'File Partially Loaded' : ''}">⚠️</span>` : '';
tabElement.innerHTML = `
<div class="tab-label">${fileName}${warningIndicator}</div>
<div class="tab-close" onclick="window.tabManager.closeTab(event, '${tabId}')">×</div>
`;
tabElement.addEventListener('click', (e) => {
if (!e.target.classList.contains('tab-close')) {
this.switchToTab(tabId);
}
});
this.tabsContainer.appendChild(tabElement);
return tabId;
}
/**
* Switch to a specific tab
* @param {string} tabId - Tab ID to switch to
*/
switchToTab(tabId) {
// Save current tab content if we have an active tab
if (this.activeTabId && this.openTabs.has(this.activeTabId)) {
const currentTab = this.openTabs.get(this.activeTabId);
currentTab.content = this.editorManager.getContent();
}
// Update active tab
this.activeTabId = tabId;
const newTab = this.openTabs.get(tabId);
if (newTab) {
// Update editor
this.editorManager.setContent(newTab.content);
// Set file type for syntax highlighting
if (newTab.fileName) {
this.editorManager.setFileType(newTab.fileName);
}
// Update tab appearance
document.querySelectorAll('.tab').forEach(tab => {
tab.classList.remove('active');
});
const tabElement = document.querySelector(`[data-tab-id="${tabId}"]`);
if (tabElement) {
tabElement.classList.add('active');
}
// Update file tree selection
if (newTab.filePath) {
document.querySelectorAll('.file-tree-item').forEach(el => el.classList.remove('selected'));
const fileTreeItem = document.querySelector(`[data-file-path="${newTab.filePath}"]`);
if (fileTreeItem) {
fileTreeItem.classList.add('selected');
}
}
// Emit tab switch event for other components
this.onTabSwitch(newTab);
}
}
/**
* Close a tab
* @param {Event} event - Click event
* @param {string} tabId - Tab ID to close
*/
closeTab(event, tabId) {
event.stopPropagation();
const tab = this.openTabs.get(tabId);
if (!tab) return;
// Check if modified
if (tab.modified) {
const result = confirm(`${tab.fileName} has unsaved changes. Do you want to close it anyway?`);
if (!result) return;
}
// Remove tab element
const tabElement = document.querySelector(`[data-tab-id="${tabId}"]`);
if (tabElement) {
tabElement.remove();
}
// Remove from data
this.openTabs.delete(tabId);
// If closing active tab, switch to another tab or show welcome screen
if (this.activeTabId === tabId) {
const remainingTabs = Array.from(this.openTabs.keys());
if (remainingTabs.length > 0) {
this.switchToTab(remainingTabs[remainingTabs.length - 1]);
} else {
// No more tabs, show welcome screen
this.activeTabId = null;
this.showWelcomeScreen();
}
}
}
/**
* Mark tab as modified
* @param {string} tabId - Tab ID
*/
markTabModified(tabId) {
const tab = this.openTabs.get(tabId);
if (tab && !tab.modified) {
tab.modified = true;
const tabElement = document.querySelector(`[data-tab-id="${tabId}"]`);
if (tabElement) {
tabElement.classList.add('modified');
}
}
}
/**
* Mark tab as clean (not modified)
* @param {string} tabId - Tab ID
*/
markTabClean(tabId) {
const tab = this.openTabs.get(tabId);
if (tab && tab.modified) {
tab.modified = false;
const tabElement = document.querySelector(`[data-tab-id="${tabId}"]`);
if (tabElement) {
tabElement.classList.remove('modified');
}
}
}
/**
* Check if file is already open in a tab
* @param {string} filePath - File path to check
* @returns {string|null} - Tab ID if found, null otherwise
*/
findTabByPath(filePath) {
for (const [tabId, tab] of this.openTabs) {
if (tab.filePath === filePath) {
return tabId;
}
}
return null;
}
/**
* Get current active tab
* @returns {Object|null} - Active tab data or null
*/
getActiveTab() {
return this.activeTabId ? this.openTabs.get(this.activeTabId) : null;
}
/**
* Update tab file info (for file operations)
* @param {string} tabId - Tab ID
* @param {string} filePath - New file path
* @param {string} fileName - New file name
*/
updateTabFile(tabId, filePath, fileName) {
const tab = this.openTabs.get(tabId);
if (tab) {
tab.filePath = filePath;
tab.fileName = fileName;
// Update tab label
const tabElement = document.querySelector(`[data-tab-id="${tabId}"]`);
if (tabElement) {
const labelElement = tabElement.querySelector('.tab-label');
if (labelElement) {
labelElement.textContent = fileName;
}
tabElement.setAttribute('data-file-path', filePath);
}
}
}
/**
* Show welcome screen
*/
showWelcomeScreen() {
this.welcomeScreen.style.display = 'flex';
this.editorArea.style.display = 'none';
this.tabsContainer.style.display = 'none';
}
/**
* Show editor
*/
showEditor() {
this.welcomeScreen.style.display = 'none';
this.editorArea.style.display = 'flex';
this.tabsContainer.style.display = 'flex';
}
/**
* Handle tab content changes (for modification tracking)
* @param {string} tabId - Tab ID
* @param {string} newContent - New content
*/
handleContentChange(tabId, newContent) {
const tab = this.openTabs.get(tabId);
if (tab && tab.content !== newContent) {
tab.content = newContent;
this.markTabModified(tabId);
}
}
/**
* Callback for when tab switches (for other components to listen to)
* @param {Object} tabData - Tab data
*/
onTabSwitch(tabData) {
// Update file type in status bar if file type utils are available
if (window.updateFileTypeStatus) {
window.updateFileTypeStatus(tabData.fileName);
}
// Update file status
this.updateFileStatus(tabData);
}
/**
* Update file status indicator
* @param {Object} tabData - Tab data
*/
updateFileStatus(tabData) {
const fileStatusElement = document.getElementById('fileStatus');
if (fileStatusElement && tabData.fileInfo) {
const { fileInfo } = tabData;
let statusText = 'UTF-8';
let statusStyle = '';
if (fileInfo.encodingWarning) {
statusText = '⚠️ Non-UTF8';
statusStyle = 'color: #f85149; cursor: pointer;';
fileStatusElement.title = 'File may contain non-UTF8 characters';
} else if (fileInfo.isPartial) {
const loadedKB = Math.round(fileInfo.loadedSize / 1024);
const totalKB = Math.round(fileInfo.totalSize / 1024);
statusText = `📄 Partial (${loadedKB}KB/${totalKB}KB)`;
statusStyle = 'color: #f0883e; cursor: pointer;';
fileStatusElement.title = 'Click to load full file';
fileStatusElement.onclick = () => this.onLoadFullFile(tabData.filePath);
} else {
statusText = 'UTF-8';
statusStyle = '';
fileStatusElement.onclick = null;
fileStatusElement.title = '';
}
fileStatusElement.textContent = statusText;
fileStatusElement.style.cssText = statusStyle;
}
}
/**
* Callback for loading full file (to be implemented by parent)
* @param {string} filePath - File path to load
*/
onLoadFullFile(filePath) {
// This will be set by the main UI controller
console.log('Load full file requested for:', filePath);
}
/**
* Create a new untitled file tab
* @returns {string} - New tab ID
*/
createNewFile() {
const fileName = 'untitled-' + (this.tabIdCounter + 1);
const tabId = this.createTab(fileName);
this.switchToTab(tabId);
this.editorManager.focus();
return tabId;
}
/**
* Switch to next tab
*/
switchToNextTab() {
const tabIds = Array.from(this.openTabs.keys());
if (tabIds.length > 0 && this.activeTabId) {
const currentIndex = tabIds.indexOf(this.activeTabId);
const nextIndex = (currentIndex + 1) % tabIds.length;
this.switchToTab(tabIds[nextIndex]);
}
}
/**
* Get all open tabs count
* @returns {number} - Number of open tabs
*/
getTabCount() {
return this.openTabs.size;
}
}
module.exports = TabManager;