From f60836eabfee07c2d49bb4b0d5a25d437b31737d Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Wed, 31 Dec 2025 07:01:59 +0000 Subject: [PATCH] feat(editor): add file explorer toolbar, empty-space context menu, editor auto-save, save-all, and keyboard save shortcuts --- changelog.md | 11 + ts_web/00_commitinfo_data.ts | 2 +- .../dees-editor-filetree.ts | 93 +++++++- .../dees-editor-workspace.ts | 209 ++++++++++++++---- 4 files changed, 261 insertions(+), 54 deletions(-) diff --git a/changelog.md b/changelog.md index c9ce991..ab95406 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,16 @@ # Changelog +## 2025-12-31 - 3.17.0 - feat(editor) +add file explorer toolbar, empty-space context menu, editor auto-save, save-all, and keyboard save shortcuts + +- Added filetree toolbar with New File / New Folder actions and toolbar styling +- Added right-click context menu for empty filetree space to create files/folders +- Implemented editor menu button with context menu (Auto Save toggle, Save, Save All) +- Added auto-save toggle with 2s interval and cleanup on disconnect +- Implemented Save and Save All APIs that persist files and update IntelliSense manager +- Added keyboard shortcuts: Cmd/Ctrl+S to save active file and Cmd/Ctrl+Shift+S to save all +- Made tabs scrollable with a tabs container and added an editor menu button + ## 2025-12-30 - 3.16.0 - feat(editor) improve TypeScript IntelliSense and module resolution for Monaco editor diff --git a/ts_web/00_commitinfo_data.ts b/ts_web/00_commitinfo_data.ts index 03ed3d9..d11cbe6 100644 --- a/ts_web/00_commitinfo_data.ts +++ b/ts_web/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@design.estate/dees-catalog', - version: '3.16.0', + version: '3.17.0', description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.' } diff --git a/ts_web/elements/00group-editor/dees-editor-filetree/dees-editor-filetree.ts b/ts_web/elements/00group-editor/dees-editor-filetree/dees-editor-filetree.ts index f7c3c51..23eeb54 100644 --- a/ts_web/elements/00group-editor/dees-editor-filetree/dees-editor-filetree.ts +++ b/ts_web/elements/00group-editor/dees-editor-filetree/dees-editor-filetree.ts @@ -203,6 +203,48 @@ export class DeesEditorFiletree extends DeesElement { color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')}; font-style: italic; } + + .filetree-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + height: 36px; + padding: 0 12px; + border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(0 0% 15%)')}; + background: ${cssManager.bdTheme('hsl(0 0% 96%)', 'hsl(0 0% 8%)')}; + position: sticky; + top: 0; + z-index: 1; + } + + .toolbar-title { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 60%)')}; + } + + .toolbar-actions { + display: flex; + gap: 4px; + } + + .toolbar-button { + padding: 4px; + border-radius: 4px; + cursor: pointer; + opacity: 0.7; + transition: opacity 0.15s, background 0.15s; + display: flex; + align-items: center; + justify-content: center; + } + + .toolbar-button:hover { + opacity: 1; + background: ${cssManager.bdTheme('hsl(0 0% 0% / 0.08)', 'hsl(0 0% 100% / 0.1)')}; + } `, ]; @@ -231,18 +273,25 @@ export class DeesEditorFiletree extends DeesElement { `; } - if (this.treeData.length === 0) { - return html` -
- No files found. -
- `; - } - return html` -
- ${this.renderTree(this.treeData)} +
+ Explorer +
+
this.createNewFile('/')} title="New File"> + +
+
this.createNewFolder('/')} title="New Folder"> + +
+
+ ${this.treeData.length === 0 + ? html`
No files found.
` + : html` +
+ ${this.renderTree(this.treeData)} +
+ `} `; } @@ -414,6 +463,30 @@ export class DeesEditorFiletree extends DeesElement { await DeesContextmenu.openContextMenuWithOptions(e, menuItems); } + private async handleEmptySpaceContextMenu(e: MouseEvent) { + // Only trigger if clicking on the container itself, not a tree item + const target = e.target as HTMLElement; + if (target.closest('.tree-item')) return; + + e.preventDefault(); + e.stopPropagation(); + + const menuItems = [ + { + name: 'New File', + iconName: 'lucide:filePlus', + action: async () => this.createNewFile('/'), + }, + { + name: 'New Folder', + iconName: 'lucide:folderPlus', + action: async () => this.createNewFolder('/'), + }, + ]; + + await DeesContextmenu.openContextMenuWithOptions(e, menuItems); + } + private async showInputModal(options: { heading: string; label: string; diff --git a/ts_web/elements/00group-editor/dees-editor-workspace/dees-editor-workspace.ts b/ts_web/elements/00group-editor/dees-editor-workspace/dees-editor-workspace.ts index f068da7..316f0d4 100644 --- a/ts_web/elements/00group-editor/dees-editor-workspace/dees-editor-workspace.ts +++ b/ts_web/elements/00group-editor/dees-editor-workspace/dees-editor-workspace.ts @@ -20,6 +20,7 @@ import '../../dees-terminal/dees-terminal.js'; import '../../dees-icon/dees-icon.js'; import { DeesEditorMonaco } from '../dees-editor-monaco/dees-editor-monaco.js'; import { TypeScriptIntelliSenseManager } from './typescript-intellisense.js'; +import { DeesContextmenu } from '../../dees-contextmenu/dees-contextmenu.js'; declare global { interface HTMLElementTagNameMap { @@ -198,6 +199,26 @@ export function createUser(firstName: string, lastName: string): IUser { private intelliSenseManager: TypeScriptIntelliSenseManager | null = null; private intelliSenseInitialized: boolean = false; + // Auto-save functionality + @state() + accessor autoSave: boolean = false; + private autoSaveInterval: ReturnType | null = null; + + // Keyboard shortcut handler (bound for proper cleanup) + private keydownHandler = (e: KeyboardEvent) => { + // Cmd+S (Mac) or Ctrl+S (Windows/Linux) - Save + if ((e.metaKey || e.ctrlKey) && e.key === 's' && !e.shiftKey) { + e.preventDefault(); + this.saveActiveFile(); + } + + // Cmd+Shift+S - Save All + if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === 's') { + e.preventDefault(); + this.saveAllFiles(); + } + }; + public static styles = [ themeDefaultStyles, cssManager.defaultStyles, @@ -390,6 +411,31 @@ export function createUser(firstName: string, lastName: string): IUser { background: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')}; } + .tabs-container { + display: flex; + flex: 1; + overflow-x: auto; + } + + .editor-menu-button { + padding: 6px 8px; + margin-right: 4px; + margin-left: auto; + border-radius: 4px; + cursor: pointer; + opacity: 0.6; + transition: opacity 0.15s, background 0.15s; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + } + + .editor-menu-button:hover { + opacity: 1; + background: ${cssManager.bdTheme('hsl(0 0% 0% / 0.08)', 'hsl(0 0% 100% / 0.1)')}; + } + .editor-content { flex: 1; position: relative; @@ -611,18 +657,23 @@ export function createUser(firstName: string, lastName: string): IUser {
- ${this.openFiles.map(file => html` -
this.activateFile(file.path)} - > - ${file.modified ? html`` : ''} - ${file.name} - this.closeFile(e, file.path)}> - - -
- `)} +
+ ${this.openFiles.map(file => html` +
this.activateFile(file.path)} + > + ${file.modified ? html`` : ''} + ${file.name} + this.closeFile(e, file.path)}> + + +
+ `)} +
+
+ +
${this.openFiles.length === 0 ? html` @@ -690,6 +741,20 @@ export function createUser(firstName: string, lastName: string): IUser { `; } + async connectedCallback() { + await super.connectedCallback(); + document.addEventListener('keydown', this.keydownHandler); + } + + async disconnectedCallback() { + await super.disconnectedCallback(); + document.removeEventListener('keydown', this.keydownHandler); + if (this.autoSaveInterval) { + clearInterval(this.autoSaveInterval); + this.autoSaveInterval = null; + } + } + public async firstUpdated() { if (this.executionEnvironment) { await this.initializeWorkspace(); @@ -880,6 +945,95 @@ export function createUser(firstName: string, lastName: string): IUser { this.isTerminalCollapsed = !this.isTerminalCollapsed; } + // ========== Save Operations ========== + + public async saveActiveFile(): Promise { + const file = this.openFiles.find(f => f.path === this.activeFilePath); + if (!file || !this.executionEnvironment) return; + + try { + await this.executionEnvironment.writeFile(file.path, file.content); + + // Update file state to mark as saved + this.openFiles = this.openFiles.map(f => + f.path === file.path ? { ...f, modified: false } : f + ); + + // Update IntelliSense manager with latest content + if (this.intelliSenseManager) { + this.intelliSenseManager.addFileModel(file.path, file.content); + } + } catch (error) { + console.error('Failed to save file:', error); + } + } + + public async saveAllFiles(): Promise { + if (!this.executionEnvironment) return; + + for (const file of this.openFiles.filter(f => f.modified)) { + try { + await this.executionEnvironment.writeFile(file.path, file.content); + + // Update IntelliSense manager + if (this.intelliSenseManager) { + this.intelliSenseManager.addFileModel(file.path, file.content); + } + } catch (error) { + console.error(`Failed to save ${file.path}:`, error); + } + } + + // Mark all files as saved + this.openFiles = this.openFiles.map(f => ({ ...f, modified: false })); + } + + // ========== Editor Menu ========== + + private async showEditorMenu(e: MouseEvent) { + e.stopPropagation(); + + const menuItems: Parameters[1] = [ + { + name: this.autoSave ? '✓ Auto Save' : 'Auto Save', + iconName: 'lucide:save', + action: async () => this.toggleAutoSave(), + }, + { divider: true }, + { + name: 'Save', + iconName: 'lucide:save', + action: async () => this.saveActiveFile(), + }, + { + name: 'Save All', + iconName: 'lucide:save', + action: async () => this.saveAllFiles(), + }, + ]; + + await DeesContextmenu.openContextMenuWithOptions(e, menuItems); + } + + private toggleAutoSave() { + this.autoSave = !this.autoSave; + + if (this.autoSave) { + // Save every 2 seconds if there are changes + this.autoSaveInterval = setInterval(() => { + const hasUnsaved = this.openFiles.some(f => f.modified); + if (hasUnsaved) { + this.saveAllFiles(); + } + }, 2000); + } else { + if (this.autoSaveInterval) { + clearInterval(this.autoSaveInterval); + this.autoSaveInterval = null; + } + } + } + private getErrorCount(): number { // Monaco MarkerSeverity: Error = 8, Warning = 4, Info = 2, Hint = 1 return this.diagnosticMarkers.filter(m => m.severity === 8).length; @@ -976,35 +1130,4 @@ export function createUser(firstName: string, lastName: string): IUser { resource: { path: m.resource.path }, })); } - - public async saveActiveFile(): Promise { - const file = this.openFiles.find(f => f.path === this.activeFilePath); - if (!file || !this.executionEnvironment) return; - - try { - await this.executionEnvironment.writeFile(file.path, file.content); - const fileIndex = this.openFiles.findIndex(f => f.path === this.activeFilePath); - this.openFiles = [ - ...this.openFiles.slice(0, fileIndex), - { ...file, modified: false }, - ...this.openFiles.slice(fileIndex + 1), - ]; - } catch (error) { - console.error('Failed to save file:', error); - } - } - - public async saveAllFiles(): Promise { - if (!this.executionEnvironment) return; - - for (const file of this.openFiles.filter(f => f.modified)) { - try { - await this.executionEnvironment.writeFile(file.path, file.content); - } catch (error) { - console.error(`Failed to save ${file.path}:`, error); - } - } - - this.openFiles = this.openFiles.map(f => ({ ...f, modified: false })); - } }