fix(dees-modal): theming
This commit is contained in:
@ -1,6 +1,4 @@
|
||||
import { type IBlock } from './wysiwyg.types.js';
|
||||
import { WysiwygBlocks } from './wysiwyg.blocks.js';
|
||||
import { WysiwygBlockOperations } from './wysiwyg.blockoperations.js';
|
||||
|
||||
export class WysiwygKeyboardHandler {
|
||||
private component: any;
|
||||
@ -12,10 +10,10 @@ export class WysiwygKeyboardHandler {
|
||||
/**
|
||||
* Handles keyboard events for blocks
|
||||
*/
|
||||
handleBlockKeyDown(e: KeyboardEvent, block: IBlock): void {
|
||||
async handleBlockKeyDown(e: KeyboardEvent, block: IBlock): Promise<void> {
|
||||
// Handle slash menu navigation
|
||||
if (this.component.showSlashMenu && this.isSlashMenuKey(e.key)) {
|
||||
this.handleSlashMenuKeyboard(e);
|
||||
if (this.component.slashMenu.visible && this.isSlashMenuKey(e.key)) {
|
||||
this.component.handleSlashMenuKeyboard(e);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -30,10 +28,22 @@ export class WysiwygKeyboardHandler {
|
||||
this.handleTab(e, block);
|
||||
break;
|
||||
case 'Enter':
|
||||
this.handleEnter(e, block);
|
||||
await this.handleEnter(e, block);
|
||||
break;
|
||||
case 'Backspace':
|
||||
this.handleBackspace(e, block);
|
||||
await this.handleBackspace(e, block);
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
await this.handleArrowUp(e, block);
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
await this.handleArrowDown(e, block);
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
await this.handleArrowLeft(e, block);
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
await this.handleArrowRight(e, block);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -54,19 +64,20 @@ export class WysiwygKeyboardHandler {
|
||||
switch (e.key.toLowerCase()) {
|
||||
case 'b':
|
||||
e.preventDefault();
|
||||
this.component.applyFormat('bold');
|
||||
// Use Promise to ensure focus is maintained
|
||||
Promise.resolve().then(() => this.component.applyFormat('bold'));
|
||||
return true;
|
||||
case 'i':
|
||||
e.preventDefault();
|
||||
this.component.applyFormat('italic');
|
||||
Promise.resolve().then(() => this.component.applyFormat('italic'));
|
||||
return true;
|
||||
case 'u':
|
||||
e.preventDefault();
|
||||
this.component.applyFormat('underline');
|
||||
Promise.resolve().then(() => this.component.applyFormat('underline'));
|
||||
return true;
|
||||
case 'k':
|
||||
e.preventDefault();
|
||||
this.component.applyFormat('link');
|
||||
Promise.resolve().then(() => this.component.applyFormat('link'));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@ -79,7 +90,18 @@ export class WysiwygKeyboardHandler {
|
||||
if (block.type === 'code') {
|
||||
// Allow tab in code blocks
|
||||
e.preventDefault();
|
||||
document.execCommand('insertText', false, ' ');
|
||||
// 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);
|
||||
}
|
||||
} else if (block.type === 'list') {
|
||||
// Future: implement list indentation
|
||||
e.preventDefault();
|
||||
@ -89,7 +111,7 @@ export class WysiwygKeyboardHandler {
|
||||
/**
|
||||
* Handles Enter key
|
||||
*/
|
||||
private handleEnter(e: KeyboardEvent, block: IBlock): void {
|
||||
private async handleEnter(e: KeyboardEvent, block: IBlock): Promise<void> {
|
||||
const blockOps = this.component.blockOperations;
|
||||
|
||||
if (block.type === 'code') {
|
||||
@ -97,7 +119,7 @@ export class WysiwygKeyboardHandler {
|
||||
// Shift+Enter in code blocks creates a new block
|
||||
e.preventDefault();
|
||||
const newBlock = blockOps.createBlock();
|
||||
blockOps.insertBlockAfter(block, newBlock);
|
||||
await blockOps.insertBlockAfter(block, newBlock);
|
||||
}
|
||||
// Normal Enter in code blocks creates new line (let browser handle it)
|
||||
return;
|
||||
@ -105,12 +127,42 @@ export class WysiwygKeyboardHandler {
|
||||
|
||||
if (!e.shiftKey) {
|
||||
if (block.type === 'list') {
|
||||
this.handleEnterInList(e, block);
|
||||
await this.handleEnterInList(e, block);
|
||||
} else {
|
||||
// Create new paragraph block
|
||||
// Split content at cursor position
|
||||
e.preventDefault();
|
||||
const newBlock = blockOps.createBlock();
|
||||
blockOps.insertBlockAfter(block, newBlock);
|
||||
|
||||
// Get the block component
|
||||
const target = e.target as HTMLElement;
|
||||
const blockWrapper = target.closest('.block-wrapper');
|
||||
const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block') as any;
|
||||
|
||||
if (blockComponent && blockComponent.getSplitContent) {
|
||||
const splitContent = blockComponent.getSplitContent();
|
||||
|
||||
if (splitContent) {
|
||||
// Update current block with content before cursor
|
||||
blockComponent.setContent(splitContent.before);
|
||||
block.content = splitContent.before;
|
||||
|
||||
// Create new block with content after cursor
|
||||
const newBlock = blockOps.createBlock('paragraph', splitContent.after);
|
||||
|
||||
// Insert the new block
|
||||
await blockOps.insertBlockAfter(block, newBlock);
|
||||
|
||||
// Update the value after both blocks are set
|
||||
this.component.updateValue();
|
||||
} else {
|
||||
// Fallback - just create empty block
|
||||
const newBlock = blockOps.createBlock();
|
||||
await blockOps.insertBlockAfter(block, newBlock);
|
||||
}
|
||||
} else {
|
||||
// No block component or method, just create empty block
|
||||
const newBlock = blockOps.createBlock();
|
||||
await blockOps.insertBlockAfter(block, newBlock);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Shift+Enter creates line break (let browser handle it)
|
||||
@ -119,8 +171,7 @@ export class WysiwygKeyboardHandler {
|
||||
/**
|
||||
* Handles Enter key in list blocks
|
||||
*/
|
||||
private handleEnterInList(e: KeyboardEvent, block: IBlock): void {
|
||||
const target = e.target as HTMLDivElement;
|
||||
private async handleEnterInList(e: KeyboardEvent, block: IBlock): Promise<void> {
|
||||
const selection = window.getSelection();
|
||||
|
||||
if (selection && selection.rangeCount > 0) {
|
||||
@ -132,7 +183,7 @@ export class WysiwygKeyboardHandler {
|
||||
e.preventDefault();
|
||||
const blockOps = this.component.blockOperations;
|
||||
const newBlock = blockOps.createBlock();
|
||||
blockOps.insertBlockAfter(block, newBlock);
|
||||
await blockOps.insertBlockAfter(block, newBlock);
|
||||
}
|
||||
// Otherwise, let browser create new list item
|
||||
}
|
||||
@ -141,7 +192,7 @@ export class WysiwygKeyboardHandler {
|
||||
/**
|
||||
* Handles Backspace key
|
||||
*/
|
||||
private handleBackspace(e: KeyboardEvent, block: IBlock): void {
|
||||
private async handleBackspace(e: KeyboardEvent, block: IBlock): Promise<void> {
|
||||
if (block.content === '' && this.component.blocks.length > 1) {
|
||||
e.preventDefault();
|
||||
const blockOps = this.component.blockOperations;
|
||||
@ -150,49 +201,184 @@ export class WysiwygKeyboardHandler {
|
||||
if (prevBlock) {
|
||||
blockOps.removeBlock(block.id);
|
||||
|
||||
setTimeout(() => {
|
||||
if (prevBlock.type !== 'divider') {
|
||||
blockOps.focusBlock(prevBlock.id, 'end');
|
||||
}
|
||||
});
|
||||
if (prevBlock.type !== 'divider') {
|
||||
await blockOps.focusBlock(prevBlock.id, 'end');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles slash menu keyboard navigation
|
||||
* Handles ArrowUp key - navigate to previous block if at beginning
|
||||
*/
|
||||
private handleSlashMenuKeyboard(e: KeyboardEvent): void {
|
||||
const menuItems = this.component.getFilteredMenuItems();
|
||||
private async handleArrowUp(e: KeyboardEvent, block: IBlock): Promise<void> {
|
||||
const selection = window.getSelection();
|
||||
if (!selection || selection.rangeCount === 0) return;
|
||||
|
||||
const range = selection.getRangeAt(0);
|
||||
const target = e.target as HTMLElement;
|
||||
|
||||
switch(e.key) {
|
||||
case 'ArrowDown':
|
||||
// Check if cursor is at the beginning of the block
|
||||
const isAtStart = range.startOffset === 0 && range.endOffset === 0;
|
||||
|
||||
if (isAtStart) {
|
||||
const firstNode = target.firstChild;
|
||||
const isReallyAtStart = !firstNode ||
|
||||
(range.startContainer === firstNode && range.startOffset === 0) ||
|
||||
(range.startContainer === target && range.startOffset === 0);
|
||||
|
||||
if (isReallyAtStart) {
|
||||
e.preventDefault();
|
||||
this.component.slashMenuSelectedIndex =
|
||||
(this.component.slashMenuSelectedIndex + 1) % menuItems.length;
|
||||
break;
|
||||
const blockOps = this.component.blockOperations;
|
||||
const prevBlock = blockOps.getPreviousBlock(block.id);
|
||||
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
this.component.slashMenuSelectedIndex =
|
||||
this.component.slashMenuSelectedIndex === 0
|
||||
? menuItems.length - 1
|
||||
: this.component.slashMenuSelectedIndex - 1;
|
||||
break;
|
||||
|
||||
case 'Enter':
|
||||
e.preventDefault();
|
||||
if (menuItems[this.component.slashMenuSelectedIndex]) {
|
||||
this.component.insertBlock(
|
||||
menuItems[this.component.slashMenuSelectedIndex].type as IBlock['type']
|
||||
);
|
||||
if (prevBlock && prevBlock.type !== 'divider') {
|
||||
await blockOps.focusBlock(prevBlock.id, 'end');
|
||||
}
|
||||
break;
|
||||
|
||||
case 'Escape':
|
||||
e.preventDefault();
|
||||
this.component.closeSlashMenu();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles ArrowDown key - navigate to next block if at end
|
||||
*/
|
||||
private async handleArrowDown(e: KeyboardEvent, block: IBlock): Promise<void> {
|
||||
const selection = window.getSelection();
|
||||
if (!selection || selection.rangeCount === 0) return;
|
||||
|
||||
const range = selection.getRangeAt(0);
|
||||
const target = e.target as HTMLElement;
|
||||
|
||||
// Check if cursor is at the end of the block
|
||||
const lastNode = target.lastChild;
|
||||
|
||||
// For different block types, check if we're at the end
|
||||
let isAtEnd = false;
|
||||
|
||||
if (!lastNode) {
|
||||
// Empty block
|
||||
isAtEnd = true;
|
||||
} else if (lastNode.nodeType === Node.TEXT_NODE) {
|
||||
isAtEnd = range.endContainer === lastNode && range.endOffset === lastNode.textContent?.length;
|
||||
} else if (block.type === 'list') {
|
||||
// For lists, check if we're in the last item at the end
|
||||
const lastLi = target.querySelector('li:last-child');
|
||||
if (lastLi) {
|
||||
const lastTextNode = this.getLastTextNode(lastLi);
|
||||
isAtEnd = lastTextNode && range.endContainer === lastTextNode &&
|
||||
range.endOffset === lastTextNode.textContent?.length;
|
||||
}
|
||||
} else {
|
||||
// For other HTML content
|
||||
const lastTextNode = this.getLastTextNode(target);
|
||||
isAtEnd = lastTextNode && range.endContainer === lastTextNode &&
|
||||
range.endOffset === lastTextNode.textContent?.length;
|
||||
}
|
||||
|
||||
if (isAtEnd) {
|
||||
e.preventDefault();
|
||||
const blockOps = this.component.blockOperations;
|
||||
const nextBlock = blockOps.getNextBlock(block.id);
|
||||
|
||||
if (nextBlock && nextBlock.type !== 'divider') {
|
||||
await blockOps.focusBlock(nextBlock.id, 'start');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to get the last text node in an element
|
||||
*/
|
||||
private getLastTextNode(element: Node): Text | null {
|
||||
if (element.nodeType === Node.TEXT_NODE) {
|
||||
return element as Text;
|
||||
}
|
||||
|
||||
for (let i = element.childNodes.length - 1; i >= 0; i--) {
|
||||
const lastText = this.getLastTextNode(element.childNodes[i]);
|
||||
if (lastText) return lastText;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles ArrowLeft key - navigate to previous block if at beginning
|
||||
*/
|
||||
private async handleArrowLeft(e: KeyboardEvent, block: IBlock): Promise<void> {
|
||||
const selection = window.getSelection();
|
||||
if (!selection || selection.rangeCount === 0) return;
|
||||
|
||||
const range = selection.getRangeAt(0);
|
||||
|
||||
// Check if cursor is at the very beginning (collapsed and at offset 0)
|
||||
if (range.collapsed && range.startOffset === 0) {
|
||||
const target = e.target as HTMLElement;
|
||||
const firstNode = target.firstChild;
|
||||
|
||||
// Verify we're really at the start
|
||||
const isAtStart = !firstNode ||
|
||||
(range.startContainer === firstNode) ||
|
||||
(range.startContainer === target);
|
||||
|
||||
if (isAtStart) {
|
||||
const blockOps = this.component.blockOperations;
|
||||
const prevBlock = blockOps.getPreviousBlock(block.id);
|
||||
|
||||
if (prevBlock && prevBlock.type !== 'divider') {
|
||||
e.preventDefault();
|
||||
await blockOps.focusBlock(prevBlock.id, 'end');
|
||||
}
|
||||
}
|
||||
}
|
||||
// Otherwise, let the browser handle normal left arrow navigation
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles ArrowRight key - navigate to next block if at end
|
||||
*/
|
||||
private async handleArrowRight(e: KeyboardEvent, block: IBlock): Promise<void> {
|
||||
const selection = window.getSelection();
|
||||
if (!selection || selection.rangeCount === 0) return;
|
||||
|
||||
const range = selection.getRangeAt(0);
|
||||
const target = e.target as HTMLElement;
|
||||
|
||||
// Check if cursor is at the very end
|
||||
if (range.collapsed) {
|
||||
const textLength = target.textContent?.length || 0;
|
||||
let isAtEnd = false;
|
||||
|
||||
if (textLength === 0) {
|
||||
// Empty block
|
||||
isAtEnd = true;
|
||||
} else if (range.endContainer.nodeType === Node.TEXT_NODE) {
|
||||
const textNode = range.endContainer as Text;
|
||||
isAtEnd = range.endOffset === textNode.textContent?.length;
|
||||
} else {
|
||||
// Check if we're at the end of the last text node
|
||||
const lastTextNode = this.getLastTextNode(target);
|
||||
if (lastTextNode) {
|
||||
isAtEnd = range.endContainer === lastTextNode &&
|
||||
range.endOffset === lastTextNode.textContent?.length;
|
||||
}
|
||||
}
|
||||
|
||||
if (isAtEnd) {
|
||||
const blockOps = this.component.blockOperations;
|
||||
const nextBlock = blockOps.getNextBlock(block.id);
|
||||
|
||||
if (nextBlock && nextBlock.type !== 'divider') {
|
||||
e.preventDefault();
|
||||
await blockOps.focusBlock(nextBlock.id, 'start');
|
||||
}
|
||||
}
|
||||
}
|
||||
// Otherwise, let the browser handle normal right arrow navigation
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles slash menu keyboard navigation
|
||||
* Note: This is now handled by the component directly
|
||||
*/
|
||||
}
|
Reference in New Issue
Block a user