@compodoc/compodoc
Version:
The missing documentation tool for your Angular application
615 lines (526 loc) • 18.4 kB
text/typescript
import { Component, OnInit, ViewChild, ElementRef, OnDestroy } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { TemplateEditorService } from './template-editor.service';
import { ZipExportService } from './zip-export.service';
import { HbsRenderService } from './hbs-render.service';
interface Template {
name: string;
path: string;
type: 'template' | 'partial';
}
interface Session {
sessionId: string;
success: boolean;
message: string;
}
interface CompoDocConfig {
hideGenerator?: boolean;
disableSourceCode?: boolean;
disableGraph?: boolean;
disableCoverage?: boolean;
disablePrivate?: boolean;
disableProtected?: boolean;
disableInternal?: boolean;
disableLifeCycleHooks?: boolean;
disableConstructors?: boolean;
disableRoutesGraph?: boolean;
disableSearch?: boolean;
disableDependencies?: boolean;
disableProperties?: boolean;
disableDomTree?: boolean;
disableTemplateTab?: boolean;
disableStyleTab?: boolean;
disableMainGraph?: boolean;
disableFilePath?: boolean;
disableOverview?: boolean;
hideDarkModeToggle?: boolean;
minimal?: boolean;
customFavicon?: string;
includes?: string;
includesName?: string;
}
@Component({
selector: 'template-playground-root',
template: `
<div class="template-playground">
<div class="template-playground-header">
<h2>Template Playground</h2>
<div class="template-playground-status">
<span *ngIf="sessionId" class="session-info">Session: {{sessionId.substring(0, 8)}}...</span>
<span *ngIf="saving" class="saving-indicator">Saving...</span>
<span *ngIf="lastSaved" class="last-saved">Last saved: {{lastSaved | date:'short'}}</span>
</div>
<div class="template-playground-actions">
<button class="btn btn-secondary" (click)="toggleConfigPanel()">⚙️ Config</button>
<button class="btn btn-primary" (click)="resetToDefault()">Reset to Default</button>
<button class="btn btn-success" (click)="exportZip()">Download Templates</button>
</div>
</div>
<!-- Configuration Panel -->
<div class="config-panel" [class.collapsed]="!showConfigPanel">
<h3>CompoDoc Configuration</h3>
<div class="config-options">
<label><input type="checkbox" [(ngModel)]="config.hideGenerator" (change)="updateConfig()"> Hide Generator</label>
<label><input type="checkbox" [(ngModel)]="config.hideDarkModeToggle" (change)="updateConfig()"> Hide Dark Mode Toggle</label>
<label><input type="checkbox" [(ngModel)]="config.minimal" (change)="updateConfig()"> Minimal Mode</label>
<label><input type="checkbox" [(ngModel)]="config.disableOverview" (change)="updateConfig()"> Disable Overview</label>
<label><input type="checkbox" [(ngModel)]="config.disableFilePath" (change)="updateConfig()"> Disable File Path</label>
<label><input type="checkbox" [(ngModel)]="config.disableSourceCode" (change)="updateConfig()"> Disable Source Code</label>
<label><input type="checkbox" [(ngModel)]="config.disableGraph" (change)="updateConfig()"> Disable Graph</label>
<label><input type="checkbox" [(ngModel)]="config.disableMainGraph" (change)="updateConfig()"> Disable Main Graph</label>
<label><input type="checkbox" [(ngModel)]="config.disableRoutesGraph" (change)="updateConfig()"> Disable Routes Graph</label>
<label><input type="checkbox" [(ngModel)]="config.disableCoverage" (change)="updateConfig()"> Disable Coverage</label>
<label><input type="checkbox" [(ngModel)]="config.disableSearch" (change)="updateConfig()"> Disable Search</label>
<label><input type="checkbox" [(ngModel)]="config.disableDependencies" (change)="updateConfig()"> Disable Dependencies</label>
<label><input type="checkbox" [(ngModel)]="config.disablePrivate" (change)="updateConfig()"> Disable Private</label>
<label><input type="checkbox" [(ngModel)]="config.disableProtected" (change)="updateConfig()"> Disable Protected</label>
<label><input type="checkbox" [(ngModel)]="config.disableInternal" (change)="updateConfig()"> Disable Internal</label>
<label><input type="checkbox" [(ngModel)]="config.disableLifeCycleHooks" (change)="updateConfig()"> Disable Lifecycle Hooks</label>
<label><input type="checkbox" [(ngModel)]="config.disableConstructors" (change)="updateConfig()"> Disable Constructors</label>
<label><input type="checkbox" [(ngModel)]="config.disableProperties" (change)="updateConfig()"> Disable Properties</label>
<label><input type="checkbox" [(ngModel)]="config.disableDomTree" (change)="updateConfig()"> Disable DOM Tree</label>
<label><input type="checkbox" [(ngModel)]="config.disableTemplateTab" (change)="updateConfig()"> Disable Template Tab</label>
<label><input type="checkbox" [(ngModel)]="config.disableStyleTab" (change)="updateConfig()"> Disable Style Tab</label>
</div>
</div>
<div class="template-playground-body">
<div class="template-playground-sidebar">
<div class="template-file-list">
<h3>Templates</h3>
<ul class="file-list">
<li *ngFor="let template of templates; trackBy: trackByName"
[class.active]="selectedFile === template"
(click)="selectFile(template)">
<i class="file-icon ion-document-text"></i>
{{template.name}}
<span class="file-type">{{template.type}}</span>
</li>
</ul>
<div *ngIf="templates.length === 0" class="loading-templates">
Loading templates...
</div>
</div>
</div>
<div class="template-playground-main">
<div class="template-playground-editor">
<div class="editor-header" *ngIf="selectedFile">
<h4>{{selectedFile.path}}</h4>
<span class="file-type-badge">{{selectedFile.type}}</span>
</div>
<div #editorContainer class="editor-container"></div>
</div>
<div class="template-playground-preview">
<div class="preview-header">
<h4>Live Preview</h4>
<button class="btn btn-sm btn-secondary" (click)="refreshPreview()">🔄 Refresh</button>
</div>
<iframe #previewFrame class="preview-frame" [src]="previewUrl"></iframe>
</div>
</div>
</div>
</div>
`,
styles: [`
.template-playground {
display: flex;
flex-direction: column;
height: 100vh;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.template-playground-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 2rem;
background: #f8f9fa;
border-bottom: 1px solid #dee2e6;
}
.template-playground-status {
display: flex;
align-items: center;
gap: 1rem;
font-size: 0.875rem;
}
.session-info {
color: #6c757d;
font-family: monospace;
}
.saving-indicator {
color: #ffc107;
font-weight: bold;
}
.last-saved {
color: #28a745;
}
.template-playground-actions {
display: flex;
gap: 0.5rem;
}
.config-panel {
background: #e9ecef;
padding: 1rem 2rem;
border-bottom: 1px solid #dee2e6;
transition: all 0.3s ease;
max-height: 200px;
overflow: hidden;
}
.config-panel.collapsed {
max-height: 0;
padding: 0 2rem;
}
.config-options {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 0.5rem;
margin-top: 0.5rem;
}
.config-options label {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
}
.template-playground-body {
display: flex;
flex: 1;
overflow: hidden;
}
.template-playground-sidebar {
width: 250px;
background: #f8f9fa;
border-right: 1px solid #dee2e6;
overflow-y: auto;
}
.template-file-list {
padding: 1rem;
}
.template-file-list h3 {
margin: 0 0 0.5rem 0;
font-size: 0.875rem;
font-weight: 600;
color: #495057;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.file-list {
list-style: none;
padding: 0;
margin: 0 0 1.5rem 0;
}
.file-list li {
display: flex;
align-items: center;
padding: 0.5rem;
cursor: pointer;
border-radius: 4px;
font-size: 0.875rem;
transition: background-color 0.15s ease;
}
.file-list li:hover {
background: #e9ecef;
}
.file-list li.active {
background: #007bff;
color: white;
}
.file-icon {
margin-right: 0.5rem;
opacity: 0.7;
}
.file-type {
margin-left: auto;
font-size: 0.75rem;
opacity: 0.7;
text-transform: uppercase;
}
.loading-templates {
text-align: center;
color: #6c757d;
font-style: italic;
padding: 2rem;
}
.template-playground-main {
flex: 1;
display: flex;
overflow: hidden;
}
.template-playground-editor {
width: 50%;
display: flex;
flex-direction: column;
border-right: 1px solid #dee2e6;
}
.editor-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
background: #f8f9fa;
border-bottom: 1px solid #dee2e6;
}
.editor-header h4 {
margin: 0;
font-size: 0.875rem;
font-weight: 600;
}
.file-type-badge {
background: #6c757d;
color: white;
padding: 0.125rem 0.5rem;
border-radius: 12px;
font-size: 0.75rem;
text-transform: uppercase;
}
.editor-container {
flex: 1;
position: relative;
}
.template-playground-preview {
width: 50%;
display: flex;
flex-direction: column;
}
.preview-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
background: #f8f9fa;
border-bottom: 1px solid #dee2e6;
}
.preview-header h4 {
margin: 0;
font-size: 0.875rem;
font-weight: 600;
}
.preview-frame {
flex: 1;
border: none;
background: white;
}
.btn {
padding: 0.375rem 0.75rem;
border: 1px solid transparent;
border-radius: 0.25rem;
font-size: 0.875rem;
font-weight: 500;
text-decoration: none;
cursor: pointer;
transition: all 0.15s ease;
}
.btn-primary {
background: #007bff;
border-color: #007bff;
color: white;
}
.btn-primary:hover {
background: #0056b3;
border-color: #004085;
}
.btn-secondary {
background: #6c757d;
border-color: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #545b62;
border-color: #4e555b;
}
.btn-success {
background: #28a745;
border-color: #28a745;
color: white;
}
.btn-success:hover {
background: #1e7e34;
border-color: #1c7430;
}
.btn-sm {
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
}
`]
})
export class TemplatePlaygroundComponent implements OnInit, OnDestroy {
@ViewChild('editorContainer', { static: true }) editorContainer!: ElementRef;
@ViewChild('previewFrame', { static: true }) previewFrame!: ElementRef;
sessionId: string = '';
templates: Template[] = [];
selectedFile: Template | null = null;
config: CompoDocConfig = {};
showConfigPanel: boolean = false;
saving: boolean = false;
lastSaved: Date | null = null;
private saveTimeout?: number;
private readonly SAVE_DELAY = 300; // 300ms debounce
get previewUrl(): string {
return this.sessionId ? `/api/session/${this.sessionId}/docs/` : '';
}
constructor(
private http: HttpClient,
private editorService: TemplateEditorService,
private zipService: ZipExportService,
private hbsService: HbsRenderService
) {}
async ngOnInit() {
try {
await this.createSession();
await this.loadSessionTemplates();
await this.loadSessionConfig();
this.initializeEditor();
} catch (error) {
console.error('Error initializing template playground:', error);
}
}
ngOnDestroy() {
if (this.saveTimeout) {
clearTimeout(this.saveTimeout);
}
}
private async createSession(): Promise<void> {
const response = await this.http.post<Session>('/api/session/create', {}).toPromise();
if (response && response.success) {
this.sessionId = response.sessionId;
console.log('Session created:', this.sessionId);
} else {
throw new Error('Failed to create session');
}
}
private async loadSessionTemplates(): Promise<void> {
if (!this.sessionId) return;
const response = await this.http.get<{templates: Template[], success: boolean}>(`/api/session/${this.sessionId}/templates`).toPromise();
if (response && response.success) {
this.templates = response.templates;
// Auto-select the first template
if (this.templates.length > 0 && !this.selectedFile) {
this.selectFile(this.templates[0]);
}
}
}
private async loadSessionConfig(): Promise<void> {
if (!this.sessionId) return;
const response = await this.http.get<{config: CompoDocConfig, success: boolean}>(`/api/session/${this.sessionId}/config`).toPromise();
if (response && response.success) {
this.config = response.config;
}
}
initializeEditor() {
this.editorService.initializeEditor(this.editorContainer.nativeElement);
// Set up debounced save on content change
this.editorService.setOnChangeCallback((content: string) => {
this.scheduleAutoSave(content);
});
}
async selectFile(template: Template) {
this.selectedFile = template;
if (!this.sessionId) return;
try {
const response = await this.http.get<{content: string, success: boolean}>(`/api/session/${this.sessionId}/template/${template.path}`).toPromise();
if (response && response.success) {
this.editorService.setEditorContent(response.content, template.type === 'template' ? 'handlebars' : 'handlebars');
}
} catch (error) {
console.error('Error loading template:', error);
}
}
private scheduleAutoSave(content: string): void {
if (!this.selectedFile || !this.sessionId) return;
// Clear existing timeout
if (this.saveTimeout) {
clearTimeout(this.saveTimeout);
}
// Set saving indicator
this.saving = true;
// Schedule new save
this.saveTimeout = window.setTimeout(async () => {
try {
await this.saveTemplate(content);
this.saving = false;
this.lastSaved = new Date();
} catch (error) {
console.error('Error saving template:', error);
this.saving = false;
}
}, this.SAVE_DELAY);
}
private async saveTemplate(content: string): Promise<void> {
if (!this.selectedFile || !this.sessionId) return;
const response = await this.http.post<{success: boolean}>(`/api/session/${this.sessionId}/template/${this.selectedFile.path}`, {
content
}).toPromise();
if (!response || !response.success) {
throw new Error('Failed to save template');
}
}
async updateConfig(): Promise<void> {
if (!this.sessionId) return;
try {
const response = await this.http.post<{success: boolean}>(`/api/session/${this.sessionId}/config`, {
config: this.config
}).toPromise();
if (response && response.success) {
// Config updated, documentation will be regenerated automatically
}
} catch (error) {
console.error('Error updating config:', error);
}
}
toggleConfigPanel(): void {
this.showConfigPanel = !this.showConfigPanel;
}
refreshPreview(): void {
if (this.previewFrame?.nativeElement) {
this.previewFrame.nativeElement.src = this.previewFrame.nativeElement.src;
}
}
resetToDefault(): void {
// Implementation for resetting to default templates
if (confirm('Are you sure you want to reset all templates to their default values? This action cannot be undone.')) {
// TODO: Implement reset functionality
console.log('Reset to default templates');
}
}
async exportZip(): Promise<void> {
try {
if (!this.sessionId) {
console.error('No active session. Please refresh the page and try again.');
return;
}
console.log('Creating template package...');
// Call server-side ZIP creation endpoint for all templates
const response = await this.http.post(`/api/session/${this.sessionId}/download-all-templates`, {}, {
responseType: 'blob',
observe: 'response'
}).toPromise();
if (!response || !response.body) {
throw new Error('Failed to create template package');
}
// Get the ZIP file as a blob
const zipBlob = response.body;
// Get filename from response headers or construct it
const contentDisposition = response.headers.get('Content-Disposition');
let filename = `compodoc-templates-${this.sessionId}.zip`;
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename="([^"]+)"/);
if (filenameMatch) {
filename = filenameMatch[1];
}
}
// Create download link and trigger download
const url = URL.createObjectURL(zipBlob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
console.log('Template package downloaded successfully!');
} catch (error) {
console.error('Error downloading template package:', error);
}
}
trackByName(index: number, item: Template): string {
return item.name;
}
}