feat(wysiwyg): Add more block types
This commit is contained in:
@ -368,12 +368,345 @@ export class DeesWysiwygBlock extends DeesElement {
|
||||
input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* YouTube block styles */
|
||||
.block.youtube {
|
||||
padding: 0;
|
||||
margin: 16px 0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.block.youtube:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.block.youtube.selected {
|
||||
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(0, 102, 204, 0.3)', 'rgba(77, 148, 255, 0.3)')};
|
||||
}
|
||||
|
||||
.youtube-container {
|
||||
position: relative;
|
||||
padding-bottom: 56.25%; /* 16:9 aspect ratio */
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.youtube-container iframe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.youtube-placeholder {
|
||||
background: ${cssManager.bdTheme('#f8f8f8', '#1a1a1a')};
|
||||
border: 2px dashed ${cssManager.bdTheme('#d0d0d0', '#404040')};
|
||||
border-radius: 8px;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.placeholder-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.placeholder-text {
|
||||
font-size: 16px;
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.youtube-url-input {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
padding: 12px;
|
||||
border: 1px solid ${cssManager.bdTheme('#ddd', '#444')};
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
margin-bottom: 16px;
|
||||
background: ${cssManager.bdTheme('#fff', '#222')};
|
||||
color: ${cssManager.bdTheme('#000', '#fff')};
|
||||
}
|
||||
|
||||
.youtube-embed-btn {
|
||||
padding: 10px 24px;
|
||||
background: ${cssManager.bdTheme('#0066cc', '#4d94ff')};
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.youtube-embed-btn:hover {
|
||||
background: ${cssManager.bdTheme('#0052a3', '#3d7dd9')};
|
||||
}
|
||||
|
||||
/* Markdown block styles */
|
||||
.block.markdown {
|
||||
padding: 0;
|
||||
margin: 16px 0;
|
||||
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.block.markdown:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.block.markdown.selected {
|
||||
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(0, 102, 204, 0.3)', 'rgba(77, 148, 255, 0.3)')};
|
||||
}
|
||||
|
||||
.markdown-toolbar,
|
||||
.html-toolbar {
|
||||
background: ${cssManager.bdTheme('#f5f5f5', '#2a2a2a')};
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
|
||||
}
|
||||
|
||||
.markdown-label,
|
||||
.html-label {
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.toggle-preview {
|
||||
padding: 6px 12px;
|
||||
background: ${cssManager.bdTheme('#fff', '#333')};
|
||||
border: 1px solid ${cssManager.bdTheme('#ddd', '#555')};
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.toggle-preview:hover {
|
||||
background: ${cssManager.bdTheme('#f0f0f0', '#444')};
|
||||
}
|
||||
|
||||
.markdown-content,
|
||||
.html-content {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.markdown-editor,
|
||||
.html-editor {
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
padding: 16px;
|
||||
border: none;
|
||||
resize: vertical;
|
||||
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
|
||||
font-size: 14px;
|
||||
background: ${cssManager.bdTheme('#fff', '#1a1a1a')};
|
||||
color: ${cssManager.bdTheme('#000', '#fff')};
|
||||
}
|
||||
|
||||
.markdown-preview,
|
||||
.html-preview {
|
||||
padding: 16px;
|
||||
min-height: 200px;
|
||||
background: ${cssManager.bdTheme('#fff', '#1a1a1a')};
|
||||
}
|
||||
|
||||
.markdown-preview h1,
|
||||
.markdown-preview h2,
|
||||
.markdown-preview h3 {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.markdown-preview h1:first-child,
|
||||
.markdown-preview h2:first-child,
|
||||
.markdown-preview h3:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/* HTML block styles */
|
||||
.block.html {
|
||||
padding: 0;
|
||||
margin: 16px 0;
|
||||
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.block.html:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.block.html.selected {
|
||||
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(0, 102, 204, 0.3)', 'rgba(77, 148, 255, 0.3)')};
|
||||
}
|
||||
|
||||
/* Attachment block styles */
|
||||
.block.attachment {
|
||||
padding: 0;
|
||||
margin: 16px 0;
|
||||
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.block.attachment:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.block.attachment.selected {
|
||||
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(0, 102, 204, 0.3)', 'rgba(77, 148, 255, 0.3)')};
|
||||
}
|
||||
|
||||
.block.attachment.drag-over {
|
||||
background: ${cssManager.bdTheme('#e3f2fd', '#1e3a5f')};
|
||||
}
|
||||
|
||||
.attachment-header {
|
||||
background: ${cssManager.bdTheme('#f5f5f5', '#2a2a2a')};
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
|
||||
}
|
||||
|
||||
.attachment-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.attachment-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.attachment-list {
|
||||
padding: 16px;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.attachment-placeholder {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
border: 2px dashed ${cssManager.bdTheme('#d0d0d0', '#404040')};
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.attachment-placeholder:hover {
|
||||
background: ${cssManager.bdTheme('#f8f8f8', '#1a1a1a')};
|
||||
border-color: ${cssManager.bdTheme('#0066cc', '#4d94ff')};
|
||||
}
|
||||
|
||||
.placeholder-hint {
|
||||
font-size: 13px;
|
||||
color: ${cssManager.bdTheme('#999', '#666')};
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.attachment-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
background: ${cssManager.bdTheme('#f8f8f8', '#222')};
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.attachment-item:hover {
|
||||
background: ${cssManager.bdTheme('#f0f0f0', '#2a2a2a')};
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
font-size: 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
font-size: 12px;
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
}
|
||||
|
||||
.remove-file {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: ${cssManager.bdTheme('#999', '#666')};
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.attachment-item:hover .remove-file {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.remove-file:hover {
|
||||
color: ${cssManager.bdTheme('#d32f2f', '#f44336')};
|
||||
}
|
||||
|
||||
.add-more-files {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background: transparent;
|
||||
border: 1px dashed ${cssManager.bdTheme('#ddd', '#444')};
|
||||
border-radius: 6px;
|
||||
color: ${cssManager.bdTheme('#666', '#999')};
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.add-more-files:hover {
|
||||
background: ${cssManager.bdTheme('#f8f8f8', '#1a1a1a')};
|
||||
border-color: ${cssManager.bdTheme('#0066cc', '#4d94ff')};
|
||||
color: ${cssManager.bdTheme('#0066cc', '#4d94ff')};
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
protected shouldUpdate(changedProperties: Map<string, any>): boolean {
|
||||
// If selection state changed, we need to update for non-editable blocks
|
||||
if (changedProperties.has('isSelected') && (this.block?.type === 'divider' || this.block?.type === 'image')) {
|
||||
const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment'];
|
||||
if (changedProperties.has('isSelected') && this.block && nonEditableTypes.includes(this.block.type)) {
|
||||
// For non-editable blocks, we need to update the selected class
|
||||
const element = this.shadowRoot?.querySelector('.block') as HTMLElement;
|
||||
if (element) {
|
||||
@ -416,6 +749,18 @@ export class DeesWysiwygBlock extends DeesElement {
|
||||
} else if (this.block.type === 'divider') {
|
||||
this.setupDividerBlock();
|
||||
return; // Divider blocks don't need the standard editable setup
|
||||
} else if (this.block.type === 'youtube') {
|
||||
this.setupYouTubeBlock();
|
||||
return;
|
||||
} else if (this.block.type === 'markdown') {
|
||||
this.setupMarkdownBlock();
|
||||
return;
|
||||
} else if (this.block.type === 'html') {
|
||||
this.setupHtmlBlock();
|
||||
return;
|
||||
} else if (this.block.type === 'attachment') {
|
||||
this.setupAttachmentBlock();
|
||||
return;
|
||||
}
|
||||
|
||||
// Now find the actual editable block element
|
||||
@ -470,7 +815,6 @@ export class DeesWysiwygBlock extends DeesElement {
|
||||
const pos = this.getCursorPosition(editableBlock);
|
||||
if (pos !== null) {
|
||||
this.lastKnownCursorPosition = pos;
|
||||
console.log('Cursor position after mouseup:', pos);
|
||||
}
|
||||
|
||||
// Selection will be handled by selectionchange event
|
||||
@ -483,7 +827,6 @@ export class DeesWysiwygBlock extends DeesElement {
|
||||
const pos = this.getCursorPosition(editableBlock);
|
||||
if (pos !== null) {
|
||||
this.lastKnownCursorPosition = pos;
|
||||
console.log('Cursor position after click:', pos);
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
@ -538,7 +881,6 @@ export class DeesWysiwygBlock extends DeesElement {
|
||||
if (startInBlock || endInBlock) {
|
||||
if (selectedText !== this.lastSelectedText) {
|
||||
this.lastSelectedText = selectedText;
|
||||
console.log('✅ Selection detected in block using getComposedRanges:', selectedText);
|
||||
|
||||
// Create range and get rect
|
||||
const range = WysiwygSelection.createRangeFromInfo(selectionInfo);
|
||||
@ -664,6 +1006,113 @@ export class DeesWysiwygBlock extends DeesElement {
|
||||
`;
|
||||
}
|
||||
|
||||
if (this.block.type === 'youtube') {
|
||||
const selectedClass = this.isSelected ? ' selected' : '';
|
||||
const videoId = this.block.metadata?.videoId || '';
|
||||
const url = this.block.metadata?.url || '';
|
||||
|
||||
return `
|
||||
<div class="block youtube${selectedClass}" data-block-id="${this.block.id}" data-block-type="${this.block.type}" tabindex="0">
|
||||
${videoId ? `
|
||||
<div class="youtube-container">
|
||||
<iframe
|
||||
src="https://www.youtube.com/embed/${videoId}"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
</div>
|
||||
` : `
|
||||
<div class="youtube-placeholder">
|
||||
<div class="placeholder-icon">▶️</div>
|
||||
<div class="placeholder-text">Enter YouTube URL</div>
|
||||
<input type="url" class="youtube-url-input" placeholder="https://youtube.com/watch?v=..." value="${url || ''}" />
|
||||
<button class="youtube-embed-btn">Embed Video</button>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (this.block.type === 'markdown') {
|
||||
const selectedClass = this.isSelected ? ' selected' : '';
|
||||
const showPreview = this.block.metadata?.showPreview !== false;
|
||||
|
||||
return `
|
||||
<div class="block markdown${selectedClass}" data-block-id="${this.block.id}" data-block-type="${this.block.type}" tabindex="0">
|
||||
<div class="markdown-toolbar">
|
||||
<button class="toggle-preview" data-active="${showPreview}">
|
||||
${showPreview ? 'Edit' : 'Preview'}
|
||||
</button>
|
||||
<span class="markdown-label">Markdown</span>
|
||||
</div>
|
||||
<div class="markdown-content">
|
||||
${showPreview ? `
|
||||
<div class="markdown-preview"></div>
|
||||
` : `
|
||||
<textarea class="markdown-editor" placeholder="Write markdown here...">${this.block.content || ''}</textarea>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (this.block.type === 'html') {
|
||||
const selectedClass = this.isSelected ? ' selected' : '';
|
||||
const showPreview = this.block.metadata?.showPreview !== false;
|
||||
|
||||
return `
|
||||
<div class="block html${selectedClass}" data-block-id="${this.block.id}" data-block-type="${this.block.type}" tabindex="0">
|
||||
<div class="html-toolbar">
|
||||
<button class="toggle-preview" data-active="${showPreview}">
|
||||
${showPreview ? 'Edit' : 'Preview'}
|
||||
</button>
|
||||
<span class="html-label">HTML</span>
|
||||
</div>
|
||||
<div class="html-content">
|
||||
${showPreview ? `
|
||||
<div class="html-preview"></div>
|
||||
` : `
|
||||
<textarea class="html-editor" placeholder="Write HTML here...">${this.block.content || ''}</textarea>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (this.block.type === 'attachment') {
|
||||
const selectedClass = this.isSelected ? ' selected' : '';
|
||||
const files = this.block.metadata?.files || [];
|
||||
|
||||
return `
|
||||
<div class="block attachment${selectedClass}" data-block-id="${this.block.id}" data-block-type="${this.block.type}" tabindex="0">
|
||||
<div class="attachment-header">
|
||||
<div class="attachment-icon">📎</div>
|
||||
<div class="attachment-title">File Attachments</div>
|
||||
</div>
|
||||
<div class="attachment-list">
|
||||
${files.length > 0 ? files.map((file: any) => `
|
||||
<div class="attachment-item">
|
||||
<div class="file-icon">${this.getFileIcon(file.type)}</div>
|
||||
<div class="file-info">
|
||||
<div class="file-name">${file.name}</div>
|
||||
<div class="file-size">${this.formatFileSize(file.size)}</div>
|
||||
</div>
|
||||
<button class="remove-file" data-file-id="${file.id}">×</button>
|
||||
</div>
|
||||
`).join('') : `
|
||||
<div class="attachment-placeholder">
|
||||
<div class="placeholder-text">Click to add files</div>
|
||||
<div class="placeholder-hint">or drag and drop</div>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
<input type="file" multiple style="display: none;" />
|
||||
${files.length > 0 ? '<button class="add-more-files">Add More Files</button>' : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const placeholder = this.getPlaceholder();
|
||||
const selectedClass = this.isSelected ? ' selected' : '';
|
||||
return `
|
||||
@ -697,16 +1146,11 @@ export class DeesWysiwygBlock extends DeesElement {
|
||||
|
||||
public focus(): void {
|
||||
// Handle non-editable blocks
|
||||
if (this.block?.type === 'image') {
|
||||
const imageBlock = this.shadowRoot?.querySelector('.block.image') as HTMLDivElement;
|
||||
if (imageBlock) {
|
||||
imageBlock.focus();
|
||||
}
|
||||
return;
|
||||
} else if (this.block?.type === 'divider') {
|
||||
const dividerBlock = this.shadowRoot?.querySelector('.block.divider') as HTMLDivElement;
|
||||
if (dividerBlock) {
|
||||
dividerBlock.focus();
|
||||
const nonEditableTypes = ['image', 'divider', 'youtube', 'markdown', 'html', 'attachment'];
|
||||
if (this.block && nonEditableTypes.includes(this.block.type)) {
|
||||
const blockElement = this.shadowRoot?.querySelector(`.block.${this.block.type}`) as HTMLDivElement;
|
||||
if (blockElement) {
|
||||
blockElement.focus();
|
||||
}
|
||||
return;
|
||||
}
|
||||
@ -735,7 +1179,8 @@ export class DeesWysiwygBlock extends DeesElement {
|
||||
|
||||
public focusWithCursor(position: 'start' | 'end' | number = 'end'): void {
|
||||
// Non-editable blocks don't support cursor positioning
|
||||
if (this.block?.type === 'image' || this.block?.type === 'divider') {
|
||||
const nonEditableTypes = ['image', 'divider', 'youtube', 'markdown', 'html', 'attachment'];
|
||||
if (this.block && nonEditableTypes.includes(this.block.type)) {
|
||||
this.focus();
|
||||
return;
|
||||
}
|
||||
@ -950,7 +1395,439 @@ export class DeesWysiwygBlock extends DeesElement {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup YouTube block functionality
|
||||
*/
|
||||
private setupYouTubeBlock(): void {
|
||||
const youtubeBlock = this.shadowRoot?.querySelector('.block.youtube') as HTMLDivElement;
|
||||
if (!youtubeBlock) return;
|
||||
|
||||
// Handle click to select
|
||||
youtubeBlock.addEventListener('click', (e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (!target.classList.contains('youtube-url-input') && !target.classList.contains('youtube-embed-btn')) {
|
||||
e.stopPropagation();
|
||||
youtubeBlock.focus();
|
||||
this.handlers?.onFocus?.();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle URL input and embed button
|
||||
const urlInput = youtubeBlock.querySelector('.youtube-url-input') as HTMLInputElement;
|
||||
const embedBtn = youtubeBlock.querySelector('.youtube-embed-btn') as HTMLButtonElement;
|
||||
|
||||
if (urlInput && embedBtn) {
|
||||
const embedVideo = () => {
|
||||
const url = urlInput.value.trim();
|
||||
if (url) {
|
||||
// Extract video ID from YouTube URL
|
||||
const videoId = this.extractYouTubeVideoId(url);
|
||||
if (videoId) {
|
||||
this.block.metadata = { ...this.block.metadata, videoId, url };
|
||||
this.block.content = url; // Store URL as content
|
||||
|
||||
// Re-render the block
|
||||
const container = this.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLDivElement;
|
||||
if (container) {
|
||||
container.innerHTML = this.renderBlockContent();
|
||||
this.setupYouTubeBlock(); // Re-setup event handlers
|
||||
}
|
||||
|
||||
// Notify parent of change
|
||||
this.handlers?.onInput?.(new InputEvent('input'));
|
||||
} else {
|
||||
alert('Invalid YouTube URL');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
embedBtn.addEventListener('click', embedVideo);
|
||||
urlInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
embedVideo();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle focus/blur
|
||||
youtubeBlock.addEventListener('focus', () => this.handlers?.onFocus?.());
|
||||
youtubeBlock.addEventListener('blur', () => this.handlers?.onBlur?.());
|
||||
|
||||
// Handle keyboard events
|
||||
youtubeBlock.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Backspace' || e.key === 'Delete') {
|
||||
e.preventDefault();
|
||||
this.handlers?.onKeyDown?.(e);
|
||||
} else {
|
||||
this.handlers?.onKeyDown?.(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup Markdown block functionality
|
||||
*/
|
||||
private setupMarkdownBlock(): void {
|
||||
const markdownBlock = this.shadowRoot?.querySelector('.block.markdown') as HTMLDivElement;
|
||||
if (!markdownBlock) return;
|
||||
|
||||
// Handle click to select
|
||||
markdownBlock.addEventListener('click', (e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (!target.classList.contains('markdown-editor') && !target.classList.contains('toggle-preview')) {
|
||||
e.stopPropagation();
|
||||
markdownBlock.focus();
|
||||
this.handlers?.onFocus?.();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle preview toggle
|
||||
const toggleBtn = markdownBlock.querySelector('.toggle-preview') as HTMLButtonElement;
|
||||
if (toggleBtn) {
|
||||
toggleBtn.addEventListener('click', () => {
|
||||
const showPreview = toggleBtn.dataset.active !== 'true';
|
||||
this.block.metadata = { ...this.block.metadata, showPreview };
|
||||
|
||||
// Re-render
|
||||
const container = this.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLDivElement;
|
||||
if (container) {
|
||||
container.innerHTML = this.renderBlockContent();
|
||||
this.setupMarkdownBlock();
|
||||
|
||||
// If switching to preview, render markdown
|
||||
if (showPreview) {
|
||||
this.renderMarkdownPreview();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle editor input
|
||||
const editor = markdownBlock.querySelector('.markdown-editor') as HTMLTextAreaElement;
|
||||
if (editor) {
|
||||
editor.addEventListener('input', () => {
|
||||
this.block.content = editor.value;
|
||||
this.handlers?.onInput?.(new InputEvent('input'));
|
||||
});
|
||||
|
||||
// Auto-resize textarea
|
||||
const autoResize = () => {
|
||||
editor.style.height = 'auto';
|
||||
editor.style.height = editor.scrollHeight + 'px';
|
||||
};
|
||||
editor.addEventListener('input', autoResize);
|
||||
autoResize();
|
||||
}
|
||||
|
||||
// Render preview if needed
|
||||
if (this.block.metadata?.showPreview) {
|
||||
this.renderMarkdownPreview();
|
||||
}
|
||||
|
||||
// Handle focus/blur
|
||||
markdownBlock.addEventListener('focus', () => this.handlers?.onFocus?.());
|
||||
markdownBlock.addEventListener('blur', () => this.handlers?.onBlur?.());
|
||||
|
||||
// Handle keyboard events
|
||||
markdownBlock.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Backspace' || e.key === 'Delete') {
|
||||
e.preventDefault();
|
||||
this.handlers?.onKeyDown?.(e);
|
||||
} else {
|
||||
this.handlers?.onKeyDown?.(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup HTML block functionality
|
||||
*/
|
||||
private setupHtmlBlock(): void {
|
||||
const htmlBlock = this.shadowRoot?.querySelector('.block.html') as HTMLDivElement;
|
||||
if (!htmlBlock) return;
|
||||
|
||||
// Handle click to select
|
||||
htmlBlock.addEventListener('click', (e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (!target.classList.contains('html-editor') && !target.classList.contains('toggle-preview')) {
|
||||
e.stopPropagation();
|
||||
htmlBlock.focus();
|
||||
this.handlers?.onFocus?.();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle preview toggle
|
||||
const toggleBtn = htmlBlock.querySelector('.toggle-preview') as HTMLButtonElement;
|
||||
if (toggleBtn) {
|
||||
toggleBtn.addEventListener('click', () => {
|
||||
const showPreview = toggleBtn.dataset.active !== 'true';
|
||||
this.block.metadata = { ...this.block.metadata, showPreview };
|
||||
|
||||
// Re-render
|
||||
const container = this.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLDivElement;
|
||||
if (container) {
|
||||
container.innerHTML = this.renderBlockContent();
|
||||
this.setupHtmlBlock();
|
||||
|
||||
// If switching to preview, render HTML
|
||||
if (showPreview) {
|
||||
this.renderHtmlPreview();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle editor input
|
||||
const editor = htmlBlock.querySelector('.html-editor') as HTMLTextAreaElement;
|
||||
if (editor) {
|
||||
editor.addEventListener('input', () => {
|
||||
this.block.content = editor.value;
|
||||
this.handlers?.onInput?.(new InputEvent('input'));
|
||||
});
|
||||
|
||||
// Auto-resize textarea
|
||||
const autoResize = () => {
|
||||
editor.style.height = 'auto';
|
||||
editor.style.height = editor.scrollHeight + 'px';
|
||||
};
|
||||
editor.addEventListener('input', autoResize);
|
||||
autoResize();
|
||||
}
|
||||
|
||||
// Render preview if needed
|
||||
if (this.block.metadata?.showPreview) {
|
||||
this.renderHtmlPreview();
|
||||
}
|
||||
|
||||
// Handle focus/blur
|
||||
htmlBlock.addEventListener('focus', () => this.handlers?.onFocus?.());
|
||||
htmlBlock.addEventListener('blur', () => this.handlers?.onBlur?.());
|
||||
|
||||
// Handle keyboard events
|
||||
htmlBlock.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Backspace' || e.key === 'Delete') {
|
||||
e.preventDefault();
|
||||
this.handlers?.onKeyDown?.(e);
|
||||
} else {
|
||||
this.handlers?.onKeyDown?.(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup Attachment block functionality
|
||||
*/
|
||||
private setupAttachmentBlock(): void {
|
||||
const attachmentBlock = this.shadowRoot?.querySelector('.block.attachment') as HTMLDivElement;
|
||||
if (!attachmentBlock) return;
|
||||
|
||||
// Handle click to select
|
||||
attachmentBlock.addEventListener('click', (e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (!target.classList.contains('remove-file')) {
|
||||
e.stopPropagation();
|
||||
attachmentBlock.focus();
|
||||
this.handlers?.onFocus?.();
|
||||
}
|
||||
});
|
||||
|
||||
// Handle file input
|
||||
const fileInput = attachmentBlock.querySelector('input[type="file"]') as HTMLInputElement;
|
||||
const placeholder = attachmentBlock.querySelector('.attachment-placeholder');
|
||||
const addMoreBtn = attachmentBlock.querySelector('.add-more-files') as HTMLButtonElement;
|
||||
|
||||
const triggerFileInput = () => {
|
||||
if (fileInput) fileInput.click();
|
||||
};
|
||||
|
||||
if (placeholder) {
|
||||
placeholder.addEventListener('click', triggerFileInput);
|
||||
}
|
||||
|
||||
if (addMoreBtn) {
|
||||
addMoreBtn.addEventListener('click', triggerFileInput);
|
||||
}
|
||||
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener('change', async (e) => {
|
||||
const files = Array.from((e.target as HTMLInputElement).files || []);
|
||||
if (files.length > 0) {
|
||||
await this.handleFileAttachments(files);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle file removal
|
||||
attachmentBlock.addEventListener('click', (e) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (target.classList.contains('remove-file')) {
|
||||
const fileId = target.dataset.fileId;
|
||||
if (fileId) {
|
||||
const files = this.block.metadata?.files || [];
|
||||
this.block.metadata = {
|
||||
...this.block.metadata,
|
||||
files: files.filter((f: any) => f.id !== fileId)
|
||||
};
|
||||
|
||||
// Re-render
|
||||
const container = this.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLDivElement;
|
||||
if (container) {
|
||||
container.innerHTML = this.renderBlockContent();
|
||||
this.setupAttachmentBlock();
|
||||
}
|
||||
|
||||
this.handlers?.onInput?.(new InputEvent('input'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle drag and drop
|
||||
attachmentBlock.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
attachmentBlock.classList.add('drag-over');
|
||||
});
|
||||
|
||||
attachmentBlock.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
attachmentBlock.classList.remove('drag-over');
|
||||
});
|
||||
|
||||
attachmentBlock.addEventListener('drop', async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
attachmentBlock.classList.remove('drag-over');
|
||||
|
||||
const files = Array.from(e.dataTransfer?.files || []);
|
||||
if (files.length > 0) {
|
||||
await this.handleFileAttachments(files);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle focus/blur
|
||||
attachmentBlock.addEventListener('focus', () => this.handlers?.onFocus?.());
|
||||
attachmentBlock.addEventListener('blur', () => this.handlers?.onBlur?.());
|
||||
|
||||
// Handle keyboard events
|
||||
attachmentBlock.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Backspace' || e.key === 'Delete') {
|
||||
e.preventDefault();
|
||||
this.handlers?.onKeyDown?.(e);
|
||||
} else {
|
||||
this.handlers?.onKeyDown?.(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract YouTube video ID from URL
|
||||
*/
|
||||
private extractYouTubeVideoId(url: string): string | null {
|
||||
const regex = /(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/ ]{11})/;
|
||||
const match = url.match(regex);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render Markdown preview
|
||||
*/
|
||||
private async renderMarkdownPreview(): Promise<void> {
|
||||
const preview = this.shadowRoot?.querySelector('.markdown-preview') as HTMLDivElement;
|
||||
if (!preview || !this.block.content) return;
|
||||
|
||||
// Simple markdown to HTML conversion (you might want to use a proper markdown parser)
|
||||
let html = this.block.content
|
||||
.replace(/^### (.*$)/gim, '<h3>$1</h3>')
|
||||
.replace(/^## (.*$)/gim, '<h2>$1</h2>')
|
||||
.replace(/^# (.*$)/gim, '<h1>$1</h1>')
|
||||
.replace(/\*\*(.*)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/\*(.*)\*/g, '<em>$1</em>')
|
||||
.replace(/\[([^\]]*)\]\(([^\)]*)\)/g, '<a href="$2">$1</a>')
|
||||
.replace(/\n/g, '<br>');
|
||||
|
||||
preview.innerHTML = html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render HTML preview
|
||||
*/
|
||||
private renderHtmlPreview(): void {
|
||||
const preview = this.shadowRoot?.querySelector('.html-preview') as HTMLDivElement;
|
||||
if (!preview || !this.block.content) return;
|
||||
|
||||
// Render HTML in a sandboxed way
|
||||
preview.innerHTML = this.block.content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle file attachments
|
||||
*/
|
||||
private async handleFileAttachments(files: File[]): Promise<void> {
|
||||
const existingFiles = this.block.metadata?.files || [];
|
||||
const newFiles: any[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
// Convert to base64 for storage (in production, upload to server)
|
||||
const reader = new FileReader();
|
||||
const base64 = await new Promise<string>((resolve) => {
|
||||
reader.onload = (e) => resolve(e.target?.result as string);
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
newFiles.push({
|
||||
id: `file-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
data: base64
|
||||
});
|
||||
}
|
||||
|
||||
this.block.metadata = {
|
||||
...this.block.metadata,
|
||||
files: [...existingFiles, ...newFiles]
|
||||
};
|
||||
|
||||
// Re-render
|
||||
const container = this.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLDivElement;
|
||||
if (container) {
|
||||
container.innerHTML = this.renderBlockContent();
|
||||
this.setupAttachmentBlock();
|
||||
}
|
||||
|
||||
this.handlers?.onInput?.(new InputEvent('input'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file icon based on mime type
|
||||
*/
|
||||
private getFileIcon(mimeType: string): string {
|
||||
if (mimeType.startsWith('image/')) return '🖼️';
|
||||
if (mimeType.startsWith('video/')) return '🎥';
|
||||
if (mimeType.startsWith('audio/')) return '🎵';
|
||||
if (mimeType.includes('pdf')) return '📄';
|
||||
if (mimeType.includes('zip') || mimeType.includes('compressed')) return '🗄️';
|
||||
if (mimeType.includes('sheet') || mimeType.includes('excel')) return '📊';
|
||||
if (mimeType.includes('document') || mimeType.includes('word')) return '📝';
|
||||
if (mimeType.includes('presentation') || mimeType.includes('powerpoint')) return '📋';
|
||||
if (mimeType.includes('text')) return '📃';
|
||||
return '📁';
|
||||
}
|
||||
|
||||
/**
|
||||
* Format file size to human readable
|
||||
*/
|
||||
private formatFileSize(bytes: number): string {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup image block functionality
|
||||
*/
|
||||
|
Reference in New Issue
Block a user