diff --git a/ts_web/elements/wysiwyg/blocks/text/code.block.ts b/ts_web/elements/wysiwyg/blocks/text/code.block.ts index 2fc1682..4ef87bd 100644 --- a/ts_web/elements/wysiwyg/blocks/text/code.block.ts +++ b/ts_web/elements/wysiwyg/blocks/text/code.block.ts @@ -161,7 +161,7 @@ export class CodeBlockHandler extends BaseBlockHandler { // Keydown handler editor.addEventListener('keydown', (e) => { - // Handle Tab + // Handle Tab key for code blocks if (e.key === 'Tab') { e.preventDefault(); const selection = window.getSelection(); @@ -179,6 +179,34 @@ export class CodeBlockHandler extends BaseBlockHandler { return; } + // Check cursor position for navigation keys + if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) { + const cursorPos = this.getCursorPosition(element); + const textLength = editor.textContent?.length || 0; + + // For ArrowLeft at position 0 or ArrowRight at end, let parent handle navigation + if ((e.key === 'ArrowLeft' && cursorPos === 0) || + (e.key === 'ArrowRight' && cursorPos === textLength)) { + // Pass to parent handler for inter-block navigation + handlers.onKeyDown(e); + return; + } + + // For ArrowUp/Down, check if we're at first/last line + if (e.key === 'ArrowUp' || e.key === 'ArrowDown') { + const lines = (editor.textContent || '').split('\n'); + const currentLine = this.getCurrentLineIndex(editor); + + if ((e.key === 'ArrowUp' && currentLine === 0) || + (e.key === 'ArrowDown' && currentLine === lines.length - 1)) { + // Let parent handle navigation to prev/next block + handlers.onKeyDown(e); + return; + } + } + } + + // Pass other keys to parent handler handlers.onKeyDown(e); }); @@ -233,6 +261,21 @@ export class CodeBlockHandler extends BaseBlockHandler { lineNumbersContainer.innerHTML = lineNumbersHtml; } + private getCurrentLineIndex(editor: HTMLElement): number { + const selection = window.getSelection(); + if (!selection || selection.rangeCount === 0) return 0; + + const range = selection.getRangeAt(0); + const preCaretRange = range.cloneRange(); + preCaretRange.selectNodeContents(editor); + preCaretRange.setEnd(range.startContainer, range.startOffset); + + const textBeforeCursor = preCaretRange.toString(); + const linesBeforeCursor = textBeforeCursor.split('\n'); + + return linesBeforeCursor.length - 1; // 0-indexed + } + private applyHighlighting(element: HTMLElement, block: IBlock): void { const editor = element.querySelector('.code-editor') as HTMLElement; if (!editor) return; diff --git a/ts_web/elements/wysiwyg/wysiwyg.keyboardhandler.ts b/ts_web/elements/wysiwyg/wysiwyg.keyboardhandler.ts index 4740c14..7c62241 100644 --- a/ts_web/elements/wysiwyg/wysiwyg.keyboardhandler.ts +++ b/ts_web/elements/wysiwyg/wysiwyg.keyboardhandler.ts @@ -93,20 +93,9 @@ export class WysiwygKeyboardHandler { */ private handleTab(e: KeyboardEvent, block: IBlock): void { if (block.type === 'code') { - // Allow tab in code blocks - 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); - } + // Allow tab in code blocks - handled by CodeBlockHandler + // Let it bubble to the block handler + return; } else if (block.type === 'list') { // Future: implement list indentation e.preventDefault(); @@ -120,7 +109,7 @@ export class WysiwygKeyboardHandler { const blockOps = this.component.blockOperations; // For non-editable blocks, create a new paragraph after - const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; + const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment']; if (nonEditableTypes.includes(block.type)) { e.preventDefault(); const newBlock = blockOps.createBlock(); @@ -209,7 +198,7 @@ export class WysiwygKeyboardHandler { const blockOps = this.component.blockOperations; // Handle non-editable blocks - const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; + const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment']; if (nonEditableTypes.includes(block.type)) { e.preventDefault(); @@ -269,7 +258,7 @@ export class WysiwygKeyboardHandler { // Get the actual editable element const target = block.type === 'code' - ? blockComponent.shadowRoot.querySelector('.block.code') as HTMLElement + ? blockComponent.shadowRoot.querySelector('.code-editor') as HTMLElement : blockComponent.shadowRoot.querySelector('.block') as HTMLElement; if (!target) return; @@ -290,7 +279,7 @@ export class WysiwygKeyboardHandler { if (prevBlock) { // If previous block is non-editable, select it first - const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; + const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment']; if (nonEditableTypes.includes(prevBlock.type)) { await blockOps.focusBlock(prevBlock.id); return; @@ -382,7 +371,7 @@ export class WysiwygKeyboardHandler { const blockOps = this.component.blockOperations; // Handle non-editable blocks - same as backspace - const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; + const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment']; if (nonEditableTypes.includes(block.type)) { e.preventDefault(); @@ -420,7 +409,7 @@ export class WysiwygKeyboardHandler { blockOps.removeBlock(block.id); // Focus the appropriate block - const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; + const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment']; if (nextBlock && !nonEditableTypes.includes(nextBlock.type)) { await blockOps.focusBlock(nextBlock.id, 'start'); } else if (prevBlock && !nonEditableTypes.includes(prevBlock.type)) { @@ -443,7 +432,7 @@ export class WysiwygKeyboardHandler { // Get the actual editable element const target = block.type === 'code' - ? blockComponent.shadowRoot.querySelector('.block.code') as HTMLElement + ? blockComponent.shadowRoot.querySelector('.code-editor') as HTMLElement : blockComponent.shadowRoot.querySelector('.block') as HTMLElement; if (!target) return; @@ -460,7 +449,7 @@ export class WysiwygKeyboardHandler { if (cursorPos === textLength) { const nextBlock = blockOps.getNextBlock(block.id); - const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; + const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment']; if (nextBlock && nonEditableTypes.includes(nextBlock.type)) { e.preventDefault(); await blockOps.focusBlock(nextBlock.id); @@ -476,7 +465,7 @@ export class WysiwygKeyboardHandler { */ private async handleArrowUp(e: KeyboardEvent, block: IBlock): Promise { // For non-editable blocks, always navigate to previous block - const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; + const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment']; if (nonEditableTypes.includes(block.type)) { e.preventDefault(); const blockOps = this.component.blockOperations; @@ -493,9 +482,9 @@ export class WysiwygKeyboardHandler { const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block'); if (!blockComponent || !blockComponent.shadowRoot) return; - // Get the actual editable element (code blocks have .block.code) + // Get the actual editable element - code blocks now use .code-editor const target = block.type === 'code' - ? blockComponent.shadowRoot.querySelector('.block.code') as HTMLElement + ? blockComponent.shadowRoot.querySelector('.code-editor') as HTMLElement : blockComponent.shadowRoot.querySelector('.block') as HTMLElement; if (!target) return; @@ -515,7 +504,7 @@ export class WysiwygKeyboardHandler { const prevBlock = blockOps.getPreviousBlock(block.id); if (prevBlock) { - const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; + const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment']; await blockOps.focusBlock(prevBlock.id, nonEditableTypes.includes(prevBlock.type) ? undefined : 'end'); } } @@ -527,14 +516,14 @@ export class WysiwygKeyboardHandler { */ private async handleArrowDown(e: KeyboardEvent, block: IBlock): Promise { // For non-editable blocks, always navigate to next block - const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; + const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment']; if (nonEditableTypes.includes(block.type)) { e.preventDefault(); const blockOps = this.component.blockOperations; const nextBlock = blockOps.getNextBlock(block.id); if (nextBlock) { - const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; + const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment']; await blockOps.focusBlock(nextBlock.id, nonEditableTypes.includes(nextBlock.type) ? undefined : 'start'); } return; @@ -545,9 +534,9 @@ export class WysiwygKeyboardHandler { const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block'); if (!blockComponent || !blockComponent.shadowRoot) return; - // Get the actual editable element (code blocks have .block.code) + // Get the actual editable element - code blocks now use .code-editor const target = block.type === 'code' - ? blockComponent.shadowRoot.querySelector('.block.code') as HTMLElement + ? blockComponent.shadowRoot.querySelector('.code-editor') as HTMLElement : blockComponent.shadowRoot.querySelector('.block') as HTMLElement; if (!target) return; @@ -567,7 +556,7 @@ export class WysiwygKeyboardHandler { const nextBlock = blockOps.getNextBlock(block.id); if (nextBlock) { - const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; + const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment']; await blockOps.focusBlock(nextBlock.id, nonEditableTypes.includes(nextBlock.type) ? undefined : 'start'); } } @@ -595,14 +584,14 @@ export class WysiwygKeyboardHandler { */ private async handleArrowLeft(e: KeyboardEvent, block: IBlock): Promise { // For non-editable blocks, navigate to previous block - const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; + const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment']; if (nonEditableTypes.includes(block.type)) { e.preventDefault(); const blockOps = this.component.blockOperations; const prevBlock = blockOps.getPreviousBlock(block.id); if (prevBlock) { - const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; + const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment']; await blockOps.focusBlock(prevBlock.id, nonEditableTypes.includes(prevBlock.type) ? undefined : 'end'); } return; @@ -613,9 +602,9 @@ export class WysiwygKeyboardHandler { const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block'); if (!blockComponent || !blockComponent.shadowRoot) return; - // Get the actual editable element (code blocks have .block.code) + // Get the actual editable element - code blocks now use .code-editor const target = block.type === 'code' - ? blockComponent.shadowRoot.querySelector('.block.code') as HTMLElement + ? blockComponent.shadowRoot.querySelector('.code-editor') as HTMLElement : blockComponent.shadowRoot.querySelector('.block') as HTMLElement; if (!target) return; @@ -637,7 +626,7 @@ export class WysiwygKeyboardHandler { if (prevBlock) { e.preventDefault(); - const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; + const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment']; const position = nonEditableTypes.includes(prevBlock.type) ? undefined : 'end'; await blockOps.focusBlock(prevBlock.id, position); } @@ -650,14 +639,14 @@ export class WysiwygKeyboardHandler { */ private async handleArrowRight(e: KeyboardEvent, block: IBlock): Promise { // For non-editable blocks, navigate to next block - const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; + const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment']; if (nonEditableTypes.includes(block.type)) { e.preventDefault(); const blockOps = this.component.blockOperations; const nextBlock = blockOps.getNextBlock(block.id); if (nextBlock) { - const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; + const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment']; await blockOps.focusBlock(nextBlock.id, nonEditableTypes.includes(nextBlock.type) ? undefined : 'start'); } return; @@ -668,9 +657,9 @@ export class WysiwygKeyboardHandler { const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block'); if (!blockComponent || !blockComponent.shadowRoot) return; - // Get the actual editable element (code blocks have .block.code) + // Get the actual editable element - code blocks now use .code-editor const target = block.type === 'code' - ? blockComponent.shadowRoot.querySelector('.block.code') as HTMLElement + ? blockComponent.shadowRoot.querySelector('.code-editor') as HTMLElement : blockComponent.shadowRoot.querySelector('.block') as HTMLElement; if (!target) return; @@ -693,7 +682,7 @@ export class WysiwygKeyboardHandler { if (nextBlock) { e.preventDefault(); - const nonEditableTypes = ['divider', 'image', 'youtube', 'markdown', 'html', 'attachment']; + const nonEditableTypes = ['divider', 'image', 'youtube', 'attachment']; await blockOps.focusBlock(nextBlock.id, nonEditableTypes.includes(nextBlock.type) ? undefined : 'start'); } }