fix(wysiwyg):Improve Wysiwyg editor
This commit is contained in:
@@ -6,7 +6,7 @@ import {
|
||||
customElement,
|
||||
type TemplateResult,
|
||||
property,
|
||||
html,
|
||||
static as html,
|
||||
cssManager,
|
||||
state,
|
||||
} from '@design.estate/dees-element';
|
||||
@@ -44,7 +44,7 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
public outputFormat: OutputFormat = 'html';
|
||||
|
||||
@state()
|
||||
private blocks: IBlock[] = [
|
||||
public blocks: IBlock[] = [
|
||||
{
|
||||
id: WysiwygShortcuts.generateBlockId(),
|
||||
type: 'paragraph',
|
||||
@@ -53,38 +53,42 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
];
|
||||
|
||||
// Not using @state to avoid re-renders when selection changes
|
||||
private selectedBlockId: string | null = null;
|
||||
public selectedBlockId: string | null = null;
|
||||
|
||||
// Slash menu is now globally rendered
|
||||
private slashMenu = DeesSlashMenu.getInstance();
|
||||
public slashMenu = DeesSlashMenu.getInstance();
|
||||
|
||||
@state()
|
||||
private draggedBlockId: string | null = null;
|
||||
public draggedBlockId: string | null = null;
|
||||
|
||||
@state()
|
||||
private dragOverBlockId: string | null = null;
|
||||
public dragOverBlockId: string | null = null;
|
||||
|
||||
@state()
|
||||
private dragOverPosition: 'before' | 'after' | null = null;
|
||||
public dragOverPosition: 'before' | 'after' | null = null;
|
||||
|
||||
// Formatting menu is now globally rendered
|
||||
private formattingMenu = DeesFormattingMenu.getInstance();
|
||||
public formattingMenu = DeesFormattingMenu.getInstance();
|
||||
|
||||
@state()
|
||||
private selectedText: string = '';
|
||||
|
||||
private editorContentRef: HTMLDivElement;
|
||||
private isComposing: boolean = false;
|
||||
private selectionChangeHandler = () => this.handleSelectionChange();
|
||||
public editorContentRef: HTMLDivElement;
|
||||
public isComposing: boolean = false;
|
||||
private selectionChangeTimeout: any;
|
||||
private selectionChangeHandler = () => {
|
||||
// Throttle selection change events
|
||||
if (this.selectionChangeTimeout) {
|
||||
clearTimeout(this.selectionChangeTimeout);
|
||||
}
|
||||
this.selectionChangeTimeout = setTimeout(() => this.handleSelectionChange(), 50);
|
||||
};
|
||||
|
||||
// Handler instances
|
||||
private blockOperations: WysiwygBlockOperations;
|
||||
public blockOperations: WysiwygBlockOperations;
|
||||
private inputHandler: WysiwygInputHandler;
|
||||
private keyboardHandler: WysiwygKeyboardHandler;
|
||||
private dragDropHandler: WysiwygDragDropHandler;
|
||||
|
||||
// Content cache to avoid triggering re-renders during typing
|
||||
private contentCache: Map<string, string> = new Map();
|
||||
|
||||
public static styles = [
|
||||
...DeesInputBase.baseStyles,
|
||||
@@ -116,6 +120,11 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
clearTimeout(this.blurTimeout);
|
||||
this.blurTimeout = null;
|
||||
}
|
||||
// Clean up selection change timeout
|
||||
if (this.selectionChangeTimeout) {
|
||||
clearTimeout(this.selectionChangeTimeout);
|
||||
this.selectionChangeTimeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
async firstUpdated() {
|
||||
@@ -141,7 +150,7 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
/**
|
||||
* Renders all blocks programmatically without triggering re-renders
|
||||
*/
|
||||
private renderBlocksProgrammatically() {
|
||||
public renderBlocksProgrammatically() {
|
||||
if (!this.editorContentRef) return;
|
||||
|
||||
// Clear existing blocks
|
||||
@@ -157,7 +166,7 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
/**
|
||||
* Creates a block element programmatically
|
||||
*/
|
||||
private createBlockElement(block: IBlock): HTMLElement {
|
||||
public createBlockElement(block: IBlock): HTMLElement {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'block-wrapper';
|
||||
wrapper.setAttribute('data-block-id', block.id);
|
||||
@@ -200,7 +209,7 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
settings.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
WysiwygModalManager.showBlockSettingsModal(block, (updatedBlock) => {
|
||||
WysiwygModalManager.showBlockSettingsModal(block, () => {
|
||||
this.updateValue();
|
||||
// Re-render only the updated block
|
||||
this.updateBlockElement(block.id);
|
||||
@@ -220,7 +229,7 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
/**
|
||||
* Updates a specific block element
|
||||
*/
|
||||
private updateBlockElement(blockId: string) {
|
||||
public updateBlockElement(blockId: string) {
|
||||
const block = this.blocks.find(b => b.id === blockId);
|
||||
if (!block) return;
|
||||
|
||||
@@ -257,7 +266,7 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
|
||||
|
||||
|
||||
private handleSlashMenuKeyboard(e: KeyboardEvent) {
|
||||
public handleSlashMenuKeyboard(e: KeyboardEvent) {
|
||||
switch(e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
@@ -306,39 +315,6 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
this.slashMenu.hide();
|
||||
}
|
||||
|
||||
private detectBlockTypeIntent(content: string): { type: IBlock['type'], listType?: 'bullet' | 'ordered' } | null {
|
||||
// Check heading patterns
|
||||
const headingResult = WysiwygShortcuts.checkHeadingShortcut(content);
|
||||
if (headingResult) {
|
||||
return headingResult;
|
||||
}
|
||||
|
||||
// Check list patterns
|
||||
const listResult = WysiwygShortcuts.checkListShortcut(content);
|
||||
if (listResult) {
|
||||
return listResult;
|
||||
}
|
||||
|
||||
// Check quote pattern
|
||||
if (WysiwygShortcuts.checkQuoteShortcut(content)) {
|
||||
return { type: 'quote' };
|
||||
}
|
||||
|
||||
// Check code pattern
|
||||
if (WysiwygShortcuts.checkCodeShortcut(content)) {
|
||||
return { type: 'code' };
|
||||
}
|
||||
|
||||
// Check divider pattern
|
||||
if (WysiwygShortcuts.checkDividerShortcut(content)) {
|
||||
return { type: 'divider' };
|
||||
}
|
||||
|
||||
// Don't automatically revert to paragraph - blocks should keep their type
|
||||
// unless explicitly changed by the user
|
||||
return null;
|
||||
}
|
||||
|
||||
private handleBlockFocus(block: IBlock) {
|
||||
// Clear any pending blur timeout when focusing
|
||||
if (this.blurTimeout) {
|
||||
@@ -533,7 +509,7 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
}
|
||||
}
|
||||
|
||||
private updateValue() {
|
||||
public updateValue() {
|
||||
if (this.outputFormat === 'html') {
|
||||
this.value = WysiwygConverters.getHtmlOutput(this.blocks);
|
||||
} else {
|
||||
@@ -625,26 +601,6 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
this.importBlocks(state.blocks);
|
||||
}
|
||||
|
||||
// Drag and Drop Handlers
|
||||
private handleDragStart(e: DragEvent, block: IBlock): void {
|
||||
if (!e.dataTransfer) return;
|
||||
|
||||
this.draggedBlockId = block.id;
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
e.dataTransfer.setData('text/plain', block.id);
|
||||
|
||||
// Add dragging class to the wrapper
|
||||
setTimeout(() => {
|
||||
const wrapper = this.editorContentRef.querySelector(`[data-block-id="${block.id}"]`);
|
||||
if (wrapper) {
|
||||
wrapper.classList.add('dragging');
|
||||
}
|
||||
|
||||
// Add dragging class to editor content
|
||||
this.editorContentRef.classList.add('dragging');
|
||||
}, 10);
|
||||
}
|
||||
|
||||
private handleDragEnd(): void {
|
||||
// Remove all drag-related classes
|
||||
if (this.draggedBlockId) {
|
||||
@@ -668,44 +624,7 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
this.dragOverPosition = null;
|
||||
}
|
||||
|
||||
private handleDragOver(e: DragEvent, block: IBlock): void {
|
||||
e.preventDefault();
|
||||
if (!e.dataTransfer || !this.draggedBlockId || this.draggedBlockId === block.id) return;
|
||||
|
||||
e.dataTransfer.dropEffect = 'move';
|
||||
|
||||
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
||||
const midpoint = rect.top + rect.height / 2;
|
||||
|
||||
// Remove previous drag-over classes
|
||||
if (this.dragOverBlockId) {
|
||||
const prevWrapper = this.editorContentRef.querySelector(`[data-block-id="${this.dragOverBlockId}"]`);
|
||||
if (prevWrapper) {
|
||||
prevWrapper.classList.remove('drag-over-before', 'drag-over-after');
|
||||
}
|
||||
}
|
||||
|
||||
this.dragOverBlockId = block.id;
|
||||
this.dragOverPosition = e.clientY < midpoint ? 'before' : 'after';
|
||||
|
||||
// Add new drag-over class
|
||||
const wrapper = e.currentTarget as HTMLElement;
|
||||
wrapper.classList.add(`drag-over-${this.dragOverPosition}`);
|
||||
}
|
||||
|
||||
private handleDragLeave(block: IBlock): void {
|
||||
if (this.dragOverBlockId === block.id) {
|
||||
const wrapper = this.editorContentRef.querySelector(`[data-block-id="${block.id}"]`);
|
||||
if (wrapper) {
|
||||
wrapper.classList.remove('drag-over-before', 'drag-over-after');
|
||||
}
|
||||
|
||||
this.dragOverBlockId = null;
|
||||
this.dragOverPosition = null;
|
||||
}
|
||||
}
|
||||
|
||||
private handleDrop(e: DragEvent, targetBlock: IBlock): void {
|
||||
public handleDrop(e: DragEvent, targetBlock: IBlock): void {
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.draggedBlockId || this.draggedBlockId === targetBlock.id) return;
|
||||
@@ -746,7 +665,7 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
}
|
||||
|
||||
|
||||
private handleTextSelection(e: MouseEvent): void {
|
||||
private handleTextSelection(_e: MouseEvent): void {
|
||||
// Don't interfere with slash menu
|
||||
if (this.slashMenu.visible) return;
|
||||
|
||||
@@ -960,69 +879,4 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
// Modal methods moved to WysiwygModalManager
|
||||
private async showLanguageSelectionModal(): Promise<string | null> {
|
||||
return new Promise((resolve) => {
|
||||
let selectedLanguage: string | null = null;
|
||||
|
||||
DeesModal.createAndShow({
|
||||
heading: 'Select Programming Language',
|
||||
content: html`
|
||||
<style>
|
||||
.language-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
.language-button {
|
||||
padding: 12px;
|
||||
background: var(--dees-color-box);
|
||||
border: 1px solid var(--dees-color-line-bright);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.language-button:hover {
|
||||
background: var(--dees-color-box-highlight);
|
||||
border-color: var(--dees-color-primary);
|
||||
}
|
||||
</style>
|
||||
<div class="language-grid">
|
||||
${['JavaScript', 'TypeScript', 'Python', 'Java', 'C++', 'C#', 'Go', 'Rust', 'HTML', 'CSS', 'SQL', 'Shell', 'JSON', 'YAML', 'Markdown', 'Plain Text'].map(lang => html`
|
||||
<div class="language-button" @click="${(e: MouseEvent) => {
|
||||
selectedLanguage = lang.toLowerCase();
|
||||
// Find and click the hidden OK button to close the modal
|
||||
const modal = (e.target as HTMLElement).closest('dees-modal');
|
||||
if (modal) {
|
||||
const okButton = modal.shadowRoot?.querySelector('.bottomButton.ok') as HTMLElement;
|
||||
if (okButton) okButton.click();
|
||||
}
|
||||
}}">${lang}</div>
|
||||
`)}
|
||||
</div>
|
||||
`,
|
||||
menuOptions: [
|
||||
{
|
||||
name: 'Cancel',
|
||||
action: async (modal) => {
|
||||
modal.destroy();
|
||||
resolve(null);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'OK',
|
||||
action: async (modal) => {
|
||||
modal.destroy();
|
||||
resolve(selectedLanguage);
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Modal methods have been moved to WysiwygModalManager
|
||||
}
|
Reference in New Issue
Block a user