feat(editor): Add wysiwyg editor

This commit is contained in:
Juergen Kunz
2025-06-23 17:52:10 +00:00
parent 6626726029
commit 58af08cb0d
2 changed files with 422 additions and 9 deletions

View File

@ -2,7 +2,242 @@ import { html, css } from '@design.estate/dees-element';
import '@design.estate/dees-wcctools/demotools';
export const demoFunc = () => html`
<dees-demowrapper>
<dees-demowrapper .runAfterRender=${async (elementArg: HTMLElement) => {
// Get editor instances
const programmaticEditor = elementArg.querySelector('#programmatic-demo') as any;
const articleEditor = elementArg.querySelector('#article-editor') as any;
// Programmatically set content for the article editor after render
if (articleEditor) {
const articleBlocks = [
{
id: 'intro-heading-' + Date.now(),
type: 'heading-2' as const,
content: 'Introduction'
},
{
id: 'intro-para-' + Date.now(),
type: 'paragraph' as const,
content: 'Modern web development has evolved significantly over the past decade. In this article, we\'ll explore the key technologies and best practices that define web development in 2024.'
},
{
id: 'tech-heading-' + Date.now(),
type: 'heading-3' as const,
content: 'Key Technologies'
},
{
id: 'tech-list-' + Date.now(),
type: 'list' as const,
content: 'TypeScript - Type-safe JavaScript development\nWeb Components - Native component model\nES Modules - Modern module system',
metadata: { listType: 'ordered' }
},
{
id: 'start-heading-' + Date.now(),
type: 'heading-3' as const,
content: 'Getting Started'
},
{
id: 'start-para-' + Date.now(),
type: 'paragraph' as const,
content: 'To begin building modern web applications, you\'ll need:'
},
{
id: 'req-list-' + Date.now(),
type: 'list' as const,
content: 'A solid understanding of JavaScript fundamentals\nFamiliarity with component-based architecture\nKnowledge of build tools and bundlers',
metadata: { listType: 'bullet' }
},
{
id: 'quote-' + Date.now(),
type: 'quote' as const,
content: 'The best way to predict the future is to invent it. - Alan Kay'
},
{
id: 'example-heading-' + Date.now(),
type: 'heading-3' as const,
content: 'Code Example'
},
{
id: 'code-example-' + Date.now(),
type: 'code' as const,
content: 'class ModernWebApp extends HTMLElement {\n constructor() {\n super();\n this.attachShadow({ mode: \'open\' });\n }\n \n connectedCallback() {\n this.render();\n }\n}'
},
{
id: 'divider-' + Date.now(),
type: 'divider' as const,
content: ' '
},
{
id: 'conclusion-heading-' + Date.now(),
type: 'heading-3' as const,
content: 'Conclusion'
},
{
id: 'conclusion-para-' + Date.now(),
type: 'paragraph' as const,
content: 'Embracing modern web standards and tools enables developers to build faster, more maintainable applications.'
}
];
// Set the blocks instead of using .value with HTML
setTimeout(() => {
articleEditor.importBlocks(articleBlocks);
}, 500);
}
// Setup button handlers for programmatic demo
const generateReportBtn = elementArg.querySelector('#generate-report-btn');
const generateRecipeBtn = elementArg.querySelector('#generate-recipe-btn');
const clearEditorBtn = elementArg.querySelector('#clear-editor-btn');
if (generateReportBtn) {
generateReportBtn.addEventListener('click', () => {
const blocks = [
{
id: 'title-' + Date.now(),
type: 'heading-1' as const,
content: 'System Performance Report'
},
{
id: 'date-' + Date.now(),
type: 'paragraph' as const,
content: 'Generated on: ' + new Date().toLocaleString()
},
{
id: 'summary-heading-' + Date.now(),
type: 'heading-2' as const,
content: 'Executive Summary'
},
{
id: 'summary-' + Date.now(),
type: 'paragraph' as const,
content: 'This report provides an analysis of system performance metrics over the last 30 days.'
},
{
id: 'metrics-heading-' + Date.now(),
type: 'heading-2' as const,
content: 'Key Metrics'
},
{
id: 'metrics-list-' + Date.now(),
type: 'list' as const,
content: 'Average response time: 124ms\nUptime: 99.97%\nCPU utilization: 45%\nMemory usage: 2.3GB / 8GB',
metadata: { listType: 'bullet' }
},
{
id: 'analysis-heading-' + Date.now(),
type: 'heading-2' as const,
content: 'Performance Analysis'
},
{
id: 'analysis-quote-' + Date.now(),
type: 'quote' as const,
content: 'System performance remains within acceptable parameters with room for optimization in memory management.'
},
{
id: 'code-heading-' + Date.now(),
type: 'heading-3' as const,
content: 'Sample Query Performance'
},
{
id: 'code-block-' + Date.now(),
type: 'code' as const,
content: 'SELECT AVG(response_time) as avg_time,\n COUNT(*) as total_requests,\n DATE(created_at) as date\nFROM performance_logs\nWHERE created_at >= NOW() - INTERVAL 30 DAY\nGROUP BY DATE(created_at)\nORDER BY date DESC;'
},
{
id: 'divider-' + Date.now(),
type: 'divider' as const,
content: ' '
},
{
id: 'footer-' + Date.now(),
type: 'paragraph' as const,
content: 'Report generated automatically by System Monitor v2.5.0'
}
];
programmaticEditor.importBlocks(blocks);
});
}
if (generateRecipeBtn) {
generateRecipeBtn.addEventListener('click', () => {
const blocks = [
{
id: 'recipe-title-' + Date.now(),
type: 'heading-1' as const,
content: 'Classic Margherita Pizza'
},
{
id: 'recipe-intro-' + Date.now(),
type: 'paragraph' as const,
content: 'A traditional Italian pizza with fresh basil, mozzarella, and tomato sauce.'
},
{
id: 'ingredients-heading-' + Date.now(),
type: 'heading-2' as const,
content: '🍕 Ingredients'
},
{
id: 'dough-heading-' + Date.now(),
type: 'heading-3' as const,
content: 'For the Dough:'
},
{
id: 'dough-list-' + Date.now(),
type: 'list' as const,
content: '500g tipo "00" flour\n325ml warm water\n10g salt\n7g active dry yeast\n2 tbsp olive oil',
metadata: { listType: 'bullet' }
},
{
id: 'toppings-heading-' + Date.now(),
type: 'heading-3' as const,
content: 'For the Toppings:'
},
{
id: 'toppings-list-' + Date.now(),
type: 'list' as const,
content: '400g canned San Marzano tomatoes\n250g fresh mozzarella\nFresh basil leaves\nExtra virgin olive oil\nSalt and pepper to taste',
metadata: { listType: 'bullet' }
},
{
id: 'instructions-heading-' + Date.now(),
type: 'heading-2' as const,
content: '👨‍🍳 Instructions'
},
{
id: 'steps-list-' + Date.now(),
type: 'list' as const,
content: 'Dissolve yeast in warm water and let stand for 5 minutes\nMix flour and salt, create a well in center\nAdd yeast mixture and olive oil\nKnead for 10 minutes until smooth\nLet rise for 1-2 hours until doubled\nPunch down and divide into portions\nRoll out each portion to 12-inch circles\nTop with crushed tomatoes, mozzarella, and basil\nBake at 475°F (245°C) for 10-12 minutes',
metadata: { listType: 'ordered' }
},
{
id: 'tip-' + Date.now(),
type: 'quote' as const,
content: 'Pro tip: For an authentic taste, use a pizza stone and preheat it in the oven for at least 30 minutes before baking.'
},
{
id: 'divider-2-' + Date.now(),
type: 'divider' as const,
content: ' '
},
{
id: 'servings-' + Date.now(),
type: 'paragraph' as const,
content: 'Servings: 4 pizzas | Prep time: 2 hours | Cook time: 12 minutes'
}
];
programmaticEditor.importBlocks(blocks);
});
}
if (clearEditorBtn) {
clearEditorBtn.addEventListener('click', () => {
programmaticEditor.importBlocks([]);
});
}
}}>
<style>
${css`
.demo-container {
@ -244,6 +479,18 @@ export const demoFunc = () => html`
></dees-input-wysiwyg>
</div>
<div class="demo-section">
<h3>📚 Tutorial & Documentation</h3>
<p>Create comprehensive tutorials and documentation with code examples, lists, and structured content.</p>
<dees-input-wysiwyg
.label=${'Git Tutorial'}
.description=${'Step-by-step guide with commands and explanations'}
.outputFormat=${'markdown'}
.value=${'# Git Tutorial for Beginners\n\nGit is a distributed version control system that helps you track changes in your code over time. This tutorial will guide you through the basics.\n\n## Prerequisites\n\nBefore starting, ensure you have:\n\n- Git installed on your system\n- A text editor or IDE\n- Basic command line knowledge\n\n## Getting Started\n\n### 1. Configure Git\n\nFirst, set up your identity:\n\n```bash\ngit config --global user.name "Your Name"\ngit config --global user.email "your.email@example.com"\n```\n\n### 2. Initialize a Repository\n\nCreate a new Git repository:\n\n```bash\nmkdir my-project\ncd my-project\ngit init\n```\n\n### 3. Basic Git Workflow\n\n#### Adding Files\n\nCreate a file and add it to staging:\n\n```bash\necho "# My Project" > README.md\ngit add README.md\n```\n\n#### Committing Changes\n\n```bash\ngit commit -m "Initial commit"\n```\n\n> **Best Practice:** Write clear, descriptive commit messages that explain what changes were made and why.\n\n### 4. Working with Branches\n\nBranches allow you to work on features independently:\n\n```bash\n# Create and switch to a new branch\ngit checkout -b feature-branch\n\n# Make changes and commit\ngit add .\ngit commit -m "Add new feature"\n\n# Switch back to main\ngit checkout main\n\n# Merge the feature\ngit merge feature-branch\n```\n\n---\n\n## Common Commands Reference\n\n| Command | Description |\n|---------|-------------|\n| `git status` | Check repository status |\n| `git log` | View commit history |\n| `git diff` | Show changes |\n| `git pull` | Fetch and merge changes |\n| `git push` | Upload changes to remote |\n\n## Next Steps\n\n1. Learn about remote repositories\n2. Explore advanced Git features\n3. Practice with real projects\n4. Contribute to open source\n\n**Happy coding!** 🚀'}
></dees-input-wysiwyg>
</div>
<div class="demo-section">
<h3>🔄 Output Formats</h3>
<p>Choose between HTML and Markdown output formats depending on your needs. Perfect for static site generators, documentation systems, or any content management workflow.</p>
@ -251,19 +498,19 @@ export const demoFunc = () => html`
<div class="output-section">
<div>
<dees-input-wysiwyg
.label=${'HTML Output'}
.description=${'Generates clean, semantic HTML'}
.label=${'Meeting Notes'}
.description=${'Structured meeting documentation'}
.outputFormat=${'html'}
.value=${'<h2>HTML Example</h2><p>This editor outputs <strong>clean HTML</strong> that can be used directly in web applications.</p><ul><li>Semantic markup</li><li>Clean structure</li><li>Ready to style</li></ul>'}
.value=${'<h2>Q4 Planning Meeting</h2><p><strong>Date:</strong> December 15, 2024<br><strong>Attendees:</strong> Product Team, Engineering, Design</p><h3>Agenda Items</h3><ol><li>Review Q3 achievements</li><li>Set Q4 objectives</li><li>Resource allocation</li><li>Timeline discussion</li></ol><h3>Key Decisions</h3><ul><li>Launch new dashboard feature by end of January</li><li>Increase engineering team by 2 developers</li><li>Implement weekly design reviews</li></ul><blockquote>"Focus on user experience improvements based on Q3 feedback" - Product Manager</blockquote><h3>Action Items</h3><ul><li>Sarah: Create detailed project timeline</li><li>Mike: Draft technical requirements</li><li>Lisa: Schedule user research sessions</li></ul><hr><p>Next meeting: January 5, 2025</p>'}
></dees-input-wysiwyg>
</div>
<div>
<dees-input-wysiwyg
.label=${'Markdown Output'}
.description=${'Perfect for documentation and static sites'}
.label=${'Recipe Blog Post'}
.description=${'Food blog with mixed content'}
.outputFormat=${'markdown'}
.value=${'## Markdown Example\n\nThis editor can also output **Markdown** format for use with:\n\n- Static site generators\n- Documentation tools\n- GitHub READMEs\n- And more!'}
.value=${'# Ultimate Chocolate Chip Cookies\n\nThere\'s nothing quite like the smell of freshly baked chocolate chip cookies. This recipe has been perfected over years of testing!\n\n## Ingredients\n\n- 2¼ cups all-purpose flour\n- 1 tsp baking soda\n- 1 tsp salt\n- 1 cup butter, softened\n- ¾ cup granulated sugar\n- ¾ cup packed brown sugar\n- 2 large eggs\n- 2 tsp vanilla extract\n- 2 cups chocolate chips\n\n## Instructions\n\n### Step 1: Preparation\n\nPreheat your oven to **375°F (190°C)**. This temperature is crucial for achieving the perfect texture.\n\n### Step 2: Mix Dry Ingredients\n\nIn a medium bowl, whisk together:\n\n1. Flour\n2. Baking soda\n3. Salt\n\n### Step 3: Cream Butter and Sugars\n\n```\nCream butter and sugars for 3-4 minutes\nuntil light and fluffy\n```\n\n> **Pro tip:** Room temperature ingredients mix better and create a more uniform dough.\n\n### Step 4: Add Wet Ingredients\n\nBeat in eggs one at a time, then add vanilla extract.\n\n### Step 5: Combine and Bake\n\nGradually blend in flour mixture, then stir in chocolate chips. Drop rounded tablespoons onto ungreased cookie sheets.\n\n---\n\n**Baking time:** 9-11 minutes or until golden brown\n\n**Yield:** About 5 dozen cookies'}
></dees-input-wysiwyg>
</div>
</div>
@ -276,7 +523,7 @@ export const demoFunc = () => html`
<dees-input-wysiwyg
.label=${'Technical Documentation'}
.description=${'Create technical docs with code examples and structured content'}
.value=${'<h1>API Documentation</h1><p>Welcome to our API documentation. Below you\'ll find examples of how to use our endpoints.</p><h2>Authentication</h2><p>All API requests require authentication using an API key:</p><pre><code>Authorization: Bearer YOUR_API_KEY</code></pre><h2>Endpoints</h2><h3>GET /users</h3><p>Retrieve a list of users from the system.</p><pre><code>curl -X GET https://api.example.com/users \\\n -H "Authorization: Bearer YOUR_API_KEY"</code></pre><hr><p>For more information, please refer to our complete documentation.</p>'}
.value=${'<h1>API Documentation</h1><p>Welcome to our API documentation. Below you\'ll find examples of how to use our endpoints.</p><h2>Authentication</h2><p>All API requests require authentication using an API key:</p><pre><code>Authorization: Bearer YOUR_API_KEY</code></pre><h2>Endpoints</h2><h3>GET /users</h3><p>Retrieve a list of users from the system.</p><pre><code>curl -X GET https://api.example.com/users \\\n -H "Authorization: Bearer YOUR_API_KEY"</code></pre><blockquote>Note: Rate limiting applies to all endpoints. You can make up to 100 requests per minute.</blockquote><h3>POST /users</h3><p>Create a new user in the system.</p><pre><code>{\n "name": "John Doe",\n "email": "john@example.com",\n "role": "user"\n}</code></pre><hr><p>For more information, please refer to our complete documentation.</p>'}
.outputFormat=${'html'}
></dees-input-wysiwyg>
</div>
@ -293,8 +540,9 @@ export const demoFunc = () => html`
></dees-input-text>
<dees-input-wysiwyg
id="article-editor"
.label=${'Article Content'}
.description=${'Write your article content here'}
.description=${'This content was created programmatically using blocks'}
.required=${true}
.outputFormat=${'markdown'}
></dees-input-wysiwyg>
@ -306,6 +554,121 @@ export const demoFunc = () => html`
></dees-input-tags>
</dees-form>
</div>
<div class="demo-section">
<h3>🧩 Programmatic Block Creation</h3>
<p>Create content programmatically using the block API for dynamic document generation.</p>
<dees-input-wysiwyg
id="programmatic-demo"
.label=${'Programmatically Generated Content'}
.description=${'This content was created using the importBlocks API'}
></dees-input-wysiwyg>
<div style="margin-top: 16px; display: flex; gap: 8px; flex-wrap: wrap;">
<button
id="generate-report-btn"
style="padding: 8px 16px; border: 1px solid #ddd; border-radius: 4px; background: #f8f8f8; cursor: pointer;"
>Generate Report</button>
<button
id="generate-recipe-btn"
style="padding: 8px 16px; border: 1px solid #ddd; border-radius: 4px; background: #f8f8f8; cursor: pointer;"
>Generate Recipe</button>
<button
id="clear-editor-btn"
style="padding: 8px 16px; border: 1px solid #ddd; border-radius: 4px; background: #f8f8f8; cursor: pointer;"
>Clear Editor</button>
</div>
</div>
<div class="demo-section">
<h3>📤 Export/Import Features</h3>
<p>The WYSIWYG editor provides multiple export formats and lossless save/restore capabilities for maximum flexibility.</p>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 16px; margin-bottom: 24px;">
<div style="background: rgba(0, 102, 204, 0.1); padding: 16px; border-radius: 8px;">
<strong style="color: #0066cc;">Lossless Blocks</strong>
<p style="margin: 8px 0 0 0; font-size: 14px;">Export and import raw block structure for perfect round-trip editing</p>
</div>
<div style="background: rgba(76, 175, 80, 0.1); padding: 16px; border-radius: 8px;">
<strong style="color: #4CAF50;">HTML Export</strong>
<p style="margin: 8px 0 0 0; font-size: 14px;">Get clean, semantic HTML regardless of output format setting</p>
</div>
<div style="background: rgba(255, 152, 0, 0.1); padding: 16px; border-radius: 8px;">
<strong style="color: #FF9800;">Markdown Export</strong>
<p style="margin: 8px 0 0 0; font-size: 14px;">Export as Markdown for docs, READMEs, and static sites</p>
</div>
<div style="background: rgba(156, 39, 176, 0.1); padding: 16px; border-radius: 8px;">
<strong style="color: #9C27B0;">State Management</strong>
<p style="margin: 8px 0 0 0; font-size: 14px;">Save and restore complete editor state including settings</p>
</div>
</div>
<dees-input-wysiwyg
id="export-demo"
.label=${'Export Demo Editor'}
.description=${'Try the export buttons below to see different output formats'}
.value=${'<h1>Software Release Notes</h1><p><strong>Version 2.5.0</strong> - Released December 15, 2024</p><h2>🎉 New Features</h2><ul><li>Added dark mode support across all components</li><li>Implemented real-time collaboration features</li><li>New dashboard analytics widgets</li><li>Export functionality for all report types</li></ul><h2>🐛 Bug Fixes</h2><ul><li>Fixed memory leak in data processing module</li><li>Resolved authentication timeout issues</li><li>Corrected timezone handling in scheduled tasks</li></ul><h2>⚡ Performance Improvements</h2><blockquote>Page load times reduced by 40% through lazy loading and code splitting</blockquote><h2>🔧 Technical Details</h2><pre><code>// New API endpoint for batch operations\nPOST /api/v2/batch\n{\n "operations": [\n { "method": "GET", "path": "/users/123" },\n { "method": "PUT", "path": "/settings", "body": {...} }\n ]\n}</code></pre><h2>💡 Migration Guide</h2><ol><li>Update your dependencies to the latest versions</li><li>Run database migrations: <code>npm run migrate</code></li><li>Clear cache: <code>npm run cache:clear</code></li><li>Restart all services</li></ol><hr><p>For questions or issues, please contact the development team or file a ticket in our issue tracker.</p>'}
></dees-input-wysiwyg>
<div style="margin-top: 16px; display: flex; gap: 8px; flex-wrap: wrap;">
<button
style="padding: 8px 16px; border: 1px solid #ddd; border-radius: 4px; background: #f8f8f8; cursor: pointer;"
@click=${(e: MouseEvent) => {
const editor = (e.target as HTMLElement).closest('.demo-section').querySelector('#export-demo') as any;
const blocks = editor.exportBlocks();
console.log('Exported blocks:', blocks);
alert('Blocks exported to console! Check developer tools.');
}}
>Export Blocks</button>
<button
style="padding: 8px 16px; border: 1px solid #ddd; border-radius: 4px; background: #f8f8f8; cursor: pointer;"
@click=${(e: MouseEvent) => {
const editor = (e.target as HTMLElement).closest('.demo-section').querySelector('#export-demo') as any;
const html = editor.exportAsHtml();
console.log('HTML Export:', html);
alert('HTML exported to console! Check developer tools.');
}}
>Export as HTML</button>
<button
style="padding: 8px 16px; border: 1px solid #ddd; border-radius: 4px; background: #f8f8f8; cursor: pointer;"
@click=${(e: MouseEvent) => {
const editor = (e.target as HTMLElement).closest('.demo-section').querySelector('#export-demo') as any;
const markdown = editor.exportAsMarkdown();
console.log('Markdown Export:', markdown);
alert('Markdown exported to console! Check developer tools.');
}}
>Export as Markdown</button>
<button
style="padding: 8px 16px; border: 1px solid #ddd; border-radius: 4px; background: #f8f8f8; cursor: pointer;"
@click=${(e: MouseEvent) => {
const editor = (e.target as HTMLElement).closest('.demo-section').querySelector('#export-demo') as any;
const state = editor.exportState();
console.log('State Export:', state);
(window as any).savedEditorState = state;
alert('State saved to window.savedEditorState!');
}}
>Save State</button>
<button
style="padding: 8px 16px; border: 1px solid #ddd; border-radius: 4px; background: #f8f8f8; cursor: pointer;"
@click=${(e: MouseEvent) => {
if ((window as any).savedEditorState) {
const editor = (e.target as HTMLElement).closest('.demo-section').querySelector('#export-demo') as any;
editor.importState((window as any).savedEditorState);
alert('State restored!');
} else {
alert('No saved state found. Save state first!');
}
}}
>Restore State</button>
</div>
</div>
</div>
</dees-demowrapper>
`;

View File

@ -561,4 +561,54 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
this.changeSubject.next(this.value);
this.requestUpdate();
}
/**
* Export the editor content as raw blocks (lossless)
*/
public exportBlocks(): IBlock[] {
return JSON.parse(JSON.stringify(this.blocks));
}
/**
* Import raw blocks (lossless)
*/
public importBlocks(blocks: IBlock[]): void {
this.blocks = JSON.parse(JSON.stringify(blocks));
this.updateValue();
this.requestUpdate();
}
/**
* Export content as HTML regardless of outputFormat setting
*/
public exportAsHtml(): string {
return WysiwygConverters.getHtmlOutput(this.blocks);
}
/**
* Export content as Markdown regardless of outputFormat setting
*/
public exportAsMarkdown(): string {
return WysiwygConverters.getMarkdownOutput(this.blocks);
}
/**
* Get a JSON representation of the editor state (for saving)
*/
public exportState(): { blocks: IBlock[], outputFormat: OutputFormat } {
return {
blocks: this.exportBlocks(),
outputFormat: this.outputFormat
};
}
/**
* Restore editor state from JSON
*/
public importState(state: { blocks: IBlock[], outputFormat?: OutputFormat }): void {
if (state.outputFormat) {
this.outputFormat = state.outputFormat;
}
this.importBlocks(state.blocks);
}
}