update
This commit is contained in:
@ -1,608 +0,0 @@
|
|||||||
import { BaseBlockHandler, type IBlockEventHandlers } from '../block.base.js';
|
|
||||||
import type { IBlock } from '../../wysiwyg.types.js';
|
|
||||||
import { cssManager } from '@design.estate/dees-element';
|
|
||||||
import { WysiwygBlocks } from '../../wysiwyg.blocks.js';
|
|
||||||
import { WysiwygSelection } from '../../wysiwyg.selection.js';
|
|
||||||
import hlight from 'highlight.js';
|
|
||||||
|
|
||||||
export class CodeBlockHandler extends BaseBlockHandler {
|
|
||||||
type = 'code';
|
|
||||||
|
|
||||||
// Track cursor position
|
|
||||||
private lastKnownCursorPosition: number = 0;
|
|
||||||
|
|
||||||
// Debounce timer for highlighting
|
|
||||||
private highlightingTimer: any = null;
|
|
||||||
|
|
||||||
render(block: IBlock, isSelected: boolean): string {
|
|
||||||
const language = block.metadata?.language || 'javascript';
|
|
||||||
const selectedClass = isSelected ? ' selected' : '';
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="code-block-container${selectedClass}">
|
|
||||||
<div class="code-language">${language}</div>
|
|
||||||
<div class="code-grid">
|
|
||||||
<div class="line-numbers"></div>
|
|
||||||
<div
|
|
||||||
class="block code"
|
|
||||||
contenteditable="true"
|
|
||||||
data-block-id="${block.id}"
|
|
||||||
data-block-type="${block.type}"
|
|
||||||
spellcheck="false"
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
setup(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void {
|
|
||||||
const codeBlock = element.querySelector('.block.code') as HTMLDivElement;
|
|
||||||
if (!codeBlock) {
|
|
||||||
console.error('CodeBlockHandler.setup: No code block element found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set initial content if needed - use textContent for code blocks
|
|
||||||
if (block.content && !codeBlock.textContent) {
|
|
||||||
codeBlock.textContent = block.content;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply initial highlighting
|
|
||||||
this.applyHighlighting(element, block);
|
|
||||||
|
|
||||||
// Variable to track if we're in composition mode
|
|
||||||
let isComposing = false;
|
|
||||||
|
|
||||||
// Input handler
|
|
||||||
codeBlock.addEventListener('input', (e) => {
|
|
||||||
handlers.onInput(e as InputEvent);
|
|
||||||
|
|
||||||
// Track cursor position after input
|
|
||||||
const pos = this.getCursorPosition(element);
|
|
||||||
if (pos !== null) {
|
|
||||||
this.lastKnownCursorPosition = pos;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update line numbers immediately
|
|
||||||
const lineNumbersContainer = element.querySelector('.line-numbers') as HTMLDivElement;
|
|
||||||
if (lineNumbersContainer) {
|
|
||||||
this.updateLineNumbers(codeBlock.textContent || '', lineNumbersContainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Debounce highlighting to avoid performance issues while typing
|
|
||||||
if (!isComposing) {
|
|
||||||
clearTimeout(this.highlightingTimer);
|
|
||||||
this.highlightingTimer = setTimeout(() => {
|
|
||||||
// Store cursor position before highlighting
|
|
||||||
const currentPos = this.getCursorPosition(element);
|
|
||||||
|
|
||||||
// Get plain text before highlighting
|
|
||||||
const plainText = codeBlock.textContent || '';
|
|
||||||
|
|
||||||
// Apply highlighting
|
|
||||||
const language = block.metadata?.language || 'javascript';
|
|
||||||
try {
|
|
||||||
const result = hlight.highlight(plainText, {
|
|
||||||
language: language,
|
|
||||||
ignoreIllegals: true
|
|
||||||
});
|
|
||||||
codeBlock.innerHTML = result.value;
|
|
||||||
} catch (error) {
|
|
||||||
// Keep plain text if highlighting fails
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore cursor position
|
|
||||||
if (currentPos !== null && document.activeElement === codeBlock) {
|
|
||||||
WysiwygSelection.setCursorPosition(codeBlock, currentPos);
|
|
||||||
}
|
|
||||||
}, 500); // Wait 500ms after user stops typing
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Keydown handler
|
|
||||||
codeBlock.addEventListener('keydown', (e) => {
|
|
||||||
// Track cursor position before keydown
|
|
||||||
const pos = this.getCursorPosition(element);
|
|
||||||
if (pos !== null) {
|
|
||||||
this.lastKnownCursorPosition = pos;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Special handling for Tab key in code blocks
|
|
||||||
if (e.key === 'Tab') {
|
|
||||||
e.preventDefault();
|
|
||||||
// Insert two spaces for tab
|
|
||||||
const selection = window.getSelection();
|
|
||||||
if (selection && selection.rangeCount > 0) {
|
|
||||||
const range = selection.getRangeAt(0);
|
|
||||||
range.deleteContents();
|
|
||||||
const textNode = document.createTextNode(' ');
|
|
||||||
range.insertNode(textNode);
|
|
||||||
range.setStartAfter(textNode);
|
|
||||||
range.setEndAfter(textNode);
|
|
||||||
selection.removeAllRanges();
|
|
||||||
selection.addRange(range);
|
|
||||||
|
|
||||||
// Trigger input event
|
|
||||||
handlers.onInput(new InputEvent('input'));
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlers.onKeyDown(e);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Focus handler
|
|
||||||
codeBlock.addEventListener('focus', () => {
|
|
||||||
handlers.onFocus();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Blur handler
|
|
||||||
codeBlock.addEventListener('blur', () => {
|
|
||||||
handlers.onBlur();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Composition handlers for IME support
|
|
||||||
codeBlock.addEventListener('compositionstart', () => {
|
|
||||||
isComposing = true;
|
|
||||||
handlers.onCompositionStart();
|
|
||||||
});
|
|
||||||
|
|
||||||
codeBlock.addEventListener('compositionend', () => {
|
|
||||||
isComposing = false;
|
|
||||||
handlers.onCompositionEnd();
|
|
||||||
|
|
||||||
// Apply highlighting after composition ends
|
|
||||||
setTimeout(() => {
|
|
||||||
this.applyHighlighting(element, block);
|
|
||||||
}, 100);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mouse up handler
|
|
||||||
codeBlock.addEventListener('mouseup', (e) => {
|
|
||||||
const pos = this.getCursorPosition(element);
|
|
||||||
if (pos !== null) {
|
|
||||||
this.lastKnownCursorPosition = pos;
|
|
||||||
}
|
|
||||||
handlers.onMouseUp?.(e);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Click handler with delayed cursor tracking
|
|
||||||
codeBlock.addEventListener('click', (_e: MouseEvent) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
const pos = this.getCursorPosition(element);
|
|
||||||
if (pos !== null) {
|
|
||||||
this.lastKnownCursorPosition = pos;
|
|
||||||
}
|
|
||||||
}, 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Keyup handler for cursor tracking
|
|
||||||
codeBlock.addEventListener('keyup', (_e) => {
|
|
||||||
const pos = this.getCursorPosition(element);
|
|
||||||
if (pos !== null) {
|
|
||||||
this.lastKnownCursorPosition = pos;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Paste handler - handle as plain text
|
|
||||||
codeBlock.addEventListener('paste', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const text = e.clipboardData?.getData('text/plain');
|
|
||||||
if (text) {
|
|
||||||
const selection = window.getSelection();
|
|
||||||
if (selection && selection.rangeCount > 0) {
|
|
||||||
const range = selection.getRangeAt(0);
|
|
||||||
range.deleteContents();
|
|
||||||
const textNode = document.createTextNode(text);
|
|
||||||
range.insertNode(textNode);
|
|
||||||
range.setStartAfter(textNode);
|
|
||||||
range.setEndAfter(textNode);
|
|
||||||
selection.removeAllRanges();
|
|
||||||
selection.addRange(range);
|
|
||||||
|
|
||||||
// Trigger input event
|
|
||||||
handlers.onInput(new InputEvent('input'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getStyles(): string {
|
|
||||||
return `
|
|
||||||
/* Code block specific styles */
|
|
||||||
.code-block-container {
|
|
||||||
position: relative;
|
|
||||||
margin: 20px 0;
|
|
||||||
background: ${cssManager.bdTheme('#f8f8f8', '#0d0d0d')};
|
|
||||||
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#2a2a2a')};
|
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.code-block-container.selected {
|
|
||||||
border-color: ${cssManager.bdTheme('#0066ff', '#4d9fff')};
|
|
||||||
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(0, 102, 255, 0.1)', 'rgba(77, 159, 255, 0.1)')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.code-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 50px 1fr;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.line-numbers {
|
|
||||||
background: ${cssManager.bdTheme('#f3f3f3', '#0a0a0a')};
|
|
||||||
border-right: 1px solid ${cssManager.bdTheme('#e0e0e0', '#2a2a2a')};
|
|
||||||
padding: 16px 12px 16px 0;
|
|
||||||
text-align: right;
|
|
||||||
user-select: none;
|
|
||||||
color: ${cssManager.bdTheme('#999999', '#666666')};
|
|
||||||
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
|
|
||||||
font-size: 14px;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.line-number {
|
|
||||||
height: 21px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.line-number:last-child {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.block.code {
|
|
||||||
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
|
|
||||||
font-size: 14px;
|
|
||||||
padding: 16px 20px;
|
|
||||||
padding-top: 32px;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
color: ${cssManager.bdTheme('#24292e', '#e1e4e8')};
|
|
||||||
line-height: 1.5;
|
|
||||||
overflow-x: auto;
|
|
||||||
margin: 0;
|
|
||||||
min-height: 100px;
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.code-language {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
background: ${cssManager.bdTheme('#e1e4e8', '#333333')};
|
|
||||||
color: ${cssManager.bdTheme('#586069', '#8b949e')};
|
|
||||||
padding: 4px 12px;
|
|
||||||
font-size: 12px;
|
|
||||||
border-radius: 0 0 0 6px;
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
text-transform: lowercase;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Highlight.js theme overrides */
|
|
||||||
.block.code .hljs-keyword {
|
|
||||||
color: ${cssManager.bdTheme('#d73a49', '#ff65ec')};
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.block.code .hljs-string {
|
|
||||||
color: ${cssManager.bdTheme('#032f62', '#ffa465')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.block.code .hljs-number {
|
|
||||||
color: ${cssManager.bdTheme('#005cc5', '#65d5ff')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.block.code .hljs-function {
|
|
||||||
color: ${cssManager.bdTheme('#6f42c1', '#6596ff')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.block.code .hljs-comment {
|
|
||||||
color: ${cssManager.bdTheme('#6a737d', '#ffd765')};
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.block.code .hljs-variable,
|
|
||||||
.block.code .hljs-attr {
|
|
||||||
color: ${cssManager.bdTheme('#e36209', '#65ff6a')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.block.code .hljs-class,
|
|
||||||
.block.code .hljs-title {
|
|
||||||
color: ${cssManager.bdTheme('#6f42c1', '#65d5ff')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.block.code .hljs-params {
|
|
||||||
color: ${cssManager.bdTheme('#24292e', '#e1e4e8')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.block.code .hljs-built_in {
|
|
||||||
color: ${cssManager.bdTheme('#005cc5', '#65ff6a')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.block.code .hljs-literal {
|
|
||||||
color: ${cssManager.bdTheme('#005cc5', '#ff65ec')};
|
|
||||||
}
|
|
||||||
|
|
||||||
.block.code .hljs-meta {
|
|
||||||
color: ${cssManager.bdTheme('#735c0f', '#ffa465')};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
getPlaceholder(): string {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper methods for code functionality
|
|
||||||
|
|
||||||
private applyHighlighting(element: HTMLElement, block: IBlock): void {
|
|
||||||
const codeBlock = element.querySelector('.block.code') as HTMLDivElement;
|
|
||||||
const lineNumbersContainer = element.querySelector('.line-numbers') as HTMLDivElement;
|
|
||||||
|
|
||||||
if (!codeBlock || !lineNumbersContainer) return;
|
|
||||||
|
|
||||||
// Store current cursor position
|
|
||||||
const cursorPos = this.getCursorPosition(element);
|
|
||||||
|
|
||||||
// Get the plain text content
|
|
||||||
const plainText = codeBlock.textContent || '';
|
|
||||||
|
|
||||||
// Apply syntax highlighting
|
|
||||||
const language = block.metadata?.language || 'javascript';
|
|
||||||
let highlightedHtml: string;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = hlight.highlight(plainText, {
|
|
||||||
language: language,
|
|
||||||
ignoreIllegals: true
|
|
||||||
});
|
|
||||||
highlightedHtml = result.value;
|
|
||||||
} catch (error) {
|
|
||||||
// Fallback to plain text if highlighting fails
|
|
||||||
highlightedHtml = this.escapeHtml(plainText);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the code block with highlighted content
|
|
||||||
codeBlock.innerHTML = highlightedHtml;
|
|
||||||
|
|
||||||
// Update line numbers
|
|
||||||
this.updateLineNumbers(plainText, lineNumbersContainer);
|
|
||||||
|
|
||||||
// Restore cursor position if we had one
|
|
||||||
if (cursorPos !== null && document.activeElement === codeBlock) {
|
|
||||||
WysiwygSelection.setCursorPosition(codeBlock, cursorPos);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateLineNumbers(content: string, container: HTMLDivElement): void {
|
|
||||||
const lines = content.split('\n');
|
|
||||||
const lineCount = lines.length;
|
|
||||||
|
|
||||||
// Generate line numbers HTML
|
|
||||||
let lineNumbersHtml = '';
|
|
||||||
for (let i = 1; i <= lineCount; i++) {
|
|
||||||
lineNumbersHtml += `<div class="line-number">${i}</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
container.innerHTML = lineNumbersHtml;
|
|
||||||
}
|
|
||||||
|
|
||||||
private escapeHtml(text: string): string {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.textContent = text;
|
|
||||||
return div.innerHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
getCursorPosition(element: HTMLElement, context?: any): number | null {
|
|
||||||
// Get the actual code element
|
|
||||||
const codeBlock = element.querySelector('.block.code') as HTMLDivElement;
|
|
||||||
if (!codeBlock) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get shadow roots from context
|
|
||||||
const wysiwygBlock = context?.component;
|
|
||||||
const parentComponent = wysiwygBlock?.closest('dees-input-wysiwyg');
|
|
||||||
const parentShadowRoot = parentComponent?.shadowRoot;
|
|
||||||
const blockShadowRoot = context?.shadowRoot;
|
|
||||||
|
|
||||||
// Get selection info with both shadow roots for proper traversal
|
|
||||||
const shadowRoots: ShadowRoot[] = [];
|
|
||||||
if (parentShadowRoot) shadowRoots.push(parentShadowRoot);
|
|
||||||
if (blockShadowRoot) shadowRoots.push(blockShadowRoot);
|
|
||||||
|
|
||||||
const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots);
|
|
||||||
|
|
||||||
if (!selectionInfo) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!WysiwygSelection.containsAcrossShadowDOM(codeBlock, selectionInfo.startContainer)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a range from start of element to cursor position
|
|
||||||
const preCaretRange = document.createRange();
|
|
||||||
preCaretRange.selectNodeContents(codeBlock);
|
|
||||||
preCaretRange.setEnd(selectionInfo.startContainer, selectionInfo.startOffset);
|
|
||||||
|
|
||||||
// Get the text content length up to cursor
|
|
||||||
const position = preCaretRange.toString().length;
|
|
||||||
|
|
||||||
return position;
|
|
||||||
}
|
|
||||||
|
|
||||||
getContent(element: HTMLElement, _context?: any): string {
|
|
||||||
const codeBlock = element.querySelector('.block.code') as HTMLDivElement;
|
|
||||||
if (!codeBlock) return '';
|
|
||||||
|
|
||||||
// For code blocks, get textContent to avoid HTML formatting
|
|
||||||
const content = codeBlock.textContent || '';
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
setContent(element: HTMLElement, content: string, context?: any): void {
|
|
||||||
const codeBlock = element.querySelector('.block.code') as HTMLDivElement;
|
|
||||||
if (!codeBlock) return;
|
|
||||||
|
|
||||||
// Store if we have focus
|
|
||||||
const hadFocus = document.activeElement === codeBlock ||
|
|
||||||
element.shadowRoot?.activeElement === codeBlock;
|
|
||||||
|
|
||||||
// Use textContent for code blocks
|
|
||||||
codeBlock.textContent = content;
|
|
||||||
|
|
||||||
// Apply highlighting
|
|
||||||
const block: IBlock = {
|
|
||||||
id: codeBlock.dataset.blockId || '',
|
|
||||||
type: 'code',
|
|
||||||
content: content,
|
|
||||||
metadata: context?.block?.metadata || {}
|
|
||||||
};
|
|
||||||
this.applyHighlighting(element, block);
|
|
||||||
|
|
||||||
// Restore focus if we had it
|
|
||||||
if (hadFocus) {
|
|
||||||
codeBlock.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setCursorToStart(element: HTMLElement, _context?: any): void {
|
|
||||||
const codeBlock = element.querySelector('.block.code') as HTMLDivElement;
|
|
||||||
if (codeBlock) {
|
|
||||||
WysiwygBlocks.setCursorToStart(codeBlock);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setCursorToEnd(element: HTMLElement, _context?: any): void {
|
|
||||||
const codeBlock = element.querySelector('.block.code') as HTMLDivElement;
|
|
||||||
if (codeBlock) {
|
|
||||||
WysiwygBlocks.setCursorToEnd(codeBlock);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
focus(element: HTMLElement, _context?: any): void {
|
|
||||||
const codeBlock = element.querySelector('.block.code') as HTMLDivElement;
|
|
||||||
if (!codeBlock) return;
|
|
||||||
|
|
||||||
// Ensure the element is focusable
|
|
||||||
if (!codeBlock.hasAttribute('contenteditable')) {
|
|
||||||
codeBlock.setAttribute('contenteditable', 'true');
|
|
||||||
}
|
|
||||||
|
|
||||||
codeBlock.focus();
|
|
||||||
|
|
||||||
// If focus failed, try again after a microtask
|
|
||||||
if (document.activeElement !== codeBlock && element.shadowRoot?.activeElement !== codeBlock) {
|
|
||||||
Promise.resolve().then(() => {
|
|
||||||
codeBlock.focus();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
focusWithCursor(element: HTMLElement, position: 'start' | 'end' | number = 'end', context?: any): void {
|
|
||||||
const codeBlock = element.querySelector('.block.code') as HTMLDivElement;
|
|
||||||
if (!codeBlock) return;
|
|
||||||
|
|
||||||
// Ensure element is focusable first
|
|
||||||
if (!codeBlock.hasAttribute('contenteditable')) {
|
|
||||||
codeBlock.setAttribute('contenteditable', 'true');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Focus the element
|
|
||||||
codeBlock.focus();
|
|
||||||
|
|
||||||
// Set cursor position after focus is established
|
|
||||||
const setCursor = () => {
|
|
||||||
if (position === 'start') {
|
|
||||||
this.setCursorToStart(element, context);
|
|
||||||
} else if (position === 'end') {
|
|
||||||
this.setCursorToEnd(element, context);
|
|
||||||
} else if (typeof position === 'number') {
|
|
||||||
// Use the selection utility to set cursor position
|
|
||||||
WysiwygSelection.setCursorPosition(codeBlock, position);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Ensure cursor is set after focus
|
|
||||||
if (document.activeElement === codeBlock || element.shadowRoot?.activeElement === codeBlock) {
|
|
||||||
setCursor();
|
|
||||||
} else {
|
|
||||||
// Wait for focus to be established
|
|
||||||
Promise.resolve().then(() => {
|
|
||||||
if (document.activeElement === codeBlock || element.shadowRoot?.activeElement === codeBlock) {
|
|
||||||
setCursor();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getSplitContent(element: HTMLElement, context?: any): { before: string; after: string } | null {
|
|
||||||
const codeBlock = element.querySelector('.block.code') as HTMLDivElement;
|
|
||||||
if (!codeBlock) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get shadow roots from context
|
|
||||||
const wysiwygBlock = context?.component;
|
|
||||||
const parentComponent = wysiwygBlock?.closest('dees-input-wysiwyg');
|
|
||||||
const parentShadowRoot = parentComponent?.shadowRoot;
|
|
||||||
const blockShadowRoot = context?.shadowRoot;
|
|
||||||
|
|
||||||
// Get selection info with both shadow roots for proper traversal
|
|
||||||
const shadowRoots: ShadowRoot[] = [];
|
|
||||||
if (parentShadowRoot) shadowRoots.push(parentShadowRoot);
|
|
||||||
if (blockShadowRoot) shadowRoots.push(blockShadowRoot);
|
|
||||||
|
|
||||||
const selectionInfo = WysiwygSelection.getSelectionInfo(...shadowRoots);
|
|
||||||
|
|
||||||
if (!selectionInfo) {
|
|
||||||
// Try using last known cursor position
|
|
||||||
if (this.lastKnownCursorPosition !== null) {
|
|
||||||
const fullText = codeBlock.textContent || '';
|
|
||||||
const pos = Math.min(this.lastKnownCursorPosition, fullText.length);
|
|
||||||
return {
|
|
||||||
before: fullText.substring(0, pos),
|
|
||||||
after: fullText.substring(pos)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure the selection is within this block
|
|
||||||
if (!WysiwygSelection.containsAcrossShadowDOM(codeBlock, selectionInfo.startContainer)) {
|
|
||||||
// Try using last known cursor position
|
|
||||||
if (this.lastKnownCursorPosition !== null) {
|
|
||||||
const fullText = codeBlock.textContent || '';
|
|
||||||
const pos = Math.min(this.lastKnownCursorPosition, fullText.length);
|
|
||||||
return {
|
|
||||||
before: fullText.substring(0, pos),
|
|
||||||
after: fullText.substring(pos)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get cursor position
|
|
||||||
const cursorPos = this.getCursorPosition(element, context);
|
|
||||||
|
|
||||||
if (cursorPos === null || cursorPos === 0) {
|
|
||||||
// If cursor is at start or can't determine position, move all content
|
|
||||||
return {
|
|
||||||
before: '',
|
|
||||||
after: codeBlock.textContent || ''
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// For code blocks, split based on text content only
|
|
||||||
const fullText = codeBlock.textContent || '';
|
|
||||||
|
|
||||||
return {
|
|
||||||
before: fullText.substring(0, cursorPos),
|
|
||||||
after: fullText.substring(cursorPos)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -33,6 +33,13 @@ export class CodeBlockHandler extends BaseBlockHandler {
|
|||||||
<div class="code-block-container${isSelected ? ' selected' : ''}" data-language="${language}">
|
<div class="code-block-container${isSelected ? ' selected' : ''}" data-language="${language}">
|
||||||
<div class="code-header">
|
<div class="code-header">
|
||||||
<span class="language-label">${language}</span>
|
<span class="language-label">${language}</span>
|
||||||
|
<button class="copy-button" title="Copy code">
|
||||||
|
<svg class="copy-icon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
||||||
|
<path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25v-7.5z"></path>
|
||||||
|
<path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0114.25 11h-7.5A1.75 1.75 0 015 9.25v-7.5zm1.75-.25a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-7.5a.25.25 0 00-.25-.25h-7.5z"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="copy-text">Copy</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="code-body">
|
<div class="code-body">
|
||||||
<div class="line-numbers">${lineNumbersHtml}</div>
|
<div class="line-numbers">${lineNumbersHtml}</div>
|
||||||
@ -51,9 +58,59 @@ export class CodeBlockHandler extends BaseBlockHandler {
|
|||||||
setup(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void {
|
setup(element: HTMLElement, block: IBlock, handlers: IBlockEventHandlers): void {
|
||||||
const editor = element.querySelector('.code-editor') as HTMLElement;
|
const editor = element.querySelector('.code-editor') as HTMLElement;
|
||||||
const container = element.querySelector('.code-block-container') as HTMLElement;
|
const container = element.querySelector('.code-block-container') as HTMLElement;
|
||||||
|
const copyButton = element.querySelector('.copy-button') as HTMLButtonElement;
|
||||||
|
|
||||||
if (!editor || !container) return;
|
if (!editor || !container) return;
|
||||||
|
|
||||||
|
// Setup copy button
|
||||||
|
if (copyButton) {
|
||||||
|
copyButton.addEventListener('click', async () => {
|
||||||
|
const content = editor.textContent || '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(content);
|
||||||
|
|
||||||
|
// Show feedback
|
||||||
|
const copyText = copyButton.querySelector('.copy-text') as HTMLElement;
|
||||||
|
const originalText = copyText.textContent;
|
||||||
|
copyText.textContent = 'Copied!';
|
||||||
|
copyButton.classList.add('copied');
|
||||||
|
|
||||||
|
// Reset after 2 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
copyText.textContent = originalText;
|
||||||
|
copyButton.classList.remove('copied');
|
||||||
|
}, 2000);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy:', err);
|
||||||
|
// Fallback for older browsers
|
||||||
|
const textArea = document.createElement('textarea');
|
||||||
|
textArea.value = content;
|
||||||
|
textArea.style.position = 'fixed';
|
||||||
|
textArea.style.opacity = '0';
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
textArea.select();
|
||||||
|
try {
|
||||||
|
// @ts-ignore - execCommand is deprecated but needed for fallback
|
||||||
|
document.execCommand('copy');
|
||||||
|
// Show feedback
|
||||||
|
const copyText = copyButton.querySelector('.copy-text') as HTMLElement;
|
||||||
|
const originalText = copyText.textContent;
|
||||||
|
copyText.textContent = 'Copied!';
|
||||||
|
copyButton.classList.add('copied');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
copyText.textContent = originalText;
|
||||||
|
copyButton.classList.remove('copied');
|
||||||
|
}, 2000);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Fallback copy failed:', err);
|
||||||
|
}
|
||||||
|
document.body.removeChild(textArea);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Track if we're currently editing
|
// Track if we're currently editing
|
||||||
let isEditing = false;
|
let isEditing = false;
|
||||||
|
|
||||||
@ -325,7 +382,7 @@ export class CodeBlockHandler extends BaseBlockHandler {
|
|||||||
border-bottom: 1px solid ${cssManager.bdTheme('#d1d5da', '#30363d')};
|
border-bottom: 1px solid ${cssManager.bdTheme('#d1d5da', '#30363d')};
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -338,6 +395,48 @@ export class CodeBlockHandler extends BaseBlockHandler {
|
|||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Copy Button */
|
||||||
|
.copy-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid ${cssManager.bdTheme('#d1d5da', '#30363d')};
|
||||||
|
border-radius: 6px;
|
||||||
|
color: ${cssManager.bdTheme('#57606a', '#8b949e')};
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-button:hover {
|
||||||
|
background: ${cssManager.bdTheme('#f3f4f6', '#1c2128')};
|
||||||
|
color: ${cssManager.bdTheme('#24292f', '#c9d1d9')};
|
||||||
|
border-color: ${cssManager.bdTheme('#8b949e', '#484f58')};
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-button:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-button.copied {
|
||||||
|
background: ${cssManager.bdTheme('#2ea043', '#238636')};
|
||||||
|
border-color: ${cssManager.bdTheme('#2ea043', '#238636')};
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-text {
|
||||||
|
min-width: 45px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
/* Code Body */
|
/* Code Body */
|
||||||
.code-body {
|
.code-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { html, type TemplateResult } from '@design.estate/dees-element';
|
import { html, type TemplateResult, cssManager } from '@design.estate/dees-element';
|
||||||
import { DeesModal } from '../dees-modal.js';
|
import { DeesModal } from '../dees-modal.js';
|
||||||
import { type IBlock } from './wysiwyg.types.js';
|
import { type IBlock } from './wysiwyg.types.js';
|
||||||
import { WysiwygShortcuts } from './wysiwyg.shortcuts.js';
|
import { WysiwygShortcuts } from './wysiwyg.shortcuts.js';
|
||||||
@ -16,37 +16,56 @@ export class WysiwygModalManager {
|
|||||||
heading: 'Select Programming Language',
|
heading: 'Select Programming Language',
|
||||||
content: html`
|
content: html`
|
||||||
<style>
|
<style>
|
||||||
|
.language-container {
|
||||||
|
padding: 16px;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
.language-grid {
|
.language-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 16px;
|
|
||||||
}
|
}
|
||||||
.language-button {
|
.language-button {
|
||||||
padding: 12px;
|
padding: 12px 8px;
|
||||||
background: var(--dees-color-box);
|
background: transparent;
|
||||||
border: 1px solid var(--dees-color-line-bright);
|
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
|
||||||
border-radius: 4px;
|
border-radius: 6px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
transition: all 0.2s;
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
color: ${cssManager.bdTheme('#374151', '#e5e7eb')};
|
||||||
}
|
}
|
||||||
.language-button:hover {
|
.language-button:hover {
|
||||||
background: var(--dees-color-box-highlight);
|
background: ${cssManager.bdTheme('#f9fafb', '#1f2937')};
|
||||||
border-color: var(--dees-color-primary);
|
border-color: ${cssManager.bdTheme('#d1d5db', '#4b5563')};
|
||||||
|
}
|
||||||
|
.language-button.selected {
|
||||||
|
background: ${cssManager.bdTheme('#f3f4f6', '#374151')};
|
||||||
|
border-color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
|
||||||
|
color: ${cssManager.bdTheme('#111827', '#f9fafb')};
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div class="language-grid">
|
<div class="language-container">
|
||||||
${this.getLanguages().map(lang => html`
|
<div class="language-grid">
|
||||||
<div class="language-button" @click="${(e: MouseEvent) => {
|
${this.getLanguages().map(lang => html`
|
||||||
selectedLanguage = lang.toLowerCase();
|
<div
|
||||||
const modal = (e.target as HTMLElement).closest('dees-modal');
|
class="language-button ${selectedLanguage === lang.toLowerCase() ? 'selected' : ''}"
|
||||||
if (modal) {
|
@click="${() => {
|
||||||
const okButton = modal.shadowRoot?.querySelector('.bottomButton.ok') as HTMLElement;
|
selectedLanguage = lang.toLowerCase();
|
||||||
if (okButton) okButton.click();
|
// Close modal by finding it in DOM
|
||||||
}
|
const modal = document.querySelector('dees-modal');
|
||||||
}}">${lang}</div>
|
if (modal && typeof (modal as any).destroy === 'function') {
|
||||||
`)}
|
(modal as any).destroy();
|
||||||
|
}
|
||||||
|
resolve(selectedLanguage);
|
||||||
|
}}">
|
||||||
|
${lang}
|
||||||
|
</div>
|
||||||
|
`)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
menuOptions: [
|
menuOptions: [
|
||||||
@ -56,13 +75,6 @@ export class WysiwygModalManager {
|
|||||||
modal.destroy();
|
modal.destroy();
|
||||||
resolve(null);
|
resolve(null);
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'OK',
|
|
||||||
action: async (modal) => {
|
|
||||||
modal.destroy();
|
|
||||||
resolve(selectedLanguage);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
@ -76,48 +88,61 @@ export class WysiwygModalManager {
|
|||||||
block: IBlock,
|
block: IBlock,
|
||||||
onUpdate: (block: IBlock) => void
|
onUpdate: (block: IBlock) => void
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
|
||||||
const content = html`
|
const content = html`
|
||||||
<style>
|
<style>
|
||||||
.settings-container {
|
.settings-container {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
.settings-section {
|
.settings-section {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.settings-section:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
.settings-label {
|
.settings-label {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
color: var(--dees-color-text);
|
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
|
||||||
|
font-size: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
}
|
}
|
||||||
.block-type-grid {
|
.block-type-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
}
|
||||||
.block-type-button {
|
.block-type-button {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
background: var(--dees-color-box);
|
background: transparent;
|
||||||
border: 1px solid var(--dees-color-line-bright);
|
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
|
||||||
border-radius: 4px;
|
border-radius: 6px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-align: center;
|
text-align: left;
|
||||||
transition: all 0.2s;
|
transition: all 0.15s ease;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: ${cssManager.bdTheme('#374151', '#e5e7eb')};
|
||||||
}
|
}
|
||||||
.block-type-button:hover {
|
.block-type-button:hover {
|
||||||
background: var(--dees-color-box-highlight);
|
background: ${cssManager.bdTheme('#f9fafb', '#1f2937')};
|
||||||
border-color: var(--dees-color-primary);
|
border-color: ${cssManager.bdTheme('#d1d5db', '#4b5563')};
|
||||||
}
|
}
|
||||||
.block-type-button.selected {
|
.block-type-button.selected {
|
||||||
background: var(--dees-color-primary);
|
background: ${cssManager.bdTheme('#f3f4f6', '#374151')};
|
||||||
color: white;
|
border-color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
|
||||||
|
color: ${cssManager.bdTheme('#111827', '#f9fafb')};
|
||||||
}
|
}
|
||||||
.block-type-icon {
|
.block-type-icon {
|
||||||
font-weight: 600;
|
font-weight: 500;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
width: 20px;
|
||||||
|
text-align: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div class="settings-container">
|
<div class="settings-container">
|
||||||
@ -131,7 +156,7 @@ export class WysiwygModalManager {
|
|||||||
content,
|
content,
|
||||||
menuOptions: [
|
menuOptions: [
|
||||||
{
|
{
|
||||||
name: 'Close',
|
name: 'Done',
|
||||||
action: async (modal) => {
|
action: async (modal) => {
|
||||||
modal.destroy();
|
modal.destroy();
|
||||||
}
|
}
|
||||||
@ -147,57 +172,55 @@ export class WysiwygModalManager {
|
|||||||
block: IBlock,
|
block: IBlock,
|
||||||
onUpdate: (block: IBlock) => void
|
onUpdate: (block: IBlock) => void
|
||||||
): TemplateResult {
|
): TemplateResult {
|
||||||
const currentLanguage = block.metadata?.language || 'plain text';
|
const currentLanguage = block.metadata?.language || 'javascript';
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<style>
|
<style>
|
||||||
.settings-section {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
.settings-label {
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
.language-grid {
|
.language-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
.language-button {
|
.language-button {
|
||||||
padding: 8px;
|
padding: 8px 4px;
|
||||||
background: var(--dees-color-box);
|
background: transparent;
|
||||||
border: 1px solid var(--dees-color-line-bright);
|
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#374151')};
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
transition: all 0.2s;
|
transition: all 0.15s ease;
|
||||||
|
font-size: 12px;
|
||||||
|
color: ${cssManager.bdTheme('#374151', '#e5e7eb')};
|
||||||
}
|
}
|
||||||
.language-button:hover {
|
.language-button:hover {
|
||||||
background: var(--dees-color-box-highlight);
|
background: ${cssManager.bdTheme('#f9fafb', '#1f2937')};
|
||||||
border-color: var(--dees-color-primary);
|
border-color: ${cssManager.bdTheme('#d1d5db', '#4b5563')};
|
||||||
}
|
}
|
||||||
.language-button.selected {
|
.language-button.selected {
|
||||||
background: var(--dees-color-primary);
|
background: ${cssManager.bdTheme('#f3f4f6', '#374151')};
|
||||||
color: white;
|
border-color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
|
||||||
|
color: ${cssManager.bdTheme('#111827', '#f9fafb')};
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<div class="settings-label">Programming Language</div>
|
<div class="settings-label">Programming Language</div>
|
||||||
<div class="language-grid">
|
<div class="language-grid">
|
||||||
${this.getLanguages().map(lang => html`
|
${this.getLanguages().map(lang => html`
|
||||||
<div class="language-button ${currentLanguage === lang.toLowerCase() ? 'selected' : ''}"
|
<div
|
||||||
@click="${(e: MouseEvent) => {
|
class="language-button ${currentLanguage === lang.toLowerCase() ? 'selected' : ''}"
|
||||||
|
@click="${() => {
|
||||||
if (!block.metadata) block.metadata = {};
|
if (!block.metadata) block.metadata = {};
|
||||||
block.metadata.language = lang.toLowerCase();
|
block.metadata.language = lang.toLowerCase();
|
||||||
onUpdate(block);
|
onUpdate(block);
|
||||||
|
|
||||||
// Close modal
|
// Close modal immediately
|
||||||
const modal = (e.target as HTMLElement).closest('dees-modal');
|
const modal = document.querySelector('dees-modal');
|
||||||
if (modal) {
|
if (modal && typeof (modal as any).destroy === 'function') {
|
||||||
const closeButton = modal.shadowRoot?.querySelector('.bottomButton') as HTMLElement;
|
(modal as any).destroy();
|
||||||
if (closeButton) closeButton.click();
|
|
||||||
}
|
}
|
||||||
}}">${lang}</div>
|
}}"
|
||||||
|
data-lang="${lang}"
|
||||||
|
>${lang}</div>
|
||||||
`)}
|
`)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -228,6 +251,8 @@ export class WysiwygModalManager {
|
|||||||
<div
|
<div
|
||||||
class="block-type-button ${block.type === item.type ? 'selected' : ''}"
|
class="block-type-button ${block.type === item.type ? 'selected' : ''}"
|
||||||
@click="${async (e: MouseEvent) => {
|
@click="${async (e: MouseEvent) => {
|
||||||
|
const button = e.currentTarget as HTMLElement;
|
||||||
|
|
||||||
const oldType = block.type;
|
const oldType = block.type;
|
||||||
block.type = item.type as IBlock['type'];
|
block.type = item.type as IBlock['type'];
|
||||||
|
|
||||||
@ -252,11 +277,10 @@ export class WysiwygModalManager {
|
|||||||
|
|
||||||
onUpdate(block);
|
onUpdate(block);
|
||||||
|
|
||||||
// Close modal after selection
|
// Close modal immediately
|
||||||
const modal = (e.target as HTMLElement).closest('dees-modal');
|
const modal = document.querySelector('dees-modal');
|
||||||
if (modal) {
|
if (modal && typeof (modal as any).destroy === 'function') {
|
||||||
const closeButton = modal.shadowRoot?.querySelector('.bottomButton') as HTMLElement;
|
(modal as any).destroy();
|
||||||
if (closeButton) closeButton.click();
|
|
||||||
}
|
}
|
||||||
}}"
|
}}"
|
||||||
>
|
>
|
||||||
|
Reference in New Issue
Block a user